Summer的小屋 2015-07-23
1 根文件系统
简单来说,(根文件系统)就是系统第一个mount的文件系统
Filesystem Handling
Like every traditional Unix system, Linux makes use of a system 's root filesystem : it is the filesystem that is directly mounted by the kernel during the booting phase and that holds the system initialization scripts and the most essential system program.
Other filesystems can be mounted either by the initialization scripts or directly by the users on directories of already mounted filesystems. Being a tree of directories every filesystem has its own root directory. The directory on which a filesystem is mounted is called the mount point. A mounted filesystem is a child of the mounted filesystem to which the mount point directory belongs. For instance, the /proc virtual filesystem is a child of the system 's root filesystem (and the system 's root filesystem is the parent of /proc). The root directory of a mounted filesystem hides the content of the mount point directory of the parent filesystem, as well as the whole subtree of the parent filesystem below the mount point.
简单的来说,我认为根文件系统就是一种目录结构,那么根文件系统和普通的文件系统有什么区别呢?我认为根文件系统就是要包括Linux启动时所必须的目录和关键性的文件,例如Linux启动时都需要有init目录下的相关文件,在Linux挂载分区时Linux一定会找/etc/fstab这个挂载文件等,根文件系统中还包括了许多的应用程序bin目录等,任何包括这些Linux系统启动所必须的文件都可以成为根文件系统。
2 mount 根文件系统
linux启动时,经过一系列初始化之后,需要mount 根文件系统,为最后运行init进程等做准备,
mount 根文件系统有这么几种方式:
(1)文件系统已经存在于硬盘(或者类似的设备)的某个分区上了,kernel根据启动的命令行参数(root=/dev/xxx),直接进行mount。这里有一个问题,在root文件系统本身还不存在的情况下,kernel如何根据/dev/xxx来找到对应的设备呢?原来kernel通过直接解析设备的名称来获得设备的主、从设备号,然后就可以访问对应的设备驱动了。所以在init/main.c中有很长一串的root_dev_names,通过这个表就可以根据设备名称得到设备号。
(2)从软驱等比较慢的设备上装载根文件系统,如果kernel支持ramdisk,在装载root文件系统时,内核判断到需要从软盘(fdx)mount,就会自动把文件系统映象复制到ramdisk,一般对应设备ram0,然后在ram0上mount 根文件系统。 从源码看,如果kernel编译时没有支持ramdisk,而启动参数又root=/dev/fd0, 系统将直接在软盘上mount,除了速度比较慢,理论上是可行的(这个我没试过,不知道是不是样?)
(3)启动时用到initrd来mount根文件系统。一开始我被ramdisk和initrd这两个东西弄胡涂了,其实ramdisk只是在ram上实现的块设备,initrd可以说是启动过程中用到的一种机制。就是在装载linux之前,bootloader可以把一个比较小的根文件系统的映象装载在内存的某个指定位置,姑且把这段内存称为initrd,然后通过传递参数的方式告诉内核initrd的起始地址和大小(也可以把这些参数编译在内核中),在启动阶段就可以暂时的用initrd来mount根文件系统。initrd的最初的目的是为了把kernel的启动分成两个阶段:在kernel中保留最少最基本的启动代码,然后把对各种各样硬件设备的支持以模块的方式放在initrd中,这样就在启动过程中可以从initrd所mount的根文件系统中装载需要的模块。这样的一个好处就是在保持kernel不变的情况下,通过修改initrd中的内容就可以灵活的支持不同的硬件。在启动完成的最后阶段,根文件系统可以重新mount到其他设备上,但是也可以不再重新mount(很多嵌入式系统就是这样)。 initrd的具体实现过程是这样的:bootloader把根文件系统映象装载到内存指定位置,把相关参数传递给内核,内核启动时把initrd中的内容复制到ramdisk中(ram0),把initrd占用的内存释放掉,在ram0上mount根文件系统。从这个过程可以看出,内核需要对同时对ramdisk和initrd的支持。
二.嵌入式系统根文件系统的一种实现方法。对于kernel和根文件系统都存储在flash中的系统,一般可以利用linux启动的initrd的机制。具体的过程前面已经比较清楚了,还有一点就是在启动参数中传递root=/dev/ram0,这样使得用initrd进行mount的根文件系统不再切换,因为这个时候实际的设备就是ram0。还有就是initrd的起始地址参数为虚拟地址,需要和bootloader中用的物理地址对应。
3 initrd
3.1
initrd = init ramdisk,是一个启动时存在于内存的文件系统。
initrd的最初的目的是为了把kernel的启动分成两个阶段:在kernel中保留最少最基本的启动代码,然后把对各种各样硬件设备的支持以模块的方式放在initrd中,这样就在启动过程中可以从initrd所mount的根文件系统中装载需要的模块。这样的一个好处就是在保持kernel不变的情况下,通过修改initrd中的内容就可以灵活的支持不同的硬件。在启动完成的最后阶段,根文件系统可以重新mount到其他设备上。
Linux启动一定要用initrd么?
不必,如果把需要的功能全都编译到内核中(非模块方式),只需要一个内核文件即可,initrd能够减小启动内核的体积并增加灵活性。如果你的内核以模块方式支持某种文件系统(例如ext3, UFS),而启动阶段的驱动模块(如jbd)放在这些文件系统上,内核是无法读取文件系统的,从而只能通过initrd的虚拟文件系统来装载这些模块。
这里有些人会问: 既然内核此时不能读取文件系统,那内核的文件是怎么装入内存中的呢?答案很简单,Grub是file-system sensitive的,能够识别常见的文件系统。
initrd文件是怎么生成的?
使用mkinitrd命令,这个命令其实是一个Bash脚本
#file `which mkinitrd`
/sbin/mkinitrd: Bourne-Again shell script text executable
该脚本先建立一个8M的空文件,并在此上建立一个文件系统,并拷贝相应的的文件。
一个默认RedHat Fedora Core 2, 它的initrd是什么内容
(跟系统的硬件相关)?
# file initrd-2.6.5-1.358.img
initrd-2.6.5-1.358.img: gzip compressed data, from Unix, max compression
# mv initrd-2.6.5-1.358.img initrd-2.6.5-1.358.gz
# gzip -d initrd-2.6.5-1.358.gz
# ll
-rw-r--r-- 1 root root 8192000 Jan 14 11:32 initrd-2.6.5-1.358
# mkdir /mnt/loop
# mount -o loop initrd-2.6.5-1.356 /mnt/loop
………… 中间修改此文件系统,等等…………
# umount loop
# cd /boot
# gzip -9 initrd-2.6.5-1.356
# mv initrd-2.6.5-1.356.gz initrd-2.6.5-1.356.img
3.2
标准的答案是:initrd是linux在系统引导过程中使用的一个临时的根文件系统,用来支持两阶段的引导过程。
再白话一点,initrd就是一个带有根文件系统的虚拟RAM盘,里面包含了根目录‘/’,以及其他的目录,比如:bin,dev,proc,sbin,sys等linux启动时必须的目录,以及在bin目录下加入了一下必须的可执行命令。
PC或者服务器linux内核使用这个initrd来挂载真正的根文件系统,然后将此initrd从内存中卸掉,这种情况下initrd其实就是一个过渡使用的东西。 当然也可以不卸载这个initrd,直接将其作为根文件系统使用,这当然是在没有硬盘的情况下了,这种情况多用在没有磁盘的超轻量级的嵌入式系统。 其实现在的大多数嵌入式系统也是有自己的磁盘的,所以,initrd在现在大多数的嵌入式系统中也作过渡使用。
Initrd的引导过程
‘第二阶段引导程序’,常用的是grub将内核解压缩并拷贝到内存中,然后内核接管了CPU开始执行,然后内核调用init()函数,注意,此init函数并不是后来的init进程!!!然后内核调用函数initrd_load()来在内存中加载initrd根文件系统。Initrd_load()函数又调用了一些其他的函数来为RAM磁盘分配空间,并计算CRC等操作。然后对RAM磁盘进行解压,并将其加载到内存中。现在,内存中就有了initrd的映象。
然后内核会调用mount_root()函数来创建真正的跟分区文件系统,然后调用sys_mount()函数来加载真正的根文件系统,然后chdir到这个真正的根文件系统中。
最后,init函数调用run_init_process函数,利用execve来启动init进程,从而进入init的运行过程。
4 Linux系统启动的标准流程
Linux系统启动的标准流程
系统的启动是指从计算机加电到显示用户登陆提示的整个过程。我们将在这里对整个流程以及关系到的一些内容做讨论。过程主要可以分为两个阶段:载入内核和准备运行环境,我们分别进行讨论。本部分的讨论只基于i386硬件架构,但大部分内容是有共通性的。
图一 启动过程综述
载入内核(将内核载入内存,并将控制权传递给它)
计算机加电到Boot Loader开始工作,硬件含量远大于软件含量,所以这里暂不提及,如果实在有关心的朋友,请先别着急,我们将在下期里讨论它。
这一阶段是 Boot Loader 的主战场。它必须将可执行的内核映像和内核启动所需的额外数据信息从存储介质上载入内存,这并不是件简单的工作,因为除了从硬盘载入,可能还会需要从网络引导服务器这样的外部介质上载入。各种纷繁芜杂的文件系统类型也给载入带来了巨大的挑战。
Boot Loader 可能还需要改变CPU的运行特权级别,然后就可以让内核投入运行了。
除此之外, Boot Loader 还要完成一些其它功能,比如从BIOS中获取系统信息,或者从启动时的命令行参数中提取信息等。有的 Boot Loader 还要扮演引导选择工具的角色,方便用户选择不同的操作系统。
Boot Loader的职责:
l 判断到底要载入什么,这可以要求用户进行选择
l 载入内核和它可能需要用到的相关数据,比如initrd或者其它参数
l 为内核准备好运行环境,比如,让CPU进入特权模式
l 让内核投入运行
Boot Loader的历史变迁:
早期的Linux只支持软盘引导扇区和 Shoelace 两种 Boot Loader。 Shoelace 是从Minix继承下来的、文件系统相关的 Boot Loader。它只支持 Minix 文件系统。当时Linux只使用 Minix 一种文件系统,所以这样做并没什么问题。可是, Minix 文件系统存在不能保存创建、修改和访问时间信息;文件名长度限制在14个字节等问题。随着Linux的发展,这些与传统Unix文件系统大相径庭的缺陷越来越让人难以忍受,它已经不适合作为Linux的主要文件系统了。
为了支持其它文件系统的实现,Linux引入了VFS(虚拟文件系统)。这个举措很快就引起了热烈的反响,一大批新的文件系统实现出现了。其中一个 Minix 文件系统的变体,扩展文件系统 Xiafs (根据它的作者命名)突破了 Minix 文件系统的文件名长度限制,将此长度一举提高到全部30个字符。当时文件系统之间的竞争着实激烈,很难看出谁会胜出,甚至搞不清楚会不会有一个最终的“赢家”。
尽管不确定性很大,但是有一点却是清楚的:不管最后哪种文件系统会受到青睐,但是除了 Minix 作为根文件系统,谁也不能从硬盘上启动,因为Shoelace只支持Minix文件系统。LILO应运而生了。由于支持多种文件系统(当时内核支持的主流文件系统已经有 Minix ,扩展文件系统 ext ,Xiafs 。还有人在移植 BSD 的 FFS ,根本看不出来什么时候是个尽头)在实现和维护上难度太大,而 Boot Loader 也不应该成为人们试验新的文件系统的绊脚石,所以LILO采取了和文件系统无关的设计。
这种设计经受住了时间的考验,被证明是非常成功的。即使在今天,LILO仍旧可以从内核支持的绝大部分文件系统的硬盘上启动。但是,由于ext2历经了这么长的时间一直没有大的演变,成为了事实上的标准,所以跟文件系统相关的Boot Loader又渐渐流行了起来。
尽管ext2已经能满足大部分人的日常需要,但是文件系统的设计者们还是在研制以日志机制为特征的新的文件系统,并且已经取得了相当大的进展。考虑到当前又有可能出现多种文件系统的实现同时并存的情况,因此对与文件系统无关的Boot Loader的需求可能会再次变得强劲。
初始化基本的操作环境
一旦内核开始运行,它会初始化内部的数据结构,检测硬件,并且激活相应的驱动程序,为应用软件的准备运行环境。期间包含一个重要操作——应用软件的运行环境必须要有一个文件系统,所以内核必须首先装载root文件系统。由于我们的目的是介绍基本流程,所以相关的硬件初始化细节就不再讨论,相关内容在下一期杂志中会有详细介绍。
硬件初始化完成后,内核着手创建第一个进程——初始进程。说是创建, 其实也不尽然,该进程其实是整个硬件上电初始化过程的延续,只不过执行到这里,进程的逻辑已经完备,所以我们就按照进程的创建方式给它进行了“规格化” ——我们把这个初始进程也叫做“硬件进程”,它会占据进程描述符表的第一个位置,所以可以用task[0][k1] 或INIT_TASK表示。该进程进而会再创建一个新进程去执行init()函数,其实,这个新进程才是系统第一个实际有用的进程,它会负责接着执行下一个阶段的初始化操作; 而初始进程(INIT_TASK)自己则会开始执行idle循环,也就是说,内核初始化完成之后,初始进程唯一的任务就是在没有任何其它进程需要执行的时候,消耗空闲的CPU时 间(因此初始进程也被称为idle进程)。
下一阶段的初始化工作要比前一阶段轻松一点,因为现在是由一个真正进程来接手负责完成它们了,而前一阶段都是由“硬件进程”手工去做的。在此阶段,这个由INIT_TASK创建的新进程需要初始化总线、网络并启动系统中的各种系统内核后台线程,然后再初始化外设、设置文件格式,在这之后,它要为进入系统做最后的准备——初始化文件系统,安装root文件系统,打开/dev/console设备,重定向stdin、stdout和stderr到控制台,然后搜索文件系统中的init程序,并使用 execve()系统调用加载执行init程序。系统自此进入了用户态。
装载root文件系统
为了装载文件系统,内核需要:1、知道root文件系统位于那个存储介质上;2有访问该种介质的驱动程序。
最常见的情况是,root文件系统是ext2文件系统,位于IDE硬盘上。这种情况下需要的操作很简单:将设备号作为参数给内核就可以了,IDE的设备驱动程序通常都是编译进内核的。
如果内核没有相关介质的驱动程序,问题就会变得更为复杂。而这种情况并不罕见,比如Linux安装盘使用的“通用”内核一般都会碰到这种情况。如果内核把所有支持的硬件的设备驱动程序都包含进来,就会变成一个庞然大物;而且一些驱动程序在检测硬件的时候会影响其它设备。
这个问题可以通过initrd机制解决,它允许在装载实际的root文件系统之前先使用RAM文件系统。除了上述两个原因,引入initrd还可以解决内核的动态合成问题。(详见参考资料一。)
不过,我们应该注意到,initrd在整个启动过程中并不是从来就有的,它可以说是一个插件,为了解决以上问题,而被加入启动过程,像图一所示,Linux系统在启动时也可以不选择它。
为什么要引入initrd?
Linux启动过程中肯定要载入内核镜像,在此过程中有些要素必须考虑:
首先,内核镜像不能太大。由于受到各种硬件和兼容性的限制,Linux的内核镜像不能太大,但是这并不容易做到。Linux内核的核心部分本身就不小了;而且还必须加入会使用到的驱动程序。
其次,要支持尽可能多的硬件设备。我们在启动过程中有一件重要工作:挂载root文件系统,因为进一步的数据和应用软件都在其上,所以我们的内核必须能够访问root文件系统。对于一般用户,如果他们使用IDE硬盘上的ext2文件分区作为root文件系统,不会有什么问题。因为不管是IDE硬盘还是ext2文件系统,它们的驱动肯定会包含在内核镜像自身里面。但是,确实存在一些特殊情况:比如说我们希望发行Linux系统的安装光盘,那么对光盘的驱动,就不一定包含在内核里面了。(有人可能要奇怪了,咦,光盘中的内核镜像不都已经读进来了吗,怎么内核还访问不了光盘呢?注意,读入内核镜像的是 Boot Loader ,内核并不具备 Boot Loader 的功能。)如果没有光盘的驱动,我们又怎么把光盘里的软件包安装到用户的计算机里呢?把驱动程序预先编译到内核里?听起来还不错,可是如果我们除了光盘还有一些其它的安装介质,那么所有这些驱动就会让内核镜像庞大不堪。
而且,还有更严重的问题,各种不同的驱动程序很有可能会发生冲突,特别是以前ISA设备占市场主导地位的时候,这种冲突简直难以避免。
那时的解决办法是发行商提供预先编译好的支持各种设备的不同内核,把每个内核放进一张软盘,随发行包一起交给用户,用户自己选择装有合适内核的软盘进行引导。或者给用户提供制作引导盘的工具,让用户在安装前制作自己的启动盘。当然,哪一种办法都不能让人满意。
唯一的希望在于使用模块化机制。在内核启动的时候调用相应的模块加载驱动程序,然后访问root文件系统。无论是通过内核对设备做进一步的分析还是直接从用户那里得到配置信息,先配置再加载模块的办法,都能有效地避免冲突的发生。
除了在安装的时候需要在挂载root文件系统之前调用相应的模块之外,在完成安装的系统上,我们可能仍然需要在挂载root文件系统之前调用一些模块。这主要是为给计算机进行配置——一般都要针对不同的计算机进行内核配置。
理想情况下,用户按照自己的实际情况配置编译文件,重新编译内核,一步步完成这种工作。但是没有几个用户喜欢这种冗长并且极易出现错误的工作。而且编译和生成内核需要相应的工具,可是大部分用户不需要这些工具。
在安装的过程中可以直接编译一个整体式的内核,但这并不能很好的解决问题:首先,所有的编译工具还是需要的,其次,编译过程中出现差错导致无法完成任务的概率太大了。所以,我们仍然要使用模块机制:模块机制很可靠,出了错误也只不过不加载对应的模块而已,不会使整个任务失败。而载入模块,像前面说的,也是在挂载root文件系统之前就要得到模块的。
基于以上理由,Linux引入了initrd机制。
initrd做什么
initrd允许系统在启动的时候载入一个RAM盘,这个RAM盘可以被当作一个root文件系统,程序可以在其上运行。(有两重含义,第一,程序在上面;第二,程序的文件系统环境也在上面。)在此之后,可以从别的设备上挂载一个新的root文件系统,先前的root文件系统(initrd)就会被移动到一个目录上去,最终被卸载掉。
为什么要使用RAM盘呢?首先,使用RAM盘能方便的支持以后可能发生的变化;另外,也是为了保持Boot Loader 工作尽可能的简单。在系统引导时,除了内核镜像之外,Boot Loader把所有相关的信息作为一个文件读入内存,内核在启动中将该文件作为一段连续的内存块看待。也就是把它当作RAM盘来 使用了。正因为如此,这种机制被称作“初始 RAM 盘(initial RAM Disk)”,缩写成 initrd。
initrd主要用来把系统的启动划分为两个阶段:初始启动的内核只需保留最精简的驱动程序最小集,此后,在启动必须加载附加的模块时,从initrd中加载。
initrd进行的操作
使用initrd的时候,典型的系统启动的流程变为:
1) Boot Loader读入内核镜像以及initrd文件
2) 内核将initrd文件转成“普通”的RAM盘,并且释放掉initrd文件占用的内存。
3) initrd被当作root文件系统,以可读可写(read-write)方式安装。
4) /linuxrc被执行(它可以是任何可执行文件,包括脚本在内;它以uid0身份执行,基本上能完成所有init程序可以做的工作)
5) linuxrc安装“实际”的root文件系统
6) linuxrc通过pivot_root系统调用将root文件系统放置在root目录下。
7) 常用的启动流程(比如调用/sbin/init)开始执行。
8) 卸载initrd文件系统。
注意,这是一个典型流程。其实initrd机制可以通过两种方式使用:要么就是作为一个普通的root文件系统使用,这样的话第5、第6两个步骤可以被略过,直接执行/sbin/init(我们的试验系统就是利用这种方法);要么作为一个过渡环境使用,通过它内核可以继续装载“实际”的root文件系统。