您的位置:首页 > 运维架构 > Linux

MOOC课程《Linux内核分析》——start_kernel();Idle进程与Init进程

2015-03-22 22:42 357 查看
许松,原创作品转载请注明出处。

《Linux内核分析》MOOC课程

Linux-Kernel源代码树的目录结构可以参见百度文库:Linux源代码目录树结构,这里不再赘述。我们当前主要关注arc/x86initkernelipc等。

linux内核启动相关的代码基本都在init目录下。内核初始化开始于init目录下的main.c文件中的start_kernel函数(位于第500行~第680行)。

lockdep_init();


首先初始化lockdep hash table(第3975行)。lockdep_hash_table用来解决OS中死锁问题的。lockdep的介绍可以参考文章:死锁检测模块lockdep简介以及arm linux启动流程二

set_task_stack_end_magic(&init_task);
// 该函数对应源码是:
// void set_task_stack_end_magic(struct task_struct *tsk)
// {
//     unsigned long *stackend;

//     stackend = end_of_stack(tsk);
//     *stackend = STACK_END_MAGIC; /* for overflow detection */
// }
// end_of_stack(p)是一个宏:
// #define end_of_stack(p) (unsigned long *)((void *)(p) + IA64_RBS_OFFSET)


该句对初始进程(也就是0号进程)的内核栈进行了处理,STACK_END_MAGIC则是用来对内核栈溢出进行检测的。下图可以看到init_task的pid = 0。



smp_setup_processor_id();//函数体为空,不做工作


这个函数是针对SMP硬件体系的,用来获取当前工作的CPU的ID。对于单核平台,该函数定义为空。

debug_objects_early_init();


对调试对象(自旋锁以及静态对象池)进行初始化。

boot_init_stack_canary();


初始化堆栈保护的canary值,用来防止栈溢出攻击。

cgroup_init_early();


该函数进行控制组(control group)的早期初始化。control group的有概念可参考文章:Linux cgroup机制分析之框架分析

local_irq_disable();
early_boot_irqs_disabled = true;


关闭当前CPU的所有中断响应。之后本CPU上的中断将会被关闭,直到所需的步骤完成之后才能再次开中断。

boot_cpu_init();
// 函数对应源码是:
// static void __init boot_cpu_init(void)
// {
//     int cpu = smp_processor_id();
//     /* Mark the boot cpu "present", "online" etc for SMP and UP case */
//     set_cpu_online(cpu, true);
//     set_cpu_active(cpu, true);
//     set_cpu_present(cpu, true);
//     set_cpu_possible(cpu, true);
// }


该函数获取当前工作的CPU的ID,并将其应用到当前环境中,也就是激活当前CPU。

page_address_init();


初始化高端内存映射表。

setup_arch(&command_line);


该函数位于arch/x86/kernel/setup.c中。对于不同的架构,Linux提供了不同的内核初始化方式。

接下来内核进行以下动作:

mm_init_owner(&init_mm, &init_task) 初始化内核本身是用的内存管理结构体系。init_mm定义了整个kernel的内存空间。

mm_init_cpumask(&init_mm) 每一个任务都有一个mm_struct结构来管理内存空间,init_mm是内核的mm_struct。

setup_command_line(command_line); 对cmdline进行备份和保存。

setup_nr_cpu_ids() 设置有多少个nr_cpu_ids结构。

setup_per_cpu_areas() 为系统中的每一个CPU的per_cpu变量申请空间,同时拷贝初始化数据

smp_prepare_boot_cpu(); 为SMP系统里引导CPU(boot-cpu)进行准备工作。在ARM系统中是空函数。

build_all_zonelists(NULL,NULL) 设置内存管理相关的node(每个CPU一个内存node)和其中的Zone,以完成内存管理子系统的初始化,并设置bootmem分配器 。

page_alloc_init() 设置内存页分配器。

parse_early_param() 解析cmdline中的启动参数。

jump_label_init() …待查找

setup_log_buf(0) 使用bootmem分配一个记录启动信息的缓冲区。

pidhash_init() 进程ID的HASH表初始化,用bootmem分配并初始化PID散列表。由pid分配器管理空闲和已指派的pid。

vfs_caches_init_early() 前期虚拟文件系统(vfs)的缓存初始化。

sort_main_extable() 对内核异常表按照异常向量号大小排序以加速访问。

trap_init() 对内核异常进行初始化。ARM系统中是空函数。

mm_init() 标记那些内存可以使用,并高度系统有多少内存可以使用(除了内核已使用内存)

sched_init() 对进程调度器的数据结构进行初始化,创建运行队列,设置当前任务的空闲线程,当前
4000
任务的调度策略为CFS。

preempt_disable() 关闭优先级调度。

idr_init_cache() 为IDR机制分配缓存。

rcu_init() 初始化直接读拷贝更新的锁机制。RCU(Read_Copy_Upate)。

radix_tree_init() 内核radis树算法初始化。

early_irq_init() 前期外部中断描述符初始化。

init_IRQ() 对应架构特定的中断初始化函数。

prio_tree_init() 初始化内核基于radix树的优先级搜索树(PST),初始化结构体。

init_timers() 初始化引导CPU的时钟相关的数据结构,注册时钟的回调函数,当时钟到达时可以回调时钟处理函数,最后初始化时钟软件中断处理。

hrtimers_init() 初始化高精度的定时器,并设置回调函数。

softirq_init() 初始化软中断。

timekeeping_init() 初始化系统时钟计时,并且初始化内核里与时钟计时相关的变量。

time_init() 初始化系统时钟。

sched_colock_postinit()

perf_event_init() CPU性能监视机制初始化。

profile_init() 分配内和性能统计保存的内存,以便统计的性能变量可以保存到这里。

call_function_init() 初始化所有CPU的call_single_queue,同时注册CPU热插拔通知函数到CPU通知链中。

early_boot_irqs_disabled = false;
local_irq_enable();


对应前面的loacl_irq_disable(),打开CPU的中断。

kmem_cahce_init_late() 内核启动时使用临时内存分配器bootmem,之后由slab接管。kmem_cache_init_late()初始化了slab分配器(内核高速缓存分配器),这意味着bootmem的结束,同时内核的内存管理系统正式开始工作。bootmem的分配必须先于kmem_cache_init_late()。

console_init() 初始化控制台,从这个函数之后就可以输出内容到控制台了。在此之前的输出保存在输出缓冲区中,这个函数被调用之后就马上把之前的内容输出出来。

lockdep_info() 打印依赖锁的信息,用来调试锁。

locking_selftest() 测试锁的API是否使用正常,进行自我检测。

page_cgroup_init() 初始化容器组的页面内存分配。mem_cgroup是cgroup体系中提供的用于memory隔离的功能。

debug_objects_mem_init()

kmemleak_init() 内核内存泄露检测机制初始化。

setup_per_cpu_pageset() 创建每个CPU的高速缓存几何数组并初始化。

numa_policy_init() 初始化NUMA的内存访问策略。NUMA,NonUniform Memory AccessAchitecture(非一致性内存访问)的缩写,主要用来提高多个CPU的内存访问速度。多个CPU访问同一个节点的内存速度比访问多个节点的内存速度大得多。

sched_clock_init() 对每个CPU进行系统进程调度时钟初始化。

calibrate_delay() 主要计算CPU需要校准的时间。

pidmap_init() 进程位图初始化。

anon_vam_init() 初始化EFI的接口,并进入虚拟模式。

acpi_early_init()

cred_init() 线程信息的缓存初始化。

fork_init(totalram_pages) 根据当前物理内存计算出来可以创建进程(线程)的最大数量,并进行进程环境初始化,为task_struct分配空间。

proc_caches_init() 进程缓存初始化。

buffer_init() 初始化文件系统的缓冲区,并计算最大可以使用的文件缓存。

key_init() 初始化内核安全键管理列表和结构,内核秘钥管理系统。

security_init() 初始化内核安全管理框架,以便提供文件\登陆等权限。

dgb_late_init() 内核调试系统后期初始化。

vfs_caches_init(totalram_pages) 虚拟文件系统进行缓存初始化,提高虚拟文件系统的访问速度。

signals_init() 初始化信号量队列缓存。

page_writeback_init() 页面写机制初始化

proc_root_init() 初始化系统进程文件系统,主要提供内核与用户进行交互的平台,方便用户实时查看进程的信息。

cgroup_init() 进程控制组正式初始化,主要用来为进程和它的子进程提供性能控制。

cpuset_init() 初始化CPUSET。CPUSET主要为控制组提供CPU和内存节点的管理的结构。

taskstats_init_early() 任务状态早期初始化,为结构体获取高速缓存,并初始化互斥机制。

delayacct_init() 任务延迟机制初始化,初始化每个任务延时计数。当一个任务等待CPU或者IO同步的时候,都需要计算等待时间。

check_bugs() 检查CPU配置、FPU等是否非法使用不具备的功能,检查CPU BUG,软件规避BUG。

sfi_init_late() SFI初始程序晚期设置函数

ftrace_init() 功能跟踪调试机制初始化,初始化内核跟踪模块。ftrace的作用是帮助开发人员了解Linux内核的运行时行为,以便于进行故障调试或者性能分析。

在上面的过程中我们并没有发现有关新进程创建的步骤,那么这个步骤就只能在最后一个函数调用中产生了:

rest_init();


该函数的函数体同样位于main.c中:

static noinline void __init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
kernel_thread(kernel_init, NULL, CLONE_FS);
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);
init_idle_bootup_task(current);
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE);
}


让我们略过RCU(RCU是一种数据同步的方式,详解可参见文章:linux内核 RCU机制详解。)的启动等一系列动作,直奔与pid相关的kernel_thread函数:

pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)


该函数调用了do_fork函数:

long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)


我们需要注意的是这两个函数的执行过程,这可以与我们在上节课中学习到的知识进行印证:

kernel_thread函数将它的第一个参数函数指针传递给了do_fork作为第二个参数;

do_fork函数依次调用了copy_process函数、wake_up_new_task函数来fork一个新的进程。

当执行完语句:pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);之后,就可以看到新进程的pid的值了:



然而rest_init函数在此之后又调用了下面的代码:

/*
1. The boot idle thread must execute schedule()
2. at least once to get things moving:
*/
init_idle_bootup_task(current);
schedule_preempt_disabled();
/* Call into cpu_idle with preempt disabled */
cpu_startup_entry(CPUHP_ONLINE);


代码注释已经说得很明白,最后的cpu_startup_entry函数是idel进程相关的。这个函数调用了另外两个函数:

arch_cpu_idle_prepare(void);

cpu_idle_loop();

由于parch_cpu_idle_prepare是一个空函数,故而重点在于cpu_idle_loop函数。

从源代码中我们可以看到这个函数就是一个while(1)的循环,但是这与0号进程有什么关系呢?

让我们回到函数init_idle_bootup_task(current):



准备给idle进程的task_struct正是0号进程的task_struct!

至此,我们可以看到,系统中常驻的Idle进程实际上就是0号进程,它是在内核初始化过程中生成的最为原始的进程,并且很显然是一个纯正的内核进程。它在内核初始化结束的最后阶段使用do_fork来生成出1号进程,并作为Idle进程常驻内存。

参考:Start_kernel函数分析
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: