linux内核启动第二阶段分析原文章:
http://blog.chinaunix.net/uid-20672257-id-2239898.html
456 asmlinkage void __init start_kernel(void)
457 {
458 char * command_line;
459 extern const struct kernel_param __start___param[], __stop___param[];这两个外部变量,是内核编译剧本定位的内核参数肇端地址
460
461 smp_setup_processor_id(); /*当只有一个CPU的时候这个函数就什么都不做,但是如果有多个CPU的时候那么它就 返回在启动的时候的那个CPU的号 */
462
463 /*
464 * Need to run as early as possible, to initialize the
465 * lockdep hash:
466 */
467 lockdep_init(); /*初始化lockdep hash 表*/
468 debug_objects_early_init();//初始化debug kernel相关
469
470 /*
471 * Set up the the initial canary ASAP:
472 */
473 boot_init_stack_canary();//stack_canary的是带防止栈溢出攻击保护的堆栈。详见http://www.cloud-sec.org/CC_STACKPROTECTOR_patch_Analysis
474
475 cgroup_init_early();//Cgroup初始化,Cgroup是近代linux kernel出现的.它为进程和其后续的子进程提供了一种性能控制机制,详见:http://linux.chinaunix.net/techdoc/net/2008/12/23/1054425.shtml
476
477 local_irq_disable(); /*关闭当前CPU的中断*/
478 early_boot_irqs_disabled = true;
479
480 /*
481 * Interrupts are still disabled. Do necessary setups, then
482 * enable them
483 */
484 tick_init(); //初始化 tick控制功能,注册clockevents的框架。
485 boot_cpu_init();//对于CPU核的系统来说,设置第一个CPU核为活跃 CPU核。对于单CPU核系统来说,设置CPU核为活跃CPU核
486 page_address_init();
/*当定义了CONFIG_HIGHMEM 宏,并且没有定义 WANT_PAGE_VIRTUAL 宏时,非空函数。其他情况为空函数。注意:ARM9不支持高端地址(大于896M),一般的嵌入式产品也不会用高端地址,所以,在ARM体系结构下,此函数为空!*/
487 printk(KERN_NOTICE "%s", linux_banner);/* 将linux_banner的内容打印到log_buf缓冲区中去,等到串口或者其它终端初始化之后,在一次性打印到终端上去*/
488 setup_arch(&command_line);
/*每种体系结构都有自己的setup_arch()函数,是体系结构相关的,具体编译哪个 体系结构的setup_arch()函数,由源码树顶层目录下的Makefile中的ARCH变量决定
例如: ARM体系结构的。
SUBARCH :=arm
ARCH ?= $(SUBARCH)
该函数在所在的路劲为 /arm/kernel/setup.c,其中那个command_line就是有bootloader传过来的!这个函数是个重量级的函数,大家不可忽视!该函数完成体系结构相关的初始化,内核移植的过程一般也就到此函数为止了,其余的就只是一些相关的外设驱动。*/
489 mm_init_owner(&init_mm, &init_task);
490 mm_init_cpumask(&init_mm);
491 setup_command_line(command_line);/*保存未改变的comand_line到字符数组static_command_line[] 中。保存 boot_command_line到字符数组saved_command_line[]中*/
492 setup_nr_cpu_ids();
493 setup_per_cpu_areas(); /* 每个CPU分配pre-cpu结构内存并复制.data.percpu段的数据 */
494 smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
495
496 build_all_zonelists(NULL);
497 page_alloc_init();
498
499 printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
500 parse_early_param();
501 parse_args("Booting kernel", static_command_line, __start___param,
502 __stop___param - __start___param,
503 &unknown_bootoption);
504 /*
505 * These use large bootmem allocations and must precede
506 * kmem_cache_init()
507 */
508 setup_log_buf(0);
509 pidhash_init();
510 vfs_caches_init_early();
511 sort_main_extable();
512 trap_init();
513 mm_init();
514
515 /*
516 * Set up the scheduler prior starting any interrupts (such as the
517 * timer interrupt). Full topology setup happens at smp_init()
518 * time - but meanwhile we still have a functioning scheduler.
519 */
520 sched_init();
521 /*
522 * Disable preemption - early bootup scheduling is extremely
523 * fragile until we cpu_idle() for the first time.
524 */
525 preempt_disable();
526 if (!irqs_disabled()) {
527 printk(KERN_WARNING "start_kernel(): bug: interrupts were "
528 "enabled *very* early, fixing it\n");
529 local_irq_disable();
530 }
531 idr_init_cache();
532 perf_event_init();
533 rcu_init();
534 radix_tree_init();
535 /* init some links before init_ISA_irqs() */
536 early_irq_init();
537 init_IRQ();
538 prio_tree_init();
539 init_timers();
540 hrtimers_init();
541 softirq_init();
542 timekeeping_init();
543 time_init();
544 profile_init();
545 call_function_init();
546 if (!irqs_disabled())
547 printk(KERN_CRIT "start_kernel(): bug: interrupts were "
548 "enabled early\n");
549 early_boot_irqs_disabled = false;
550 local_irq_enable();
551
552 /* Interrupts are enabled now so all GFP allocations are safe. */
553 gfp_allowed_mask = __GFP_BITS_MASK;
554
555 kmem_cache_init_late();
556
557 /*
558 * HACK ALERT! This is early. We're enabling the console before
559 * we've done PCI setups etc, and console_init() must be aware of
560 * this. But we do want output early, in case something goes wrong.
561 */
562 console_init();
563 if (panic_later)
564 panic(panic_later, panic_param);
565
566 lockdep_info();
567
568 /*
569 * Need to run this when irqs are enabled, because it wants
570 * to self-test [hard/soft]-irqs on/off lock inversion bugs
571 * too:
572 */
573 locking_selftest();
574
575 #ifdef CONFIG_BLK_DEV_INITRD
576 if (initrd_start && !initrd_below_start_ok &&
577 page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
578 printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
579 "disabling it.\n",
580 page_to_pfn(virt_to_page((void *)initrd_start)),
581 min_low_pfn);
582 initrd_start = 0;
583 }
584 #endif
585 page_cgroup_init();
586 enable_debug_pagealloc();
587 debug_objects_mem_init();
588 kmemleak_init();
589 setup_per_cpu_pageset();
590 numa_policy_init();
591 if (late_time_init)
592 late_time_init();
593 sched_clock_init();
594 calibrate_delay();
595 pidmap_init();
596 anon_vma_init();
597 #ifdef CONFIG_X86
598 if (efi_enabled)
599 efi_enter_virtual_mode();
600 #endif
601 thread_info_cache_init();
602 cred_init();
603 fork_init(totalram_pages);
604 proc_caches_init();
605 buffer_init();
606 key_init();
607 security_init();
608 dbg_late_init();
609 vfs_caches_init(totalram_pages);
610 signals_init();
611 /* rootfs populating might need page-writeback */
612 page_writeback_init();
613 #ifdef CONFIG_PROC_FS
614 proc_root_init();
615 #endif
616 cgroup_init();
617 cpuset_init();
618 taskstats_init_early();
619 delayacct_init();
620
621 check_bugs();
622
623 acpi_early_init(); /* before LAPIC and SMP init */
624 sfi_init_late();
625
626 ftrace_init();
627
628 /* Do the rest non-__init'ed, we're now alive */
629 rest_init();
630 }
kernel/lockdep.c
3757 void lockdep_init(void)
3758 {
3759 int i;
3760
3761 /*
3762 * Some architectures have their own start_kernel()
3763 * code which calls lockdep_init(), while we also
3764 * call lockdep_init() from the start_kernel() itself,
3765 * and we want to initialize the hashes only once:
3766 */
3767 if (lockdep_initialized)
3768 return;
3769
3770 for (i = 0; i < CLASSHASH_SIZE; i++)
3771 INIT_LIST_HEAD(classhash_table + i);
3772
3773 for (i = 0; i < CHAINHASH_SIZE; i++)
3774 INIT_LIST_HEAD(chainhash_table + i);
3775
3776 lockdep_initialized = 1;
3777 }
该函数用于启动Lock dependency validator,本质上是建立两个散列表classhash_table和chainhash_table,并初始化全局变量lockdep_initialized。对Linux内核的链表和散列表不熟悉的同志可以看看博客“Linux内核入门(三)—— C语言基本功http://blog.csdn.net/yunsongice/article/details/5471096”的相关内容。
468行,debug_objects_early_init()函数,用于内核的对象调试。由于我们.config文件中没有配置CONFIG_DEBUG_OBJECTS,所以这个函数为空函数。
468行,调用boot_init_stack_canary()函数,同样在也.config文件中没有配置CONFIG_CC_STACKPROTECTOR,所以这个函数也为空。
473行,boot_init_stack_canary()函数,stack_canary的是带防止栈溢出攻击保护的堆栈。详见http://www.cloud-sec.org/CC_STACKPROTECTOR_patch_Analysis
475行,CONFIG_CGROUPS也没有配置,所以cgroup_init_early()函数也为空。
477行,local_irq_disable(),终于不为空了,在include/linux/irqflags.h的92行定义:
#define local_irq_disable() \
do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
59行定义
#define raw_local_irq_disable() arch_local_irq_disable()
注意,这个定义必须是CONFIG_TRACE_IRQFLAGS_SUPPORT编译选项存在于.config文件,该宏才有效。我查了查.config,是存在的,所以又定位到raw_local_irq_disable和trace_hardirqs_off两个宏。先看第一个,raw_local_irq_disable,全内核四个地方有定义,取决于一个重要的CONFIG_PARAVIRT编译选项是否被配置。我们看到.config中根本不存在这个编译选项,所以根本不用考虑arch/x86/include/asm/paravirt.h里面的内容。而剩下的定义来自哪儿,取决于宏__ASSEMBLY__。
是该讲讲__ASSEMBLY__宏了,这个宏的作用是避免gcc在进行汇编编译的时候,不定义后续相关内容。什么意思?我们看到顶层Makefile有一个:
375 KBUILD_AFLAGS := -D__ASSEMBLY__
这个宏变量实际上是在编译汇编代码的时候,由编译器使用-D这样的参数加进去的,KBUILD_AFLAGS这个变量也定义了这个变量,gcc会把这个宏定义为1。我们知道,gcc编译一个c程序大致分为三步:编译c文件生成临时汇编程序;编译汇编程序生成.o对象文件;链接对象文件生成c程序。而KBUILD_AFLAGS指定__ASSEMBLY__,是为了编译汇编程序时,不会用到类似于“# define”定义的属性(注意!不是宏定义#define,#后面有一个空格,Linux内核中定义了很多这样的定义,在编译c文件时,就相当于#define),因为这样的属性是在进行C语言编译的时候加的,通过__ASSEMBLY__来避免在C语言编译成汇编程序后,不在编译这些汇编代码时候的引用,从而避免不必要的宏或函数在编译汇编代码时候的引用。
虽然我们顶层Makefile设置了__ASSEMBLY__,但是,我们现在分析的是c语言下的代码,__ASSEMBLY__暂没定义,所以来到iarch/arm/include/asm/irqflags.h中33行:
33 static inline void arch_local_irq_disable(void)
34 {
35 asm volatile(
36 " cpsid i @ arch_local_irq_disable"
37 :
38 :
39 : "memory", "cc");
40 }
调用 cpsid取消eflags的IF标志位,屏蔽可屏蔽中断。有关可屏蔽中断、非屏蔽中断和异常的基本概念请参考博文“中断的分类”。后面由于我们没有设置CONFIG_TRACE_IRQFLAGS和CONFIG_IRQSOFF_TRACER编译选项,所以trace_hardirqs_off()函数为空,回到start_kernel中。
注册时钟事件监听器
由于我们在.config文件中设置了CONFIG_GENERIC_CLOCKEVENTS,所以,start_kernel函数的第558行调用kernel/time/tick-common.c中的tick_init函数,来注册一个时钟事件监听器。tick_init只调用一个clockevents_register_notifier函数:
407 static struct notifier_block tick_notifier = {
408 .notifier_call = tick_notify,
409 };
416 void __init tick_init(void)
417 {
418 clockevents_register_notifier(&tick_notifier);
419 }
kernel/time/clockevents.c
138 int clockevents_register_notifier(struct notifier_block *nb)
139 {
140 unsigned long flags;
141 int ret;
142
143 raw_spin_lock_irqsave(&clockevents_lock, flags);
144 ret = raw_notifier_chain_register(&clockevents_chain, nb);
145 raw_spin_unlock_irqrestore(&clockevents_lock, flags);
146
147 return ret;
148 }
这个函数很简单,只要先明白一个notifier_block结构就行了:
struct notifier_block {
int (*notifier_call)(struct notifier_block *, unsigned long, void *);
struct notifier_block *next;
int priority;
};
tick_notifier全局变量在编译vmlinux的时候其notifier_call被设置成了tick_notify函数:
kernel/time/tick-common.c
362 static int tick_notify(struct notifier_block *nb, unsigned long reason,
363 void *dev)
364 {
365 switch (reason) {
366
367 case CLOCK_EVT_NOTIFY_ADD:
368 return tick_check_new_device(dev);
369
370 case CLOCK_EVT_NOTIFY_BROADCAST_ON:
371 case CLOCK_EVT_NOTIFY_BROADCAST_OFF:
372 case CLOCK_EVT_NOTIFY_BROADCAST_FORCE:
373 tick_broadcast_on_off(reason, dev);
374 break;
375
376 case CLOCK_EVT_NOTIFY_BROADCAST_ENTER:
377 case CLOCK_EVT_NOTIFY_BROADCAST_EXIT:
378 tick_broadcast_oneshot_control(reason);
379 break;
380
381 case CLOCK_EVT_NOTIFY_CPU_DYING:
382 tick_handover_do_timer(dev);
383 break;
384
385 case CLOCK_EVT_NOTIFY_CPU_DEAD:
386 tick_shutdown_broadcast_oneshot(dev);
387 tick_shutdown_broadcast(dev);
388 tick_shutdown(dev);
389 break;
390
391 case CLOCK_EVT_NOTIFY_SUSPEND:
392 tick_suspend();
393 tick_suspend_broadcast();
394 break;
395
396 case CLOCK_EVT_NOTIFY_RESUME:
397 tick_resume();
398 break;
399
400 default:
401 break;
402 }
403
404 return NOTIFY_OK;
405 }
这上面一串函数是什么意思?这里要学一个新知识——通知链技术。我们知道内核一些驱动或文件系统是被编译成模块了的,当某个模块的某些事件对其模块是很有用的,则本模块应该定义一个通知链,并导出接口。那么其它模块对其事件感兴趣时,可以调用这些接口注册(注销)当事件发生时的行为。而本模块在事件发生时,调用通知其链上的所有订阅者(调用其函数)。而这个notifier_block就是通知链中的元素,记录了当发出通知时,应该执行的操作(即回调函数)。这里tick_notifier是一个时钟事件监听器模块,当发出通知是,就执行tick_notify函数。所以,clockevents_register_notifier函数就调用raw_notifier_chain_register来注册,其实就是把三步:获得自旋锁、把tick_notifier加入clockevents_chain、释放自旋锁。
激活第一个CPU
回到start_kernel,485行,boot_cpu_init函数,跟start_kernel位于同一文件:
421 static void __init boot_cpu_init(void)
422 {
423 int cpu = smp_processor_id();
424 /* Mark the boot cpu "present", "online" etc for SMP and UP case */
425 set_cpu_online(cpu, true);
426 set_cpu_active(cpu, true);
427 set_cpu_present(cpu, true);
428 set_cpu_possible(cpu, true);
429 }
423行,第一次见到smp_processor_id宏。由于没有配置CONFIG_DEBUG_PREEMPT,所以其等价于调用宏raw_smp_processor_id,其意义在于SMP的情况下,获得当前CPU的ID。如果不是SMP,那么就返回0。那么在CONFIG_X86_32_SMP的情况下:
#define raw_smp_processor_id() (percpu_read(cpu_number))
千万要注意,这里cpu_number来自arch/x86/kernel/setup_percpu.c的30行:
30DEFINE_PER_CPU(int, cpu_number);
31EXPORT_PER_CPU_SYMBOL(cpu_number);
这个东西不像是c语言全局变量,而是通过两个宏来定义的。要读懂这两个宏,必须对每CPU变量这个概念非常了解,如果还不是很清楚的同学请查阅一下博客“每CPU变量”http://blog.csdn.net/yunsongice/archive/2010/05/18/5605239.aspx。
其中DEFINE_PER_CPU来自文件include/linux/percpu-defs.h:
#define DEFINE_PER_CPU(type, name) /
DEFINE_PER_CPU_SECTION(type, name, "")
该宏静态分配一个每CPU数组,数组名为name,结构类型为type。由于我们没有设置CONFIG_DEBUG_FORCE_WEAK_PER_CPU编译选项,所以DEFINE_PER_CPU_SECTION又被定义为:
#define DEFINE_PER_CPU_SECTION(type, name, sec) /
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES /
__typeof__(type) name
其中,__PCPU_ATTRS(sec)在include/linux/percpu-defs.h中定义:
#define __PCPU_ATTRS(sec) /
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) /
PER_CPU_ATTRIBUTES
__percpu是个编译扩展类型,大家可以去看看include/linux/compile.h这个文件,里面的__percpu是空的。而传进来的sec也是空的,PER_CPU_ATTRIBUTES也是空的,而上面PER_CPU_DEF_ATTRIBUTES还是空代码,可能都是留给将来内核代码扩展之用的吧,所以DEFINE_PER_CPU(int, cpu_number)展开就是:
__attribute__((section(PER_CPU_BASE_SECTION))) /
__typeof__(int) cpu_number
所以现在只关注PER_CPU_BASE_SECTION,来自include/asm-generic/percpu.h
#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data.percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif
好了,介绍一下GCC的一些扩展,首先如何使用typeof来支持一些“genericprogramming”。gcc支持一种叫做类型识别的技术,通过typeof(x)关键字,获得x的数据类型。而如果是在一个要被一些c文件包含的头文件中获得变量的数据类型,就需要用__typeof__而不是typeof关键字了,比如说我们这里。最后,这里就是声明一个int类型的cpu_number指针,编译的时候把他指向.data.percpu段的开始位置。
当然,我们自己写程序的时候没有这么地generic programming,所以没必要这么做,不过想要成为一个内核达人,掌握这些技术还是很有必要的。DEFINE_PER_CPU_SECTION宏又是什么意思呢?定义在arch/x86/kernel/percpu-defs.h的147行:
#define EXPORT_PER_CPU_SYMBOL(var) EXPORT_SYMBOL(var)
EXPORT_SYMBOL是什么东西啊?还记得我们在编译内核时有个kallsyms目标么?这里就用到了。所以我说过,内核的编译过程很重要,请大家务必掌握!这个对象对应于“/proc/kallsyms”文件,该文件对应着内核符号表,记录了符号以及符号所在的内存地址。模块可以使用如下宏导出符号到内核符号表:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名)
导出的符号可以被其他模块使用,不过使用之前一定要声明一下。EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块。所以,EXPORT_SYMBOL(cpu_number)就是把cpu_number变量声明出去。接下来重点介绍一下percpu_read这个宏,来自arch/x86/include/asm/percpu.h:
#define percpu_read(var) percpu_from_op("mov", var, "m" (var))
看到mov,我们就知道可能要调用汇编语言了,所以这个宏调用percpu_from_op("mov", cpu_number, "m" (cpu_number)),这个宏位于同一个文件:
#define percpu_from_op(op, var, constraint) \
({ \
typeof(var) pfo_ret__; \
switch (sizeof(var)) { \
case 1: \
asm(op "b "__percpu_arg(1)",%0" \
: "=q" (pfo_ret__) \
: constraint); \
break; \
case 2: \
asm(op "w "__percpu_arg(1)",%0" \
: "=r" (pfo_ret__) \
: constraint); \
break; \
case 4: \
asm(op "l "__percpu_arg(1)",%0" \
: "=r" (pfo_ret__) \
: constraint); \
break; \
case 8: \
asm(op "q "__percpu_arg(1)",%0" \
: "=r" (pfo_ret__) \
: constraint); \
break; \
default: __bad_percpu_size(); \
} \
pfo_ret__; \
})
当然,我们为了获得CPU号,cpu_number用一个字节就够了,所以上面代码翻译过来就是:
asm("movb "__percpu_arg(1)",%0" /
: "=q" (pfo_ret__) /
: constraint);
其中,__percpu_arg(1)是这样定义的:
#ifdef CONFIG_SMP
#define __percpu_arg(x) "%%"__stringify(__percpu_seg)":%P" #x
#ifdef CONFIG_X86_64
#define __percpu_seg gs
#define __percpu_mov_op movq
#else
#define __percpu_seg fs
#define __percpu_mov_op movl
#endif
所以__percpu_arg(x)翻译过来就是:
"%%"__stringify(fs)":%P" #x
请注意,这是大家第一次遇到这样的定义方式,跟我们传统的C语言宏不一样了。不错,这里要学习新知识了,首先讲讲关于#和##的知识。
在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。比如__percpu_arg宏:
#define __percpu_arg(x) "%%"__stringify(__percpu_seg)":%P" #x
而x,我们传入的是1;__percpu_seg,我们传入的是fs,所以__percpu_arg展开:
%%"__stringify(fs)":%P" "1"
而##,虽然这里没有用到,但是我还是一并讲讲,供大家扩充c语言知识。##被称为连接符(concatenator),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。比如:
#define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d
typedef struct record_type LINK_MULTIPLE(name,company,position,salary);
这里这个语句将展开为:
typedef struct record_type name_company_position_salary;
再来一个新知识,可变参数宏,于1999年的ISO C标准中定义,也就十年前而已。我们看到这里用到了__stringify宏,其定义是这样的:
#define __stringify(x...) __stringify_1(x)
其中…为可变参数,什么意思?就是这个宏除x以外,还可以有额外的多个参数。另外,如果是可变参数宏,那么可变参数x可能会展开为多个参数,那么比如上面也可定义为:
#define __stringify(...) my__stringify(y,z) 0
不过如果指定了参数名x,则后者必须包含一个x:
#define __stringify(x...) my__stringify(x,y,z)
那么我要问了,如果我定义成上面三个参数的形式,但是给的却又只有两个参数怎么办?比如执行一个__stringify(a,b)语句,会不会出错?当然会!因为既然是可变参数的宏,那么传递进去一个空参数也是对的啊,只不过你得有多余的逗号啊,也就是__stringify(,a,b)这样。
不过GCC还有一个东西可以避免这样的“bug”,那就是##符号。如果我们定义成:
#define __stringify(x...) my__stringify(##x,y,z)
这时,##这个连接符号充当的作用就是当x为空的时候,消除前面的那个逗号,这样就不会有上面的bug了。
所以宏:
#define __stringify_1(x...) #x
我们就可以看懂了吧,
所以__percpu_arg(1)最终翻译过来就是:
"%%" "fs:%P" "1"
因此上边的汇编代码翻译过来就是:
asm("movb %%fs:%P1, %0" /
: "=q" (pfo_ret__) /
: "m" (var));
其中pfo_ret__是输出部%0,q,表示寄存器eax、ebx、ecx或edx中的一个,并且变量pfo_ret__存放在这个寄存器中。var就是刚才我们建立的那个临时的汇编变量cpu_number,作为输入部%1。
还记得“加载全局/中断描述符表”中把__KERNEL_PERCPU段选择子赋给了fs了吗,不错,fs: cpu_number就获得了当前存放在__KERNEL_PERCPU段中cpu_number偏移的内存中,最后把结果返回给pfo_ret__。所以这个宏最后的结果就是pfo_ret__的值,其返回的是CPU的编号,把它赋给boot_cpu_init函数的内部变量cpu。
那么这个偏移cpu_number到底是多少呢?众里寻他千百度,猛回首,这个偏移在生成的vmlinux.lds文件的504行被我发现了:
__per_cpu_load = .; .data.percpu 0
不错,刚才.data.percpu处被编译成0,所以内部cpu的值就是0。我为什么把这一部分讲得这么详细呢,因为这部分内容包含了很多内核代码的细节,以后遇到同样的问题我们就照这样的方法来分析,举一反三。随后的四个函数set_cpu_online、set_cpu_active、set_cpu_present和set_cpu_possible我不想多说了,就是激活当前CPU的cpu_present_bits中四个标志位online、active、present和possible,感兴趣的同学可以通过上面学到的方法去详细分析。
初始化地址散列表
486行,page_address_init()函数,来自mm/highmem.c:
418 void __init page_address_init(void)
419 {
420 int i;
421
422 INIT_LIST_HEAD(&page_address_pool);
423 for (i = 0; i < ARRAY_SIZE(page_address_maps); i++)
424 list_add(&page_address_maps[i].list, &page_address_pool);
425 for (i = 0; i < ARRAY_SIZE(page_address_htable); i++) {
426 INIT_LIST_HEAD(&page_address_htable[i].lh);
427 spin_lock_init(&page_address_htable[i].lock);
428 }
429 spin_lock_init(&pool_lock);
430 }
这个函数很简单,首先422初始化一个链表page_address_pool,这个链表在编译的时候被定义成一个全局变量在同一个文件的318行:
318 static struct list_head page_address_pool;
423行,page_address_maps也是个全局变量,来自同一个文件:
416 static struct page_address_map page_address_maps[LAST_PKMAP];
其每一个元素是一个page_address_map结构:
struct page_address_map {
struct page *page;
void *virtual;
struct list_head list;
};
OK,第一次见到我们熟悉的page数据结构了。一个page代表一个物理页面,想要深入了解这方面知识的请访问博客“Linux页框管理”http://blog.csdn.net/yunsongice/archive/2010/01/22/5224494.aspx。
ARRAY_SIZE获得数组的元素个数,这里肯定就是LAST_PKMAP了,由于我们没有启动PAE,这个值等于1024,也就是说page_address_maps共有1024个元素。随后424行,1024个循环以后,page_address_pool通过每个page_address_maps的元素的list字段将他们全部链接起来形成个双向循环链表,又page_address_pool作为head。
425行,page_address_htable,也定义于同一文件:
324 static struct page_address_slot {
325 struct list_head lh; /* List of page_address_maps */
326 spinlock_t lock; /* Protect this bucket's list */
327 } ____cacheline_aligned_in_smp page_address_htable[1<<pa_hash_order];
____cacheline_aligned_in_smp是一个编译优化选项,用于SMP方式的缓存优化。PA_HASH_ORDER被定义为7,所以2^7=128。所以ARRAY_SIZE(page_address_htable)为128,那么经过128个循环以后,每个page_address_htable元素的lh都被初始化成一个链表头了,而对于的互斥锁lock也通过spin_lock_init函数进行初始化了。具体的初始化过程请查阅博客“Linux内核入门(三)—— C语言基本功”以及“自旋锁”。
http://blog.csdn.net/yunsongice/article/details/5471096 http://blog.csdn.net/yunsongice/article/details/5605264
最后page_address_pool的自旋锁pool_lock初始化之后,函数就结束了。简单归简单,但是大家必须知道这个函数的意义。我们知道一个page结构代表一个4k的页面,我们内核初始化这么一个全局的page_address_maps和page_address_htable这么两个结构,作为页面的地址映射。
打印版本信息
487行,执行一个printk函数。start_kernel()开始执行至此,会显示“Linux version 3.0…”信息。
我们来简单的看看在内核中,在屏幕上打印一条信息的原理是怎么实现的。由于我们配置了CONFIG_PRINTK编译选项,所以调用位于kernel/printk.c中的printk函数:
linkage int printk(const char *fmt, ...)
{
va_list args;
int r;
#ifdef CONFIG_KGDB_KDB
if (unlikely(kdb_trap_printk)) {
va_start(args, fmt);
r = vkdb_printf(fmt, args);
va_end(args);
return r;
}
#endif
va_start(args, fmt);
r = vprintk(fmt, args);
va_end(args);
return r;
}
又看到了“…”,不错,跟刚才讲的可变参数宏差不多,只不过这里是可变参函数。那么start_kernel中调用的printk(KERN_NOTICE "%s", linux_banner),其中linux_banner是个全局字符串常量,定义在init/version.c:
const char linux_banner[] =
"Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "/n";
上面那些大写的东西都在我们顶层Makefile中定义过了,就是一些版本信息,整个函数的功能没有问题,我们主要关注本质。所以来看看va_start、vprintk和va_end是怎么协作的。
首先,va_list并不是一个什么复杂的数据结构,而是:
typedef char *va_list;
仅仅就是一个char*。所以,根据va_start的定义:
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T) /
(*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
这就是一个可变参数的调整。调整好以后,我们就可以使用它了。
826 asmlinkage int vprintk(const char *fmt, va_list args)
827 {
828 int printed_len = 0;
829 int current_log_level = default_message_loglevel;
830 unsigned long flags;
831 int this_cpu;
832 char *p;
833 size_t plen;
834 char special;
835
836 boot_delay_msec();
837 printk_delay();
838
839 preempt_disable();
840 /* This stops the holder of console_sem just where we want him */
841 raw_local_irq_save(flags);
842 this_cpu = smp_processor_id();
843
844 /*
845 * Ouch, printk recursed into itself!
846 */
847 if (unlikely(printk_cpu == this_cpu)) {
848 /*
849 * If a crash is occurring during printk() on this CPU,
850 * then try to get the crash message out but make sure
851 * we can't deadlock. Otherwise just return to avoid the
852 * recursion and return - but flag the recursion so that
853 * it can be printed at the next appropriate moment:
854 */
855 if (!oops_in_progress) {
856 recursion_bug = 1;
857 goto out_restore_irqs;
858 }
859 zap_locks();
860 }
861
862 lockdep_off();
863 spin_lock(&logbuf_lock);
864 printk_cpu = this_cpu;
865
866 if (recursion_bug) {
867 recursion_bug = 0;
868 strcpy(printk_buf, recursion_bug_msg);
869 printed_len = strlen(recursion_bug_msg);
870 }
871 /* Emit the output into the temporary buffer */
872 printed_len += vscnprintf(printk_buf + printed_len,
873 sizeof(printk_buf) - printed_len, fmt, args);
874
875 p = printk_buf;
876
877 /* Read log level and handle special printk prefix */
878 plen = log_prefix(p, ¤t_log_level, &special);
879 if (plen) {
880 p += plen;
881
882 switch (special) {
883 case 'c': /* Strip KERN_CONT, continue line */
884 plen = 0;
885 break;
886 case 'd': /* Strip KERN_DEFAULT, start new line */
887 plen = 0;
888 default:
889 if (!new_text_line) {
890 emit_log_char('\n');
891 new_text_line = 1;
892 }
893 }
894 }
895
896 /*
897 * Copy the output into log_buf. If the caller didn't provide
898 * the appropriate log prefix, we insert them here
899 */
900 for (; *p; p++) {
901 if (new_text_line) {
902 new_text_line = 0;
903
904 if (plen) {
905 /* Copy original log prefix */
906 int i;
907
908 for (i = 0; i < plen; i++)
909 emit_log_char(printk_buf[i]);
910 printed_len += plen;
911 } else {
912 /* Add log prefix */
913 emit_log_char('<');
914 emit_log_char(current_log_level + '0');
915 emit_log_char('>');
916 printed_len += 3;
917 }
918
919 if (printk_time) {
920 /* Add the current time stamp */
921 char tbuf[50], *tp;
922 unsigned tlen;
923 unsigned long long t;
924 unsigned long nanosec_rem;
925
926 t = cpu_clock(printk_cpu);
927 nanosec_rem = do_div(t, 1000000000);
928 tlen = sprintf(tbuf, "[%5lu.%06lu] ",
929 (unsigned long) t,
930 nanosec_rem / 1000);
931
932 for (tp = tbuf; tp < tbuf + tlen; tp++)
933 emit_log_char(*tp);
934 printed_len += tlen;
935 }
936
937 if (!*p)
938 break;
939 }
940
941 emit_log_char(*p);
942 if (*p == '\n')
943 new_text_line = 1;
944 }
945
946 /*
947 * Try to acquire and then immediately release the
948 * console semaphore. The release will do all the
949 * actual magic (print out buffers, wake up klogd,
950 * etc).
951 *
952 * The console_trylock_for_printk() function
953 * will release 'logbuf_lock' regardless of whether it
954 * actually gets the semaphore or not.
955 */
956 if (console_trylock_for_printk(this_cpu))
957 console_unlock();
958
959 lockdep_on();
960 out_restore_irqs:
961 raw_local_irq_restore(flags);
962
963 preempt_enable();
964 return printed_len;
965 }
966 EXPORT_SYMBOL(printk);
967 EXPORT_SYMBOL(vprintk);
vprintk是个大家伙!该函数负责解析并组成打印格式。836~872行是几个必要的操作,这几个函数都是跟同步互斥以及调试相关。872行,调用vscnprintf将打印信息拷贝到printk_buf缓存中。比如printk("%d, %s", 5, "hello"),printk_buf中将是5,hello格式化后的数据不带'/0'。
随后,p指向这个缓存,存放着linux_banner对应的字符串。878,879行,根据linux_banner的第一个字符“<”,识别log级别,否则直接到900行进入一个循环。909行emit_log_char函数是将打印信息已经保存到log_buf中,这个log_buf有什么用,待会再说。
919行,printk_time,由于没有定义CONFIG_PRINTK_TIME宏 printk_time = 0,所以忽略919到935行代码。好了,941行,打印信息已经保存到log_buf中了,最后957行调用console_unlock()函数启动console输出。
注意,我们这里分析的printk和printf不同。回顾一下,他首先输出到系统的一个缓冲区内,大约4k,如果登记了console,则调用console->wirte函数输出,否则就一直在buffer里呆着。所以,用printk输出的信息,如果超出了4k,会冲掉前面的。在系统引导起来后,用dmesg看的也就是这个buffer中的东西。
此时的信息还在buf中躺着呢,因为console还没有初始化。
static size_t log_prefix(const char *p, unsigned int *level, char *special)
{
unsigned int lev = 0;
char sp = '\0';
size_t len;
if (p[0] != '<' || !p[1])
return 0;
if (p[2] == '>') {
/* usual single digit level number or special char */
switch (p[1]) {
case '0' ... '7':
lev = p[1] - '0';
break;
case 'c': /* KERN_CONT */
case 'd': /* KERN_DEFAULT */
sp = p[1];
break;
default:
return 0;
}
len = 3;
} else {
/* multi digit including the level and facility number */
char *endp = NULL;
if (p[1] < '0' && p[1] > '9')
return 0;
lev = (simple_strtoul(&p[1], &endp, 10) & 7);
if (endp == NULL || endp[0] != '>')
return 0;
len = (endp + 1) - p;
}
/* do not accept special char if not asked for */
if (sp && !special)
return 0;
if (special) {
*special = sp;
/* return special char, do not touch level */
if (sp)
return len;
}
if (level)
*level = lev;
return len;
}
执行setup_arch()函数
回到start_kernel当中,488行,调用setup_arch函数,传给他的参数是那个未被初始化的内部变量command_line。这个setup_arch()函数是start_kernel阶段最重要的一个函数,每个体系都有自己的setup_arch()函数,是体系结构相关的,具体编译哪个体系的setup_arch()函数,由顶层Makefile中的ARCH变量决定:
arch/arm/kernel/setup.c
877 void __init setup_arch(char **cmdline_p)
878 {
879 struct machine_desc *mdesc;
880
881 unwind_init();
882
883 setup_processor();
884 mdesc = setup_machine_fdt(__atags_pointer);
885 if (!mdesc)
886 mdesc = setup_machine_tags(machine_arch_type);
887 machine_desc = mdesc;
888 machine_name = mdesc->name;
889
890 if (mdesc->soft_reboot)
891 reboot_setup("s");
892
893 init_mm.start_code = (unsigned long) _text;
894 init_mm.end_code = (unsigned long) _etext;
895 init_mm.end_data = (unsigned long) _edata;
896 init_mm.brk = (unsigned long) _end;
897
898 /* populate cmd_line too for later use, preserving boot_command_line */
899 strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
900 *cmdline_p = cmd_line;
901
902 parse_early_param();
903
904 sanity_check_meminfo();
905 arm_memblock_init(&meminfo, mdesc);
906
907 paging_init(mdesc);
908 request_standard_resources(mdesc);
909
910 unflatten_device_tree();
911
912 #ifdef CONFIG_SMP
913 if (is_smp())
914 smp_init_cpus();
915 #endif
916 reserve_crashkernel();
917
918 cpu_init();
919 tcm_init();
920
921 #ifdef CONFIG_MULTI_IRQ_HANDLER
922 handle_arch_irq = mdesc->handle_irq;
923 #endif
924
925 #ifdef CONFIG_VT
926 #if defined(CONFIG_VGA_CONSOLE)
927 conswitchp = &vga_con;
928 #elif defined(CONFIG_DUMMY_CONSOLE)
929 conswitchp = &dummy_con;
930 #endif
931 #endif
932 early_trap_init();
933
934 if (mdesc->init_early)
935 mdesc->init_early();
936 }