本篇文章分析的是一个经过精简后的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
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选项
另开一个shell
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之前,也可以在之后
通过设置断点来观察启动的过程。设置断点后输入c点enter,程序继续执行,到断点时停止。这时候可以输入list指令显示断点前后的代码。
然后继续执行,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