仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.0的Linux!欢迎各位大佬观摩!喜欢的话点个⭐!
仓库地址:https://github.com/Awesome-Embedded-Learning-Studio/imx-forge
静态网页:https://awesome-embedded-learning-studio.github.io/imx-forge/

老实说,当我第一次在代码里遇到并发bug的时候,我是真的懵了。
不是因为代码有多难理解——那只是几个全局变量和一个简单的计数器——而是因为这个问题完全不可复现。开发环境跑得好好的,到了用户手里就莫名其妙崩溃;调试了一百次都没问题,第一百零一次,因为timing的微小差异,系统崩了。
这类bug最折磨人的地方在于它的「幽灵性」。你调了一整天,什么都查不出来,准备下班的时候,它又出现了。你开始怀疑人生,怀疑CPU,甚至怀疑是不是量子力学在作祟。
但当你真正理解了并发之后,你会发现这一切都有迹可循。而且,在2026年的今天,这个问题比以往任何时候都重要——因为单核时代已经彻底结束了。
我们现在面对的ARM SoC,双核、四核甚至八核都是标配。i.MX6ULL虽然是2016年的芯片,但也是双核Cortex-A7。多核意味着什么?意味着真正的物理并行,意味着你的代码可能在Core 0上跑的同时,另一个线程在Core 1上跑。它们是同时执行的,不是轮流切换的。
这就导致了一个残酷的现实:你在单核思维里显而易见的「顺序」,在多核系统的现实中根本不存在。
在开始之前,先说明一下我们的环境:
这个配置很重要,因为并发问题的表现形式会根据配置不同而变化。比如在单核非抢占内核上,某些竞争条件可能永远不会出现;但在我们的双核抢占式内核上,它们就是家常便饭。
我们要做的第一件事,就是搞清楚「并发」到底从哪里来。很多初学者以为并发就是「多线程」,其实在Linux内核里,并发来源要复杂得多。
这是最基本、最容易理解的。Linux是贪婪的多任务系统,多个进程/线程本来就在竞争CPU。如果你的驱动被多个进程打开,它们可能同时进入你的read或write函数。
/* 两个进程同时打开同一个设备 */
$ cat /dev/mydevice & /* 进程A */
$ cat /dev/mydevice & /* 进程B */
这两个进程可能同时在不同的CPU核心上执行mydevice_read()函数。如果它们操作同一个全局变量,那就完了。
从2.6版本内核开始,Linux内核是可抢占的。这意味着什么?意味着你的内核代码正在运行时,调度器可能会突然说:「停,你太慢了,换别人跑。」然后把你强行换下CPU。
/* 你正在执行这个函数 */
voidmy_function(void) {
global_counter++; /* 刚执行完这一行 */
/* ← 调度器突然把你切走了! */
global_counter *= 2; /* 等你回来的时候,global_counter已经被别人改了 */
}
等你恢复运行时,全局变量已经被别人改了。这叫「上下文切换导致的竞争」。
中断的优先级很高。无论你的代码执行到哪一步,一旦硬件中断来了,CPU必须立刻响应。
voidmy_function(void) {
spin_lock(&lock); /* 获取锁 */
/* ← 硬件中断发生!CPU跳转到ISR执行 */
/* 中断服务程序ISR */
voidirq_handler(void) {
spin_lock(&lock); /* 试图获取同一把锁...死锁! */
}
spin_unlock(&lock);
}
中断说:「你先把锁放开我才能干活。」 线程说:「你把CPU还给我,让我跑完,我就能放开锁。」 互相指着鼻子,谁也动不了。这就是死锁。
现在的ARM处理器,双核、四核甚至八核都很常见。如果是多核CPU,情况就更糟了。
/* Core 0 上执行 */
voidcore0_function(void) {
global_var = 10;
}
/* 同时,Core 1 上执行 */
voidcore1_function(void) {
global_var = 20;
}
这两个函数是真正物理意义上的「同时」执行。即使关掉内核抢占,两个核心依然可以同时读写同一块内存。这是最难以调试的并发问题,因为你无法通过单步调试来重现它——单步调试时,时间被拉长了,竞争条件消失了。
前面一直在说「防止并发访问」、「防止竞争」,听起来像是在守卫一座城堡。那么问题来了:城堡里到底有什么?
我们需要保护的内容,是数据。
一定要记住:我们保护的不是代码,而是数据。
某个线程的局部变量是不需要保护的。为什么?因为局部变量存在栈上,每个线程都有自己的私有栈,互不干扰。你改你的,我改我的,井水不犯河水。
我们要保护的,是那些多个执行流都能看到、摸到的共享数据。这通常包括:
在代码中找到「谁需要保护」,是解决并发问题最难的一步。这需要你对着自己的代码,像审视犯罪现场一样分析:
open函数里被改吗?read函数里被读吗?一般来说,像全局变量、设备结构体这种「大家伙」,肯定是要重点保护的。至于其他的临时变量或局部缓存,就要视具体逻辑而定了。
理解了要保护什么,我们还需要引入一个非常重要的概念:临界区。
你可以把临界区理解为「独木桥」——一次只能过一个人。
但这个比喻有一处是不准确的:过独木桥的时候,你看得见对面有没有人;而在代码里,你看不见别的线程在干什么。
实际上,代码是静态的,执行流是动态且不可见的。你无法知道你的代码执行到哪一行时,调度器会不会突然切换到另一个线程,或者硬件会不会突然抛出一个中断。这种不可见的打断,就是「并发」的来源。
临界区就是那些「如果被打断就会出问题」的代码段。
举个简单的例子:
/* 假设 global_counter 是一个全局变量 */
global_counter++; /* 这一行代码就是临界区! */
为什么?因为在C语言层面,这只是一行代码。但在CPU层面,这变成了三件事:
global_counter的值到寄存器如果在这三步中间被打断,比如在步骤1和步骤2之间,另一个线程也读取了global_counter的值,那么两个线程都会基于同一个旧值进行加1操作。最终结果只加了1,而不是预期的2。
这就是竞争条件(Race Condition)。多个执行流「竞争」同一个资源,谁先谁后不确定,结果也不确定。
现在的问题是:怎么在代码里找到这些危险区域?
这里有一个实用的方法论:
/* 你的驱动代码 */
staticint global_counter = 0; /* ← 共享数据 */
staticstructmy_device_data *dev_data;/* ← 共享数据 */
staticintmy_open(struct inode *inode, struct file *file) {
/* ... */
}
staticssize_tmy_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
/* 这些函数都会访问上面的共享数据 */
}
/* 路径1:my_open() -> 修改 global_counter */
/* 路径2:my_read() -> 读取 global_counter */
/* 路径3:中断处理函数 -> 修改 global_counter */
my_open()my_read()会被多个进程同时调用吗?my_open()执行期间发生吗?一旦你确定了哪些代码会并发执行,就把它们标记为临界区:
/* 临界区开始 */
global_counter++;
/* 临界区结束 */
这些临界区,就是我们需要加锁保护的地方。
到这里,我们已经建立了一个核心认知:并发是不可避免的,我们必须主动管理它。
但识别并发只是第一步。接下来,我们需要学习如何控制并发。Linux内核提供了丰富的工具:
这些工具各有各的使用场景和代价,选错了锁,比不锁更可怕。在接下来的几节里,我们会逐一拆解这些武器库,让你知道什么时候用什么,以及——更重要的是——什么时候不能用什么。
因为记住,在并发世界里,错误的锁比没有锁更危险。