系列第 4 篇|用户空间如何从 init 展开到应用读完本篇,启动阶段与流程的后半段就齐了
内核挂好根文件系统并启动 init 之后,故事进入用户空间(user space)。本篇解决这些事:
可以把一台在跑的 Linux 想成两层楼:
两边怎么交流?主要通过系统调用(system call / syscall)。
你日常用 C 写的大多数程序,都在用户空间。程序调用 printf、open、read、malloc,底层往往落到 libc,再通过系统调用进入内核。打开 /dev 下的设备节点、读写普通文件,最后也多半是这条路。
为什么启动流程要强调这个分界?因为「内核启动完成」只意味着底层引擎就绪;真正让你登录、开网络、跑业务程序的,是用户空间里的 init、服务和应用。而这些东西,全都住在 rootfs 里。
Rootfs = Root File System = 根文件系统。
当内核把某个文件系统挂载为 / 之后,你看到的整棵目录树(至少是根那一层)就是 rootfs 的内容,例如:
/bin/usr/bin | |
/lib/usr/lib | |
/etc | |
/sbin/usr/sbin | |
/dev/proc、/sys | |
/home |
可以记一句很实用的话:
内核镜像是发动机;rootfs 是车厢里的座位、说明书和乘客(程序)。两者都要有,车才能载人上路。
第 3 篇讲到:内核挂上 / 之后,会执行第一个用户进程——通常是 /sbin/init(可用 cmdline 的 init= 覆盖)。
这里有一个容易漏掉、但对做系统至关重要的事实:
init 不是内核自带的,而是 rootfs 里的一个普通可执行文件。
因此:
No init found,进不了用户空间 | |
把关系压成一句话:
内核负责「找到并执行」init;rootfs 负责「提供」init、它的库、配置,以及后续所有服务与应用。对 C 开发者,可把 init 想成用户空间的 main():内核只调用它一次;之后整个用户态世界由它(以及它拉起的子进程)展开。
「能启动到可用 shell」的最小集合,通常至少包括:
/sbin/init | ||
ld-linux-*.so*libc.so* 等 | ||
shmount、ls 等 | ||
/dev/proc、/sys、/tmp、/run | devtmpfs/proc/sysfs 等 | |
/etc/inittab | ||
/dev/console/dev/null 等 | devtmpfs 自动生成,极简方案可能需预置 |
目录骨架示意(极简):
/├── bin/ # 或与 /usr/bin 合并(UsrMerge)├── sbin/ # init、getty…├── lib/ # 或 lib64;动态库与 ld-linux├── etc/ # inittab、fstab、rc 脚本或 systemd 配置├── dev/ # 设备节点挂载点├── proc/ # procfs 挂载点├── sys/ # sysfs 挂载点├── tmp/└── root/ # 或 home/记住:最小不等于「只有这几个空目录」。空目录里还要有真正能执行的二进制和正确的配置。发行版 rootfs 庞大,是因为在此基础上叠了大量软件、库、本地化和默认服务。
init → services (initrc / systemd) → application完整过程:
init 是第一个用户进程,也是很多后续进程的祖先。它若异常退出,系统通常无法按正常方式继续运行。
init 的核心职责可以概括为四件事:
常见实现:
inittab/etc/init.d 脚本 | ||
.service | ||
本教程重点对比 initrc(SysV/BusyBox 风格) 与 systemd。
内核不会替你:
这些都是用户空间服务的工作。可以按「必要性」粗分:
gettylogin / sshd | ||
udev/mdev、日志(syslog/journald) | ||
networkingNetworkManager、systemd-networkd | ||
不做定制时,发行版会默认起一大串服务「保底好用」;嵌入式/Yocto 场景则会只保留真正需要的服务,以节省内存、缩短启动时间、减少攻击面。
在 BusyBox init 或传统 SysV init 里,常见做法是:
inittab 描述「开机执行什么、哪个终端上跑登录程序」;/etc/init.d/ 下的脚本,按编号启动 / 停止服务;/etc/rcS.d、/etc/rc2.d 这类目录里的符号链接,控制启动顺序(S 开头启动,K 开头停止)。BusyBox inittab 示意:
::sysinit:/etc/init.d/rcS::respawn:/sbin/getty 115200 ttyS0::ctrlaltdel:/sbin/reboot白话翻译:
sysinit | rcS(里面常会挂载文件系统、启动基础服务) |
respawn | |
ctrlaltdel |
rcS / init.d 里常见的一段逻辑(概念示意):
#!/bin/shmount -t proc proc /procmount -t sysfs sysfs /sysmount -t devtmpfs devtmpfs /dev# 按需启动服务/etc/init.d/networking start/etc/init.d/syslog start启动流程可以记成:
内核 → /sbin/init → 读 inittab → 跑 sysinit(rcS / init.d 脚本) → 挂载、基础环境 → 启动各项服务脚本 → respawn getty → 用户登录 → 跑应用特点:直观、脚本化、好改;依赖关系多靠「命名编号」人工保证,复杂系统上容易乱。
现代 Ubuntu、Debian、Fedora、CentOS 等大多使用 systemd 作为 init 系统。它用 unit 文件(常见为 .service)描述「一个服务怎么启动、依赖谁、失败了是否重启」,并用 target(如 multi-user.target)表达「运行到哪个阶段」。
最小示例:
[Unit]Description=Hello Demo ServiceAfter=network.target[Service]ExecStart=/usr/bin/helloRestart=on-failure[Install]WantedBy=multi-user.target白话翻译:
After=network.target | |
ExecStart= | |
Restart=on-failure | |
WantedBy=multi-user.target |
systemd 启动主线(概念):
内核 → /sbin/init(实际常是 systemd) → 进入默认 target(如 multi-user.target / graphical.target) → 按依赖并行拉起各 unit → getty / display manager / 你的 .service → 用户登录或业务进程就绪与 initrc 的对比:
你不必一次学完 systemd,但要建立印象:服务不是凭空出现的,而是被 init 系统按配置拉起来的。
日常口语里「服务」和「应用」常混用。做系统时建议这样区分:
sshdsystemd-networkd、日志服务、你的开机自启 daemon | ||
bash |
关系通常是:
init(PID 1) ├── 服务 A(如网络) ├── 服务 B(如 sshd) ├── 服务 C(你的业务 daemon)─── 可能再 fork 出工作进程 └── getty / 登录 shell ─────────── 用户再手动启动应用三点务实结论:
在完整发行版上,安装这些程序最常见的方式,就是下一篇的包管理器(apt / yum)。在 Yocto/Buildroot 场景,则更多是在构建 rootfs 时就把二进制和服务配置打进去。
同样叫 rootfs,来源可以差很多:
注意区分:
后面学 Yocto 时你会发现:Yocto 做的事情,本质上也包括「按你的菜谱生成一套 rootfs,并决定里面默认有哪些服务和应用」。
构建系统通常先得到一个「目录形式的 rootfs」,再按目标介质打包成镜像:
rootfs/./ 打包前目录 | ||
.tar.tar.gz、.tar.bz2、.tar.xz | ||
.cpio.cpio.gz | ||
.squashfs | ||
.ext4.img | ||
.wic.hddimg、.iso | ||
实践中常见流水线:
选工具制作 rootfs 目录 → 写入 init、库、配置、应用 → 打包成 tar / ext4 / wic / … → 烧写到 SD/eMMC/U 盘,或交给 QEMU对本教程后续 Demo 的预告:Yocto 会按 MACHINE 产出可在 QEMU 运行或可烧写到 x86_64 机器的镜像;你改的往往就是 rootfs 里有哪些包、哪些服务默认启用。
到这里,第 1 篇那条链的每一截你都应该能用白话解释了:
power on → bootloader → linux kernel→ rootfs → init → services → application自定义系统时,你裁剪最多的往往正是 rootfs 和服务集合——这也是 Yocto 最常动手的地方。
/ 的目录树;init 是其中的 PID 1 程序,二者缺一不可。