linux启动过程分析


在描述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内核在内存中的布局 这张图摘自《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次阅读