你有没有想过一件事——一个在MCU上写裸机程序的工程师,第一次接触嵌入式Linux时,最困惑的是什么?

不是语法,不是API,而是"操作系统"这三个字到底意味着什么。

从单片机切到嵌入式Linux,最核心的思维转变不是学会调用 open/read/write,而是理解你的程序不再拥有整个CPU。它只是个"租客"——在操作系统的管辖下,分时地使用硬件资源。而且这还不是唯一的变化,你还要面对用户态和内核态的划分、进程调度、虚拟内存……一堆MCU上根本不存在的概念。

我们来拆解这个过程。

程序是怎么跑起来的

先看一段代码:

#include <stdio.h>
#include <unistd.h>

int main() {
    FILE *fp = fopen("/sys/class/gpio/gpio17/value", "r");
    if (!fp) {
        perror("open failed");
        return 1;
    }
    char buf[4];
    fgets(buf, sizeof(buf), fp);
    printf("GPIO17 value: %s", buf);
    fclose(fp);
    return 0;
}

这段代码在嵌入式Linux上干什么?读一个GPIO的电平状态。看起来和STM32上的 HAL_GPIO_ReadPin 没什么两样,对吧?

但背后的机制完全不同。

三个层面的分工

每次你的程序调用 fopen,实际上经历了一次"权力交接":

第一层是C库。 glibc 或者 musl 接收你的调用,把它包装成一个系统调用(syscall)请求。这时候程序还在用户态,做的是参数检查、缓冲区准备这些整理工作。C库在这里像个翻译官,把标准的C函数调用翻译成内核能理解的指令。

第二层是内核。 syscall 指令会触发一个软中断,CPU从用户态切换到内核态。内核拿到你的请求后,根据文件路径去查找对应的驱动程序——在这里是 sysfs 文件系统中的 gpio 驱动。内核代表你的程序去访问硬件寄存器,然后把结果打包返回。整个过程对应用程序是完全透明的。

第三层才回到你的程序。 数据从内核空间拷贝到用户空间的缓冲区,你的 fgets 拿到了想要的内容。这层拷贝是有成本的——每次系统调用都涉及一次数据复制,这也是为什么某些性能敏感的嵌入式场景会考虑用 mmap 来避免这种开销。

有意思的是,你的程序全程不知道 GPIO 的物理地址在哪里,也不关心它。它只是通过一个文件路径和操作系统打交道。

为什么这样设计

一个常见的问题是:为什么不直接在应用程序里操作 GPIO 寄存器?在MCU上我们就是这么干的啊。

关键在于"隔离"两个字。

嵌入式Linux上跑的往往不止一个进程。你的应用程序可能只是系统中十几个服务中的一个。如果每个程序都能直接读写硬件寄存器,那任何一个程序的bug都能把整个系统拉垮。更糟糕的是,恶意程序可以直接操控所有外设,安全无从谈起。

内核在这里扮演的是"交通警察"的角色。它管着所有的硬件资源,应用程序想要什么,得按规矩来——通过系统调用提交申请。

这种设计带来了另一个好处:你的应用程序和硬件解耦了。同样一段读取 GPIO 的代码,内核驱动层改一改适配新的芯片,应用程序完全不需要动。这在产品快速迭代的场景下价值非常大。

从请求到响应

换个角度看,嵌入式Linux应用程序本质上就是个"请求-响应"的循环体:

请求(系统调用)→ 阻塞/等待 → 响应(数据返回)→ 处理 → 下一个请求

这和MCU上的事件循环有异曲同工之处,只不过"事件"变成了系统调用,"处理"变成了用户态的业务逻辑。区别在于中间多了一层操作系统调度,你的程序可能会被随时打断——这不是坏事,这恰好是公平分配CPU资源的体现。

一个有意思的推论是:你的程序跑得快不快,不完全取决于你的代码写得多高效。还取决于内核的调度策略、其他进程的负载、中断的频率。这些在MCU上根本不是问题,到了Linux上就成了必须正视的现实。

回到开头的代码

那个读取GPIO的程序,核心逻辑就三件事:fopen 打开资源、fgets 读取状态、fclose 释放资源。这种模式在嵌入式Linux应用开发中是无处不在的,不管你是操作 GPIO、I2C、SPI 还是网络socket,套路都是"打开→读写→关闭"。

换个芯片?换内核版本?只要 sysfs 或者类似的接口还在,应用代码基本不用动。

/* 同样的模式,换个设备而已 */
int fd = open("/dev/i2c-1", O_RDWR);
ioctl(fd, I2C_SLAVE, 0x48);
write(fd, &reg_addr, 1);
read(fd, &value, 1);
close(fd);

有没有发现,读 I2C 和读 GPIO 的代码框架是一模一样的?这就是Linux"一切皆文件"哲学在实际开发中的体现。

下次在嵌入式Linux上写应用的时候,不妨想想你的程序每次调用背后,内核帮你干了哪些活,你手里的CPU时间片又是从哪里来的。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐