MOOC课程《Linux内核分析》——start_kernel();Idle进程与Init进程
2015-03-22 22:42
357 查看
许松,原创作品转载请注明出处。
《Linux内核分析》MOOC课程
Linux-Kernel源代码树的目录结构可以参见百度文库:Linux源代码目录树结构,这里不再赘述。我们当前主要关注arc/x86、init、kernel、ipc等。
linux内核启动相关的代码基本都在init目录下。内核初始化开始于init目录下的main.c文件中的start_kernel函数(位于第500行~第680行)。
首先初始化lockdep hash table(第3975行)。lockdep_hash_table用来解决OS中死锁问题的。lockdep的介绍可以参考文章:死锁检测模块lockdep简介以及arm linux启动流程二。
该句对初始进程(也就是0号进程)的内核栈进行了处理,STACK_END_MAGIC则是用来对内核栈溢出进行检测的。下图可以看到init_task的pid = 0。
这个函数是针对SMP硬件体系的,用来获取当前工作的CPU的ID。对于单核平台,该函数定义为空。
对调试对象(自旋锁以及静态对象池)进行初始化。
初始化堆栈保护的canary值,用来防止栈溢出攻击。
该函数进行控制组(control group)的早期初始化。control group的有概念可参考文章:Linux cgroup机制分析之框架分析
关闭当前CPU的所有中断响应。之后本CPU上的中断将会被关闭,直到所需的步骤完成之后才能再次开中断。
该函数获取当前工作的CPU的ID,并将其应用到当前环境中,也就是激活当前CPU。
初始化高端内存映射表。
该函数位于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通知链中。
对应前面的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内核的运行时行为,以便于进行故障调试或者性能分析。
在上面的过程中我们并没有发现有关新进程创建的步骤,那么这个步骤就只能在最后一个函数调用中产生了:
该函数的函数体同样位于main.c中:
让我们略过RCU(RCU是一种数据同步的方式,详解可参见文章:linux内核 RCU机制详解。)的启动等一系列动作,直奔与pid相关的kernel_thread函数:
该函数调用了do_fork函数:
我们需要注意的是这两个函数的执行过程,这可以与我们在上节课中学习到的知识进行印证:
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函数在此之后又调用了下面的代码:
代码注释已经说得很明白,最后的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函数分析
《Linux内核分析》MOOC课程
Linux-Kernel源代码树的目录结构可以参见百度文库:Linux源代码目录树结构,这里不再赘述。我们当前主要关注arc/x86、init、kernel、ipc等。
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函数分析
相关文章推荐
- MOOC课程《Linux内核分析》——start_kernel();Idle进程与Init进程
- 从start_kernel到init进程启动 《Linux内核分析》笔记
- MOOC-Linux内核lab3 调试内核从start_kernel到init进程启动
- 跟踪内核从start_kernel到init进程启动
- Linux系统启动分析-从start_kernel到init进程的启动
- start_kernel到init进程启动的过程
- kernel 启动过程之四,start_kernel中的rest_init函数到init进程
- 跟踪分析Linux内核的启动过程(start_kernel到init进程启动)
- Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动
- Linux内核分析之三——使用gdb跟踪调试内核从start_kernel到init进程启动
- 使用gdb跟踪调试内核从start_kernel到init进程启动
- (作业3)Linux内核的启动过程(从start_kernel到init进程启动)
- start_kernel函数及init进程创建的简单分析
- 《Linux内核分析》MOOC课程之从迷你Linux内核角度理解进程时间轮片调度(未完)
- start_kernel到init进程启动的过程
- linux启动流程(从start_kernel中的rest_init函数到init进程(1))
- 构建一个简单的Linux系统 MenuOs —— start_kernel到init进程(20135304刘世鹏)
- 读核笔记-内核初始化-从start_kernel到init
- kernel 启动过程之三, start_kernel()函数 概叙!init/main.c
- Linux内核源码分析(六)--start_kernel之lockdep_init