大家好,我是飞哥!
不知道大家有没有产生过一个疑问:从给 Linux 服务器按下开机电源按钮后到启动成功的一段时间里,在这中间 Linux 操作系统都做了哪些事情?
在 Linux 服务器没有通电的时候,操作系统还只是躺在硬盘启动区中的一段程序,CPU 没有工作,内存也没有启动。但是在你按下开机键后,Linux 服务器内部就开始变得热闹了起来。一个电子世界被你激活了。经过几十秒或者几分钟的等待,Linux 服务器就可以使用了。
但这中间操作系统都干了啥呢?我有一位朋友叫闪客,就是因为对这个问题的好奇,花了两年时间写了 50 多篇文章,并还总结成了一本书。叫做《Linux 源码趣读》。这周,这本新书正式印刷出炉了。
对于 Linux 启动过程,代码的运行在书中总结成下面一张图来表示。但分了 50 小节来讲述每一个步骤中的细节。全书共 400 多页,250 多张精美的图解,五大部分,从头到尾把 Linux 0.11 全部核心代码讲述的一清二楚
当你按下电源后,在主板上提前写死的固件程序 BIOS 会将硬盘启动区中的 512 字节 的数据原封不动地复制到内存中的 0x7c00 这个位置。
然后 CPU 开始跳转到这个位置开始启动引导执行。接着 CPU 的数据段寄存器 ds 、代码段集寄存器 cs 和 栈段寄存器 ss、栈基址寄存器 sp 都会进行初始化。接着除了引导区的 512 字节外的 200 多个扇区的内核代码都会被加载到内存中。
再接着内核会设置全局描述符表 GDT 也会被初始化。全局描述符表入口位置保存在 gdtr 寄存器中,是为了后面逻辑地址转化成物理地址时使用的。接着进入保护模式、开启分页机制后会进入到内核的 main 函数中。
是的,内核也有一个 main 函数。这个 main 函数非常重要,把操作系统的整个骨架都勾勒出来了。
代码语言:javascript复制void main(void) {
......
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
move_to_user_mode();
if (!fork()) {
init();
}
for(;;) pause();
}
可见 main 函数中包含了很多 init 函数。其中每一个 init 函数都对应着操作系统某个模块的初始化过程。具体包括内存模块初始化 mem_init、中断初始化 trap_init、块设备初始化 blk_dev_init、进程调度初始化 sched_init、硬盘初始化 hd_init 等。
- 内存初始化是在内存中准备了一个数组。这个数组中的每一个元素是用来表示该页面是被使用了还是空闲。将所有的物理页都管理了起来。
- 进程调度初始化中初始化了一个 task_struct 数组。每一个元素将来都会表示一个进程。并设置了时钟中断,将来用作触发调度。
- 硬盘初始化后,内核打开允许硬盘控制器发送中断请求信号。
- ......
上面描述的这些都是操作系统的第 0 号内核线程处理的。在操作系统中,0号内核线程是所有进程祖先,是操作系统Linux在初始化阶段从无到有的第一个内核线程
接着最后会切换到用户态模式。并通过 fork 系统调用创建一个 1 号内核线程。并在这个新的进程中继续进行初始化。这个新内核线程中 中会将眼影盘的基本信息(磁头数、柱面数、扇区数)存到一个内核数组中。还会加载根文件系统。顺着 inode 可以找到所有文件。通过 open 系统调用打开了 /dev/tty0,为进程设置好了 0 号、1 号、2号文件描述符。这就是 Linux 的标准 IO:stdin、stdout、stderr。
后面创建的所有进程也好线程也罢,都会继承进程 1 的 0 号、1 号、2号文件描述符。可以使用这 3 个标准 IO。
内核线程 1 还会再创建一个新的内核线程 2,并加载 /bin/sh 进程的代码。这就是 shell 程序,它用来接收用户的命令。也会展示我们熟悉的 shell 画面就展示出来了。
以上是我对闪客这本书中知识的一个概括。不过篇幅所限,不能把技术细节讲清楚。