自启动

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

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 程序接入系统启动流程。目标有两个:

  1. 开机时自动加载 mpu6050_drv.ko 和 oled_drv.ko;
  2. 自动启动 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 内测试,就无法提前发现相对路径问题。

测试时要同时观察三类结果:

  1. insmod 是否报错;
  2. 内核日志中是否出现驱动初始化和匹配信息;
  3. 用户态应用是否持续输出合理的传感器数据,并能驱动 OLED。

1.12 查看驱动文件是否加载

视频中可见 MPU6050 驱动初始化与匹配输出,以及 OLED 驱动的相关日志,随后应用开始输出传感器数据。这说明本次启动链基本贯通。

还可以用以下方式交叉验证:

lsmod
dmesg
ps

lsmod 检查模块是否在内核模块列表中,dmesg 查看驱动初始化或探测错误,ps 检查用户态应用是否仍在运行。单独看到应用进程并不能证明两个驱动都正确加载;单独看到模块也不能证明应用已启动。

1.13 登录界面问题

去掉 &、让 app 前台运行时,应用会一直循环输出,串口登录提示没有出现。准确原因不是“CPU 没有执行空间”,而是同步控制流被阻塞:

  1. init 同步等待 rcS 完成;
  2. rcS 同步等待 run.sh 完成;
  3. run.sh 同步等待前台 app 退出;
  4. 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+CVINTRSIGINT终止前台进程组中的进程
Ctrl+ZVSUSPSIGTSTP暂停(停止)前台进程组中的进程,可再恢复

在终端启用 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 异步通知定义

信号从使用角度看包含三个阶段:

  1. 产生:某个事件导致信号出现;
  2. 挂起与投递:若被阻塞,信号可先保持 pending;条件允许时由内核投递;
  3. 处理:执行默认动作、忽略,或进入用户登记的处理函数。

普通进程没有为某个信号安装处理函数时,内核按照该信号的默认动作处理。SIGKILL 和 SIGSTOP 是例外:它们不能被捕获、阻塞或忽略。Linux signal(7)

2.2 信号介绍

本节涉及的常见信号如下:

信号Linux 常见编号默认动作本节语境
SIGINT2终止终端 Ctrl+C 通常发给前台进程组
SIGTSTP20停止终端 Ctrl+Z 通常发给前台进程组
SIGTERM15终止kill PID 的默认信号,允许程序安装清理逻辑
SIGKILL9终止强制结束,不能捕获或忽略

源码应优先写 SIGINT、SIGTERM 等符号名,而不是硬编码数字。符号能直接表达意图,也能避免把某个系统的编号假定成所有实现都相同。

2.3 信号处理程序示例

标题下方保留了视频展示的 C 示例,其流程是:注册 SIGINT 处理函数,主循环等待,收到 SIGINT 后打印提示并退出。它适合观察“默认终止动作被用户处理函数替换”的现象,但有三点不能照搬到严谨程序:

  1. while(1); 是空忙等,会持续占用 CPU;
  2. signal() 的历史语义存在可移植性差异,Linux man-pages 建议新程序优先使用 sigaction();
  3. 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 总结

本节的完整因果关系如下:

  1. 先把加载模块和运行应用的命令写进 run.sh;
  2. 通过 /etc/inittab 确认该镜像会执行 /etc/init.d/rcS;
  3. 在 rcS 中加入 run.sh 的入口;
  4. 发现开机环境的当前目录不同,修正模块路径;
  5. 发现前台长驻应用阻塞 rcS,用 & 让 shell 不再等待它;
  6. 重启后同时检查模块日志、应用进程和串口登录提示;
  7. 用 ps 获取本次实际 PID,优先以 SIGTERM 正常结束进程,必要时才使用 SIGKILL。

最终效果虽然满足课程实验,但仍要记住两个边界:./oled_6050/app& 仍依赖当前目录,后台程序也没有完整的服务管理与日志策略。课程演示说明了启动链、相对路径、前后台执行和信号的核心关系;产品化时还应补充错误检查、退出清理、日志管理以及可控的 start/stop 流程。