Linux TCP/IP协议栈学习
(一)系统初始化
Daniel Wood 20110307
转载时请注明出处和作者
文章出处:http://danielwood.cublog.cn
作者:Daniel Wood
---------------------------------------------------------------------------
写在前面的话:前些日子在网上下到一本《Linux TCP/IP 协议栈分析》,自己本来对协议比较感兴趣,所以慢慢地看起,希望把这本数一张一张地看清楚,搞明白,因为大学以后就没有认认真真地看过书了,希望自己能坚持下去。
注:《Linux TCP/IP 协议栈分析》的源代码版本是2.6.18,我的源代码版本是2.6.36。下文中不注明版本的都为2.6.36,《书》指《Linux TCP/IP 协议栈分析》。
1. 系统初始化
一切的一切还是从最源头说起,那就是系统的启动,不过这里的启动并不是从加电的一瞬间开始,而是从init/main.c的start_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版本把i386和x86_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下。这个头文件是通用的,其他的平台如arm的vmlinux.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.c的do_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中。
阅读(2676) | 评论(0) | 转发(0) |