build_embed_linux_system

文件系统构建综述

本节目录如下所示。

overview

Linux文件系统是用于管理和存储的一系列的文件的集合,按照功能上包含如下内容。

对于文件系统,可能接触过的有busybox、buildroot、yocto、openwrt,类似桌面端的debian、ubuntu、armbian、麒麟等。回顾构建和使用这些系统的经历,有问题一直在困扰我,为什么使用这些工具能够构建可用的文件系统,它们实现的原理是什么,为什么按照这些的步骤就可以实现可用的文件系统。busybox,buildroot,debian, openwrt等它们是否有什么联系?在理解这个问题以前,无论是构建文件系统,还是交叉编译软件,可以说迷雾重重,直到系统总结这个问题,才开始有了主动思考,分析并解决问题的能力。

这里用一句话解释,文件系统是被内核访问的,由目录和文件构成的树结构的集合体。它设计上是为内核提供可以对外的访问接口,而在Linux中”一切皆文件的思想”指导下,这些接口又以文件的形式展示。基于这些信息,就可以将文件系统进一步解构,包含如下结构。

  1. 创建包含系统工作需要的目录(bin, dev, etc…)
  2. 必须的配置文件和服务,以及权限管理机制(/etc/profile, /etc/init.d/…)
  3. 运行的必要环境变量和执行程序(bash, tar…)
  4. 支持上述程序或脚本运行的库(libc.so, libdrm.so…)
  5. 用户安装的扩展系统功能的程序(/usr/bin/, /sbin/)。

理解了上述这些,就可以进一步去掌握文件系统各目录的功能,具体如下。

上面是文件系统的目录结构和功能说明,对于用户来说,主要接触的目录为/dev、/sys、/proc目录。关于rootfs目录的标准定义参考网址: http://www.pathname.com/fhs

文件系统的构建过程,就是创建上述目录,并在对应目录下添加相应文件,库或者程序的过程。而上面提到文件系统的构建过程,本质上也是实现这些目录和文件,当然方法可能千差万别,既有手动编译构建,也有下载安装。理解了文件系统的本质,那么构建系统和交叉编译的问题就有另外的思路去理解。

构建系统的步骤可以分为如下:

  1. 创建系统启动的目录(包含上面的必须目录)
  2. 完善系统启动必须的动态库,放置在lib目录
  3. 创建系统执行的必要配置和服务,启动进程,配置服务等,这个在etc目录
  4. 安装系统启动的必要命令工具,如init,bash(sh), cp, ls等,放置在bin, sbin目录

至此,一个基于命令行的,可命令行访问的最小文件系统构建完成。BusyBox的构建就是基于此逻辑。不过这样构建的系统只支持最基本的功能,如etc启动配置,shell命令(ls, cd,mv)等,参考章节: Linux Shell命令说明。如果想支持更多的功能,就需要去找源码单独编译移植,然后将动态库和程序安装到相应的目录中,前面的交叉编译就是讲述了这个过程,参考章节: 嵌入式Linux平台交叉编译。可以看到在这里,知识终于串联在一起。

基于BusyBox的系统构建仅提供必要的命令工具和文件,系统中需要的目录,动态库和配置文件都需要手动创建或添加。这种方式会十分麻烦,而且不同版本或库的兼容性问题处理也十分繁杂,只能用于了解系统构建过程,在实践中很少使用。为了解决这类问题,就有技术人员发起项目,通过脚本工具链,以BusyBox这类方案为基础,集合Linux平台常用工具,配合界面化的管理,实现通过配置命令的方式直接生成打包完整的文件系统,这就是buildroot的框架本质。当然buildroot项目支持更加全面,不仅包含以busybox,systemd的基础系统,还支持扩展添加python,lua等软件,通过类似内核Kconfig的图形化配置,兼顾了自动化构建和可裁剪系统的优点,对于低性能嵌入式soc方案,目前就是最优选择。

Buildroot是通用的方案,如果应用需要很多适配硬件的配置项的调整,这也是一大难点。因此部分厂商参考这个思路,根据自家平台进一步整理个性化,包含私有的配置,然后打包以SDK形式发布,允许用户编译使用。以NXP为例,发布的Yocto系统就是按照这个思路设计的。当然,有些芯片厂商或者方案商也会根据行业的特殊需求定制对应的系统,例如Openwrt就服务于软路由实现,自带网络层转发管理和远程Web控制,其内部也集成下载工具,支持通过opkg安装离线和本地软件,是为路由应用专门定制的os系统。

BusyBox,Buildroot,Yocto和Openwrt这些文件系统的构建都涉及到环境构建和编译,不仅构建需要耗费大量的时间,而且文件系统的编译也十分依赖系统环境的支持,开发者不同版本的系统环境以及库支持的千差万别,如何解决编译的兼容问题也是构建平台的大麻烦。那么有没有将需要的二进制执行文件,脚本,配置文件压缩后放置在指定地址,用户只需要下载解压,执行安装既可以生成,不需要编译就可以完善系统呢,类似桌面端的Ubuntu使用iso安装,apt更新版本。这当然存在,嵌入式端也有同样的解决方案,Debian文件系统就是基于此方法构建的(Ubuntu属于Debian的进一步整合)。当然还有其它的文件系统,如Armbian,宝塔os等,虽然构建方式不同,也更加复杂,但仍然属于这类操作方式;理解了这些,就可以更清晰的构建文件系统。

startup

内核在启动的最后阶段,会查找并执行init对应程序,之后会挂载文件系统,通过脚本执行必要的初始化,进入shell命令行。这时就进入熟悉的命令行界面,可以通过串口进行打印访问。对于嵌入式Linux平台,主流文件系统启动方式分为SysVinit和Systemd。

  1. SysVinit管理的系统,启动执行init进程,然后再这里以基于BusyBox,添加启动脚本最终构建文件系统为例,如BusyBox,Yocto,OpenWrt,Buildroot(两种都支持)等
  2. Systemd管理的系统,这一类主要是Debian,Ubuntu,Armbian这类文件系统,包含一系列二进制程序和服务单元。通过单元为基本管理对象,每个单元代表系统中的一个资源或服务进行管理

sysvinit

Sysvinit,即System V风格的init系统,是早期Linux发行版中使用的默认init系统。init进程是Linux系统中的第一个用户级进程,其进程号始终为1,负责启动其他用户级进程或服务,系统。这里以编译的Buildroot文件系统启动流程来说明执行流程。

/etc/inittab说明

/etc/inittab是Linux系统中一个非常重要的配置文件,它用于定义init进程的行为,格式:

| identifier(标识符) | run_level(运行级别) | action(动作关键字) | process(要执行的shell命令)|

identifier(标识符) :用于唯一标识/etc/inittab文件中的每一个登记项。它通常是一个简短的字符串,最多为4个字符
run_level(运行级别) :指定相应的登记项适用于哪一个运行级。运行级别是系统的一种状态,表示系统当前运行的模式。在该字段中,可以同时指定一个或多个运行级,其中各运行级分别以数字0、1、2、3、4、5、6或字母a、b、c表示,且无需对其进行分隔。如果为空,则相应的登记项将适用于所有的运行级
    0: 关机状态
    1: 单用户模式,只有系统管理员能够登录
    2: 多用户模式,但不支持网络
    3: 完全的多用户模式,支持网络,是大多数服务器的默认模式
    4: 用户自定义的运行级别,通常不使用
    5: 图形用户界面模式,启动X-Window系统
    6: 重启系统
action(动作关键字): 用于指定init命令或进程对相应进程(在“process”字段定义)所实施的动作。具体的动作包括:
    sysinit :系统初始化,只在系统启动或重新启动时执行一次
    respawn :如果相应的进程不存在,则启动该进程;如果进程终止,则重新启动该进程
    askfirst :与respawn类似,但在运行进程前会打印提示信息,等待用户敲入回车后再执行
    wait :启动进程并等待其结束,然后再处理/etc/inittab文件中的下一个登记项
    once :启动相应的进程,但不等待该进程结束便继续处理/etc/inittab文件中的下一个登记项;当该进程死亡时,init也不重新启动该进程
    ondemand :与respawn的功能相同,但只用于运行级为a、b或c的登记项
    ctrlaltdel :当用户按下Ctrl+Alt+Del组合键时执行对应的进程
    shutdown :用于关闭系统执行的动作
    powerfail、powerwait、powerokwait、powerfailnow :与电源故障相关的动作
    boot和bootwait :在引导过程中执行的进程
    off :如果相应的进程正在运行,则发出警告信号,等待20秒后强行终止该进程
    initdefault :设置系统的默认运行级别
process(要执行的shell命令) :指定要执行的进程和它的命令行。任何合法的shell语法都适用于该字段

这里以Buildroot生成的inittab说明。

# Buildroot中的/etc/inittab文件
::sysinit:/bin/mount -t proc proc /proc         # 创建了一个到内核数据结构的接口
::sysinit:/bin/mount -o remount,rw /            # 重新挂载一个已经挂载的文件系统,改变其挂载选项为可读写
::sysinit:/bin/mkdir -p /dev/pts /dev/shm       
::sysinit:/bin/mount -a                         # 用于根据/etc/fstab文件挂载所有未挂载的文件系统
::sysinit:/bin/mkdir -p /run/lock/subsys
::sysinit:/sbin/swapon -a                       # 用于根据/etc/fstab文件启用所有标记为交换空间的设备或文件
null::sysinit:/bin/ln -sf /proc/self/fd /dev/fd
null::sysinit:/bin/ln -sf /proc/self/fd/0 /dev/stdin
null::sysinit:/bin/ln -sf /proc/self/fd/1 /dev/stdout
null::sysinit:/bin/ln -sf /proc/self/fd/2 /dev/stderr
::sysinit:/bin/hostname -F /etc/hostname        # 指定/etc/hostname作为系统hostname来源
# now run any rc scripts
::sysinit:/etc/init.d/rcS                       # 执行rcS脚本

# Put a getty on the serial port
console::respawn:/sbin/getty -L  console 0 vt100 # 指向界面显示/dev/console
ttymxc0::respawn:/sbin/getty -L  ttymxc0 0 vt100 # 指向串口显示/dev/ttymxc0

# Stuff to do for the 3-finger salute
#::ctrlaltdel:/sbin/reboot

# Stuff to do before rebooting
::shutdown:/etc/init.d/rcK
::shutdown:/sbin/swapoff -a                     # 移除swap空间
::shutdown:/bin/umount -a -r                    # 移除/etc/fstab挂载的文件系统

如果需求实现root用户直接登录。

# 串口请求修改如下
# Put a getty on the serial port
ttymxc0::respawn:-/bin/sh

/etc/fstab说明

/etc/fstab文件是Linux系统中用于定义和管理文件系统的挂载信息的配置文件。详细解释如下。

  1. 设备名:直接使用物理设备名或分区名,/dev/root表示根分区
  2. UUID:每个分区都有一个唯一标识符(UUID),使用UUID=xxxx-xxxx-xxxx格式可以更加稳定地标识设备,因为设备名在不同的启动过程中可能会改变
  3. Label:如果为分区设置了标签(Label),可以通过LABEL=MyData的方式引用设备。
  1. ext4:目前大多数Linux系统的默认文件系统,支持日志记录和大文件;
  2. vfat:FAT32文件系统,用于U盘和移动设备兼容Windows系统的情况;
  3. nfs: 网络文件系统,允许从远程服务器挂载文件系统
  4. sysfs: 虚拟文件系统,提供了内核对象的接口,用于查看和修改内核参数
  5. proc: 虚拟文件系统,提供了内核运行时信息的接口,如进程、内存、文件系统等
  1. defaults:这是一组默认的选项,包括rw(读写)、suid(允许setuid位)、dev(解释字符和块设备)、exec(允许可执行文件)、auto(自动挂载)、nouser(用户无法挂载)、async(异步I/O操作)
  2. ro/rw:以只读(ro)或读写(rw)模式挂载。
  3. noatime:不更新文件访问时间,提升性能,特别适合SSD和高性能服务器。
  4. nodiratime:不更新目录访问时间,进一步优化性能
  5. noauto:文件系统不会在系统启动时自动挂载,用户需要手动执行mount命令来挂载它。这个选项适用于不常用的设备或网络文件系统,防止因设备不可用导致的错误。
  6. noexec:禁止在文件系统上执行可执行文件。
  7. nofail:即使设备在启动时不可用,系统仍会继续正常启动,不会中断或进入应急模式。
# <file system>    <mount pt>    <type>    <options>    <dump>    <pass>
/dev/root           /           ext2      rw,noauto     0         1
proc                /proc       proc      defaults      0         0
devpts              /dev/pts    devpts    defaults,gid=5,mode=620,ptmxmode=0666    0    0
tmpfs               /dev/shm    tmpfs     mode=0777     0         0
tmpfs               /tmp        tmpfs     mode=1777     0         0
tmpfs               /run        tmpfs     mode=0755,nosuid,nodev    0    0
sysfs               /sys        sysfs     defaults      0         0

/etc/init.d/rcS说明

#!/bin/sh

# 执行/etc/init.d目录下以S*开头的脚本进行执行,以.sh结尾的直接执行,其它带start参数执行
# Start all init scripts in /etc/init.d
# executing them in numerical order.
#
for i in /etc/init.d/S??* ;do

     # Ignore dangling symlinks (if any).
     [ ! -f "$i" ] && continue

     case "$i" in
    *.sh)
        # Source shell script for speed.
        (
        trap - INT QUIT TSTP
        set start
        . $i
        )
        ;;
    *)
        # No sh extension, so fork subprocess.
        $i start
        ;;
    esac
done

对于rcS中执行的脚本,主要包含seedrng, syslogd, klogd, sysctrl, network等执行流程。

  1. seedrng: 用于为随机数生成器(RNG)提供种子的工具或命令
  2. syslogd是一个守护进程,用于记录系统运作中由kernel或应用程序产生的各种信息
  3. klogd从内核读取日志信息,解码并转发给其他服务,如syslogd,有助于系统维护和故障排查
  4. sysctl命令提供了一些选项,用于查询和修改内核参数
  5. network: 启动网络服务,执行”/sbin/ifup”,解析/etc/net/interface,进行网络处理

/etc/net/interface说明

/etc/network/interfaces文件是Linux系统中用于配置网络接口的一个关键配置文件,特别是在基于Debian的Linux发行版(如Ubuntu)中。该文件包含了网络接口的设置信息,如IP地址、子网掩码、网关、DNS服务器等。系统管理员通过编辑此文件来配置或修改网络接口的设置。

auto eth0
iface eth0 inet static
    address 192.168.1.100
    netmask 255.255.255.0
    gateway 192.168.1.1
    dns-nameservers 8.8.8.8 8.8.4.4
auto eth0
iface eth0 inet dhcp
auto lo
iface lo inet loopback

/etc/passwd说明

账户信息保存在/etc/passwd中, 包含了所有系统用户账户以及每个用户的基本配置信息,口令信息一般保存在/etc/shadow中。

# /etc/passwd说明
freedom:x:1000:1000:,,,:/home/freedom:/bin/bash
登录用户名:用户口令:用户UID:组UID:备注:$HOME:用户启动得shell
# /etc/shadow说明
freedom:$$$$$$$$$$$$$$$$$$:19831:0:99999:7:::
登录用户名:用户口令:密码最后修改时间:最小密码效期:最大密码效期:密码警告周期:密码不活跃周期:账户到期日期:保留字段
# /etc/group说明
netdev:x:116:freedom
组名:加密密码:组ID:用户列表

systemd

systemd是Linux系统中的一个系统和服务管理器,它负责在系统启动时启动系统服务、管理系统进程和资源,以及在系统运行时提供各种系统管理功能。systemd的设计目标是提供一个高效、可靠、灵活的系统管理框架,以取代传统的SysV init系统。

systemd的主要特点和功能包含:

  1. 并行启动:systemd可以并行启动多个系统服务,提高系统启动速度。
  2. 依赖管理:systemd支持服务之间的依赖关系,可以确保服务按照正确的顺序启动。
  3. 资源管理:systemd可以管理系统资源,如内存、CPU、网络等,确保系统资源的合理分配。
  4. 日志管理:systemd集成了日志管理功能,可以方便地查看和管理系统日志。
  5. 自动重启:systemd可以自动重启失败的服务,确保系统的稳定性。
  6. 动态配置:systemd支持动态配置,可以在系统运行时修改服务的配置。
  7. 用户管理:systemd支持用户管理,可以管理用户的登录和会话

systemd系统启动流程通常包括以下几个阶段.

  1. systemd进程启动:内核启动后,执行init程序,即systemd进程。systemd进程成为PID为1的进程,即系统的初始进程。
  2. 读取配置文件:systemd读取配置文件和设置,包括全局系统配置(/etc/systemd/system.conf)、用户级别的配置(/etc/systemd/user.conf)、网络配置(/etc/systemd/network/*.network)、时间同步配置(/etc/systemd/timesyncd.conf)、DNS解析配置(/etc/systemd/resolved.conf)、日志配置(/etc/systemd/journald.conf)、登录配置(/etc/systemd/logind.conf)、用户级别的服务配置(/etc/systemd/user@.service)、默认目标配置(/etc/systemd/system/default.target)等。此外,systemd还会读取环境变量、命令行参数等设置。
  3. 启动各个单元(Units):unit是systemd中的一个基本概念,表示一个系统功能或服务。systemd会根据配置文件和设置,启动各种units,包括服务(service)、设备(device)、挂载点(mount)等。每个unit都有一个名称和一个类型,systemd使用依赖关系来确保正确的启动顺序,即先启动依赖的服务或设备,再启动其他服务或设备。
  4. 启动服务:对于服务类型的units,systemd会启动相应的服务进程,并将其运行在指定的用户和组下。服务进程可以是任何可执行文件,例如HTTP服务器、数据库服务器等。systemd会监控服务的运行状态,并在需要时重新启动服务或重新加载配置文件。
  5. 监听和处理信号:systemd会监听各种信号,例如SIGTERM、SIGINT等,以响应用户的请求或系统事件。当收到信号时,systemd会根据信号的类型和目标units的状态,采取相应的操作,例如停止服务、重启服务等。

接下来systemd就会启动系统服务单元,执行相应的启动程序,关于systemd服务格式,如下所示。

[Unit]
    Description: service的简单描述
    Requires(可选): 设置服务的强依赖性,表示当前服务启动之前必须启动的其他服务
    After(可选):指定服务启动的顺序,表示当前服务应该在哪些服务之后启动
    Before(可选):与After相反,指定服务启动的顺序,表示当前服务应该在哪些服务之前启动
    Wants(可选):设置服务的弱依赖性,表示当前服务启动之后可能还需要启动的其他服务
    Conflicts(可选):定义服务间的冲突关系,表示如果当前服务启动,则指定的服务不能启动
[Service]
    Type:服务进程启动类型,如simple、forking、oneshot、dbus、notify和idle等
    ExecStart:指定启动服务时要运行的命令或脚本的绝对路径。
    ExecStartPre(可选):在ExecStart之前运行的命令或脚本
    ExecStartPost(可选):在ExecStart之后运行的命令或脚本
    ExecStop:指定停止服务时要运行的命令或脚本
    ExecReload(可选):指定重新加载服务配置时要运行的命令或脚本
    Restart:指定服务在失败或停止时是否自动重启,以及重启的策略(如always、on-failure等)
    RestartSec(可选):在服务重启之前等待的时间间隔
    EnvironmentFile(可选):指定环境配置文件的路径。
[Install]
    WantedBy:指定服务应该与哪个目标(target)一起启动,通常设置为multi-user.target,表示服务将在多用户模式下启动
    Also(可选):指定在安装本服务时还要安装的其他相关服务
    Alias(可选):为服务指定别名,方便使用systemctl命令进行管理

这边举例实现一个简单的服务,实现开机自启。

首先,创建一个名为my-service.service的systemd服务文件,内容如下:

[Unit]
Description=My Custom Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/my-service.sh
Restart=on-failure

[Install]
WantedBy=multi-user.target

然后,将该文件保存到/etc/systemd/system/目录下。

接下来,启动并启用该服务:

sudo systemctl start my-service
sudo systemctl enable my-service

这样,my-service就会在系统启动时自动启动,并且在服务失败时会自动重启。

catalogue

下面以主流的文件系统构建方法,展示如何实现用于产品运行的系统平台,具体如下所示。

next_chapter

返回目录

直接开始下一章节说明: 基于busybox构建文件系统