Linux TCP/IP协议栈学习笔记


写在前面的话:前些日子在网上下到一本《Linux TCP/IP 协议栈分析》 下载见 ,自己本来对协议比较感兴趣,所以慢慢地看起,希望把这本数一张一张地看清楚,搞明白,因为大学以后就没有认认真真地看过书了,希望自己能坚持下去。

    注:《Linux TCP/IP 协议栈分析》的源代码版本是2.6.18,我的源代码版本是2.6.36。下文中不注明版本的都为2.6.36,《书》指《Linux TCP/IP 协议栈分析》。

1.    系统初始化

    一切的一切还是从最源头说起,那就是系统的启动,不过这里的启动并不是从加电的一瞬间开始,而是从init/main.cstart_kernel函数开始,先来看一下它的源代码。

main.c [\init]

asmlinkage void __init start_kernel(void)
{
    char * command_line;
    extern const struct kernel_param __start___param[], __stop___param[];
    smp_setup_processor_id();
    
/*
     * Need to run as early as possible, to initialize the
     * lockdep hash:
     */

    lockdep_init();
    debug_objects_early_init();
    
/*
     * Set up the the initial canary ASAP:
     */

    boot_init_stack_canary();
    cgroup_init_early();
    local_irq_disable();
    early_boot_irqs_off();
    early_init_irq_lock_class();
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */

    tick_init();
    ...
    ...
    /* Do the rest non-__init'ed, we're now alive */
    rest_init();
}

函数中包含了很多初始化系统所要做的工作,我们关心的是它的最后一个函数调用,rest_init。我们步步跟进:

main.c [\init]

static noinline void __init_refok rest_init(void)
    __releases(kernel_lock)
{
    int pid;
    rcu_scheduler_starting();
    
/*
     * We need to spawn init first so that it obtains pid 1, however
     * the init task will end up wanting to create kthreads, which, if
     * we schedule it before we create kthreadd, will OOPS.
     */

    
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
    numa_default_policy();
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    rcu_read_lock();
    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    rcu_read_unlock();
    complete(&kthreadd_done);
    
/*
     * The boot idle thread must execute schedule()
     * at least once to get things moving:
     */

    init_idle_bootup_task(current);
    preempt_enable_no_resched();
    schedule();
    preempt_disable();
    /* Call into cpu_idle with preempt disabled */
    cpu_idle();
}

好吧,我们只抓最主要的,为什么呢?因为其他函数我还不明白:-)我们只关注协议栈相关的,所以有本好书来指导还是相当有好处的。

rest_init函数里面最主要的工作就是启动了一个线程kernel_init。不过在《书》中的2.6.18版本这个线程的名字是init,调用如下:

kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);

好吧,我们来看一下kernel_init线程执行了哪些。(又贴代码了,真的是代码工啊,用代码说话^_^

static int __init kernel_init(void * unused)
{
    
/*
     * Wait until kthreadd is all set-up.
     */

    wait_for_completion(&kthreadd_done);
    
/*
     * init can allocate pages on any node
     */

    set_mems_allowed(node_states[N_HIGH_MEMORY]);
    
/*
     * init can run on any cpu.
     */

    set_cpus_allowed_ptr(current, cpu_all_mask);
    
/*
     * Tell the world that we're going to be the grim
     * reaper of innocent orphaned children.
     *
     * We don't want people to have to make incorrect
     * assumptions about where in the task array this
     * can be found.
     */

    init_pid_ns.child_reaper = current;
    cad_pid = task_pid(current);
    smp_prepare_cpus(setup_max_cpus);
    do_pre_smp_initcalls();
    smp_init();
    sched_init_smp();
    do_basic_setup();
    /* Open the /dev/console on the rootfs, this should never fail */
    if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
        printk(KERN_WARNING "Warning: unable to open an initial console.\n");
    (void) sys_dup(0);
    (void) sys_dup(0);
    
/*
     * check if there is an early userspace init. If yes, let it do all
     * the work
     */

    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();
    }
    
/*
     * Ok, we have completed the initial bootup, and
     * we're essentially up and running. Get rid of the
     * initmem segments and start the user-mode stuff..
     */

    init_post();
    return 0;
}

关键部分是do_basic_setup函数:

static void __init do_basic_setup(void)
{
    cpuset_init_smp();
    usermodehelper_init();
    init_tmpfs();
    driver_init();
    init_irq_proc();
    do_ctors();
    do_initcalls();
}

很快就到尽头了,下面一个是do_initcalls

static void __init do_initcalls(void)
{
    initcall_t *fn;
    for (fn = __early_initcall_end; fn < __initcall_end; fn++)
        do_one_initcall(*fn);
    /* Make sure there is no pending stuff from the initcall sequence */
    flush_scheduled_work();
}

终于到了,这个for循环就是我们的主角。先来看看两个循环变量的定义:

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

这两个变量在.c.h文件中是找不到的,因为他们定义在链接脚本中,关于链接脚本可以参考《GNU_链接脚本分析》,另外建议你同时参看我的博文《ELF文件格式学习》。

Linux创建内核的链接脚本是arch\x86\kernel目录下的vmlinux.lsd.S,而在2.6.18中,上述变量是定义在该链接脚本中的,2.6.18中的目录是arch\i386\kernel,后来的Linux版本把i386x86_64目录合并成x86目录。而且2.6.18中没有__early_initcall_end这个变量。

vmlinux.lsd.S [arch\i386\kernel] (2.6.18)

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

而在2.6.36中则不同,它定义了一个链接脚本的头文件vmlinux.lds.h,在目录include\asm-generic下。这个头文件是通用的,其他的平台如armvmlinux.lds.S [arch\arm\kernel],也会用到里面定义的变量/宏。

vmlinux.lds.h [include\asm-generic]

#define INITCALLS                            \
    *(.initcallearly.init)                        \
    VMLINUX_SYMBOL(__early_initcall_end) = .;            \
      *(.initcall0.init)                        \
      *(.initcall0s.init)                        \
      *(.initcall1.init)                        \
      *(.initcall1s.init)                        \
      *(.initcall2.init)                        \
      *(.initcall2s.init)                        \
      *(.initcall3.init)                        \
      *(.initcall3s.init)                        \
      *(.initcall4.init)                        \
      *(.initcall4s.init)                        \
      *(.initcall5.init)                        \
      *(.initcall5s.init)                        \
    *(.initcallrootfs.init)                        \
      *(.initcall6.init)                        \
      *(.initcall6s.init)                        \
      *(.initcall7.init)                        \
      *(.initcall7s.init)

#define INIT_CALLS                            \
        VMLINUX_SYMBOL(__initcall_start) = .;            \
        INITCALLS                        \
        VMLINUX_SYMBOL(__initcall_end) = .;

INITCALLS 宏中定义了__early_initcall_end 这个变量,而INIT_CALLS宏中又包含了INITCALLS 宏以及__initcall_start__initcall_end变量,__initcall_start用在main.cdo_pre_smp_initcalls函数中。

INIT_CALLS宏又被包含在宏INIT_DATA_SECTION(initsetup_align)中,所以在内核链接脚本vmlinux.lds.S中只能看到宏INIT_DATA_SECTION(initsetup_align)

vmlinux.lds.h [include\asm-generic]


#define INIT_DATA_SECTION(initsetup_align)                \
    .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {        \
        INIT_DATA                        \
        INIT_SETUP(initsetup_align)                \
        INIT_CALLS                        \
        CON_INITCALL                        \
        SECURITY_INITCALL                    \
        INIT_RAM_FS                        \
    }

vmlinux.lds.S [arch\x86\kernel]

    ...
INIT_DATA_SECTION(16)
    ...

好了,找到这两个变量定义在vmlinux.lsd.h中,do_initcalls函数会调用在这两个变量之间定义的函数。这里有一个问题:这两个变量之间的*(.initcall*.init)中存放着什么函数调用呢??

在解决这个问题之前,我们先来总结一下上述的调用过程:

start_kernelàrest_initàkernel_thread(kernel_init)à do_basic_setupàdo_initcallsèfor (fn = __early_initcall_end; fn < __initcall_end; fn++)

下面我们来看解决上面的问题。我们从include\linux\init.h入手。先抓一些亮眼的东西。

init.h[include\linux]

#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 postcore_initcall(fn)        __define_initcall("2",fn,2)
#define postcore_initcall_sync(fn)    __define_initcall("2s",fn,2s)
#define arch_initcall(fn)        __define_initcall("3",fn,3)
#define arch_initcall_sync(fn)        __define_initcall("3s",fn,3s)
#define subsys_initcall(fn)        __define_initcall("4",fn,4)
#define subsys_initcall_sync(fn)    __define_initcall("4s",fn,4s)
#define fs_initcall(fn)            __define_initcall("5",fn,5)
#define fs_initcall_sync(fn)        __define_initcall("5s",fn,5s)
#define rootfs_initcall(fn)        __define_initcall("rootfs",fn,rootfs)
#define device_initcall(fn)        __define_initcall("6",fn,6)
#define device_initcall_sync(fn)    __define_initcall("6s",fn,6s)
#define late_initcall(fn)        __define_initcall("7",fn,7)
#define late_initcall_sync(fn)        __define_initcall("7s",fn,7s)

这里的定义貌似在__early_initcall_end变量定义的时候看到过啊,没错!凡是通过这些宏定义的其实都是放在__early_initcall_end__initcall_end之间的函数。我们来看看__define_initcall的具体定义。

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

这里出现了一个__attribute__,顺带讲一下,这个是GCC的扩展,上述的语句表示将fn函数放到名为”.initcall.level.init”section中,如果对于section不清楚的话,可以参考我的博文《ELF文件格式》,对其有个大概了解。

好了顺带讲一下另外一个宏定义:

#define module_init(x)    __initcall(x);

...
#define __initcall(fn) device_initcall(fn)

module_init这个宏相信在许多驱动程序中都是司空见惯的,其实它都指向device_initcall这个宏,也就是说会被放到initcall.6.init这个section中。

  • 1
  • 2
  • 3
  • 下一页
【内容导航】
第1页:系统初始化 第2页:协议栈各部分初始化1
第3页:协议栈各部分初始化2

相关内容