构建Linux根文件系统

时间:2021-03-11 16:31:33

本文主要是对韦东山老师的《嵌入式Linux应用开发完全手册》中第17章中的小结,以及一些自己的经验看法。


概要:本文讲述了Linux根目录中各个目录的作用和内容、busybox工具的使用、内核在加载init进程后进行的操作、mdev是如何被加入Linux的、glibc库的移植。

Linux文件系统概述

Linux中没有像windows的C、D、E盘之分,只有一个根文件系统,其它文件系统通过挂载到根文件系统中的某个目录下,当挂载目录中的其它文件将会变成不可访问,所以挂载点最好是空目录。

根文件目录结构

为了在安装软件时能够预知文件、目录的存放位置,为了让用户方便地找到不同类型文件的位置,在构造根文件系统时,建议遵循FHS标准(Filesystem Hierarchy Standard)。它顶一个文件系统中目录、文件分类存放的原则,定义了系统运行时所需的最小文件、目录的集合,并举例了不遵循这些原则的例外情况及其原因。FHS并不是一个强制的标准。FHS文档可以从网站http://www.pathname.com下载。

  • /bin目录
    该目录下存放的是所有用户(系统管理员和普通用户)都可以使用的命令,这些命令在加载其它文件系统之前就需要使用,所以该目录必须和根文件系统在同一个分区中。
    /bin目录下常用命令:cat、chgrp、chmod、cp、ls、sh、kill、mount、umount、mkdir、mknod、test。

  • /sbin目录
    该目录中存放的是系统命令,只有系统管理员可以使用,系统命令还可以存放在/usr/sbin、/usr/local/sbin目录下。/sbin目录下的系统命令主要用来启动系统、修复系统等。该目录必须要和根文件系统在同一分区下。
    /sbin下常用命令:shutdown、reboot、fdisk、fsck等。不是急迫需要使用的命令存放在/usr/sbin中,本地安装的系统命令安装在/usr/local/sbin。

  • /dev目录
    创建设备文件的方法有3种:

(1)手动创建

在制作根文件系统的时候,在/dev目录下就要创建好要使用的设备文件,之后在系统运行时动态添加设备时,要通过查阅/proc/devices来获得主设备号,然后再在/dev目录下创建设备节点。

(2)使用devfs文件系统

这种方法已经过时了,《嵌入式Linux应用开发完全手册》里面有较详细介绍。

(3)使用udev

udev是个用户程序,它能够根据系统状态动态地更新设备文件,包括设备文件的创建和删除。在busybox中有一个mdev命令,它是udev的简化版本,适合嵌入式应用。

  • /etc目录

该目录下存放了各种配置文件。在PC的Linux系统中该目录下的文件非常多,但是这些文件是可以删选的。在嵌入式应用中,可以根据需求大大减少该目录下的文件。

/etc目录下的子目录

目录 描述
opt 用来配置opt下的程序
X11 用来配置X windows
sgml 用来配置SGML
xml 用来配置XML

/etc 目录下的文件

文件 描述
export 用来配置NFS文件系统,只有当作为NFS服务器时使用
fstab 用来指明”mount -a”执行时,需要挂载的文件系统
mtab 用来显示已经加载的文件系统,通常是/proc/mounts的链接文件
ftpusers 启动FTP服务器时,用来配置用户的访问权限
group 用户的组文件
inittab init进程的配置文件
ld.so.conf 其它共享库的路径
passwd 密码文件
  • /lib目录

该目录下存放共享库和可加载模块,共享库用于运行根文件系统中的可执行程序。其它不是根文件系统所必须的库文件可以放在其他目录,比如/usr/lib、/usr/X11R6/lib、/var/lib.

/lib目录中的内容

目录/文件 描述
libc.so.* 动态连接C库
ld* 连接器、加载器
modules 内核可加载模式存放的目录
  • /home目录

用户目录,它是可选的,存放用户相关的配置文件。

  • /root目录

root用户的目录,存放root用户相关的配置文件。

  • /usr目录

/usr目录的内容可以存放在另一个分区中,系统启动后子再挂接到根文件目录下。里面存放的是共享、只读的程序和数据,这表明/usr目录下的内容可以在多个主机间共享。

/usr目录中的内容

目录 描述
bin 很多用户命令存放在这个目录下
include C程序的头文件,这在PC上进行开发时才用得到,在嵌入式系统中不需要
lib 库文件
local 本地目录
sbin 非必须的系统命令
share 架构无关的数据
X11R6 XWindow系统
game 游戏
src 源代码
  • /var目录

与/usr相反,里面存放的是可变的数据,比如spool目录(mail、news、打印机等用的),log文件、临时文件。

  • /proc目录

这是一个空目录,作为proc文件系统的挂载点。

  • /mnt目录

用于临时挂载某个文件系统,通常是空目录,也可以在里面创建空的子目录,比如/mnt/cdrom、/mnt/hdal等,用来临时挂接光盘和硬盘。

  • /tmp

用于存放临时文件,通常是空文件。一些需要生成临时文件的程序要用到/tmp目录,所以/tmp目录必须存在并可以访问。为减少对FLASH的操作,当在/tmp目录下挂接内存文件系统。


移植Busybox

Busybox概述:

Busybox相比同类软件的优点:比较小巧,动态连接的Busybox只有几百KB,静态连接的只有1MB左右;Busybox按模块设计,可以很容易地加入、去除某些命令。
Busybox支持uClibc库和glibc库,对Linux2.2.x之后的内核支持良好。
Busybox做的工作是,在/bin目录、/sbin目录下创建各种命令。

init进程介绍及用户程序启动过程

init进程是由内核启动的第一个(也是唯一一个)用户进程(进程ID为1),它根据配置文件决定启动哪些程序,比如执行某些脚本、启动shell、运行用户指定的程序等。init进程是后续所有的进程的发起者,比如init进程启动/bin/sh程序后,才能在控制台上输入各种命令。
在Linux系统中有两种init程序:BSD init和System V init。这两种程序各有优缺点,现在大多数Linux发行版使用System V init。但在嵌入式领域,通常使用Busybox的init。

1.内核如何启动init进程

static noinline int init_post(void)
    __releases(kernel_lock)
{   
    ...
    if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
        printk(KERN_WARNING "Warning: unable to open an initial console.\n");

    (void) sys_dup(0);
    (void) sys_dup(0);

    current->signal->flags |= SIGNAL_UNKILLABLE;

    if (ramdisk_execute_command) {
        run_init_process(ramdisk_execute_command);
        printk(KERN_WARNING "Failed to execute %s\n",ramdisk_execute_command);
    }
    if (execute_command) {
        run_init_process(execute_command);
        printk(KERN_WARNING "Failed to execute %s. Attempting " "defaults...\n",execute_command);
    }
    run_init_process("/sbin/init");
    run_init_process("/etc/init");
    run_init_process("/bin/init");
    run_init_process("/bin/sh");

    panic("No init found. Try passing init= option to kernel.");
}

接下来分析该段代码:

    if (sys_open((const char __user *)
"/dev/console", O_RDWR, 0) < 0)

负责打开设备文件/dev/console,这时候mdev还没有启动,所以必须手动创建该设备文件,当打开失败时,为打开/dev/null,所以也必须手动创建该设备文件。

    (void) sys_dup(0);
    (void) sys_dup(0);

将文件描述符0复制给文件描述符1,2,这样标准输入、标准输出、标准错误都都对应同一个设备。

if (ramdisk_execute_command) {
        run_init_process(ramdisk_execute_command);
        printk(KERN_WARNING "Failed to execute %s\n",ramdisk_execute_command);
    }

ramdisk_excute _command有三种取值:

  • 如果命令行参数指定”rdinit = …”,则ramdisk_excute _command就取改值。

  • 否则,如果/init存在,ramdisk_excute _command就等于“/init”.

  • 否则,ramdisk_excute _command为空。

注意:run_init_process函数是不返回的,所以一旦成功就没有printk(KERN_WARNING "Failed to execute %s\n",ramdisk_execute_command); 这条语句的执行。

if (execute_command) {
        run_init_process(execute_command);
        printk(KERN_WARNING "Failed to execute %s. Attempting " "defaults...\n",execute_command);
    }

execute_command有两种取值:

  • 如果命令行参数指定”init = …”,则execute_command就等于这个参数。

  • 否则,execute_command为空,该语句不执行。

    run_init_process("/sbin/init");
    run_init_process("/etc/init");
    run_init_process("/bin/init");
    run_init_process("/bin/sh");

然后,会查看这4个目录下面有没有init和sh,Busybox创建的init程序在/sbin下,所以真正会执行的是run_init_process("/sbin/init");

2.Busybox init进程的启动过程

Busybox的init进程分为两个阶段:初始化阶段执行inittab命令阶段

我们无法控制初始化阶段的执行,但是我们能控制inittab命令执行阶段。

初始化阶段

  • 设置信号处理函数

  • 初始化控制台

  • 解析inittab

当内核没有传递给init进程环境变量CONSOLE、console时,init进程将会沿用之前打开的console,同时回去检测console能否使用,若不能这打开/dev/null设备。因为init进程不与用户交互,所以会将console关闭。之后会解析inittab文件,inittab文件的详细信息可以在Busybox的/Example/inittab.txt文件中。


inittab介绍

inittab的每一个条目都确定一个子进程,并确定进程的启动方式,条目格式如下:

<id>:<runlevels>:<action>:<process>

<id>表示进程选择控制台,如果省略,使用init进程所使用的控制台。
<runlevels>对Busybox没有意义。
<action>决定了进程在哪一个阶段进行执行。
<process>将要执行的进程。前面有-,表示该进程是交互型的,比如-/bin/sh.

action属性的详细分析

action名称 执行条件 说明
sysinit 系统启动之后最先执行 只执行一次
wait 系统执行完sysinit进程之后 只执行一次
once 系统执行完wait进程之后 只执行一次
respawn 启动完once进程后(不必等待其执行结束) init进程检测发现子进程退出时,重新启动它
askfirst 启动完respawn进程后 与respwn相似,不过init进程先输出”Please press Enter to activate this console“后执行
shutdown 当系统关系时 即重启、关闭系统命令时
restart Busybox配置了CONFIG_FEATURE_USE_INITTAB,并且init进程接收到SIGHUP信号时 先重新读取、解析/etc/inittab文件,在执行restart程序
ctraltdel 按下CTRL+Alt+DEL组合键时 -

在系统启动前期,init进程首先启动sysinit、wait、once;在系统正常运行期间。init进程执行respawn、askfirst子进程,并且监视它们,发现某个子进程退出时重新启动它;在系统退出时,执行shutdown、restart、ctraltdel(之一或全部)

如果根文件系统中没有/etc/inittab文件,Busybox init 程序将使用如下的默认inittab条目:

# ::sysinit:/etc/init.d/rcS
# ::askfirst:/bin/sh
# ::ctrlaltdel:/sbin/reboot
# ::shutdown:/sbin/swapoff -a
# ::shutdown:/bin/umount -a -r
# ::restart:/sbin/init

3.编译安装Busybox

编译Busybox和编译内核的方式相似,也提供了图形界面的配置方式。

  • 修改Makefile
 CROSS_COMPILE ?= ARCH ?= $(SUBARCH)

修改成

 CROSS_COMPILE ?= arm-linux- ARCH ?= arm 
  • 配置Busybox

Busybox配置选项分类

配置项类型 说明
Busybox Setting–>General Configuration 一些通用的设置,一般不用理会
Busybox Setting–>Build Option 连接方式、编译方式
Busybox Setting–>Debugging Option 调试选项,使用Busybox时将打印一些调试信息,一般不选
Busybox Setting–>Installation Options Busybox 的安装路径,不需设置,可以在命令行中指定
Busybox Setting–>Busybox Library Tuning Busybox的性能微调,比如设置在控制台可以输入的最大字符个数,一般采用默认值
Archival Utilities 各种压缩、解压缩工具,根据需要选择相关命令
Coreutils 核心的命令,比如ls、cp
Console Utilities 控制台相关的命令,比如清屏命令clear。知识提供一些方便而已,可以不理会
Debian Utilities Debian命令
Editors 编辑命令,一般都选种vi
Finding Utilities 查找命令,一般不用
Init Utilities init程序的配置选项,比如是否读取inittab文件,使用默认配置即可
Login/Password Management Utilities 登入、用户账户、密码等方面的命令
Linux Ext2 FS Progs Ext2 文件系统的一些工具
Linux Module Utilities 加载/卸载模块的命令,一般都选中
Linux System Utilities 一些系统命令,比如显示内核打印信息
Miscellaneous Utilities 一些杂项
Networking Utilities 网络相关的命令,可以选择一些方面调试的命令,比如ping、tftp
Process Utilities 进程相关的命令,比如ps、free、kill、top,为方便调试,可以全都选中
Shells 有多种shell,比如msh、ash等,一般选择ash
System Logging Utilities 系统记录(log)方面的命令
Runit Utilities 没有用到
ipsvd Utilities 监听TCP、DPB端口,发现有新的连接时启动某个程序
  • 编译和安装Busybox
    #make

编译Busybox

#make CONFIG_PREFIX=/nfsroot install

安装Busybox

4.使用glibc库

在开发板上运行的动态连接的程序,包括我们自己编译的Busybox,都需要动态连接库,而且动态连接库的加载需要动态加载器。所以我们的lib主要有动态库文件动态库加载器

接下来确定需要哪些动态库文件和动态加载器,在我们的交叉工具链下有很多的库文件。

glibc库可以在http://ftp.gnu.org/gnu/libc/下载

glibc库的组成

  • 加载器
    动态程序启动前,它们被用来加载动态库:如ld-2.3.4.so、ls-linux.so.2

  • 目标文件(.o)
    比如cetl.o、crti.o、crtn.o等。在生成应用程序时,这些像一般的目标文件一样被连接。

  • 静态库文件(.a)
    比如静态数学库libm.a、静态C++库libstdc++.a等,编译静态文件时会连接它们。

  • 动态库文件(.so、.so.[0-9]*)
    比如动态数学库libm.so、动态C++库libstdc++.so等,它们可能是一个链接文件。

  • libtool库文件(.la)
    在连接库文件时,这些文件会被用到,比如它们列出了当前库文件所依赖的其它库文件,程序运行时无需这些文件。

  • gconv目录
    里面是有头字符集的动态库,比如ISO8859-1.so。

  • ldscripts目录
    里面是各种连接脚本,在编译应用程序时,它们被用来指定程序的运行地址、各段的位置等。

查看Busybox需要哪些动态链接库

方法一:

#arm-linux-readelf -a busybox | grep "Shared"

这种方法能够得到Busybox需要的动态库,但是不能知道Busybox需要哪些动态加载器。

方法二:

需要有ldd.host工具

    #ldd.host busybox 

这种方法能够确定动态加载器。

其实我们可以找出glibc中的动态所有加载器,然后将其全部拷贝。
在交叉工具链目录执行如下命令:

#find -name *ld* -print

然后将所需的动态库和动态库加载器全部都拷贝到我们制作的根文件系统的/lib下。


创建/etc目录

在/etc下必须要创建三个文件/etc/init.d/rcS/etc/inittab/etc/fstab.

  • 创建/etc/inittab文件
    内容如下
# /etc/inittab
::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh
::crtlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
  • 创建/etc/init.d/rcS文件

rcS是一个脚本文件,是init进程启动的第一个程序。

#!/bin/sh
mount -a 
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s 

上面的代码完成了两个内容,一个是挂在了/etc/fstab中列举的所有文件系统;另一部分都是为了完成mdev启动需要的必要代码。

  • 创建/etc/fstab文件
# device mount-point type optons dump fsck order
proc    /proc  proc    defaults   0  0 
tmpfs   /tmp   tmpfs   defaults   0  0
sysfs   /sys   sysfs   defaults   0  0
tmpfs   /dev   tmpfs   defaults   0  0

device : 要挂载的设备
mount-point:挂载点
Type:文件系统的类型
options:挂载参数
dump和fsck order :用来机额定控制dump、fsck程序的行为

详细介绍请看:《嵌入式Linux应用开发完全手册》


创建/dev设备

我们采用mdev动态创建设备文件,所以只需要创建在mdev运行前用到的两个设备文件。

#mknod console c 5 1 
#mknod null c 1 3

主次设备号是规定好的。


创建其它的目录

#mkdir proc mnt sys tmp root 

mdev介绍

mdev是udev的简化版本,它通过读取内核信息来创建设备节点文件。mdev能够初始化/dev目录,而且支持热插拔事件。要使用mdev必须要支持sysfs文件系统,同时为了减少对flash的操作,还要支持tmpfs文件系统。在配置内核的时候要选中两个文件系统。
mdev的实现过程在《Linux设备驱动程序》的Linux设备模型一节有比较详细的介绍。mdev的用法参考busybox源码中的/doc/mdev.txt

Mdev has two primary uses: initial population and dynamic updates. Bothrequire sysfs support in the kernel and have it mounted at /sys. For dynamic updates, you also need to have hotplugging enabled in your kernel.

mdev需要的代码如下:

mount -t proc proc /proc
mount -t sysfs sysfs /sys
/*下列三行可选*/
mount -t tmpfs -o size=64k,mode=0755 tmpfs /dev 
mkdir /dev/pts 
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

至此,最小的根文件系统已经做完,可以之间拿来当作NFS网络文件系统,如果需要将其制作成yaffa2文件系统,需要进一步处理。