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次阅读