嵌入式linux基础教程第二版 第五章 内核初始化

时间:2021-09-14 18:46:51

                                                                                            第五章      内核初始化

        5.1合成内核镜像:Piggy 及其他

        make ARCH=arm CROSS_COMPILE=xcale_be- zImage

              LD                 vmlinux

              SYSMAP       System.map

              SYSMAP       .tmp_System.map

              OBJCOPY     arch/arm/boot/Image

              Kernel:           arch/arm/boot/Image is ready

              AS                  arch/arm/boot/compressed/head.o

              GZIP               arch/arm/boot/compressed/piggy.gz

              AS                   arch/arm/boot/compressed/piggy.o

              CC                  arch/arm/boot/compressed/misc.o

              AS                   arch/arm/boot/compressed/head-xscale.o

              AS                   arch/arm/boot/compressed/big-endian.o

              LD                   arch/arm/boot/compressed/vmlinux

              OBJCOPY      arch/arm/boot/zImage

              Kernel:            arch/arm/boot/zImage is ready

        构建系统产生了vmlinux镜像。之后,构建系统处理了很多其他对象模块。其中,包括head.o、piggy.o以及与具体架构相关的head-xscale.o等。AS 表示构建系统掉用了汇编器,GZIP表明在进行压缩。一般来说,这些对象模块都是和具体架构相关的,并且包含了一些底层函数。用于在特定架构上引导内核。

嵌入式linux基础教程第二版 第五章 内核初始化


上图为合成内核镜像的结构

        Image对象:当内核ELF文件构建成功之后,内核构建系统继续处理其他的目标。Image对象是由vmlinux对象生成的。去掉ELF文件中的冗余段,并去掉所有可能存在的调试符号,就是Image了。下面这条命令用于该用途

        xscale_be-objcopy -O binary -R .note -R .note.gnu.build-id -R .comment -S vmlinux arch/arm/boot/Image

        -O选项指示objcopy生成一个二进制文件,-R删除ELF文件中的.note、.note.gnu.build.id和.comment这三个段。-S选项用于去除调试符号。objcopy以ELF的镜像vmlinux为输入,生成名为Image的目标二进制文件。Image将vmlinux从ELF转换成二进制形式,并去除了调试信息和前面的.note*和,comment段

       与具体架构相关的对象:由几个汇编源文件编译的对象(head.o和head-xscale.o),他们完成与底层具体架构及处理器相关的一些任务。创建piggy.o对象,用gzip命令对Image文件进行压缩生成piggy.o。

           cat  Image   |    gzip  -f  -9 > piggy.gz

       汇编器汇编名为piggy.S的汇编语言文件,而这个文件包含了一个对压缩文件piggy.gz的引用。从本质上说,二进制内核镜像以负载的形式依附在了一个底层的启动加载程序之上,采用汇编语言编写。启动加载程序先初始化处理器和必须的内存区域,然后解压二进制内核镜像(piggy.gz)并将解压后的内核镜像(Image)加载到系统内存的合适位置,最后将控制权转交给它。

       piggy.s

       .section    .piggydata,  #alloc

       .global      input_data

   input_data

       .incbin   "arch/arm/boot/compressed/piggy.gz"

       .global      end_data

   input_data_end:

       汇编器汇编这个文件并生成一个ELF格式的镜像piggy.o,该镜像包含一个名为.piggydata的段,这个文件的作用是将压缩后的二进制内核镜像(piggy.gz)放到这个段中,成为其内容。该文件通过汇编器的预处理指令.incbin将piggy.gz包含进来,incbin类似于include只是它包含的是二进制数据。总之,该汇编文件的作用是将压缩的二进制内核镜像(piggy.gz)放在另一个镜像(piggy.o)中。启动加载程序利用input_data和input_data_end来确定包含的内核镜像的边界

       启动加载程序:启动加载程序将linux内核镜像加载到内存中。有些启动加载程序会对内核镜像进行校验和检查,而大多数启动加载程序会解压并重新部署内核镜像。当硬件单板加电时,引导加载程序获得其控制权,根本不依赖于内核。引导加载程序将内核镜像从工作站加载到开发板的物理内存上而启动加载程序将内核镜像从开发板的物理存储加载到内存中。启动加载程序负责提供合适的上下文让内核运行于其中,并且执行必要的步骤以解压和重新部署内核二进制镜像。

       启动加载程序和内核镜像拼接在一起用于加载

        嵌入式linux基础教程第二版 第五章 内核初始化

       针对ARM XScale的合成内核镜像

       在我们研究的这个例子中,启动加载程序包含了上图显示的二进制镜像,这个启动加载程序完成以下功能

       (1)底层的用汇编语言编实现的处理器初始化,这包括支持处理器内部指令和数据缓存、禁止中断并建立C语言运行环境。这部分功能由head.o和head-xscale.o完成

       (2)解压并重新部署镜像,这部分功能由misc.o完成

       (3)其他与处理器相关的初始化,比如big-endian.o,将特定处理器的字节设置为大端字节序

        引导消息:

        在PC上引导某种linux发行版,在PC自身的BIOS消息之后,你会看到很多由linux输出控制台消息,表明它正在初始化各个内核子系统。在嵌入式系统中启动linux时的情况与PC工作站类似。

         Using  base  address  0x01000000  and  length  0x001ce114

         Uncompressing  Linux.....done,  booting  the  kernel

         Linux  version  2.6.32-07500-g8bea867 (chris@brutus2)  (gcc  version  4.2.0  20070126(prerelease)(MontaVista  4.2.0-3.0.0.0702771  2007-03-10))  #12  Wed  Dec  16  23:07:01  EST  2009

          .

          .

          .

          .

       第一行是由板卡上的引导加载程序Redboot产生的,第二行是启动加载程序产生的。这行消息由.../arch/arm/boot/compressed/misc.c中的函数decompress_kernel()产生的。第三行是内核版本字符串这是内核本身输出的第一行消息,内核进入函数start_kernel()(这个函数在源文件.../init/main.c中)之后,首先执行的几行代码中包括下面这行

         printk(KERN_NOTICE:"%s",linux_banner);

       这些内核版本字符串包括了内核版本、内核编译时侯所使用的用户名/机器名、工具连信息、构建号、编译内核镜像时的日期和时间

       产品开发人员一般使用构建号来自动跟踪构建轨迹。构建号保存在.version的隐藏文件中,这个文件位于内核源码的顶层目录,构建号会由构建脚本.../scripts/mkversion自动构建。它是一个数字的字符串标签,当内核代码有重大变化并重新编译时自动递增。

        5.2初始化时的控制流

       我们来研究一个完整的启动周期(从引导加载程序到内核)中的控制流。引导加载程序是一种底层软件,存储在系统的非易失性内存(闪存或ROM)中,系统加电时他立即获得控制权。它通常体积很小,包含一些简单函数,主要用于底层初始化、操作系统镜像的加载和系统诊断。它可能会包含读写内存的函数,以检查和修改内存的内容。它包含底层的板卡自检程序,包括检测内存和I/O设备。引导加载程序还包含了一些处理逻辑,用于将控制权转交给另一个程序,一般是操作系统,比如linux。

        基于ARM  XScale平台;名为Redboot的引导加载程序,当系统第一次加电时这个引导加载程序开始执行,然后会加载操作系统。当引导加载程序部署并加载了操作系统镜像(这个镜像可能存储在本地的闪存,硬盘驱动器中,或通过局域网或其他设备)之后,就将控制权转交给那个镜像。

        对于特定的ARM  XScale平台,引导加载程序将控制权转交给启动加载程序(第二阶段引导装入程序)的head.o模块,

       嵌入式linux基础教程第二版 第五章 内核初始化

     

       处于内核镜像之前的启动加载程序有个很重要的任务:创建合适的环境,解压并重新部署内核镜像,并将控制权转交给它。启动加载程序将控制权转交给内核主体中的一个模块,通常这个模块的名字都是head.o。

       当启动加载程序完成它的工作后,将控制权转交给内核主体的head.o,之后再转到文件main.c中的函数start_kernel()

       内核入口:head.o

       内核开发人员的目的是让head.o这个与架构相关的模块通用化,不依赖于任何机器类型。这个模块由head.S生成,它的具体路径是.../arch/<ARCH>/kernel/head.s,<ARCH>为具体的架构。

       head.o模块完成与架构和CPU相关的初始化,为内核主体的执行做好准备。与CPU相关的初始化工作尽可能的做到了在同系列处理器中通用。与机器相关的初始化工作是在别处完成的。head.o还要执行下列底层任务。

      (1)检查处理器和架构的有效性

      (2)创建初始的页表表项

      (3)启用处理器的内存管理单元(MMU)

      (4)进行错误检测并报告

      (5)调转到内核主体的执行位置,也就是文件main.c中的函数start_kernel()

       嵌入式新手企图单步调试这些代码,但最终发现调试器并不能派上用场。当启动加载程序第一次将控制权交给内核的head.o时,处理器运行于我们过去常说的实地址模式。处理器的程序计数器或其他类似寄存器中所包含的值成为逻辑地址,而处理器内存地址引脚上的电信号地址成为物理地址。在实地址模式下,这两者相等。为了启用内存地址转换,需要先初始化相关的寄存器和内核数据结构,当这些初始化完成后,就会开启处理器的MMU。在MMU开启的一瞬间,处理器看到的地址空间被替换成了一个虚拟地址空间,而这个空间的结构和形式由内核开发者决定。当MMU功能开启的一瞬间物理地址被替换成了逻辑地址,这就是为什么不能像调试普通代码那样调试这段代码。

       在内核引导过程的早期阶段,地址的映射范围有限。很多开发者试图修改head.o以适应特定平台,但因为这个限制而犯错。假设你有一个硬件设备,你需要在系统引导的早期阶段加载一个固件。一种方法是将必须的固件静态编译到内核镜像中,然后使用一个指针引用它,并将他下载到你的设备中。然而由于在内核引导的早期阶段,其地址映射存在限制,很有可能固件镜像所处的位置超出了这个范围,当代吗执行时,它产生一个页面错误,因为这时你想访问一个内存区域,但处理器内部还没有建立起对这块区域的有效映射。在早期阶段页面错误处理程序还没有安装到位,所以最终结果会莫名其妙的系统崩溃。在系统引导早期阶段有一点非常确定,不会有任何错误消息能够帮你找到问题所在。

       明智的做法是尽可能推迟所有硬件的初始化工作,直到内核完成引导之后。用这种方式,你可以使用众所周知的设备驱动程序模型来访问定制硬件,而不是去修改复杂很多的汇编语言代码。这一层及的代码使用了很多技巧,而他们都没有说明文档可供参考。这方面最常见的一个例子就是解决硬件上的一些错误,而这些错误可能有说明文档也可能没有。如果你必须修改语言早期启动代码,你需要开发时间、费用和复杂度等方面付出更高的代价。硬件和软件开发工程师需要在硬件开发早期阶段讨论这些问题。此时往往硬件设计的一个小小改变可以大大减少软件开发时间。

       嵌入式开发人员必须对虚拟内存环境熟悉。

       


       内核启动:main.c

       内核自身的head.o模块完成的最后一个任务就是将控制权交给一个由C语言编写的,负责内核启动的源文件。我们后续主要介绍这个文件,

       控制权从内核的第一个对象模块(head.o)转交至C语言函数start_kernel( ),这个函数位于文件.../init/main.c中,内核从这里开始了它新的生命旅程。任何想要深入学习linux内核的人都要仔细研究main.c文件,研究它由哪些部分组成的以及这些成员是如何初始化和是实例化的。汇编语言之后的大部分linux内核启动工作是由main.c来完成的,从初始化第一个内核线程开始,直到挂载根文件系统并执行最初的用户空间linux应用程序。

       函数start_kernel()目前是main.c中最大的一个函数。大多数linux内核初始化工作都是在这个函数中完成的。此处的目的是要突出那些在嵌入式系统开发环境中由用的部分,再说一遍如果你想更系统的理解linux内核,花时间研究以下main.c是个很好的方法。

      架构设置

       .../init/main.c中的函数start_kernel()在其执行的开始阶段会调用setup_arch(),而这个函数是在文件.../arch/arm/kernel/setup.c中定义的。该函数接受一个参数——一个指向内核命令行的指针
               setup_arch(&command_line);

       该语句调用一个与具体架构相关的设置函数,linux支持的每种架构都提供了这个函数,它负责完成那些对某种具体架构通用的初始化工作。函数setup_arch()会调用其他具体识别CPU的函数,并提供了一种机制,用于调用高层特定CPU的初始化函数。例如setup_arch()直接调用setup_processor(),它位于.../arch/arm/kernel/setup.c中。这个函数会验证CPU的ID和版本,并调用特定CPU的初始化函数,同时会在系统引导时向控制台打印几行相关信息。

       4 CPU:XScale-IXP42x  Family  [690541c1]  version  1  (ARMv5TE),  cr = 000039ff

       5 CPU:  VIVT  data  cache,  VIVT instruction  cache

       6 MACHINE: ADI  Engineering  Coyote

       在这里你可以看到CPU类型、ID字符串和版本,这些信息都是从处理器核心直接读取的。接着是处理器缓存和机器类型的详细信息。

       架构设置的最后的工作中有一项是完成那些依赖于机器类型的初始化。不同架构采用的机制有所不同。对于ARM架构,可以在.../arch/arm/mach-*系列目录中找到与具体机器相关的初始化代码文件,具体的文件取决于具体的机器类型。

       内核命令行的处理

       在设置了架构之后,main.c开始执行一些通用的早期的初始化工作,并显示内核命令行。

       linux一般由一个引导加载程序(或启动加载程序)启动的,它会向内核传递一系列参数,这些参数称为内核命令行。虽然你不会真正的使用shell命令来启动内核,但很多引导加载程序都可以使用这种大家熟悉的方式向内核传递参数。在有些平台上,引导加载程序不能识别Linux,这时可以在编译的时候定义内核命令行,并硬编码到内核的二进制镜像中。在其他平台上,用户可以修改命令行的内容,而不需要重新编译内核。这时,bootsrap loader从一个配置文件生成内核命令行,并在系统引导时将它传递给内核。这些内核命令行参数相当于一种引导机制用于设置一些必须的初始设置,以正确引导特定的机器。

       linux内核中定义了大量的命令行参数。内核源码的.../Documentation子目录中有一个名为kernel-parameters.txt的文件,其中按字典顺序列出了所有的内核命令行参数。内核的变化远远快于它的文档,使用这个文档作为指南而不是权威参考。虽然这个文件中记录了上百个不同的内核命令行参数,但这个列表不一定完整,因此,你必须参考源代码。

       内核命令行的参数使用语法很简单,

               Kernel  command  line:  console=ttyS0, 115200, root = /dev/nfs ip = dhcp

       可以是单个单词,一个建/值对,或是key = value1, value2...这种一个键和多个值的形式。命令行是全局可访问的,并且可以由很多模块处理。我么前面说的,main.c中的start_kernel()函数在掉用函数setup_arch()时会传入内核命令行作为参数,这也是唯一的参数。通过这个调用,与架构相关的参数和配置就被传递给了那些与架构和机器代码相关的代码。

       设备驱动程序的编写者和内核开发人员都可以添加额外的内核命令行参数,以满足自身的具体需求。遗憾的是,在使用和处理命令行的过程中会涉及一些复杂的内容。首先,原来的机制已弃用,取而代之的是一个更加健壮的实现机制。另外,为了完全理解这个机制,你需要理解复杂的连接脚本文件。

       __setup宏

       考虑以下如何指定控制台设备,这可以作为一个使用内核命令行参数的例子。

       我们希望在系统引导的早期阶段初始化控制台,以便我们可以在引导过程中将消息输出到这个目的的设备上。初始化工作由一个名为printk.o的内核对象完成的。这个模块的C语言源文件位于.../kernel/printk.c中。控制台设备的初始化函数名为console_setup().并且这个函数只有一个参数就是内核命令行。

       现在面临的问题是如何以一种模块化和通用的方式传递配置参数,我们在内核命令行中指定了控制台相关的参数,但要将他们传递给需要此数据的相关设置函数和驱动程序。问题更复杂一些,因为一般情况下命令行参数在较早的时候就会用到,在模块使用他们之前或者就在此时。内核命令行主要时在main.c中处理的,但其中的启动代码不可能知道对应于每个内核命令行参数的目标处理函数,因为这样的参数有上百个。我们需要一种灵活和通用的机制,用于将内核命令行参数传递给它的使用者。

       文件.../include/linux/init.h中定义了一个特殊的宏,用于将内核命令行的字符串的一部分同某个函数关联起来,而这个函数会处理字符串的那个部分。下面举一个例子说明__setup宏是如何使用的。

       下面这个字符串是传给内核的一个完整的命令行参数,

                  console=ttyS0, 115200

        下列代码清单是一个.../kernel/printk.c中的一个代码片段,我们省略了函数体的内容,因为这个和我们的讨论无关。代码清单中最后以行,调用__setup宏,这个宏需两个参数,在这里我们传入了一个字符串字面量和一个函数指针。传递给__setup宏的字符串字面量是console=,这正好是内核命令行中有关控制台参数的前八个字节,而这并非巧合。

        设置控制台列表,有init/main.c调用

        static  int  __init  console_setup(char *str)

        {

              char buf[sizeof(console_cmdline[0].name) + 4];

              char *s, *options,*brl_options = NULL;

              ...

              ....

              return 1;

        }

        __setup("console=", console_setup);

      

        可以将__setup宏看作一个注册函数,即为内核命令行中的控制台相关参数注册的处理函数。实际上指,当在内核命令中碰到console=字符串时,就调用__setup宏的第二个参数所指定的函数,这里就是调用函数console_setup()。但这个信息是如何传递给早期的设置代码的?这些代码在模块之外也不了解控制台相关的函数。这个机制非常巧妙,并且依赖于编译连接时生成的列表

        具体的细节隐藏在一组宏之中,这些宏设计用于省去繁琐的语法规则,方便将段属性(或其他属性)添加到一部分目标代码中。这里的目标是建立一个静态列表,其中的每一项包含一个字符串字面量及关联的函数指针。这个列表由编译器生成,存放在一个单独命名的ELF段中,而该段是最终的ELF镜像vmlinux的一部分。理解这个技术很重要,内核中很多地方都使用这个技术来完成一些特殊的处理。

        init.h中定义了__setup系列宏(.../include/linux/init.h)

        ...

        #define  __setup_param(str, unique_id, fn, early)                      \

                static  const char __setup_str_##unique_id[ ] __initconst           \

                        __aligned(1) = str;      \

                static  struct  obs_kernel_param  __setup_##unique_id        \

                       __used  __section(.init.setup)             \

                       __attribute__((aligned((sizeof(long)))))         \

                       = {__setup_str_##unique_id, fn, early}

       

       #define  __setup(str, fn)

              __setup_param(str, fn, fn, 0)

       ...

       宏调用时这样使用的

               __setup("console=", console_setup);

       编译器预处理后是下面的样子

       static const  cahr  __setup_str_console_setup[ ]  __initconst \

       __aligned(1) = "console = ";

       static  struct  obs_kernel_param  __setup_console_setup __used  \

       __section(.init.setup)  __attribute__ ((aligned((sizeof(long)))))      \

       = { __setup_str_console_setup, console_setup, early};

        

       __used宏指示编译器生成函数或变量。__attribute__((aligned))宏指示编译器将结构体对齐到一个特定的内存边界上--在这里是指按sizeof(long)的长度进行对齐。将这两个宏去掉以简化后:

       static  struct  obs_kernel_param  __setup_console_setup  \

       __section(.init.setup) = {__setup_str_console_setup, console_setup, early};

       首先编译器生成一个字符数组,__setup_str_console_setup[ ], 并将其内容初始化为console=。接着编译器生成一个结构体其中包含三个成员:一个指向内核命令行字符串(就是刚刚声明的字符数组)的指针,一个指向设置函数本身的指针,和一个简单的标志。这里最关键的一点就是为结构体指定了一个段属性(__section)。段属性指示编译器将结构体放在ELF对象模块的一个特殊的段中,名字为.init.setup。在连接阶段所有使用__setup宏定义的结构体都会汇集在一起,并放到.init.setup段中,实际的结果是生成了一个结构体数组。下面代码是../init/main.c中的一个代码片段,其中显示了如何访问和使用这块数据。

       extern  struct  obs_kernel_param  __setup_start[], __setup_end[ ];

       

       static  int  __init  obsolete_checksetup(char *line)

       {

              struct  obs_kernel_param *p;

              int hard_early_param = 0;

             

              p = __setup_start;

              do{

                    int n = strlen(p->str)

                    if(!strcmp(line,p->str,n)){

                         if(p->early) {

                               if(line[n] == '\0' || line[n] == '=')

                                     had_early_param = 1;

                               }else if(!p->setup_func) {

                                     printk(KERN_WARNING "Parameter %s is obsolete,"

                                                         "ignored\n", p->str);

                                     return 1;

                              }else if(p->setup_func(line+n))

                                     return 1;

                    }

                    p++;

              }while(p < __setup_end);

              return had_early_param;

        }

       这个函数只有一个参数就是内核命令行字符串的相关部分。内核命令行字符串是由文件.../kernel/params.c中的函数解析的。在我们上述例子中,line指向字符串"console=ttyS0, 115200",它是内核命令行的一部分,两个外部结构体指针,__setup_start和__setup_end是在连接器脚本中定义的,这两个标签标记一个结构体数组的起始和结尾,这个数组就是放在目标文件的.init.setup段中的类型为obs_kernel_param的结构体数组。

       上述代码通过指针p扫描所有结构体,并找到一个与内核命令行参数匹配的结构体。

       这段代码寻找与字符串console=匹配的结构体,会调用结构体中的函数指针成员,及调用函数指针成员也就是函数console_setup(),并将命令行字符串的剩余部分(即字符串ttyS0,115200)传给他,作为这个函数的唯一参数。对于内核命令行中的每个部分这个过程都要重复一次,知道处理完内核命令行的所有内容。

       刚才讲述的这个技术的另一个使用是__init宏,这些宏用于将所有一次性的初始化函数放到对象文件的同一个段中。还有一个类似的宏__initconst,用于标记一次性使用的数据,在__setup宏中就使用了它。使用这些宏标记的初始化函数和数据会被汇集到ELF的特殊段中。当这些一次性初始化函数和数据使用完成以后内核就会释放他们所占据的内存。在系统引导接近尾声的时候内核会输出一条常见的消息:Free init memory:296k;

       __setup宏展开后生成一个obs_kernel_param的结构体,而这个结构体中包含了一个用作标志的成员,我们要说的就是这个标志成员的作用,early用于表示特定命令行参数是否早已在早期的引导过程中被处理了。这些命令行参数特意用于引导早期,而这个标志提供了一种实现早期解析算法的机制。在main.c中找到一个名为do_early_param()的函数,该函数遍历一个由连接器生成的数组并进行处理,而这个数组的每个成员都是由__setup宏展开的,并且标记为早期处理。这允许开发人员对引导过程中参数的处理时机进行一些控制。

       子系统初始化

       很多内核子系统的初始化工作都是由main.c中的代码完成的。有些是通过显式函数调用完成的,比如调用函数init_timers()和console_init(),他们需要很早就被调用。其他子系统需要通过一种非常类似__setup宏的技术来初始化的,我们前面介绍过这中技术。简单来说,就是连接器首先构造一个函数指针的列表,其中每个指针指向一个初始化函数,然后在代码中使用简单的循环,依次执行这些函数。

        下面是一个简单的初始化函数,位于.../arch/arm/kernel/setup.c,该函数用某个特定的硬件板卡进行一些定制的初始化。

        static int __init  customize_machine(void)

        {

               if(init_machine)

                    init_machine();

                    return 0;

         }

         arch_initcall(customize_machine);

       *__initcall系列宏

      上述代码有两点值的我们注意,首先,函数定义使用了__init宏,这个宏为函数指定了section属性,因此编译器会将函数放在vmlinux的一个名为.init.text的段中。回想以下这样做的目的—将这个函数放在目标文件的一个特殊的段中,不需要这个函数的时候,内核就可以回收它所占用的内存空间。

       需要注意的另外一点是紧跟在函数后面的宏,arch_initcall(customize_machine)。文件.../include/linux/init.h中定义了一系列的宏。arch_initcall就是其中之一。类似于__setup宏,这些宏声明了一个基于函数名的数据项,他们同样也使用了section属性,从而将这些数据项放置到vmlinux(ELF文件格式)中的一个特别命名的段中。这种方法的好处是main.c可以调用任意子系统初始化函数,而不需要对此子系统有任何的了解。除此以外的唯一方法就是在main.c中添加内核中所有子系统的相关信息,而这会使main.c中内容非常混乱。

       initcall系列宏

       #define  __define_initcall(level, fn, id)   \

            static  initcall_t  __initcall_##fn##id  __used    \

            __attribute__((__section__(".initcall" "level" ".init"))) = fn

        

       #define  early_initcall(fn)  __define_initcall("early",fn,early)

      

       #define pure_initcall(fn)               __define_initcall("0", fn, 0)

       #define core_initcall(fn)               __define _initcall("1", fn, 1)

       #define core_initcall_sync(fn)         __define_initcall("1s",fn, 1s)

       #define pistcore_initcall(fn)         __define_initcall("2",fn,2)

       ;;;


       从上述代码可以推断出段名是.initcallN.init。其中N是定义的级别,从1-7.其中以s结尾的是针对同步的initcall。每个宏都定义了一个数据项(变量),宏的参数是一个函数地址,并且这个函数地址的值被赋给了该数据项。例如

       arch_initcall(customize_machine)

       展开后会像下面

       static  initcall_t  __initcall_customize_machine = customize_machine;

       该数据项被放置在内核目标文件的名为.initcall3.init的段中

       级别的值(N)用于对所有的初始化调用进行排序。使用core_initcall()宏声明的函数会在所有其他函数之前调用。根据N值来先后调用。

       类似于__setup宏,可以将*_initcall系列宏看作一组注册函数,用于注册内核子系统的初始化函数,他们只需要在内核启动时运行一次,之后就没有用了。这些宏提供了一种机制,让这些初始化函数在系统启动时得以执行,并在这些函数执行完后丢弃其代码并回收内存。开发人员可以为初始化函数指定执行级别,有多达7个级别可选。因此,如果你有一个子系统依赖于另一个子系统,可以使用级别来保证子系统的初始化顺序。使用grep命令在内核代码中搜索字符串【a-z】*_initcall,你会发现这一系列的宏使用的非常广泛。

       关于*_initcall系列宏,多级别是在开发2.6系列内核的过程中引入的,早期内核版本使用__initcall()宏来达到这个目的。在设备驱动中这个宏使用的也特别广泛。

       init线程

       文件.../init/main.c中代码赋予内核“生命”。函数start_kernel()执行基本的内核初始化,并显示调用一些早期初始化函数之后,就会生成第一个内核线程,这个线程就是称为init的内核线程,它的进程ID是1。init是所有用户空间linux进程的父进程。系统引导到这个时间点,有两个显著不同的线程正在运行;一个是函数start_kernel()代表的线程,另一个就是init线程。前者在完成它的工作后变成系统的空闲进程,而后者会变成init进程。

       内核init线程的创建

       static  noinline  void  __init_refok  rest_init(void)

              __release(kernel_lock)

       {

               int pid;

              

               rcu_scheduler_starting();

               kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);

               numa_default_policy();

               pid = kernel_thread(kthread, NULL,CLONE_FS|CLONE_FILES);

               kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);

               unlock_kernel();


               启动时空闲线程必须至少调用schedule()

               一次,以便系统运作

               init_idle_bootup_task(current);

               preempt_enable_no_resched();

               schedule();

               preempt_disable();

               当抢占禁用后调用cpu_idle

               cpu_idle();

      }

       函数start_kernel()调用函数rest_init()函数。内核的init进程是通过调用kernel_process()生成的,以函数kernel_init()作为其第一个参数。init会继续完成剩余的系统初始化,而执行函数start_kernel()的线程会在调用cpu_idle()之后进入无限循环。

       为什么要采用这样的结构呢?也许你已经发现了start_kernel()是一个相当大的函数,而且是由__init宏标记的,这意味着在内核初始化的最后阶段,它所占用的内存会被回收。而在回收内存之前,必须要退出这个函数和它所占的地址空间。解决的办法是让start_kernel()调用rest_init()。rest_init()这个函数相对来说小很多,它会最终变成系统的空闲进程。

      通过initcalls进行初始化

      当执行函数kernel_init的内核线程诞生后,它会最终调用do_initcalls()。我们在前边讲过,*_initcalls系列宏可以用于注册初始化的函数,而函数do_initcalls()则负责调用其中的大部分函数。

       通过initcalls进行初始化

       extern  initcall_t  __initcall_start[], __initcall_end[], __early_initcall_end[];

       static  void  __init  do_initcalls(void)

       {

               initcall_t  *fn;

               for(fn = __early_initcall_end;  fn < __initcall_end;  fn++)

                        do_one_initcall(*fn);

               flush_scheduled_work();确保initcall序列中没有悬而未决的事情。

       }


       初始化过程由两处代码比较类似,do_pre_smp_initcalls()处理从__initcall_start到__early_initcall_end的部分,而do_initcalls处理剩余部分。这些标签是在vmlinux的连接阶段由连接器的脚本文件定义的。这些标签标记代码清单的开始和结尾,代码清单中的成员都是使用*_initcall系列宏注册的初始化函数,顶层内核源码目录中包含一个名为system.map的文件,从中可以看到这些标签。

        initcall_debug

       它允许你观察启动过程中的函数调用,只需在启动内核时设置initcall_debug,就可以看到系统输出的相关诊断消息。这是个查看内核初始化细节的好办法,特别是可以了解内核调用各个子系统和模块的顺序。更有趣的是函数调用的持续时间,如果你关心系统启动的时间,通过这种方法可以确定启动时间是在哪些地方被消耗的。

        最后的引导步骤

        在生成kernel_init()线程,并调用各个初始化函数之后,内核开始执行引导过程的最后一些步骤。这包括释放初始化函数和数据所占用的内存打开系统控制台设备,并启动第一个用户空间进程。下面代码显示了内核的init进程执行的最后一些步骤,代码来自main.c

        static  noinline  int  init_post(void)

               __release(kernel_lock)

        {

                  if(execute_command)

                  {

                         run_init_process(execute_command);

                         printk(KERN_WARNING“Failed  to  execute  %s.Attempting”

                                    "defaults...\n",  execute_command);

                   }

                   run_init_process("/sbin/init");

                   run_init_process("/etc/init");

                   run_init_process("/bin/init");

                   run_init_process("/bin/sh");


                   panic("no init found.try  passing  init = option to kernel");

}

       如果代码执行到init_post()的最后会产生一个内核错误(kernel  panic),它通常是控制台的最后一条输出消息。无论如何,run_init_process()命令中至少有一个必须正确无误的执行,run_init_process()函数在成功调用后不会返回,他用一个新的进程覆盖调用进程,实际上是用一个新的进程替换了当前进程(execve)。最常见的系统配置一般会生成/sbin/init作为用户空间区的初始化进程。下一张详细研究

       嵌入式开发人员可以选择定制的用户空间区初始化程序,。这就是前面代码片段中的条件语句的意图。如果execute_comand非空,它会指向一个运行在用户空间的字符串,而这个字符串中包含了一个定制的、由用户提供的命令。开发人员在内核命令行中指定这个命令,并且它会由我们前面所研究的__setup宏进行设置。 

      

      

        

        

      

        

          /