_define_initcall 作用
前言

  宏定义__define_initcall(level,fn)对于内核的初始化很重要,它指示
  编译器在编译的时候,将一系列初始化函数的起始地址值按照一定的顺序
  放在一个section中。在内核初始化阶段,do_initcalls() 将按顺序从该
  section中以函数指针的形式取出这些函数的起始地址,来依次完成相应
  的初始化。由于内核某些部分的初始化需要依赖于其他某些部分的初始化
  的完成,因此这个顺序排列常常非常重要。

  下面将从__define_initcall(level,fn) 宏定义的代码分析入手,依次
  分析名称为initcall.init的section的结构,最后分析内核初始化函数
  do_initcalls()是如何利用宏定义__define_initcall(level,fn)及其相
  关的衍生的7个宏宏定义,来实现内核某些部分的顺序初始化的。

1、分析 __define_initcall(level,fn) 宏定义

   1) 这个宏的定义位于inlclude\linux\init.h中:

      #define __define_initcall(level,fn)   \
         static initcall_t __initcall_##fn  \
         __attribute__((__section__(".initcall" level ".init"))) \
         = fn

      其中 initcall_t 是一个函数指针类型:

        typedef int (*initcall_t)(void);

      而属性 __attribute__((__section__())) 则表示把对象放在一个这个
      由括号中的名称所指代的section中。

      所以这个宏定义的的含义是:1) 声明一个名称为__initcall_##fn的函数
      指针(其中##表示替换连接,);2) 将这个函数指针初始化为fn;3) 编译
      的时候需要把这个函数指针变量放置到名称为 ".initcall" level ".init"
      的section中(比如level="1",代表这个section的名称是 ".initcall1.init")。

   2) 举例:__define_initcall(6, pci_init)

      上述宏调用的含义是:1) 声明一个函数指针__initcall_pic_init = pci_init;
      且 2) 这个指针变量__initcall_pic_init 需要放置到名称为 .initcall6.init
      的section中( 其实质就是将 这个函数pic_init的首地址放置到了这个
      section中)。

    3) 这个宏一般并不直接使用,而是被定义成下述其他更简单的7个衍生宏
       这些衍生宏宏的定义也位于 inlclude\linux\Init.h 中:

       #define core_initcall(fn)         __define_initcall("1",fn)
       #define postcore_initcall(fn)     __define_initcall("2",fn)
       #define arch_initcall(fn)         __define_initcall("3",fn)
       #define subsys_initcall(fn)       __define_initcall("4",fn)
       #define fs_initcall(fn)           __define_initcall("5",fn)
       #define device_initcall(fn)       __define_initcall("6",fn)
       #define late_initcall(fn)         __define_initcall("7",fn)

       因此通过宏 core_initcall() 来声明的函数指针,将放置到名称为
       .initcall1.init的section中,而通过宏 postcore_initcall() 来
       声明的函数指针,将放置到名称为.initcall2.init的section中,
       依次类推。

     4) 举例:device_initcall(pci_init)

        解释同上 1-2)。

2、与初始化调用有关section--initcall.init被分成了7个子section

   1) 它们依次是.initcall1.init、.initcall2.init、...、.initcall7.init

   2) 按照先后顺序依次排列

   3) 它们的定义在文件vmlinux.lds.S中

      例如 对于i386+,在i386\kernel\vmlinux.lds.S中有:

          __initcall_start = .;
          .initcall.init : {
                *(.initcall1.init)
                *(.initcall2.init)
                *(.initcall3.init)
                *(.initcall4.init)
                *(.initcall5.init)
                *(.initcall6.init)
                *(.initcall7.init)
                }
          __initcall_end = .;

       而在makefile 中有
     
       LDFLAGS_vmlinux += -T arch/$(ARCH)/kernel/vmlinux.lds.s

    4) 在这7个section总的开始位置被标识为__initcall_start,
       而在结尾被标识为__initcall_end。

3、 内核初始化函数do_basic_setup(): do_initcalls() 将从.initcall.init
    中,也就是这7个section中依次取出所有的函数指针,并调用这些
    函数指针所指向的函数,来完成内核的一些相关的初始化。

    这个函数的定义位于init\main.c中:

        extern initcall_t __initcall_start, __initcall_end;
        static void __init do_initcalls(void)
        {
            initcall_t *call;
            ....
            for (call = &__initcall_start; call < &__initcall_end; call++)
            {
                ....
                (*call)();
                ....
            }
            ....
         }

     这些函数指针指向的函数就是通过宏__define_initcall(level,fn)
     赋值的函数fn,他们调用的顺序就是放置在这些section中的顺序,
     这个顺序很重要, 这就是这个宏__define_initcall(level,fn)的作用。
     注意到,这里__initcall_start 和 __initcall_end 就是section
     initcall.init的头和尾。

4、 归纳之

    1) __define_initcall(level,fn)的作用就是指示编译器把一些初始化函数
       的指针(即:函数起始地址)按照顺序放置一个名为 .initcall.init 的
       section中,这个section又被分成了7个子section,它们按顺序排列。
       在内核初始化阶段,这些放置到这个section中的函数指针将供
       do_initcalls() 按顺序依次调用,来完成相应初始化。

    2) 函数指针放置到的子section由宏定义的level确定,对应level较小的
       子section位于较前面。而位于同一个子section内的函数指针顺序不定,
       将由编译器按照编译的顺序随机指定。

    3) 因此,如果你希望某个初始化函数在内核初始化阶段就被调用,那么你
       就应该使用宏__define_initcall(level,fn) 或 其7个衍生宏 把这个
       函数fn的对应的指针放置到按照初始化的顺序放置到相关的 section 中。
       同事,如果某个初始化函数fn_B需要依赖于另外一个初始化函数fn_A的
       完成,那么你应该把fn_B放在比fn_A对应的level值较大的子section中,
       这样,do_initcalls()将在fn_A之后调用fn_B。
 
 
***********************************************************************
 
 如果你希望某个初始化函数在内核初始化阶段就被调用,那么你
就应该使用宏__define_initcall(level,fn) 或 其7个衍生宏来
把这个初始化函数fn的起始地址按照初始化的顺序放置到相关的
section 中。 内核初始化时的do_initcalls()将从这个section
中按顺序找到这些函数来执行。 如果你希望某个初始化函数在内核初始化阶段就被调用,那么你
就应该使用宏__define_initcall(level,fn) 或 其7个衍生宏来
把这个初始化函数fn的起始地址按照初始化的顺序放置到相关的
section 中。 内核初始化时的do_initcalls()将从这个section
中按顺序找到这些函数来执行。
 
*******************************************************************
 
 
今天在做一个驱动的时候要用到另一个驱动(I2C)提供的API,在内核初始化时碰到了一个依赖问题。

  我的驱动在I2C初始化之前就运行起来了,而这时I2C提供的API还处于不可用状态。查了很多资料,网上有人说所有使用module_init这个宏的驱动程序的起动顺序都是不确定的(我没有查到权威的资料)。

  所有的__init函数在区段.initcall.init中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些__init函数指针,并在整个初始化完成后,释放整个init区段(包括.init.text,.initcall.init等)。

  注意,这些函数在内核初始化过程中的调用顺序只和这里的函数指针的顺序有关,和1)中所述的这些函数本身在.init.text区段中的顺序无关。在 2.4内核中,这些函数指针的顺序也是和链接的顺序有关的,是不确定的。在2.6内核中,initcall.init区段又分成7个子区段,分别是

.initcall1.init
.initcall2.init
.initcall3.init
.initcall4.init
.initcall5.init
.initcall6.init
.initcall7.init

  当需要把函数fn放到.initcall1.init区段时,只要声明

core_initcall(fn);

  即可。

  其他的各个区段的定义方法分别是:

core_initcall(fn) --->.initcall1.init
postcore_initcall(fn) --->.initcall2.init
arch_initcall(fn) --->.initcall3.init
subsys_initcall(fn) --->.initcall4.init
fs_initcall(fn) --->.initcall5.init
device_initcall(fn) --->.initcall6.init
late_initcall(fn) --->.initcall7.init

  而与2.4兼容的initcall(fn)则等价于device_initcall(fn)。各个子区段之间的顺序是确定的,即先调用. initcall1.init中的函数指针,再调用.initcall2.init中的函数指针,等等。而在每个子区段中的函数指针的顺序是和链接顺序相关的,是不确定的。

  在内核中,不同的init函数被放在不同的子区段中,因此也就决定了它们的调用顺序。这样也就解决了一些init函数之间必须保证一定的调用顺序的问题。按照include/linux/init.h文件所写的,我在驱动里偿试了这样两种方式:

__define_initcall("7", fn);
late_initcall(fn);

  都可以把我的驱动调整到最后调用。实际上上面两个是一回事:

#define late_initcall(fn) __define_initcall("7", fn)




Linux-2.6.20的cs8900驱动分析(一)
一、初始化阶段
    网络初始化被调用的路径为:
init->do_basic_setup->do_initcalls->net_olddevs_init->ethif_probe2->probe_list2->cs89x0_probe->cs89x0_probe1
真是不容易啊,终于进到 cs89x0_probe1 了,在这里开始探测和初始化 cs8900 了。下面就按照这个顺序来说明网络驱动第一阶段的工作。注意:这里的调用顺序是将 cs8900 驱动编入内核所产生的,如果将 cs8900 驱动选为模块,这个路径: init->do_basic_setup->do_initcalls->net_olddevs_init->ethif_probe2->probe_list2 也会执行。
1.1 init 函数
我们知道当 start_kernel 函数完成后就会启动 init 进程执行,在真正的应用程序 init 进程(如 busybox 的 /sbin/init )之前, Linux 还需要执行一些初始化操作。 init 的代码可以在 \init\main.c 中找到,它的代码如下:
static int init(void * unused)
       {
               lock_kernel();
                ……                                                         // 省略多 cpu 的初始化代码先
                do_basic_setup();                                   // 我们所关注的初始化函数
                ……
        if (!ramdisk_execute_command)
                    ramdisk_execute_command = "/init";
      if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0)    {
                     ramdisk_execute_command = NULL;
                     prepare_namespace();                                       // 挂接根文件系统      
             }
             ……
free_initmem();                                                       // 释放初始化代码的空间
       unlock_kernel();
……                                                                      // 这几段没看懂
 
       if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) // 检查控制台 console 是否存在
              printk(KERN_WARNING "Warning: unable to open an initial console.\n");
……// 这几段没看懂
       if (ramdisk_execute_command) {           // 运行 ramdisk_execute_command 指定的 init 用户进程
              run_init_process(ramdisk_execute_command);
              printk(KERN_WARNING "Failed to execute %s\n",
                            ramdisk_execute_command);
       }
       ……
       if (execute_command) {       // 判断在启动时是否指定了 init 参数,如果指定,此值将赋给 execute_command
              run_init_process(execute_command);              // 开始执行用户 init 进程,如果成功将不会返回。
              printk(KERN_WARNING "Failed to execute %s. Attempting "
                                   "defaults...\n", execute_command);
       }
// 如果没有指定 init 启动参数,则查找下面的目录 init 进程,如果找到则不会返回
       run_init_process("/sbin/init");
       run_init_process("/etc/init");
       run_init_process("/bin/init");
       run_init_process("/bin/sh");
   // 如果上面的程序都出错,则打印下面的信息,如果内核找到 init 进程,则程序不会指向到此处
       panic("No init found. Try passing init= option to kernel.");
}
1.2 do_basic_setup 函数
在这里我们最关心的是 do_basic_setup 函数,顾名思义该函数的功能就是“做基本设置”,它的实现代码也在 \init\main.c 中。 do_basic_setup() 完成外设及其驱动程序的加载和初始化。该函数代码如下所示:
static void __init do_basic_setup(void)
{
       /* drivers will send hotplug events */
       init_workqueues();     // 初始化工作队列
       usermodehelper_init(); // 初始化 khelper 内核线程,还没弄清楚
 
       driver_init();           // 初始化内核的设备管理架构需要的数据结构,很复杂,以后在谈这部分。
 
#ifdef CONFIG_SYSCTL
       sysctl_init();          // 没搞懂
#endif
       do_initcalls();         // 重点函数,初始化的主要工作就靠它了
}
1.3 do_ initcalls 函数
       do_initcalls 函数将会调用内核中所有的初始化函数,它的代码同样在 \init\main.c 中。 do_initcalls 函数调用其他初始化函数相当简洁,它的关键代码如下所示:
initcall_t *call;
for (call = __initcall_start; call < __initcall_end; call ) {
……
       result = (*call)();
……
       简洁归简洁,但这段代码是什么意思呢?这说来就话长了,最重要的应该是先了解 Linux 处理初始化的大体思想,由于 Linux 有很多部分需要初始化,每个部分都有自己的初始化函数,如果按照常理一个一个的调用未免显得冗长,而且也不便于扩展。那么 Linux 是怎么处理的呢?首先, Linux 将各个部分的初始化函数编译到一个块内存区中,当初始化完了以后释放这块内存区,这就是 init 函数中 free_initmem 所要做的事。然后,再在另外的内存区中存放一些函数指针,让每个指针指向一个初始化函数。然后在 do_initcalls 中依次根据这些指针调用初始化函数。
上面一段就是 Linux 实现初始化的大体思想,下面我们看看它最终是怎么实现的。首先要了解的是 __define_initcall 宏,该宏的定义在 \ include\linux\init.h 中,它的原型如下所示:
#define __define_initcall(level,fn,id) static initcall_t __initcall_##fn##id __attribute_used__ \
       __attribute__((__section__(".initcall" level ".init"))) = fn
 
__define_initcall 宏有三个参数, level 表示初始化函数的级别, level 值的大小觉得了调用顺序, level 越小越先被调用, fn 就是具体的初始化函数, id 简单标识初始化函数,现在还没找到有什么用 ^_^ 。 __define_initcall 的功能为,首先声明一个 initcall_t 类型的函数指针 __initcall_##fn##id , initcall_t 的原型为:
typedef int (*initcall_t)(void);
该类型可简单理解为函数指针类型 ^_^ 。然后,让该函数指针指向 fn 。最后,通过编译器的编译参数将此指针放到指定的空间 ".initcall" level ".init" 中, __attribute_used 向编译器说明这段代码有用,即使在没用到的时候,编译器也不会警告。 __attribute__ 的 __section__ 参数表示该段代码放入什么内存区域中,也即指定编译到什么地方,编译参数更详细的地方可以查阅 GCC 文档,在 gcc 官方网站 http://gcc.gnu.org/onlinedocs/ 中能找到各个版本的手册。这样说来还是比较抽象,下面举个例子来说明:
       假如有初始化函数 init_foolish 函数,现在使用 __define_initcall 宏向内核加入该函数。假如调用方式如下:
__define_initcall("0",init_foolish,1) ;
那么, __define_initcall 宏首先申请一个 initcall_t 类型的函数指针 __initcall_init_foolish1 (注意替换关系),且使该指针指向了 init_foolish ,函数指针 __initcall_init_foolish1 被放到 .initcall.0.init 内存区域中,这个标志在连接时会用到。
       有了上面的基础知识,现在回到 do_initcalls 函数中,首先注意到是 __initcall_start 和 __initcall_end ,它们的作用就是界定了存放初始化函数指针区域的起始地址,也即从 __initcall_start 开始到 __initcall_end 结束的区域中存放了指向各个初始化函数的函数指针。换句话说,只要某段程序代码从 __initcall_start 开始依次调用函数指针,那么就可以完成各个部分的初始化工作,这显得十分优雅而且便于扩充,再看看 do_initcalls ,它何尝不是如此呢。这里还有一个有用的技巧就是 __initcall_start 和 __initcall_end 的原型是 initcall_t 型的数组,以后可以使用这种技巧 ^_^ 。
       现在我们知道了 do_initcalls 函数的实现原理,那么到底它调用了多少初始化函数呢?我们怎样才能知道呢?根据上面的分析,我们知道所有的初始化函数的指针都放在 __initcall_start 和 __initcall_end 区域期间,而函数指针与它指向的函数之间又有固定的关系,如上面的例子,初始化函数名为 init_foolish ,指向它的函数指针就是 __initcall_init_foolish1 ,即在此函数加上前缀 __initcall_ 和一个数字后缀,反之,从函数指针也可推出初始化函数名。有了这两个信息,我们就可以很方便的找个初始化函数。怎么找呢??首先打开 Linux 完后产生的 System.map 文件,然后找到 __initcall_start 和 __initcall_end 字符串,你会发现它们之间有很多类似于 __initcall_xxx1 这样的符号,这些符号就是我们需要的函数指针了,这样就可推出初始化函数的名字。比如,我们这里需要的函数指针 __initcall_net_olddevs_init6 ,按照上面的名字规则,很容易推出它所指向的初始化函数名字是 net_olddevs_init 。
       得到了初始化函数的名字又怎么样呢?又不知道它在哪个文件里,不要着急!请打开你的浏览器登陆 http://lxr.linux.no/ident 网站,然后选择 Linux 版本和架构,然后可以搜索我们想要的信息。比如我输入 net_olddevs_init ,然后我就会得到该函数所在文件的相关信息。
1.4 net_olddevs_init 函数
       我们知道 net_olddevs_init 函数在 do_initcalls 函数中被调用并执行,那么它到底要做什么呢?看看实现代码就知道了,它的实现代码可以在 \drivers\net\Space.c 中找到。对于网络驱动部分的主要实现代码如下:
static int __init net_olddevs_init(void){  
……
       int num;
       for (num = 0; num < 8; num)
              ethif_probe2(num);
       ……
}
这段代码就不用讲解了吧,嘿嘿!就是调用了 8 次 ethif_probe2 ,赶快去看看 ethif_probe2 长什么样子。
1.5 ethif_probe2 函数
       先看看该函数的实现代码,该代码也在 \drivers\net\Space.c 文件中。
static void __init ethif_probe2(int unit)
{
       unsigned long base_addr = netdev_boot_base("eth", unit);   // 由于 ethif_probe2 被 net_olddevs_init 调用了 8 次,
                                          // 所以 unit 的值为 0 ~ 7 ,也即在这里可以注册 eth0 ~ eth7 八个网络设备
       if (base_addr == 1)
              return;

       (void)(    probe_list2(unit, m68k_probes, base_addr == 0) &&
              probe_list2(unit, eisa_probes, base_addr == 0) &&
              probe_list2(unit, mca_probes, base_addr == 0) &&
              probe_list2(unit, isa_probes, base_addr == 0) &&
              probe_list2(unit, parport_probes, base_addr == 0));
}
       该函数首先调用 netdev_boot_base 所给的设备是否已经向内核注册,如果已注册 netdev_boot_base 返回 1 ,随后推出 ethif_probe2 。如果设备没注册,则又调用函数 probe_list2 四次,每次传递的传输不同,注意到每次传递的第二个参数不同,这个参数也是相当重要的,这里拿 isa_probes 参数为例说明,因为这个参数与 cs89x0_probe 有关, isa_probes 的定义也在 \drivers\net\Space.c 中,它的样子形如:
static struct devprobe2 isa_probes[] __initdata = {
……
#ifdef CONFIG_SEEQ8005
       {seeq8005_probe, 0},
#endif
#ifdef CONFIG_CS89x0
     {cs89x0_probe, 0},
#endif
#ifdef CONFIG_AT1700
       {at1700_probe, 0},
#endif
       {NULL, 0},
……
};
如果把 cs8900 的驱动选为非编译进内核,那么它的探测函数 cs89x0_probe 就不会存在于 isa_probes 数组中,所以在初始阶段就不能被调用。从上面的代码可以知道 devprobe2 类型至少包括两个域,至少一个域为函数指针,看看它的原型如下:
struct devprobe2 {
       struct net_device *(*probe)(int unit);                         // 函数指针,指向探测函数
       int status;       /* non-zero if autoprobe has failed */
};
下面看看 probe_list2 函数是怎么表演的。
1.6 ethif_probe2 函数
       对于 ethif_probe2 函数也没有什么需要说明的,它的主要任务是依次调用 devprobe2 类型的 probe 域指向的函数。他的实现代码同样在 \drivers\net\Space.c 中,它的关键代码如下:
static int __init probe_list2(int unit, struct devprobe2 *p, int autoprobe)
{
       struct net_device *dev;
       for (; p->probe; p ) {
           ……
              dev = p->probe(unit);
              ……
       }
……
}
1.7 cs89x0_probe 函数
       从该函数起,真正开始执行与 cs8900 驱动初始化程序,该函数在 \drivers\net\cs89x0.c 文件实现。下面依次解释该函数。
 
struct net_device * __init cs89x0_probe(int unit)
{
       struct net_device *dev = alloc_etherdev(sizeof(struct net_local)); // 该函数申请一个 net_device +
//sizeof(struct net_local) 的空间, net_local 是 cs8900 驱动的私有数据空间。
       unsigned *port;
       int err = 0;
       int irq;
       int io;
      
       if (!dev)
              return ERR_PTR(-ENODEV);
       sprintf(dev->name, "eth%d", unit);                 // 初始化 dev->name 域
       netdev_boot_setup_check(dev);                  // 检查是否给定了启动参数,如果给定了启动参数,此函数将初始
// 化 dev 的 irq 、 base_addr 、 mem_start 和 mem_end 域。
       io = dev->base_addr;                                 //io 实际实质 cs8900 所占地址空间的起始地址,此地址为虚拟地址
       irq = dev->irq;
 
       if (net_debug)
              printk("cs89x0:cs89x0_probe(0x%x)\n", io);
// 下面根据 io 的值调用 cs89x0_probe1 函数
       if (io > 0x1ff) {/* Check a single specified location. */// 此段没搞懂,由于没给启动参数,这里也不会执行
       err = cs89x0_probe1(dev, io, 0);
       } else if (io != 0) { /* Don''''''''''''''''''''''''''''''''t probe at all. */
              err = -ENXIO;
       } else {
              for (port = netcard_portlist; *port; port ) {// netcard_portlist 为 unsigned int 型数组,在 cs89x0.c 文件中定
// 义,里面列出了 cs8900 可能占用空间的起始地址,这些地址
// 将在 cs89x0_probe1 函数中用于向内核申请。
                     if (cs89x0_probe1(dev, *port, 0) == 0) // cs89x0_probe1 探测成功就返回 0
                            break;
                     dev->irq = irq;
              }
              if (!*port)
                     err = -ENODEV;
       }
       if (err)
              goto out;
       return dev;
out:
       free_netdev(dev);   // 表示探测失败,这里就释放 dev 的空间,随后打印些消息
       printk(KERN_WARNING "cs89x0: no cs8900 or cs8920 detected. Be sure to disable PnP with SETUP\n");
       return ERR_PTR(err);
}
       从上面的程序清单可以看到该函数还没有真正的开始探测 cs8900 ,实质的探测工作是让 cs89x0_probe1 完成的。在解释 cs89x0_probe1 之前先提一下网络驱动程序中非常重要的一些函数。内核需要一个数据结构来管理或者描述每个网络驱动程序,这个数据类型就是 struct net_device ,该数据类型包括很多域,详细的解释可以参见《 Linux 设备驱动程序》一书中的描述,也可以参见源代码(在 \include\linux\netdevice.h 中,源码中也有详细的注解)。内核为了编程方便特地实现了函数 alloc_netdev 来完成对 net_device 的空间分配。那么 alloc_etherdev 函数主要针对以太网在 alloc_netdev 基础上封装的一个函数,它除了申请 net_device 空间外,还会初始化 net_device 的相关域。

Linux下网络子系统的初始化

首先,我的最初出发点是从init进程开始的,如果你的系统的引导之类的感兴趣,那请在白云黄鹤的Linux版上查找相关资料或去其它地方查找.然后我的Code也是基于单CPU的机器,SMP相关的话请参考其它的资料.分析的代码是2.6.18+
 
一.init进程与网络子系统的第一个函数.  
       init进程相关的知识相信是总所周之,这就不提了(init进程是所有进程之父,使用ps -aux可以查看init的进程号为1)
         init进程的入口函数即为init()函数,定义于init/main.c中。
    1.init()函数调用do_basic_setup()函数完成最后的初始化工作。
    2.调用free_initmem()把内核中标记有__init、__initfunc、__initdata的程序代码和数据所占据的内存全部释放。
    3.调用open("/dev/console", O_RDWR, 0)打开控制台设备。同时调用复制标准输出设备和标准出错设备的描述符。
    4.调用execve()执行init程序,该程序完成各种脚本的执行。
    
    好了,我们的故事就此开始:),来看看网络子系统是如何登上历史的舞台,继续自己的辉煌之旅.
    
    前面说道了do_basic_setup(),我们的故事,其实真真是从它开始的,不过开头似乎有些隐晦,但只要认真看来,紧抓蛛丝马迹,自然抽丝剥茧不再话下:)
    
do_basic_setup()[init/main.c]
    |--------do_initcalls()[init/main.c]
    
我们贴出do_initcalls()的全部代码,以备分析:

__setup("initcall_debug", initcall_debug_setup);
struct task_struct *child_reaper = &init_task;
extern initcall_t __initcall_start[], __initcall_end[];

static void __init do_initcalls(void)
{
    .........
    for (call = __initcall_start; call < __initcall_end; call++) {
        char *msg = NULL;
        char msgbuf[40];
        int result;

        if (initcall_debug) {
            printk("Calling initcall 0x%p", *call);
            print_fn_descriptor_symbol(": %s()",
                    (unsigned long) *call);
            printk("\n");
        }
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       {1}:需要注意的代码
        result = (*call)();
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

        if (result && result != -ENODEV && initcall_debug) {
            sprintf(msgbuf, "error code %d", result);
            msg = msgbuf;
        }
        if (preempt_count() != count) {
            msg = "preemption imbalance";
            preempt_count() = count;
        }
        if (irqs_disabled()) {
            msg = "disabled interrupts";
            local_irq_enable();
        }
        if (msg) {
            printk(KERN_WARNING "initcall at 0x%p", *call);
            print_fn_descriptor_symbol(": %s()",
                    (unsigned long) *call);
            printk(": returned with %s\n", msg);
        }
    }

    /* Make sure there is no pending stuff from the initcall sequence
     */
    flush_scheduled_work();
    
    ........
}

上面的Code有一个很是奇怪的extern initcall_t __initcall_start[], __initcall_end[];生命,仿佛initcall_t只是个一般的数据类型而已,但真的是这样吗?

grep一下:
typedef (*initcall_t)(void);[include/init.h]
原来如此,不就一函数指针而已么?犯的着吗?但是,后面的变量__initcall_start[], __initcall_end[];你有仔细的查找过么?

恩,那就查找一番,原来,居然在[arch/i386/kernel/vmlinux.lds.S],如果这时你进去看了这个文件依然是雾水一头,那看来只能出杀手锏了:).(请参考网上的Using LD的中文译文)

###########################################################################
关键时刻,除了的确是语法上的东西,还是考虑GNU的哪些工具:),还有就是,万千万千不要忘了,就算Kernel,它也是个software,自然,少不了:
预处理->编译->链接->运行,在这方面来说,与其它程序并无二置
###########################################################################

好了,我们继续,查看[include/init.h]的代码,
/* initcalls are now grouped by functionality into separate
 * subsections. Ordering inside the subsections is determined
 * by link order.
 * For backwards compatibility, initcall() puts the call in
 * the device init subsection.
 */

#define __define_initcall(level,fn) \
    static initcall_t __initcall_##fn __attribute_used__ \
    __attribute__((__section__(".initcall" level ".init"))) = fn

#define core_initcall(fn)            __define_initcall("1",fn)
#define postcore_initcall(fn)        __define_initcall("2",fn)
#define arch_initcall(fn)            __define_initcall("3",fn)
#define subsys_initcall(fn)          __define_initcall("4",fn)
#define fs_initcall(fn)              __define_initcall("5",fn)
#define device_initcall(fn)          __define_initcall("6",fn)
#define late_initcall(fn)            __define_initcall("7",fn)
结合上面的ld脚本,基本可以开始猜测了,原来do_initcalls()中的:
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       {1}:需要注意的代码
        result = (*call)();
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        
是按照声明为core_initcall()->late_initcall()这样的顺序来调用这些函数啊(看到声明对应的1,2,3...,7可以猜测啊)

你或许又开始发问了,这和网络子系统初始化,有关系么?
怎么没有,请参考[net/socket.c]
static int __init sock_init(void)
{
    /*
     *    Initialize sock SLAB cache.
     */
    sk_init();

    /*
     *    Initialize skbuff SLAB cache
     */
    skb_init();

    /*
     *    Initialize the protocols module.
     */
    init_inodecache();
    register_filesystem(&sock_fs_type);
    sock_mnt = kern_mount(&sock_fs_type);

    /* The real protocol initialization is performed in later
     * initcalls.
     */
#ifdef CONFIG_NETFILTER
    netfilter_init();
#endif

    return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{2}:和我们上面的叙述是不是可以联系上了:),说了这久,原来只是为了说这一点啊:)
core_initcall(sock_init);    /* early initcall */
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

因此啊,我可以断定sock_init()[net/socket.c]是网络子系统初始化的第一个函数,不信,可以在net目录里面"grep core_initcall * -rn",看声明为core_initcall函数有几个,然后采用排除法:)

    恩,可以喝口水了,是否有拨云见日的感觉:),可打铁得趁热,继续吧:)
    
二. 网络子系统的初始化
    在上面得分析中,既然都见到了第一个与网络子系统相关得函数,后面自然要好过一些了.
    
    在上面得Code中可以看到几点:
sock_init()[net/socket.c]
    |--------sk_init()[net/core/sock.c]
    |--------skb_init()[net/core/skbuff.c]
    |--------init_inodecache()    |#|
    |--------register_filesystem()    |#|
    |--------kern_mount()        |#|
    |--------netfilter_init()[net/netfilter/core.c]
    
简单说明一下这几个函数:
    1.函数sk_init()
这个函数初始化slab cache给struct sock类型的对象使用。
if (num_physpages <= 4096) {            /* 4096=16MB 在x86 */
        sysctl_wmem_max = 32767;         /* 32767=32Kb-1    */
        sysctl_rmem_max = 32767;
        sysctl_wmem_default = 32767;
        sysctl_rmem_default = 32767;
} else if (num_physpages >= 131072) {    /* 131072=512MB 在x86 */
        sysctl_wmem_max = 131071;          /* 131071=128Kb-1     */
        sysctl_rmem_max = 131071;
}
这设置了读或写的buffer最大值,如果物理页数目(num_physpages)在16MB~512MB之间,默认分配为(net/core/sock.c):
/* Run time adjustable parameters. */
__u32 sysctl_wmem_max = SK_WMEM_MAX;
__u32 sysctl_rmem_max = SK_RMEM_MAX;
__u32 sysctl_wmem_default = SK_WMEM_MAX;
__u32 sysctl_rmem_default = SK_RMEM_MAX;
其中,SK_WMEM_MAX和SK_RMEM_MAX均在skbuff.h中设置为65535b(64Kb-1)

    2.函数skb_init()
这个函数创建struct sk_buff的对象的cache(参考sk_init()很明显)

    3.注册sock文件系统(详细内容参考文件系统相关文档).
    
    4.Netfilter框架初始化