本篇文章分析的是一个经过精简后的Linux系统MENUOS,通过对idle进程、1号进程的分析。来说明系统中进程的启动过程。
相关知识
首先关于这篇文章会介绍一些用到的知识。
一、什么是中断的上下文和进程的上下文。在这里大家很容易混淆这两个概念。先看下面这句话。
处理器总处于以下状态中的一种:
1、内核态,运行于进程上下文,内核代表进程运行于内核空间;
2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;
3、用户态,运行于用户空间。
对于上面的话,我觉得可以理解为程序本身在执行的时候由于内陷指令、读数据或者其他的原因,导致进入到内核态,那么这时候保存的就是进程上下文。如果在内核态下由于硬件等的原因,导致的中断,保存的就是中断上下文,其中包含硬件的信息等。
这是另一个解释:陷入(或异常)到内核时,此时内核代表某个进程运行,一般要访问进程的数据结构,此时的上下文称进程上下文。中断时,内核不代表任何进程运行,一般不访问当前进程的数据结构,此时的上下文称中断上下文。
这里有兴趣的同学可以参考什么是进程上下文,什么是中断上下文。
而这两者的差别是进程上下文可以睡眠,阻塞,但是中断上下文不行。(请参考再思linux内核在中断路径内不能睡眠/调度的原因(2010)和关于中断上下文为什么不能睡眠?)
二、idle进程的相关知识简单的说idle是一个进程,其pid号为 0。其前身是系统创建的第一个进程,也是唯一一个没有通过fork()产生的进程。
这里用到的代码可以在一下地址知道:Latest Stable Kernel:linux-3.18.6
首先使用自己的Linux系统环境搭建MenuOS的过程:
# 下载内核源代码编译内核 cd ~/LinuxKernel/ wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz xz -d linux-3.18.6.tar.xz tar -xvf linux-3.18.6.tar cd linux-3.18.6 make i386_defconfig make # 一般要编译很长时间,少则20分钟多则数小时 # 制作根文件系统 cd ~/LinuxKernel/ mkdir rootfs git clone https://github.com/mengning/menu.git # 如果被墙,可以使用附件menu.zip cd menu gcc -o init linktable.c menu.c test.c -m32 -static –lpthread cd ../rootfs cp ../menu/init ./ find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img # 启动MenuOS系统 cd ~/LinuxKernel/ qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
重新配置编译Linux使之携带调试信息
在原来配置的基础上,make menuconfig选中如下选项重新配置Linux,使之携带调试信息
kernel hacking—> [*] compile the kernel with debug info
然后就可以进行下一步:
打开shell
cd LinuxKernel/ qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img使用gdb跟踪调试内核
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明: # -S freeze CPU at startup (use ’c’ to start execution) # -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
gdb (gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表 (gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行 (gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
然后继续执行,menuOS加载完成,系统现在提供有三个指令help、version、quit。
现在我们就可以用gdb的调试断点来帮助我们跟踪和调试系统了。
我们可以简单的来看一下:
在main.c中首先将各种变量进行初始化,这些变量一般是宏静态定义。
然后执行asmlinkage __visible void __init start_kernel(void)这个函数,在其中的有一个set_task_stack_end_magic(&init_task);函数,这个函数中该结构体(init_task)在linux启动时被设置为current_task。(此时idle进程已经启动)
在 /linux-3.18.6/arch/x86/kernel/cpu/common.c中:
DEFINE_PER_CPU(struct task_struct *, current_task) ____cacheline_aligned = &init_task;
然后对其他的信息也进行初始化。接着进行到了rest_init();这个地方。
static noinline void __init_refok rest_init(void) 394{ 395 int pid; 396 397 rcu_scheduler_starting(); 398 /* 399 * We need to spawn init first so that it obtains pid 1, however 400 * the init task will end up wanting to create kthreads, which, if 401 * we schedule it before we create kthreadd, will OOPS. 402 */ 403 kernel_thread(kernel_init, NULL, CLONE_FS); 404 numa_default_policy(); 405 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 406 rcu_read_lock(); 407 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); 408 rcu_read_unlock(); 409 complete(&kthreadd_done); 410 411 /* 412 * The boot idle thread must execute schedule() 413 * at least once to get things moving: 414 */ 415 init_idle_bootup_task(current); 416 schedule_preempt_disabled(); 417 /* Call into cpu_idle with preempt disabled */ 418 cpu_startup_entry(CPUHP_ONLINE); 419}
当初始化到rest_init函数中时调用kernel_thread(kernel_init, NULL, CLONE_FS);函数启动第一个内核线程kernel_init。由kernel_init再通过do_execve启动/sbin/init。这就是我们看到的init进程,进程号为1。初始化完成后linux调用scheule整个系统就运行起来了。
结论:
idle是一个进程,其pid为0。是Linux引导中创建的第一个进程,完成加载系统后,演变为进程调度、交换及存储管理进程。主处理器上的idle由原始进程(pid=0)演变而来。从处理器上的idle由init进程fork得到,但是它们的pid都为0。Idle进程为最低优先级,且不参与调度,只是在运行队列为空的时候才被调度。Idle循环等待need_resched置位。1号进程是init 进程,由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程
备注:
杨峻鹏 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000