也许你会好奇Linux是如何启动的?本文围绕Linux如何启动起来展开一些介绍。本文涉及grub、uboot、initrd、根文件系统、设备树、以及Linux内核编译等内容。
对那些好奇系统是如何启动的人本文非常适合,当然对于由于涉及操作系统的方方面面,bsp的开发人员也有点价值,但是这里没有对应用做介绍;本文讨论两种平台下的启动方式,因为它们均是对应体系架构下的典型。
1、 通用的PC平台架构(X86)
2、 嵌入式平台架构(ARM,S3C2440)
本文可能一些实验性的操作,如grub、内核编译等,建议这些动手也做一下,能够加深对这一过程的理解。
对于存储器、磁盘、CPU具有一定的了解会比较好,不过这不是这里的重点,并且会稍微提及一下,所以不必担心成为障碍。 对于uboot、BIOS、grub、vmlinuz、initrd有点认识会更好,但是对操作系统是需要有个概念性的认识,很多基本概念性的没有在这里讲解了。当然,如果你还不会C语言,那就别向下看了,赶紧学学C吧,有点汇编、编译、链接的知识会更好,对Makefile、Kconfig、shell如果也知道那就更好。
PC下我的Linux系统:
Fedora9, 桌面版 内核版本2.6.25;VMware9下虚拟机。
www.kerenel.org下载Linux-3.10.0,这里将使用Linux-3.10.0升级Fedora 9内核。
所有的故事从升级Linux内核开始,首先,让我们来配置Linux内核,解压Linux内核,我解压的目录为/usr/src/下,不过我可不建议你解压到该目录,Linux内核的Header文件链接到该目录下,尽管该版本下不会带来大的问题,但是建议不要“污染”了这个目录。
步骤一:然后使用menuconfig,生成配置文件。
cd/usr/src/linux-3.X
步骤二:编译内核目标文件。
make {O=/home/name/build/kernel}menuconfig
步骤三:编译module和内核。
make {O=/home/name/build/kernel}
步骤四:安装module和内核以及启动必备文件
sudo make {O=/home/name/build/kernel}modules_install install
经过如上四个步骤,以root权限reboot系统,进入系统,如果在编译和链接时想看看详细的执行过程可以在步骤三中使用makeV=1 all该命令让编译时输出更多的过程信息。
后面会将启动代码和启动输出的信息进行关联分析,这样的认识会更深刻些;如果您手头资源有限或者时间等原因没能完成这个步骤,那么还有一个补救措施,笔者将上述的2.6和3.10内核的两个版本的启动显示信息录制了一个视频,该视频下载网址:XXX
如果您没有自己升级,那么我强烈建议您看一下,总时间不到十分钟,您就可以看完2.6和3.10两个版本启动的过程了。编译命令中大括号里的内容可选,这里就是输出路径设置,可以不关心,这和启动没有关系,升级完该放下它,后面会再提及它的,bios可能大家都听说过,PC启动的最先代码执行的就是这里的代码,第二个执行的代码是grub或者lilo,grub会将initrd和内核拷贝到内存中,然后进行解压并执行内核。
首先看一下在2.6内核版本下启动时和Linux相关的一些文件。
[root@shichaog boot]# ls -lt
total 6282
drwxr-xr-x 2 root root 1024 2012-09-18 04:34 grub
drwxr-xr-x 3 root root 1024 2012-09-18 04:06 efi
drwx------ 2 root root 12288 2012-09-18 03:55 lost+found
-rw------- 1 root root 3312447 2012-09-1720:52 initrd-2.6.25-14.fc9.i686.img
-rw-r--r-- 1 root root 86348 2008-05-01 18:34config-2.6.25-14.fc9.i686
-rw-r--r-- 1 root root 892575 2008-05-01 18:34System.map-2.6.25-14.fc9.i686
-rwxr-xr-x 1 root root 2088288 2008-05-0118:34 vmlinuz-2.6.25-14.fc9.i686
grub启动加载程序,当系统按下电源键开机时,第一条指令对应的逻辑地址(段:偏移)FFFF:0000,也就是物理地址的FFFF0H,这个在8086时代就这么决定了,这个地址一般存放第一条BIOS的指令,这条指令一般又是个长跳转指令,因为8086时代地址线只有20根,所以寻址只能是1M范围内的空间,而到了酷睿或者i5系列早已是32位或者64位了,所以Bios的大小和物理地址都进行了扩充,所以该指令一般跳转到bios执行指令。Bios存放在ROM中掉电不会失去。
efi bios的升级方案, 您可以将其理解为功能和bios差不多,但是运行的是32为指令并且地址有了突破,此外和pc操作系统的接口也有一点区别就可以了。这里还是以bios为主。
lost+found存放修复或者损坏的文件,正常的启动过程用不到。
Initrd 这个是initial RAM disk的简称,系统在运行时建立在一个存储系统上的,initrd使用软件模拟磁盘系统,它就是个文件系统,一般的嵌入式系统的文件系统就是它,但是在pc环境下initrd只在启动过程中起作用。
config文件内核的配置,和make menuconfig生成的文件一样,是关于当前系统的配置情况,如处理器类型、mmu、cgroup功能是否支持等。
system.map,这个文件是内核映像文件vmlinux使用nm导出的符号表,vmlinux是ELF文件格式,含有一些信息,nm命令导出该文件的信息,该文件对于分析内核映像文件还是有一定帮助的。vmlinuz,加工过的vmlinux,加工内容包括压缩、去除ELF文件信息,并且添加了bst(bootstrap)部分代码。
这里也看一下升级后boot目录下的文件内容,注意黄色加深部分,至于没有着色的部分也许你也看到了系统有两个,是的,没看错,这个之前我也编译并安装过另一个2.6.25版本的内核了。
drwxr-xr-x 2 root root 1024 2014-08-17 14:43 grub
-rw------- 1 root root 3377030 2014-08-17 14:43 initrd-3.10.0.img
lrwxrwxrwx 1 root root 232014-08-17 14:40 System.map -> /boot/System.map-3.10.0
-rw-r--r-- 1 root root 1398825 2014-08-17 14:40 System.map-3.10.0
lrwxrwxrwx 1 root root 202014-08-17 14:40 vmlinuz -> /boot/vmlinuz-3.10.0
-rw-r--r-- 1 root root 3012160 2014-08-17 14:40 vmlinuz-3.10.0
drwxr-xr-x 3 root root 1024 2014-07-23 04:53 efi
drwx------ 2 root root 12288 2014-07-23 04:45 lost+found
-rw------- 1 root root 3311461 2014-07-2221:55 initrd-2.6.25-14.fc9.i686.img
-rw-r--r-- 1 root root 86348 2008-05-01 18:34config-2.6.25-14.fc9.i686
-rw-r--r-- 1 root root 892575 2008-05-01 18:34System.map-2.6.25-14.fc9.i686
-rwxr-xr-x 1 root root 2088288 2008-05-0118:34 vmlinuz-2.6.25-14.fc9.i686
-rwxr-xr-x 1 root root 822228 2008-04-26 01:25xen-syms-2.6.25-2.fc9.i686.xen
-rw-r--r-- 1 root root 86316 2008-04-26 01:18config-2.6.25-2.fc9.i686.xen
-rw-r--r-- 1 root root 907057 2008-04-26 01:18System.map-2.6.25-2.fc9.i686.xen
-rwxr-xr-x 1 root root 2495757 2008-04-2601:18 vmlinuz-2.6.25-2.fc9.i686.xen
-rw-r--r-- 1 root root 373850 2008-04-26 01:10xen.gz-2.6.25-2.fc9.i686.xen
对于X86PC系统上电后,会执行特定地址上的一条指令,而目前这条指令就是一个长跳转指令,该指令跳转至真正的bios入口地址执行bios,bios程序会进行加电自检(POST),其次会本地设备初始化,并按照启动顺序搜索可以引导的设备,这里我们从硬盘引导我们的系统,这是bios将硬盘的0磁道0柱面1扇区的内容拷贝至内存中运行,之所以拷贝至内存中,因为内存的存储速率远远高于ROM或者硬盘,不过最近的固态盘速率提高的很快,好多pc开始采用固态盘(SSD)作为PC的主存储器了。一个扇区是512字节,当这些512字节拷贝至内存后,bios会将跳转至该内存里的程序(grub)继续执行,grub将负责实际的内核加载,即将内核加载到内存中,然后将控制权交给内核,内核会解压缩自身的内核映像到特定的地址,然后运行该内核,内核会启动分页机制,内存管理、设备初始化等一些的任务,执行init进程,创建三个内核线程,其中一个线程会创建内核守护进程如swap进程,还有一个线程会启动shell进程,提供操作系统登录这登录。
Bios会将0磁道0柱面1扇区(sector)的512字节内容(grub stage1)拷贝至0x7c00所在的物理内存处,并且将控制权交给grub,该512B中只有446B是引导代码,剩下的是分区表信息。grub的第二个阶段代码可以可以跨越一个或者多个sector。所以实际上grub大部分的工作是由stage2来完成的,其会被stage1 阶段的代码拷贝至内存运行。
图1.1.1 Linux PC(X86)启动流程
先来看看grub的配置文件的内容(/boot/grub/grub.conf)【该版本的grub是0.97,有stage1.5,grub2部分见2.1节】:
…
title Fedora (3.10.0)
root (hd0,0)
kernel /vmlinuz-3.10.0 ro root=/dev/VolGroup00/LogVol00 rhgb quiet
initrd /initrd-3.10.0.img
title Fedora (2.6.25-14.fc9.i686)
root (hd0,0)
kernel /vmlinuz-2.6.25-14.fc9.i686 ro root=UUID=aeca4624-c3e9-45af-b01b-125b2bcbec7arhgb quiet
initrd /initrd-2.6.25-14.fc9.i686.img
root (hd0,0) root系统文件目录,第一块硬盘的第一个分区(主分区)。
kernel /vmlinuz-3.10.0 内核映像文件
ro只读
root=/dev/VolGroup00/LogVol00 根分区用户
rhgb quiet 图形redhat graphic boot ,不显示dmesg信息
initrd 启动时的文件系统。
device.map文件(boot/grub/):
(hd0) /dev/sda
PC的1M内容如下:Documentation/x86/boot.txt
图1.1.2内存布局
grub执行完毕后就会将cpu转交给Linux内核,内核的bzImage格式是经过压缩的内核格式,首先进行解压缩,然后才是真正意义上的启动内核。这一解压缩和启动的过程对应于下图所示。
这一段信息中Decompressing Linux… done意味着内核刚解压完毕,紧接着启动内核,即图中显示的Booting the kernel。
图1.1.3内核解压缩
在Linux内核booting后期,会调用start_kernel()函数,该函数会创建内核守护进程, init进程,内核守护进程用于例行性管理类任务,如swapd等, /sbin/init会读取/etc/inittab,根据不同的运行级别去初始化相关的服务。
# Default runlevel. The runlevels used are:
# 0- halt (Do NOT set initdefault to this)
# 1- Single user mode
# 2- Multiuser, without NFS (The same as 3, if you do not have networking)
# 3- Full multiuser mode
# 4- unused
# 5- X11
# 6- reboot (Do NOT set initdefault to this)
#
id:5:initdefault:
接下来init进程会调用/etc/rc.sysinit脚本初始化很多换环境参数,如PATH、网络的设定等。加载内核模块。同样是依赖于inittab中的运行级别,执行对应运行级别初始化脚本(/etc/rc0.d~/etc/rc6.d目录下),最后还有一个/etc/rc.local目录下脚本也会运行,这里用户可以定制一些自己的服务在里面,这样每次系统启动就可以运行一些自己定制化的东东。至此所有的准备工作已经做好了,就是让用户登录了,调用脚本/bin/login,给出用户登录界面。登录进shell。
这一过程直观上的感觉就是启动时下面会出现的启动过程显示1.1.4图,这是一张启动截屏图。
图1.1.4:启动过程截图
1.1.5运行脚本rc5.d目录下内容。
图中划横线的部分就是在启动过程显示的服务的由来。至此整个过程有了清楚的认识了。
http://www.linuxhomenetworking.com/wiki/index.php/Quick_HOWTO_:_Ch07_:_The_Linux_Boot_Process#.VCa1mOOSwU0
对于嵌入式平台ARM平台,说说其NANDFlash的启动过程,请先看图2.2描述的NAND flash中的程序布局,上电时,首先cpu会自动将自动从NAND flash中拷贝一定代码到内存中执行,这是任何支持nand方式启动必须支持的,一般我见到的有2K还有4K的,这部分的代码我们将其称为bootstrap,这个有点类似MBR中的执行代码,那部分代码是grub的stage1代码,bootstrap然后会拷贝bootloader到内存中,这个就类似PC中的grub的stage2,这部分代码大小是有限的,遇到过32K,128K的bootloade。Bootstrap会将32K或者128K的空间里的东西拷贝到内存中而不管实际的bootloader大小,然后将控制权移交给bootloader,相比bootstrap而言,bootloader发挥的空间较大了,它会读取一个叫ptb里的内容,这里存的是分区信息,根据nand的存储特点记录app,kernel…bootstrap的大小,其实各部分的block,page的信息,然后bootloader将这些内容统统拷贝到内存中,至于拷贝到内存中的地址各个平台的差异性就比较大了,不像PC加载内核到内存中的地址是固定的。
图1.2.1嵌入式ARM启动流程
Uboot主要还是为kernel服务的,所以它准备好外部环境后将控制权交给kernel,其实转交过程就是jump,并且会将相关的信息传递给内核,比如设备树表的地址,kernel部分的代码然后开始执行,主要系统初始化工作还是在startkernel中完成的,例如会解析命令,然后还有一个重要动作,那就是解析设备树,并使用一个链表将其串接在一块,在后续驱动注册的probe方法中会用到这个链表,3.10的内核是这么处理设备的。接着一系列的初始化,初始化的工作交给一个内核线程完成,该线程会执行/sbin/init的内容,这个内容就涉及到根文件系统了,在PC情况下就是initrd了,ARM下initramfs了,这里需要说明的是init并不属于内核,为了保持内核的精简,底层服务搭建好了以后,初始化任务还是交给了用户空间去完成了,最后启动shell,至此PC下的任务完成了,嵌入式下会把需要的应用,即app里的内容放在init里去加载,或者init下调用另一个去加载。至此整个加载过程就完毕了。
图1.2.2NAND Flash中所有的布局
本文会讲解两种架构下的启动流程,各个阶段对用的概念是一样的,如grub等价于uboot,PC下的initrd对应于嵌入式的根文件系统等,到Linux启动时,他们的代码概念上都是一样的,如建立页表、中断等。
Linux内核启动时的一些信息就是MBR和bootloader给的,那么就先将讲的MBR的分区信息吧,bootloader和内核都会依赖分区,因为分区信息指明了代码在存储介质上的分布信息,一些操作依赖于该分区信息,比如,内核在哪里,一般在/boot分区,这些分区信息在安装操作系统格式化磁盘时就会存在,或者新的硬盘的格式化,也会有分区信息存在里面。这里使用的两个命令查看了硬盘的分区情况,在MBR中有该分区信息。
图2.1.1 分区信息
由fdisk命令可以知道系统共两个分区一个硬盘,通常第一块硬盘被称为sda,第二块硬盘被称为sdb,表示我的系统有一块硬盘,两个分区,/sda1和/sda2,其中/sda1是boot分区(星号),/sda2是LVM类型的分区,一个逻辑管理,适用于需要动态调整大小的场合。从df命令可以看出/boot分区的确挂在/dev/sda1下。前面的device.map文件(boot/grub/):下的(hd0) /dev/sda,也就不难理解了,/sda表示整个第一块硬盘。如果有兴趣可以使用dd if=/dev/sda1 of=XXX bs=512 count=1, od-x XXX 查看第一个扇区的内容。最后一个必然是aa55,因为这是一个合法的mbr的必然信息,前面是grub的stage1,后面是分区信息,每个分区16个字节,由于我的盘只有两个分区,所以aa55的前面会有那么多零是正常的,关于mbr的分析,如果感兴趣自己查查,这里讲篇幅留给其它更有意义的部分吧。
2.1.2MBR内容
好了,接下来正式进入grub2,grub在2014年就一直没有跟新过了,但uboot更新可真是勤快啊,建议按照这个步骤先做一下,有点成就感,这样会有信心往下走,而且PC应该不难找到,找个虚拟机就可以试,在弄个snapshot就不用担心系统坏掉了。先来一个grub2的启动界面:
2.1.3 grub启动界面
1、 首先去gnu的官网下载一个grub-1.97.2.tar.gz源码包,地址
ftp://alpha.gnu.org/gnu/grub/
2、 解压 tar xvzfgrub.1.97.2.tar.gz
3、 进入解压后目录 cdgrub-1.97.2
4、 配置grub, ./configure
5、 编译 make
6、 安装grub-install/dev/sda
7、 更新grub grub2-mkconfig-o /boot/grub/grub.cfg,这里的grub.cfg和原来grub.conf的作用是一样的,该命令会根据/boot下的kernel image和initrd信息生成启动信息。如果是Ubuntu下,需要使用update-grub/dev/sda命令
8、 reboot重启可以看到该系统启动grub的信息。
下面就来说说grub的启动步骤吧:
1、Bios将MBR中的512B东西拷贝到0x7c00处,然后跳至该处执行,然后装载start模块。这部分代码参看/boot/i386/pc/boot.S
2、start模块加载剩余的grub stage2部分,参看boot/i386/pc/diskboot.S。
3、 grub stage引导CPU保护模式,自解压并释放到0x10000开始内存处,解压完成后再拷贝回原来位置,然后调用grub_main,见kern/main.c,grub_main初始化系统,加载模块,并进入normal或者rescue模式。GRUB将根据配置文件grub.cfg或者用户输入,加载操作系统并执行操作系统。
在这里就赘述Makefile以及镜像文件的链接关系等相关的内容了,这方面的知识会在内核映像生成过程中体现出来。针对越来越大的硬盘存储容量,开始出现了GTP取代MBR的趋势,GTP特性需要EFI特性的支持。
一个bootloader应当提供一下功能,
1. Setup and initialise the RAM.
2. Initialise one serial port.
3. Detect the machine type.
4. Setup the kernel tagged list or bdt.
5. Call the kernel image.
对其的介绍和使用这里就不写了,网上针对2440的文章可谓泛滥了,自己找找动手实践一下。
链接的语法可以参看:http://sourceware.org/binutils/docs-2.21/ld/
也可以查看精简版的链接脚本的书写格式。
http://www.slac.stanford.edu/comp/unix/package/rtems/doc/html/ld/ld.info.Scripts.html
内核镜像一般包括两个部分,一个部分是针对特定处理器、特定平台的启动代码,这部分通常被称为boot代码,另一部分就是Linux系统本身,这包括内存管理、进程调度、文件系统、进程间通讯、网络子系统等部分。
如果你查下Linux的代码会发现,就arch/(x86,arm)/boot下的内容差别就很大,arch/arm/boot目录下的内容如下:
图2.3.1 ARM启动代码目录
arch/x86/boot下的内容如下:
图2.3.2 X86启动代码目录
从上面就可以看出X86的启动代码会比arm的启动多一些,但是上述关于arm的boot代码在arch/arm下还有一个板级的初始化,如s3c2440板级一些工作被放在了,arch/arm/mach-s3c24xx下了。一个内核映像的组成至少包括boot部分和kernel部分,我们暂且这么定,并且后面无特殊说明,也会boot指cpu和板级的初始化代码,kernel指脱离了硬件差别的启动代码。
两种平台启动时,其中一个重要的差别是文件系统,因为arm通常用于嵌入式系统,其内存和Flash相对PC而受限,比较出名的嵌入式文件系统有yaffs2、jffs2、cramfs,由于工作的关系这里我们就说ubifs文件系统了,该文件系统是新一代为NAND Flash设计的文件系统,但其本身相对也较大,8M左右。PC架构采用ext3文件系统。
另一差别在对硬件的处理上,arm采用了设备树,arch/arm/boot/dts,并且uboot现在也可以采用设备方法来解析硬件了。嵌入式设备对硬件的处理(依赖设备树)较PC差异较大。这里先综述PC下的启动流程。
首先来看一个缩略的内核镜像vmlinux(kernel部分,非boot)输入文件编译过程,使用的链接脚本是内核源码文件/arch/x86/kernel/vmlinux.lds。
上述文件中总共着色了红、橙、绿、蓝、紫,(紫色的是和根文件系统相关的部分,暂时先不谈。)
剩下的着色部分在启动时会依上述顺序执行,好了到这里了,该稍微总结一下了,根据Makefile规则,可以生成上述的启动和内核文件。这基本上是一个能够启动的内核了,但是还确实根文件系统,也就是紫色部分会用到的,这部分会留到后面讲述,这里仍以内核镜像的生成和使用过程为主剖析。先来看一张图,然后再将映像文件是如何链接起来的。
图2.4.3 启动过程图
这张图的有半部分在1.1节就见过了,是内存中代码的架构,其中X = 0x001000 + grub的大小,因为grub大小在编译时才确定,grub将上图中红色的512字节内容代码拷贝到右图红色箭头所指的地址处并将控制权较给这部分代码,至此Linux内核代码正式开始登场接管后续操作系统的启动工作。然后将实模式代码拷贝到墨绿色箭头处执行,这部分代码被称为setup代码,开始的代码是上图中黄色和绿色的两个代码,实模式跳转到保护模式开始执行,是蓝色箭头表示的。实际的过程和上面讲的还请有点区别,因为内核时经过压缩的,由/arch/x86/boot/compressed/head_32.S调用misc.c中的decompress_kernel()函数解压到0x100000地址处的,所以是整个一次性有grub拷到内存中的,然后会进行自解压等,这里就简化了这一处理过程,不过压缩的内核代码会被grub加载1M+的地方,这些地址的在链接时就确定了的。
当内核跳到0x100000处时,控制权由bst转交给了真正意义上的kernel,这就是vmlinux的入口(可使用make vmlinux生成的),这时/arch/x86/kernel/head32.c中的i386_start_kerne()将会被调用,该函数会调用start_kernel()函数。该函数调用若干函数和脚本建立Linux的核心环境,start_kernel()之后调用init(),创建系统级线程,比较重要的如do_basic_setup()完成外设及驱动的加载,很多设备和驱动的源头就来自这里,同时这里也会完成根文件系统的挂载工作。
完成上述工作后,init()会打开/dev/console设备,重定向stdin、stdout、stderr,最后使用execve()系统调用执行init程序。到这里可以说引导工作结束了。
init()进程开启后,系统核心环境已经准备好了,接着init()读取其配置文件/etc/inittab。
上述中setup大小是18K,这表示的是我的机器上是这样的,也许你的会有差异,具体查看和计算方法如下:
链接过程就是将若干个输入文件链接成一个文件,这编写经典的helloword程序时,你可能使用了printf或者printk,这里就涉及到了库文件和动态还是静态编译了,暂时先整个内核的链接过程按照上述也分为boot和kernel两个部分,接下来就来看看这两个部分,如果细心,就会发现有很多built-in.o,如ipc/built-in.o,并且它们的获得方式不是cc而是ld,就是将对应目录下文件链接成一个文件,如:
最后这些built-in.o文件和链接脚本一起作为输入连接器的文件,最终将生成vmlinuz。
setup链接脚本,相关的注释使用了华文隶书:
ENTRY表示程序的入口,在head.S文件的274行定义如下。
下面显示了为什么是bstext段的内容放在这里了,在header.S文件中已经制定的生成的段类型了。.o类型文件的所有段的段类型使用对应平台的readelf命令可以查看到。
./boot/setup.ld: .bstext : {*(.bstext) }
Binary file ./boot/header.o matches
./boot/header.S: .section ".bstext", "ax"
下面同理
好了上面就是图2.4.1中关于bootsector和setup的内容了。下面看看kernel的链接脚本,有点长,这里略去了64位总线PC情况,只留下32的SECTIONS定义。注释方法同上。
首先来看看大体的链接成什么样子,
可以看到依赖三个文件,这里BITS直接给出32,因为我们的系统是X86_32的,将所有符号带入可得到:
head-y := head_32.S head32.c head.c
关于init-y 的内容类似,并且init-y部分非常中要,很多系统开机的设置均由这里代码完成的,所以这部分会放在Linux系统启动来讲,如果想抢先看,情况init/main.c 下的start_kernel()函数,该函数做了非常多的系统初始化工作。下面就给出arch/X86/kernel/vmlinux.lds的注释。
程序加载地址和运行地址是对应的两种地址,链接脚本使用两种地址来表示(虚拟/运行地址VMA,加载地址LMA),LMA地址由 AT指定。
从上面的链接脚本你会发现图2.4.3左边的镜像缺少一部分,正确的形式如下,之所以上下没有对齐,是因为地址啦,内核的VMA地址不是从零开始的,而是0xC0000000 + 0x1000000:
图2.4.4内核镜像和启动内存布局
本节对PC下的根文件系统做个简单的过程浏览,深入的文件系统的部分放在嵌入式根文件系统的制作上。Initrd是initial ramdisk的简称,initramfs是initial ram filesystem的简称,是一种cpio格式的内存文件系统,在pc下init-3.10.0.img就是一个cpio格式的,目前initrd类型的启动方式少见了。
先来看看/boot/grub/grub.cfg文件里的内容,下图只截图了3.10内核启动用到的部分,有这么一行:
Initrd /init-3.10.0.img
如果其在启动配置文件grub.cfg里,那么可以说该文件比较重要了,因为系统启动需要使用它,实际上也可以不使用initrd,只需要将上面那行改成no initrd就可以了,大多数情况下系统仍然可以正常启动,但是对于服务器多半会启动不了,这取决于具体的硬件配置。
图2.4.1.1 grub配置信息
早期的Linux内核中是没有initrd的,在Linux 0.11版本中,系统启动时直接挂载floppy(软盘),然后启动系统的init进程,该进程在任何Linux系统中都存在,到目前是必不可少的,0.11将init存储在floppy中,但是随着时代的变迁新的存储器也出现了,比如scsi硬盘,sd、usb存储设备等相继出现,这时如果将这些存储设备的驱动编译进内核,可以想象,内核的将会非常大,Linux内核的一个宗旨就是简单,为了保持内核代码简洁,就将这些设备的驱动程序放在了文件系统里。这就意味着要想获得init程序需要先挂载设备的驱动,但是驱动代码在文件系统里,所以系统要先挂载设备的驱动,然后从设备中找到对应的init,这个加载驱动和启动init的工作就放在了initrd中来完成了,下面就来揭开initrd的神秘面纱,首先我们看看initrd-3.10.0.img的内容,注意该文件为cpio格式的,请按照下述方式操作。
cp /boot/initrd-3.10.0.img initrd.gz
lsinitrd initrd.gz
到这已经可以看到initrd的内容,也可以使用下述命令去详细的看看该文件的组织结构。
gzip –d initrd.gz
cpio –ivdm < initrd.gz
cat init
文件输出如下:
上述文件可以看出,该文件的作用就是创建一些系统运行的依赖目标,然后挂载了几个存储设备的驱动,挂载了根文件系统,这里的文件系统是真正操作系统起来后使用的文件系统,最后启动init进程就结束了。
如果看cpio命令后的目录下内容,黑色的那两个文件不是cpio解压出来的。主要包括启动的程序、启动工具和初始化服务,如网络、终端、设备驱动的加载以及加载其它文件系统。
图2.4.1.2 cpio文件解压内容
在来看看bin目录下的内容:
图2.4.1.3 PC根文件系统bin内容
这里可以看到好多命令都在这里了,但是对于嵌入式环境看到的也许有点不同哦,由于嵌入式环境的存储空间是非常珍贵的,多以这里看到的这么多命令在嵌入式环境就变成busybox了,所有的命令均是链接到busybox,由busybox解析命令。
这里在总结一下这个过程,首先grub根据配置文件grub.cfg解析是否时能initrd,如果时能将内核镜像和initrd拷贝到物理内存中,然后grub将控制权交给内核,内核加载initrd,解压并将内容拷贝到/dev/ram0中,解压后的内容就是一个文件系统了,该文件系统在RAM上,这就是说内存盘(ramdisk)中存在一个文件系统了,内核这是会将其挂载为rootfs(root file system)根文件系统,然后内核创建线程执linuxrc(嵌入式系统),完成后会写在initrd并进行最后的启动,进程这是使用initrd去初始化一些系统资源,挂载根文件系统,调用init进程初始化系统资源并启动shell。至此对用户而言系统可用了。
对于嵌入式设备,如果实际的根设备(非易失性存储器)存在/initrd目录,Linux挂载并将其做为最终的根文件系统,如果不存在initrd的镜像将被丢弃。但是如果在内核命令行中指定了root=/dev/ram0之类,linuxrc的执行将被跳过并且也不会尝试挂载其它文件系统做为最终的根文件系统。
接下来会讲嵌入式系统下根文件系统的制作,不再讲述PC下initrd的制作,有兴趣可以自己尝试制作一个,嵌入式下的根文件系统制作基于busybox,并且个人觉得嵌入式下的根文件系统的制作更能让人理解根文件系统的方方面面。
http://busybox.net/
1、 解压busybox-1.22.1.tar.gz
2、 配置源码 makemenuconfig ARCH=arm CROSS_COMPILE=arm-linux-
3、 make
4、 make install
图2.4.2.1 安装busybox过程
5、 进入上一步生成的_install目录,该目录下会生成以下几个文件
图2.4.2.2 安装生成内容
6、 添加其它目录mkdirdev etc mnt proc var tmp sys root lib
至于根文件系统的各个目录的作用可以参看FHS,《filesystem Hierarchy Standard》
/dev 设备文件目录,应用程序和驱动程序的桥梁,udev支持的热插拔设备自动创建。
/etc 配置文件,许多系统启动的配置选项均在此,PC下和嵌入式下配置文件的内容差别比较大,有兴趣自己比较。
/mnt 挂载目录点,可挂载其它文件系统类型的存储设备。
/proc 虚拟设备文件系统,一些内核参数由此导出,一般不允许更改权限,驱动程序的一些辅助调试接口通常导出到该目录下。
/lib 存放动态、静态库等文件,命令以及应用程序会使用该库文件,glibc库必须有,否则基本的ls、cd命令可能无法运行。
/sys虚拟文件系统,用于设备和驱动的管理,驱动的class接口位于此,udev自动创建设备节点的功能依赖于该文件系统。
/root根用户登录目录
7、 添加动态库cp –a$(TOOLCHIAN)arm-none-linux-gnueabi/sys-root/lib/*so* ./lib/
该目录下还有静态库,系统命令会调用该库里的文件。
8、 添加系统文件
[root@ge _install]# cd etc/
[root@ge etc]# vim inittab
[root@ge etc]# vim fstab
[root@ge etc]# Vim passwd
[root@ge etc]# mkdir init.d
[root@ge etc]# cd init.d/
[root@ge init.d]# vim rcS
[root@ge init.d]# chmod +x rcS
[root@ge init.d]# vim profile
这里的文件都是在/etc目录下的文件,主要用于Linux系统启动阶段,inittab属于内核标准文件,有其编写标准,该文件是Linux系统启动时该目录下第一个使用到的文件。
Inittab内容如下:
Fstab文件内容如下:
#device mount-point type options dump fsch order
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
该文件同样是内核标准文件,可以没有,在rcS下创建上述设备目录。上述表示文件系统的挂载相关信息,如挂载点、文件系统类型等。
rcS的内容如下:
#!/bin/sh
#This is the firstscript called by initprocess
/bin/mount –a
通常这里的内容会比较多,一般会调用其它的脚本启动一些常驻的应用程序,为shell脚本,内容差别可能非常大。
Profile文件内容
#!/bin/sh
exportHOSTNAME=ge
export USER=root
export HOME=root
#exportPS1="[$USER@$HOSTNAME\W]#"
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=lib:/usr/lib:$LD_LIBRARY_PATH
export PATHLD_LIBRARY_PATH
该文件设置一些环境参数。
在passwd文件中添加如下行:
root::0:0:root:/root:/bin/sh,即只保存与root相关项
9、 创建设备节点
mknod dev/console c 5 1
mknod dev/null c 1 3
通常必须有console这个目录,null目录也常常需要,如果没有console目录,启动会导致失败。到此busybox部分工作完成了,因为选择的是动态编译方式,所以ls等命令依赖的glibc库文件需要添加到/lib目录下,但是真正的根文件系统制作完毕还有/lib没有完成,实际使用中通常会进行压缩,一般两种方式,一种.tar.gz,还有一种一种就是cpio格式。在根目录下生成cpio格式的压缩文件,使用的如下命令:
Find . | cpio–quiet –o –H newc | gzip > ./rootfs.cpio.gz
上面已经构建了一个可以使用的根文件系统了,ls、cd命令已经可以正确运行,现在给两个题目,接下来就这两个题目想阐述如何针对项目需求去定制一个根文件系统。
题目一:物联网发展趋势下,以后的嵌入式设备会联网,如何添加防火墙的功能呢?
题目二:听说具有加密功能的ssh能够为在网络上传输提供一层保障,该如何实现该功能呢?
对于题目一使用开源软件包iptables,交叉编译iptables-1.4.18.tar.gz软件包,
./configure CC=arm-linux-gcc --enable-static –disable-shared–host=arm-linux –prefix=$(YOUR INSTALL DIRECTORY)
Make
Make install
这样就编译好了,可以把install目录下的相关内容拷贝到上述对应的目录中,可执行文件到/sbin 、库文件到/lib中,这样在shell下就可以使用iptables命令了。
题目二比题目一稍微复杂一点,ssh功能的支持包是dropbear,但是呢编译该时它还依赖zlib这个压缩包提供的功能,所以呢,需要首先编译zlib包,而后才是编译dropbear包。
zlib-1.2.8.tar.gz编译如下:
export CC=arm-linux-gcc
./configure –prefix=/$(YOUR INSTALLDIRECTORY)
Make && make install
Dropbear-2013.5.58 的编译命令如下
./configrure –with-zlib=$(YOUR zlib INSTALLDIRECTORY) cc=arm-linux –host=arm-linux
Make && make install
同样将生成的可执行文件以及库文件复制到对应的根文件系统的目录中,这样就可以使用该功能,如果对configure不太懂,./configure –-help命令可以查看。
通过上面的两个例子,来总结一下如何向根文件系统添加新功能的思路,首先根据要提供的功能,(一般可能是/bin、/sbin之类下的命令,可能是/lib下的库(应用程序使用)文件,抑或两者兼有),查找有没有现成的开源包,如果有,那么交叉编译,拷贝,如果没有那就自己写,然后交叉编译,拷贝。一切都是那么的顺利成章啊~!
最后需要提醒一下上文提到的udev机制在也是需要编安装和拷贝的。
这里的加载指的是操作系统启动时的加载过程,在系统启动时start_kernel()->vfs_cache_init()->mnt_init()->init_rootfs(),该函数会挂载rootfs作为根文件系统,可以使用cat /proc/mounts查看,然后会解压用gzip压缩的归档文件到rootfs目录,接着执行解压后的init程序,一旦该程序运行,那么就称其为init进程,并将控制权移交给init进程,这也是系统由内核到用户态的标志,如果在解压的rootfs目录下找不到init程序,那么会根据解析的cmdline参数,找“root=XXX”,并挂载,该参数有devicetree的chosen节点提供。在Linux内核编译时CONFIG_INITRAMFS_SOURCE指明intramfs的位置。通常一个系统可用的文件系统在/etc/fstab文件中,但是嵌入式环境下通常不用。
对于Linux系统,通常启动分为bld(bootloader),bst(bootstrap),vmlinux(这里的vmlinux不包括bst,但是通常Linux内核源码包,编译出来的最终vmlinux是包含bst,这里将其分开讲解了)。当你在Linux-3.10根目录下使用make vmlinux时,你会发现确实有该目标,可是这里所说的vmlinux是用来生成vmlinuz的,所以概念上还是有点区别的,区别在于前一个vmlinux并不包括bst部分,而最终的vmlinuz是包括bst的。这个关于Makefile的分析就略过了,关键的执行步骤会在下面展现出来的。
这里先提一下,由于当/sbin/init进程运行起来以后,其读取/etc/inittab配置文件,根据配置信息确定运行级别,笔者对应的运行级别为5,即图形界面方式,在启动时的视频中就显示了该信息。Init进程会执行系统初始化脚本/etc/rc.d/rc.sysinit,对系统进行配置,以读写方式挂载根文件系统,至此,系统基本运行起来了,接着运行/etc/rc.d/rc,该文件会判断配置文件中的启动运行级别,并定义了服务启动的顺序,并启动/etc/rc.d/rc5.d,该目录下的内容链接至/etc/init.d目录下,后面会启动虚拟终端/sbin/mingetty,在运行级别5上启动xwindow,这是就是大家看到的登录界面了,用户输入用户名和密码,然后/etc/passwd完成密码验证,登录了。这一过程的文档确实很多,所以这里打算深入两个细节分析一下,这也是笔者在设备驱动的道路上吃过苦头才认认真真看了源码的。
网上分析Linux内核启动的文章很多了,就算不是针对3.10版本的内核,也可以参考,并结合3.10版的代码进行分析,这里打算重点讲下嵌入式下的device tree的解析和设备的构建,未来嵌入式系统解析设备信息将会采用设备树的方式,这也是为什么这里会详细讨论该解析过程了,但是这是和嵌入式处理器息息相关的,不同的处理器的设备树的构建和解析也是有区别的,所以Linux内核会将这部分的解析代码放在setup_arch()函数中完成。
由于3.10版本的嵌入式环境下大多数使用的是device tree的方法来描述设备,不仅如此,uboot也可以并且也正在采用device tree的方法,所以这里会花点篇幅介绍它,然后才是Linux系统的启动。
为什么先介绍device tree:
在2.3节中bootloader功能的提到如下两个步骤:
4. Setup the kernel tagged list or dtb.
5. Call the kernel image.
对于3.10版本的内核使用dtb(device tree block)方法来描述设备拓扑,所以上面的4里的tagged list并没有被使用,而是使用了dtb,对于上面的5,call内核前将前三个寄存器按如下方式设定。
- CPU register settings
r0= 0,
r1= machine type number.
r2= physical address of tagged list in system RAM, or
physical address of device tree block (dtb) in system RAM
对于r2寄存器的值为RAM中dtb的地址。
内核在启动时会查r2地址为的4偏移处的值,如果r2地址偏移零处的值为零,那么表示没有taglist或者dtb被传递过来,如果4的偏移处为0xd00dfeed,那么表示传递过来的是dtb。
同时也会传递系统内存和根文件系统。这是通过device tree的chosen节点实现的。关于device tree的语法参看:
http://www.devicetree.org/Device_Tree_Usage
关于device tree 的用法可以参看:
https://wiki.freebsd.org/FlattenedDeviceTree
在操作系统源码中/arch/arm/boot/dts/目录下常见以.dts和.dtsi为后缀的文件,通常来说.dtsi一般是cpu级别的描述,而.dts则是板级的描述,在编译生成dtb是通常.dts会包含.dtsi,以构成完整的设备树,当还有一个skeleton.dts的通常会被包含。
这里给出一个
/proc/device-tree 是device tree的一个调试入口,
Documentation/devicetree/booting-without-of.txt.
先来看看devicetree的解析过程:
首先看arch/arm/kernel/head.S,注意和arch/arm/boot/compressed下的head.S区别,后者属于bst的部分, arch/arm/boot/compressed下的内容,最终会生成bst部分的代码,这里从内核的head.S开始讲起。该函数调用需要满足时各个寄存器存储的内容如下:
R0:内核所在地址 R1 : machine number
R2: dtb or atags R8: phys_offset R9:cupid R10:procinfo
其中100行完成将__atags_pointer设置为dtb的地址。104行跳转到start_kernel函数执行。该函数定义init/main.c中,网上对start_kernel()的分析非常多,该函数会间接调用很多子系统的代码该start_kernel()函数的501行如下:
setup_arch(&command_line);
上述函数会解析dtb。
该函数最终将解析的根节点存放于of_allnodes中,在后续设备注册时,会扫描该节点,和该节点匹配上才进行实际的注册,这里设备分为两种情况,举个例子如果U盘想要工作,一般嵌入式Soc会集成usb控制器,这里usb控制器和u盘都会用到of_allnodes,所不同的是usb控制器的初始化会在start_kernel()中的do_basic_setup()中完成,而U盘的初始化则会延迟到U盘插入,热插拔机制会启动相应的支持。
在setup_arch配置完成之后还会有一个board的初始化,arch/arm/match-XXX/目录下的一些平台相关的代码在这里会被调用,比如该目录下的init_irq、init_machine函数就会被调用。
在init_machine函数中会调用of_platform_populate()注册平台总线和平台设备。对于像i2c、spi之类设备则会在early_platform_add_devices()完成注册。
开发中的Linux内核源码,Git.kernel.org/cgit/linux/kernel/git/Torvalds/linux.git
上述的vmlinux是没压缩过的内核映像,真正使用时会将内核压缩省以减小内核映像的大小和以及节约映像从Flash或磁盘拷贝到内存的时间。去了压缩链接的过程,这一过程在ARM的嵌入式平台和PC平台均存在,但是由上面的基础来看压缩部分的应该不难了。X86部分的压缩实现见arch/x86/boot/compressed/。
最后来点收尾的,你可能发现在1.1节中提及Linux内核升级的时候讲到了make modules 和make modules_install,这里一直未提到该内容,从该命令可以看出来是和module有关系的,config时会选择编译成module还是编译进内核抑或不编译,如果选择编译成module,这里两个命令就是用来处理那部分代码的,简单点说就是做如下的工作(注意在现有ext3文件系统上操作,系统启动时只有根文件系统可有):
cp -f/opt/linux-3.10/modules.order /lib/modules/3.10.0/
cp -f/opt/linux-3.10/modules.builtin /lib/modules/3.10.0/
…
该文件夹下的modules.dep,描述模块的依赖关系,当有热插拔事件发生时,内核会将处理过程推到用户态处理,主要关系udev实现机制,该机制会创建必要的设备节点和加载对应的驱动需要的模块,模块的依赖关系就在modules.dep里描述了。这里就可以看出来为什么和make vmlinux分开处理了,因为一个编译生成可执行文件,一个是在现有文件系统中插入一部分“module”。
在为了让上面setup_arch()函数所表示的启动过程更具有真实性,这里我们举个devicetree的例子,首先知道通常嵌入式下使用的处理器被称为SOC 而不是cpu,因为该芯片通常集成了许多控制器,所以假设我们的cpu是cotex-A9的,并且假设其集成有apb总线,在ahb总线上挂有以太网控制器,apb总线挂有i2c控制器,在实际的电路板上还有一个i2c控制的4951的音频芯片。
这里讲Soc具有的东西写成.dtsi的文件,将板上其它的外设写成.dts文件,这样在.dts文件中include该文件就可以了,这样可具有良好的可移植性。
ABCD.dtsi文件,文件在内核中的位置,arch/arm/boot/dts文件下,
好了,设备树构建完毕了,这里省去内存信息,中断等很多其它内容,关于devicetree内容参看,http://www.devicetree.org/Main_Page,接着向下看,可以说devicetree的三分之一的内容体现在这里。
图2.7.1 setup_arch()函数调用过程
Setup_arch函数里调用的第一个函数,也就是这里唯一出现等号的地方,这里mdesc,这就是和devicetree相关的一个非常重要的东西,
DT_MACHINE_START(exp_DT, “Aexp(Flattened Device Tree)”)
.restart_mode = ABCD _map_io,
.init_early= ABCD _init_early,
.init_irq=irqchip_init,
.init_machine=ABCD_init_machine,
.dt_compat= exp_dt_board_compat;
首先使用DT_MACHINE_START定义一个体系结构相关的函数,后续的的一些初始化函数将调用这里,这就有一个问题,Linux如何知道是这个呢,根据dt_compat,其指向一个compatible属性的字段,结合上面dts文件的定义,定义如下的exp_dt_board_compat;
Static const char *const exp_dt_board_compat= {
“ABCD,exp”,
NULL,
}
直觉告诉我们,这两者匹配上了,但是具体的匹配过程是什么呢?耐心往下看,
ach/arm/include/asm/mach/arch.h
#defineDT_MACHINE_START(_name, _namestr) \
static conststruct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init")))= { \
.nr =~0, \
.name =_namestr,
#endif
可以看到该宏定义的,将放在.arch.info.init段中,再来看setup_arch中获得的对应代码,这里我们使用该宏定义一个和上述devicetree相关的结构体出来,
for_each_machine_desc(mdesc){
score= of_flat_dt_match(dt_root, mdesc->dt_compat);
if(score > 0 && score < mdesc_score) {
mdesc_best= mdesc;
mdesc_score= score;
}
}
一看到到for就该笑了,就是从上面那个段,遍历一遍查看字段是否相等就可以了,为了验证我们的想法,向下看:
#define for_each_machine_desc(p) \
for(p = __arch_info_begin; p < __arch_info_end; p++)
这里用了一个非常巧妙的方法,__arch_info_begin和__arch_info_end在lds脚本里定义的,在链接时确定的,参看脚本arch/arm/kernel/vmlinux.lds.S的180行。
到此我们获得了体系结构相关的初始化代码,接着看of_get_flat_dt_root(),该函数获得设备树的根节点,这很好办,因为前面话了很多篇幅说的r2就是设备树的地址,但是如果看代码,可能会觉得很复杂,这是因为设备树在编译时会加上头信息,此外还要进行align操作。
setup_machine_fdt里其它函数的意义从字面上就可以看出来,这里就是针对该machine解析一些devicetree的字段,因为它们的优先级比较高,unflatten_device_tree()函数,中解析设备树,并且前面提到的ofallnode的信息来自于此。那我们看看那些优先级不是很高的设备树字段是如何使用的,这也是个初始化的过程哦,看前面设备树中的mac字段,do_basic_setup()中调用驱动初始化相关函数,mac字段是以太网信息的表示,所以这里会进行以太网相关的初始化的工作了,整个devicetree对设备的处理都是按下面的方式处理的。
设备驱动的框架一般是,写一个driver结构体,里面包括了设备的操作方法集,然后注册该结构,该结构调用driver的probe方法。
ABCD_probe(struct platform_device *pdev),该函数的参数中的pdev,该函数中有一个获得devicetree的方法,如pdev->dev.of_node,这是匹配上,那就获得上了该设备相关的信息,后面的过程类似以前的处理了,注册向下进行了。
由2.5.3知道了初始化主要在start_kernel中完成,
图2.6.1 start_kernel初始化
上图中可以知道,内存子系统已经初始化完毕了,后面初始化其它子系统时就可以直接申请内存了。
图2.6.2 rest_init初始化流程
rest_init首先调用kernel_thread创建连个进程,Linux内核规定init进程的ID等于1,所以先创建kernel_init,但是进程号等于一的进程,但是ID号等于1的进程在结束时会创建内核守护进程,守护进程相关的初始化会由后面的kernel_thread(kthread…)创建,这里使用了competition机制在kernel_init获得ID号1后,该进程处于等待状态,等待kernel_thread(kthread…)初始化完毕。
接下来的do_basic_setup()中非常重要的两个函数driver_init()和do_initcalls(),
前者完成总线等子系统的初始化,而后这则是根据如下initcall的优先级调用。
根据initcall级别,调用相应的函数,如下:
后面调用2.4介绍的文件系统执行/sbin/init,该/init会执行/etc下的配置文件,内容在2.4节介绍过了,init最终启动shell给用户。
最后一行的init_idle_bootup_task(current);是创建ideal进程,可以知道该函数的ID等于0。