linux内核启动第二阶段分析

时间:2021-09-09 16:46:04
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 }