自启动
344.4.5自启动
# 加载 MPU6050 的内核驱动模块;这是课程随附 run.sh 中的原始命令。
insmod /oled_6050/mpu6050_drv.ko
# 加载 OLED 的内核驱动模块;绝对路径从根目录“/”开始。
insmod /oled_6050/oled_drv.ko
# 在后台启动应用程序;“&”前没有空格仍是合法的 shell 写法,保留课程源码原样。
./oled_6050/app&
# 下面是课程在 /etc/init.d/rcS 末尾追加的启动命令;它从当前目录查找 oled_6050/run.sh。
./oled_6050/run.sh
// 引入标准库声明;课程展示的信号示例要用到 exit()。
#include "stdlib.h"
// 引入标准输入输出声明;课程展示的信号示例要用到 printf()。
#include "stdio.h"
// 引入信号相关声明;signal() 和 SIGINT 均在这里声明或定义。
#include "signal.h"
// 定义收到 SIGINT 后执行的处理函数;num 用来接收信号编号。
void sigint_handler(int num)
{ // 信号处理函数开始。
// 打印提示,说明进程已经进入 SIGINT 处理函数。
printf("\r\nSIGINT signal!\r\n");
// 以状态码 0 结束进程;这是课程示例的原始写法。
exit(0);
} // 信号处理函数结束。
// 程序入口。
int main(void)
{ // main 函数开始。
// 将 SIGINT 的处理动作登记为调用 sigint_handler。
signal(SIGINT,sigint_handler);
// 持续空循环,等待信号到来;末尾分号表示循环体为空。
while(1);
// 课程源码保留了这一行,但正常情况下前面的无限循环不会走到这里。
return 0;
} // main 函数结束。
// 下面两行是视频回顾 RT-Thread 示例时展示的关键片段,不属于上面的 Linux C 程序。
// 为线程登记 SIGUSR1 的信号处理函数 thread1_signal_handler。
rt_signal_install(SIGUSR1, thread1_signal_handler);
// 解除当前线程对 SIGUSR1 的屏蔽,使该信号能够被接收。
rt_signal_unmask(SIGUSR1);
源码边界:上面前三行是项目随附
run.sh的最终内容;./oled_6050/run.sh是视频中追加到rcS的命令;C 程序是视频打开网页时展示的信号示例。为保持课程原貌,命令、函数和控制流程均未改写,只增加了逐行注释。C 示例适合解释概念,不应原样当作健壮的生产代码。
一、自启动
本节在 i.MX6UL/i.MX6ULL 开发板上,把上一节已能手工运行的 OLED 与 MPU6050 程序接入系统启动流程。目标有两个:
- 开机时自动加载
mpu6050_drv.ko和oled_drv.ko; - 自动启动
app,同时不能阻塞后续登录终端。
本课程镜像中的实际链路是:
U-Boot → Linux 内核 → 挂载根文件系统 → /sbin/init
→ /etc/inittab 的 sysinit 项 → /etc/init.d/rcS
→ run.sh → 加载两个驱动 → 后台启动 app
→ rcS 返回 → init 继续启动串口 getty → 出现登录提示
这里的关键不是“把命令随便放进某个脚本”,而是先确认当前根文件系统采用哪套 init 机制,再把命令接到它真正会执行的路径上。
1.1 写脚本
视频先在开发板根目录下的 /oled_6050 目录创建 run.sh,把原来需要逐条输入的三条命令组合起来。第一次试验时使用的是相对路径:
insmod mpu6050_drv.ko
insmod oled_drv.ko
./app
在 /oled_6050 目录中手工执行时,这三条命令可以工作,因为当时 shell 的当前工作目录正好包含这些文件。脚本本质上只是按顺序执行命令,不会自动把工作目录切换到脚本所在目录。
视频用“最高权限”的方式让脚本可执行,对应常见命令是:
chmod 777 run.sh
777 会让所有用户都能读、写、执行该文件,写权限过宽。个人实验可以复现课程操作;工程中通常只需:
chmod u+x run.sh
# 或按文件归属与部署策略使用:chmod 755 run.sh
项目随附的 run.sh 没有 #!/bin/sh。本课程的 BusyBox shell 环境仍能执行它,所以精校稿不擅自补行;自己编写可移植脚本时,通常应在首行明确解释器。
1.2 自启动修改文件
视频修改的是开发板根文件系统中的:
/etc/init.d/rcS
并在末尾追加:
./oled_6050/run.sh
这项修改必须发生在开发板实际启动所使用的根文件系统中。本课程使用 NFS 根文件系统时,Ubuntu 主机导出的目录与开发板挂载的根目录是同一份文件;在主机端修改该目录,开发板自然能看到变化。这不是“SSH 和串口自动同步文件”,而是两个终端以及主机导出目录都在操作同一套目标文件。
1.3 让Linux系统实现开机自启动
Linux 并不存在一套对所有发行版都完全相同的自启动文件。常见情况包括 BusyBox init、System V init 和 systemd。本课程随附根文件系统的 /etc/inittab 明确包含:
::sysinit:/etc/init.d/rcS
因此,init 在系统初始化阶段执行 rcS。同一文件后面还配置了串口终端:
/dev/ttymxc0::respawn:/sbin/getty -L /dev/ttymxc0 115200 vt100
也就是说,rcS 能否返回,会直接影响 init 何时继续运行到串口 getty,进而影响登录提示何时出现。Buildroot 手册也说明,采用 BusyBox init 时会运行 /etc/init.d/rcS,并由 inittab 启动终端登录程序;具体文件仍应以当前镜像为准。Buildroot init system 说明
1.4 rcS文件
本项目根文件系统中的 rcS 会遍历 /etc/init.d/S??*:
- 名称以
.sh结尾的项目会在子 shell 中被加载执行; - 其他普通启动脚本会收到
start参数; - 循环按文件名的数字顺序处理启动项。
课程为了快速演示,直接把 ./oled_6050/run.sh 追加到 rcS 末尾。这能在该镜像中工作,但规模较大的项目更适合把服务封装成独立的 S??name 启动脚本,便于控制顺序、停止服务和维护升级。
inittab 中的 sysinit 动作是同步执行的:init 会等待该命令结束。BusyBox 对 sysinit 的说明也明确指出,初始化命令完成后才会继续处理其他动作。BusyBox init 的 sysinit 执行语义
1.5 绝对路径与相对路径
路径是否可靠,取决于它如何解析:
| 写法 | 类型 | 解析起点 | 在本节中的结果 |
|---|---|---|---|
mpu6050_drv.ko | 相对路径 | 当前工作目录 | 手工在 /oled_6050 中运行成功;开机时找不到 |
./app | 相对路径 | 当前工作目录 | 手工运行成功;开机时找不到 |
/oled_6050/mpu6050_drv.ko | 绝对路径 | 根目录 / | 不依赖当前目录 |
./oled_6050/app | 相对路径 | 当前工作目录 | 本次启动时当前目录为 /,所以恰好成功 |
/oled_6050/app | 绝对路径 | 根目录 / | 更明确,不依赖当前目录 |
POSIX 路径解析规则规定,以 / 开头的路径从根目录解析;否则从当前工作目录解析。POSIX Pathname Resolution
视频把修改后的三行都称为“绝对路径”,这里需要纠正:随附源码中的第三行 ./oled_6050/app& 仍是相对路径。它在本镜像中成功,是因为执行到这里时工作目录恰好为 /;这不等于它本身是绝对路径。课程源码必须原样保留,自己写新脚本时可使用 /oled_6050/app &,或者先显式 cd /oled_6050 并检查是否成功。
1.6 重启板子
课程使用重启来检验启动脚本,而不是只在当前 shell 中试运行。重启验证能覆盖完整链路:根文件系统挂载、init、rcS、驱动模块和应用程序。
重启前应先执行 sync,确保编辑内容写回存储或 NFS 服务端;然后使用系统提供的 reboot 命令。不要把直接断电作为常规重启方式,因为正在写入的文件系统可能损坏。
1.7 查看自启动效果
第一次把相对路径版本接入 rcS 后,启动日志出现:
insmod: can't insert 'mpu6050_drv.ko': No such file or directory
insmod: can't insert 'oled_drv.ko': No such file or directory
./oled_6050/run.sh: line 3: ./app: not found
这些错误共同指向同一原因:脚本确实已经被执行,但它运行时的当前工作目录不是 /oled_6050,所以三个相对路径均未找到。由此也能证明 rcS 中追加的启动入口已生效,问题位于 run.sh 的路径写法,而不是 rcS 完全没有执行。
1.8 登录系统
第一次启动失败后,应用程序迅速退出,rcS 随后能够结束,因此串口登录提示仍会出现。登录后可以检查:
pwd
ls -l /oled_6050
cat /oled_6050/run.sh
这三步分别确认当前目录、文件是否存在,以及板端实际执行的脚本内容。排查开机脚本时,应以板端文件和启动日志为证据,不要只看主机编辑器中尚未保存的内容。
1.9 重新开启查看
在 /oled_6050 中手动执行相对路径版本仍然成功,并不与开机失败矛盾。两次执行的环境不同:
- 手工执行前,操作者已经
cd /oled_6050; rcS执行脚本时,没有自动切换到脚本目录。
因此,定位脚本问题时要同时记录“执行了什么命令”和“从哪个当前目录执行”。只比较脚本文本,容易遗漏环境差异。
1.10 修改脚本路径
视频最终把脚本改为随附源码中的版本:
insmod /oled_6050/mpu6050_drv.ko
insmod /oled_6050/oled_drv.ko
./oled_6050/app&
前两个模块使用真正的绝对路径。第三行仍以 . 开头,是相对路径;本次启动环境使它可以工作。末尾的 & 后面会在 1.14 节解释。
路径修正后若仍失败,应逐项检查文件名大小写、权限、根文件系统是否已挂载,以及模块与当前内核是否匹配。
1.11 试运行
在接入自启动之前,先手工执行脚本能够缩小排错范围:
cd /
./oled_6050/run.sh
这样能模拟课程镜像启动时的工作目录。若只在 /oled_6050 内测试,就无法提前发现相对路径问题。
测试时要同时观察三类结果:
insmod是否报错;- 内核日志中是否出现驱动初始化和匹配信息;
- 用户态应用是否持续输出合理的传感器数据,并能驱动 OLED。
1.12 查看驱动文件是否加载
视频中可见 MPU6050 驱动初始化与匹配输出,以及 OLED 驱动的相关日志,随后应用开始输出传感器数据。这说明本次启动链基本贯通。
还可以用以下方式交叉验证:
lsmod
dmesg
ps
lsmod 检查模块是否在内核模块列表中,dmesg 查看驱动初始化或探测错误,ps 检查用户态应用是否仍在运行。单独看到应用进程并不能证明两个驱动都正确加载;单独看到模块也不能证明应用已启动。
1.13 登录界面问题
去掉 &、让 app 前台运行时,应用会一直循环输出,串口登录提示没有出现。准确原因不是“CPU 没有执行空间”,而是同步控制流被阻塞:
init同步等待rcS完成;rcS同步等待run.sh完成;run.sh同步等待前台app退出;app长期运行,所以init还不能继续启动后面的串口getty。
Linux 仍在调度内核、驱动和应用,CPU 并非只能执行 app。问题是启动脚本没有返回,而不是系统失去多任务能力。
1.14 让应用程序后台执行
在 shell 命令末尾加 & 会形成异步列表:shell 启动该命令后,不等待它结束就继续执行后面的命令。因此 run.sh 和 rcS 能够返回,init 随后可以继续启动登录终端。POSIX Shell Command Language:Asynchronous Lists
./oled_6050/app&
& 解决的是“是否等待”,并不会自动关闭标准输出,也不会把程序变成完整的系统服务。因此应用在后台运行后,传感器输出仍会写到串口,与登录提示和用户输入交错。需要安静运行时,应按需求重定向输出、使用日志设施,并设计启动、停止和退出清理流程。
1.15 ssh修改脚本
课程通过 Ubuntu 端编辑 NFS 根文件系统中的 run.sh,同时在串口上观察开发板运行结果。需要区分三种角色:
- 串口终端:直接连接开发板控制台;
- SSH 会话:通过网络登录到某台系统后获得另一个 shell;
- Ubuntu 主机上的 NFS 导出目录:开发板把它当作根文件系统挂载。
文件变化立即可见的根本原因是共享同一套 NFS 文件,而不是串口协议或 SSH 协议提供了文件同步。若开发板改为从 eMMC/SD 卡启动,主机目录的修改不会自动出现在板端。
1.16 断电重启
视频通过重新上电验证最终效果。实际开发时优先使用正常关机或重启命令,让进程退出并让文件系统完成写回。只有设备已经失去响应、且已接受潜在数据损坏风险时,才考虑直接断电。
重新上电后,本节应同时满足:
- 两个驱动模块加载成功;
app自动运行并持续输出;- 串口登录提示能够出现;
- 登录后的 shell 仍可接受命令。
1.17 后台执行查看
加入 & 后,应用输出和登录界面同时出现,证明脚本不再等待 app。但“后台运行”不等于“与终端完全脱离”:程序仍继承 rcS 的标准输出和标准错误,所以串口上继续看到数据。
这也是登录时字符可能与传感器输出混排的原因。若只是课程实验,可以接受;若作为产品服务,需要规划日志去向,并避免高频输出长期占用串口与存储。
1.18 暂停问题
视频口语把 Ctrl+C 和 Ctrl+Z 都近似描述为“暂停”,这里需要严格区分:
| 按键 | 终端控制字符 | 通常产生的信号 | 默认效果 |
|---|---|---|---|
Ctrl+C | VINTR | SIGINT | 终止前台进程组中的进程 |
Ctrl+Z | VSUSP | SIGTSTP | 暂停(停止)前台进程组中的进程,可再恢复 |
在终端启用 ISIG 时,终端驱动把这些控制字符转换成相应信号。Linux termios(3)
后台启动的 app 不属于当前 shell 的前台进程组,因此登录后按 Ctrl+C 通常不会把这个后台进程作为目标。这不是 Ctrl+C 失效,而是信号发送对象不同。
1.19 ps命令
视频用 ps 找到进程:
150 root ./oled_6050/app
其中 150 只是那一次启动分配的 PID,重启或重开进程后会变化,不能写死。课程随后执行 kill -9 150,即向 PID 150 发送 SIGKILL,应用立即终止。
工程中更合适的顺序是:
ps
kill PID
# 进程在合理时间内仍不退出时,再使用:kill -KILL PID
kill PID 默认发送 SIGTERM,让程序有机会捕获信号、释放设备和保存状态;SIGKILL 不能被捕获、阻塞或忽略,内核会直接终止目标,因此也没有用户态清理机会。kill(1) signal(7)
再次对已经结束的旧 PID 执行 kill,出现“No such process”是正常现象,表示那个 PID 当前不存在;不代表 kill 命令本身损坏。
1.20 异步通知
Linux 信号是一种由内核投递给进程或特定线程的异步通知机制。产生信号的来源可以是终端控制字符、另一个进程的 kill()、内核检测到的异常,或定时器等。进程可以对多数信号采用默认动作、忽略或安装处理函数。
“异步”表示信号到达的时刻不由普通顺序代码精确决定。它不表示一定创建新线程,也不表示信号处理函数与原执行流在两个 CPU 上并行运行。
二、异步通知概念
2.1 异步通知定义
信号从使用角度看包含三个阶段:
- 产生:某个事件导致信号出现;
- 挂起与投递:若被阻塞,信号可先保持 pending;条件允许时由内核投递;
- 处理:执行默认动作、忽略,或进入用户登记的处理函数。
普通进程没有为某个信号安装处理函数时,内核按照该信号的默认动作处理。SIGKILL 和 SIGSTOP 是例外:它们不能被捕获、阻塞或忽略。Linux signal(7)
2.2 信号介绍
本节涉及的常见信号如下:
| 信号 | Linux 常见编号 | 默认动作 | 本节语境 |
|---|---|---|---|
SIGINT | 2 | 终止 | 终端 Ctrl+C 通常发给前台进程组 |
SIGTSTP | 20 | 停止 | 终端 Ctrl+Z 通常发给前台进程组 |
SIGTERM | 15 | 终止 | kill PID 的默认信号,允许程序安装清理逻辑 |
SIGKILL | 9 | 终止 | 强制结束,不能捕获或忽略 |
源码应优先写 SIGINT、SIGTERM 等符号名,而不是硬编码数字。符号能直接表达意图,也能避免把某个系统的编号假定成所有实现都相同。
2.3 信号处理程序示例
标题下方保留了视频展示的 C 示例,其流程是:注册 SIGINT 处理函数,主循环等待,收到 SIGINT 后打印提示并退出。它适合观察“默认终止动作被用户处理函数替换”的现象,但有三点不能照搬到严谨程序:
while(1);是空忙等,会持续占用 CPU;signal()的历史语义存在可移植性差异,Linux man-pages 建议新程序优先使用sigaction();printf()等 stdio 函数通常不是异步信号安全函数,在处理函数中调用可能遇到重入问题;exit()还会执行 stdio 刷新和退出处理。健壮实现通常让处理函数只做极少工作,例如设置volatile sig_atomic_t标志,或使用被标准列为异步信号安全的接口,再由正常执行流完成日志和清理。
参考:signal(2)、sigaction(2)、signal-safety(7)。这些是对示例适用范围的校正,不是对课程源码的改写。
三、异步通知
3.1 回顾外部中断原理及处理流程
视频用单片机外部中断帮助理解信号,这个类比可以说明“事件发生后转去处理”的思想,但两者不是同一种机制:
| 对比项 | 硬件中断 | Linux 信号 |
|---|---|---|
| 事件入口 | 外设通过中断控制器请求 CPU | 内核把信号标记并投递给进程或线程 |
| 主要处理位置 | 内核或裸机的中断上下文 | 用户态信号处理函数,或内核执行默认动作 |
| 上下文约束 | 中断上下文限制严格,不能任意睡眠 | 处理函数仍有异步重入限制,只能安全调用特定接口 |
| 目标 | CPU/内核的中断处理路径 | 某个进程、线程或进程组 |
因此,可把两者都理解为“事件驱动地打断原有顺序”,但不能把 SIGINT 说成 CPU 外部中断,也不能把用户态信号处理函数当作硬件 ISR。
3.2 信号处理函数示例
视频还回顾了 RT-Thread 课程中的 rt_signal_install() 与 rt_signal_unmask();标题下方保留了画面涉及的两行关键代码。它与 Linux 示例在“信号编号、屏蔽状态、处理函数”这些概念上相似,因此适合帮助迁移理解;但 RT-Thread 与 Linux 的内核实现、API、进程/线程模型及可用信号集合并不相同。实际编程必须查对应系统的文档和头文件,不能仅凭名称相同就假设行为完全一致。
四、虚拟文件系统
4.1 自启动脚本
视频末尾从根文件系统角度总结了自启动脚本。精确说法是:自启动入口由 init 系统及其配置决定,而不是由“虚拟文件系统”这一概念本身决定。本项目随附根文件系统的直接证据是 /etc/inittab 指向 /etc/init.d/rcS。
4.1.1 自启动脚本/etc/rc.local
视频引用网页,介绍一些发行版会通过 /etc/rc.local 启动用户命令,并口述 rcS 会调用 rc.local。这不是本项目镜像的事实:随附 rootfs.tar.bz2 中没有 /etc/rc.local,实际 /etc/init.d/rcS 也没有调用它。
所以,单独创建 /etc/rc.local 并不会自动获得启动能力;必须有 inittab、rcS、兼容服务或 systemd 单元明确执行它。对其他开发板或发行版,也应先检查其真实启动链,不能机械套用本节路径。
4.1.2 自启动脚本/etc/init.d
本镜像确实使用 /etc/init.d:
/etc/inittab以sysinit方式启动/etc/init.d/rcS;rcS按顺序处理/etc/init.d/S??*;- 课程直接在
rcS末尾调用/oled_6050/run.sh。
对于同一镜像,独立的 S??name 脚本通常比不断修改公共 rcS 更易维护;而在 systemd 系统中,应使用合适的 service unit。选择哪种方式取决于当前根文件系统,不能只依据目录名称判断。
4.1.3 总结
本节的完整因果关系如下:
- 先把加载模块和运行应用的命令写进
run.sh; - 通过
/etc/inittab确认该镜像会执行/etc/init.d/rcS; - 在
rcS中加入run.sh的入口; - 发现开机环境的当前目录不同,修正模块路径;
- 发现前台长驻应用阻塞
rcS,用&让 shell 不再等待它; - 重启后同时检查模块日志、应用进程和串口登录提示;
- 用
ps获取本次实际 PID,优先以SIGTERM正常结束进程,必要时才使用SIGKILL。
最终效果虽然满足课程实验,但仍要记住两个边界:./oled_6050/app& 仍依赖当前目录,后台程序也没有完整的服务管理与日志策略。课程演示说明了启动链、相对路径、前后台执行和信号的核心关系;产品化时还应补充错误检查、退出清理、日志管理以及可控的 start/stop 流程。