5.11 走进start_kernel尾声
中断体系建立起来后,虽然后面还有很多行代码,但是都是些比较好理解的初始化函数了,也就是说start_kernel进入尾声了。
5.11.1 初始化slab的后续工作
继续分析start_kenel的下一个函数,613行,profile_init函数,用于对系统剖析做相关初始化,系统剖析用于系统调用:
int __ref profile_init(void) { int buffer_bytes; if (!prof_on) return 0;
/* only text is profiled */ prof_len = (_etext - _stext) >> prof_shift; buffer_bytes = prof_len*sizeof(atomic_t);
if (!alloc_cpumask_var(&prof_cpu_mask, GFP_KERNEL)) return -ENOMEM;
cpumask_copy(prof_cpu_mask, cpu_possible_mask);
prof_buffer = kzalloc(buffer_bytes, GFP_KERNEL|__GFP_NOWARN); if (prof_buffer) return 0;
prof_buffer = alloc_pages_exact(buffer_bytes, GFP_KERNEL|__GFP_ZERO|__GFP_NOWARN); if (prof_buffer) return 0;
prof_buffer = vmalloc(buffer_bytes); if (prof_buffer) { memset(prof_buffer, 0, buffer_bytes); return 0; }
free_cpumask_var(prof_cpu_mask); return -ENOMEM; } |
函数无非就是初始化一些简单的全局变量。继续在start_kenel中前进,614~616行,根据irqs_disabled是否返回成功而打印一些信息。617行,由于CONFIG_PROVE_LOCKING没有被配置,所以early_boot_irqs_on是一个空函数。618行,local_irq_enable函数将打开可屏蔽中断,与549行的local_irq_disable遥相呼应。621行设置全局GFP常量gfp_allowed_mask,注释上写得很清楚,中断处理模块已经成型,所以可以进行GFP分配了。继续走,623行,调用kmem_cache_init_late函数。我们在“初始化内存管理”讲解的那个mm_init()中曾经调用过一个kmem_cache_init,用于初始化内核slab分配体系,还记得吧。那么这个加了_late后缀的函数是啥意思呢,它也来自mm/slab.c:
void __init kmem_cache_init_late(void) { struct kmem_cache *cachep;
/* 6) resize the head arrays to their final sizes */ mutex_lock(&cache_chain_mutex); list_for_each_entry(cachep, &cache_chain, next) if (enable_cpucache(cachep, GFP_NOWAIT)) BUG(); mutex_unlock(&cache_chain_mutex);
/* Done! */ g_cpucache_up = FULL;
/* Annotate slab for lockdep -- annotate the malloc caches */ init_lock_keys();
/* * Register a cpu startup notifier callback that initializes * cpu_cache_get for all new cpus */ register_cpu_notifier(&cpucache_notifier);
/* * The reap timers are started later, with a module init call: That part * of the kernel is not yet operational. */ } |
很简单,就是做几个slab分配器初始化的后续工作,其实就只做一件事,遍历cache_chain链表,把里面的所有作为slab缓存头的kmem_cache结构通过enable_cpucache函数重新计算他们的batchcount、limit和shared字段,并为NUMA体系中那些还没有初始化nodelists[node]的节点进行初始化工作。最后调用register_cpu_notifier函数,将全局变量cpucache_notifier挂到全局cpu_chain链中,具体的代码我就不去分析了。
5.11.2 启动console
回到start_kenel函数中,下一个函数630行的console_init(),用于初始化系统控制台结构。该函数执行后可调用printk()函数将log_buf中符合打印级别要求的系统信息打印到控制台上。还记得我们在讲printk函数的时候说过,这个函数是把字符串写到log_buf中,而启动了系统控制台后,这个log_buf中的信息就会显示在屏幕上方了。
void __init console_init(void) { initcall_t *call;
/* Setup the default TTY line discipline. */ tty_ldisc_begin();
/* * set up the console device so that later boot sequences can * inform about problems etc.. */ call = __con_initcall_start; while (call < __con_initcall_end) { (*call)(); call++; } } |
函数虽然简单,但涉及到的东西很多。首先看到tty_ldisc_begin函数:
void tty_ldisc_begin(void) { /* Setup the default TTY line discipline. */ (void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY); } |
tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY)这段代码主要的用途就是注册tty线路规程的,大家研究tty的驱动就会发现,在用户和硬件之间的tty驱动是分了三层的,其中最底层是tty驱动程序了,主要负责从硬件接受数据,和格式化上层发下来的数据后给硬件。
在驱动程序之上就是线路规程,他负责把从tty核心层或者tty驱动层接受的数据进行特殊的按着某个协议的格式化,就像是ppp或者蓝牙协议,然后在分发出去的。
在tty线路规程之上就是tty核心层。大家可参考ldd3学习一下。那么,如何初始化终端呢?这又要从编译说起了,看看我的vmlinux.lds.S中连接脚本汇编中有这段代码:
__con_initcall_start = .;
*(.con_initcall.init)
__con_initcall_end = .;
原来的call = __con_initcall_start就是把__con_initcall_start的虚拟地址给call,去执行在 __con_initcall_start = .;和__con_initcall_end = .;之间的con_initcall.init。这就应该是linux惯用做法,把某个实际初始化函数的指针数据是放到了con_initcall.init段中,那么是怎么放的呢?
在linux/init.h里面有这么一句宏定义:
#define console_initcall(fn) /
static initcall_t __initcall_##fn /
__used __section(.con_initcall.init) = fn
在我的Linux2.6.34.1的所有驱动程序里面,大约有48个文件调用了这个console_initcall宏,那么到底用的是哪个驱动程序呢?这得看看配置文件了,找到了CONFIG_SERIAL_8250