linux启动过程分析
在描述linux系统启动过程之前,先上一张著名的图:
这张图为我们的linux系统启动之后,物理内存的前几兆字节的布局情况。我们可以通过查看文件/proc/iomen获得更详细的物理内存布局图,在我的系统中,/proc/iomen文件内容如下:
peter@initroot:/boot$ sudo cat /proc/iomem
[sudo] password for peter:
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c7fff : Video ROM
000e2000-000e2fff : Adapter ROM
000f0000-000fffff : Reserved
000f0000-000fffff : System ROM
00100000-dffeffff : System RAM
d2600000-d32031d0 : Kernel code
d32031d1-d3c6a5bf : Kernel data
d3ee2000-d413dfff : Kernel bss
dfff0000-dfffffff : ACPI Tables
e0000000-fdffffff : PCI Bus 0000:00
f0000000-f0ffffff : 0000:00:02.0
f0000000-f0ffffff : vmwgfx probe
f1000000-f11fffff : 0000:00:02.0
f1000000-f11fffff : vmwgfx probe
f1200000-f121ffff : 0000:00:03.0
f1200000-f121ffff : e1000
f1400000-f17fffff : 0000:00:04.0
f1400000-f17fffff : vboxguest
f1800000-f1803fff : 0000:00:04.0
f1804000-f1804fff : 0000:00:06.0
f1804000-f1804fff : ohci_hcd
f1805000-f1805fff : 0000:00:0b.0
f1805000-f1805fff : ehci_hcd
f1806000-f1807fff : 0000:00:0d.0
f1806000-f1807fff : ahci
fec00000-fec00fff : Reserved
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
fee00000-fee00fff : Reserved
fffc0000-ffffffff : Reserved
100000000-15fffffff : System RAM
很多人在用cat或者vi查看这个文件的时候,发现前面的地址值都是清一色的0,不知道为什么?是时候展现真正的技术了,只需要在cat或者vi前面加上sudo就好了,
可能这是linux比较隐私的部位,没有点特殊权限是不会让你看的。另外,上面这张图是在32位系统上的布局图。如果你想深入研究linux的内核原理,建议在32位系统上,
这样在查看内存布局方面比较方便。如果只是“使用”linux,可以直接用64位的。
在形成上图这样的布局之前,系统做了哪些工作呢? 这里再上一张图
这张图摘自《linux内核完全剖析-基于0.12内核》,在该书中详细描述了内核启动加载的详细过程,不过这本书因为描述的是0.12版本内核,所以有些内容和最新的内核加载过程有些不同。
在0.12系统中,由于内核小于640kb,所以被加载到0-640k空间内。而我们在上图中看到,内核是从1M开始的内存区域开始加载的。
这是因为,在后来版本的linux内核大小超过了640kb,如果加载到从4kb开始的640kb空间里,就会占用后面显示缓冲区和各种ROM区域,该部分区域是由系统保留的,
用于映射系统BIOS和显卡ROM。而内核必须被装载到一个连续的内存区中,所以只能从1M开始的位置进行加载。这里注意BIOSROM和显卡ROM是和内存RAM统一编址的。
首先,在我们启动电源的那一刻,cpu首先跳转到内存0xFFFFFF0处读取指令并执行,该地址位置正是BIOS ROM的映射区,存放的是BIOS的代码。
系统执行BIOS用于各个硬件子系统的自检和初始化。执行完BIOS后,我们的硬盘等设备都已经处于可用状态了,系统就会去读取第一个可启动设备(通常就是硬盘)的第一个扇区,
也就是我们经常说的MBR,这里存放的就是用于加载内核的bootloader,现在基本都是grub了。grub会将我们的linux内核加载到内存中,那么grub是从哪里加载linux内核的呢?
当然是从硬盘加载到内存了。我们的linux内核映像文件就放在硬盘/boot分区里。在我的系统中,/boot目录如下:
peter@initroot:~$ cd /boot/
peter@initroot:/boot$ ls -al
total 69608
drwxr-xr-x 3 root root 4096 Dec 23 10:43 .
drwxr-xr-x 23 root root 4096 Dec 23 10:43 ..
-rw-r--r-- 1 root root 217278 Jun 24 17:39 config-4.15.0-54-generic
drwxr-xr-x 5 root root 4096 Dec 17 20:50 grub
-rw-r--r-- 1 root root 58128186 Dec 23 10:43 initrd.img-4.15.0-54-generic
-rw-r--r-- 1 root root 182704 Jan 28 2016 memtest86+.bin
-rw-r--r-- 1 root root 184380 Jan 28 2016 memtest86+.elf
-rw-r--r-- 1 root root 184840 Jan 28 2016 memtest86+_multiboot.bin
-rw------- 1 root root 4051606 Jun 24 17:39 System.map-4.15.0-54-generic
-rw-r--r-- 1 root root 8294136 Jul 29 19:13 vmlinuz-4.15.0-54-generic
其中vmlinuz-4.15.0-20-generic这个文件就是grub将会加载到内存中的linux内核映像文件了,这里采用的是压缩的形式存放的,所以加载到内存中还需要解压缩。
而grub目录中存放的是grub的配置文件,grub被加载到内存中,首先会读取该目录中的配置文件,所以如果你想对grub进行配置,就可以在这个目录里修改配置文件。
注意,该目录中存放的是配置文件,并不是grub的可执行代码文件,grub的可执行代码我们刚才已经说了,是存放在硬盘的第一个扇区中,这个是在安装系统的时候写入该扇区的。
在该目录中,值得关注的还有一个initrd.img-4.15.0-20-generic文件,这个文件是用来做什么的呢?我们知道,内核在启动的过程中,要先挂载根目录,挂载根目录肯定要能识别硬盘,
而识别硬盘需要硬盘设备驱动程序,而我们的linux为了减少自己的体积,并不会将设备驱动程序编译到自己的内核映像中,而是将大部分设备驱动程序编译成单独的模块,
动态的加载设备驱动程序。模块文件一般都放置在/lib/moduls/目录下。这时候就出现了一个问题了,linux要挂载硬盘,首先需要能识别硬盘,需要硬盘设备驱动程序,
而设备驱动程序又在硬盘的/lib/moduls/目录下,而此时硬盘还没有挂载,就无法取得该目录,无法取得该目录就没法加载设备驱动程序,没有设备驱动程序,就无法加载硬盘,
好像陷入了无法解决的死循环。Linux内核的开发者们,当然不允许这种互相踢皮球的现象存在,于是想出了一个办法,那就是虚拟文件系统RAM disk,
也就是我们看到的这个文件initrd.img-4.15.0-20-generic,该文件也会在系统启动的过程中由boot loader(grub)加载,加载到内存中解压缩后,会临时充当linux的根文件系统,
该文件系统中存放了linux所需设备的驱动程序。我们可以查看该文件的内容,就是一个比较完整的跟文件系统。具体的查看方式,这里就不详细阐述了。
/boot目录下还有几个文件,config是内核在编译阶段的一些配置选项,system.map为系统的符号在虚拟地址空间的地址。
到这里我们的内核就加载到内存中了,并开始执行,内核会在执行一系列的初始化操作后,变成0号idle进程,0号进程会启动一个叫kthreadd的内核守护线程,
由该线程启动一系列的内核线程,最后0号进程会启动我们的第一个用户层进程init,目前大部分发行版的init进程执行的是systemd了。我们用ps -ef可以查看系统中启动的进程,
在我的系统中执行ps -ef命令如下:
[root@initroot ~]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Oct30 ? 00:02:55 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
root 2 0 0 Oct30 ? 00:00:00 [kthreadd]
...省略...
root 501 1 0 Oct30 ? 00:00:09 /usr/sbin/crond -n
root 506 1 0 Oct30 ? 00:00:00 /usr/sbin/atd -f
root 530 1 0 Oct30 ttyS0 00:00:00 /sbin/agetty --keep-baud 115200,38400,9600 ttyS0 vt220
...省略...
root 3311 2 0 Dec20 ? 00:00:02 [kworker/u2:2]
root 7393 1 0 Dec21 ? 00:00:00 npm
root 7404 7393 0 Dec21 ? 00:00:11 node /home/wwwroot/default/kblog/node_modules/.bin/nuxt start
apache 8272 10351 0 Dec05 ? 00:01:13 php-fpm: pool www
...省略...
其中COMMAND列中带中括号的就是系统的内核线程,剩下的就是系统启动的用户层进程。其实他们本质上都是一样的,都是linux进程,只是一个是在内核空间执行,
另一个是在用户空间执行的。通过pid和ppid我们也能看出,所有内核守护线程都是由kthreadd启动的,而kthreadd的父进程id号是0。而我们看到pid号为1的进程就是我们的init进程,
他同样也是由0号进程启动的。通过COMMAND列我们看到init进程启动的是/sbin/init这个可执行文件,通过ls
-al发现,该文件正是systemd的链接,所以我们的init进程其实就是systemd了。
systemd正是用户空间所有进程的父进程了,所有的用户空进进程都是由他来启动的,我们也可以通过pstree命令直观的看到系统中进程的父子关系:
[root@initroot ~]# pstree
systemd─┬─AliYunDun───22*[{AliYunDun}]
├─AliYunDunUpdate───3*[{AliYunDunUpdate}]
├─2*[agetty]
├─aliyun-service───2*[{aliyun-service}]
├─atd
├─auditd───{auditd}
├─crond
├─dbus-daemon
├─dhclient
├─mysqld_safe───mysqld───18*[{mysqld}]
├─nginx───nginx
├─npm─┬─node───10*[{node}]
│ └─10*[{npm}]
├─ntpd
├─php-fpm───15*[php-fpm]
├─polkitd───6*[{polkitd}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd─┬─4*[sshd───sftp-server]
│ └─sshd───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
└─tuned───4*[{tuned}]
我们通过pstree发现系统中所有的进程都是由systemd启动的,由于我是通过ssh远程登录的linux,所以也可以看到bash是由sshd启动的,这个也就是我在前几篇文章重点讨论的shell了。
再往下就是systemd的启动过程了,我不打算在这篇文章中详细讨论了,我会在下一篇尝试探讨systemd的启动过程。为什么说用尝试探讨,因为我发现,很多东西,
懂了跟写出来是完全不一样的,就像这篇文章,一开始我以为写个启动过程会很容易,但是写着写着就会发现,内容实在是太庞大了,很多东西即使弄懂了,
但是把它写出来好像也没有那么容易,所以只能说尝试了。
结合上一篇文章
《linux shell环境配置文件》
,到这里,基本上把linux启动过程的方面方面都涉及到了,但是不可能在一篇文章中讨论所有的细节。
比如linux内核的启动过程,只是几句话带过了,建议看看《linux内核设计与实现》和《linux内核架构》,这两本书都对启动过程有详细的讨论。本文权当是给各位抛砖引玉了。
100次点赞
100次阅读