线程通信、同步、互斥
怎么解决《2.2使用RTOS的优势与挑战》里提到的问题?
比如我们的音乐任务里,即使控制线程没有获得用户输入,播放线程也在“空转”——它在不断判断“g_flag”,这使得CPU的利用率很低。这个问题可以使用队列来解决。
再比如LED任务里,它需要不断轮询按键是否被按下,CPU利用率也很低。可以把程序拆分为2部分:按键检测(在中断里释放信号量),LED操作(获得信号量后操作LED)。
APP获取数据的4种方式
4种方式
以下图生活场景为例:

妈妈怎么知道卧室里小孩醒了?
- 时不时进房间看一下:查询方式
简单,但是累
- 进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
不累,但是妈妈干不了活了
- 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll方式
要浪费点时间,但是可以继续干活。
妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
- 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
妈妈、小孩互不耽误。
这4种方法没有优劣之分,在不同的场合使用不同的方法。
在前面的项目里,播放线程就是以“查询方式”不断地查询g_flag是否为1。
后续我们可以把它改造为:
-
使用消息队列的mq_receive:可以即刻返回(NON_BLOCK),也可以休眠-唤醒
-
使用消息队列的mq_timedreceive:定个闹钟
-
使用消息队列的mq_notify:异步通知
消息队列有这4种使用方式,其他对象比如信号量、互斥量,也有这些使用模式。
openvela里常用的机制
openvala中,使用消息队列传递数据,使用信号量进行通知,使用互斥量进行独占式访问,使用信号实现异步通知,如下图所示:

openvela中常用的消息队列、信号量、互斥量、信号,列表如下:
<table style="width:100%;">
<colgroup> <col style="width: 4%" /> <col style="width: 20%" /> <col style="width: 18%" /> <col style="width: 28%" /> <col style="width: 27%" /> </colgroup> <tbody> <tr class="odd"> <td><strong>特性</strong></td> <td><strong>消息队列(Message Queue)</strong></td> <td><strong>信号量(Semaphore)</strong></td> <td><strong>互斥量(Mutex)</strong></td> <td><strong>信号(Signal)</strong></td> </tr> <tr class="even"> <td><strong>核心作用</strong></td> <td>进程 / 线程间传递数据(消息),实现异步通信</td> <td>控制多个进程 / 线程对共享资源的访问数量(如限制并发数)</td> <td>保证同一时间只有一个进程 / 线程访问共享资源(互斥访问)</td> <td>进程 / 线程间的异步事件通知(如异常处理、外部中断)</td> </tr> <tr class="odd"> <td><strong>数据传递</strong></td> <td>支持传递结构化数据(消息内容),可附带优先级</td> <td>不传递数据,仅通过计数器控制资源访问权限</td> <td>不传递数据,仅标记资源是否被占用</td> <td>可传递信号值(整数),部分信号携带附加信息(如 SIGSEGV 的内存地址)</td> </tr> <tr class="even"> <td><strong>使用场景</strong></td> <td><ol type="1"> <li><p>进程间需要传递复杂数据(如日志、命令)</p></li> <li><p>异步通信场景(如生产者 - 消费者模型)</p></li> <li><p>解耦发送方和接收方(无需实时响应)</p></li> </ol></td> <td><ol type="1"> <li><p>限制资源并发访问数(如数据库连接池、线程池)</p></li> <li><p>同步多个线程的执行顺序(如 barrier 机制)</p></li> <li><p>控制任务队列的处理节奏</p></li> </ol></td> <td><ol type="1"> <li><p>保护共享资源(如全局变量、硬件设备)</p></li> <li><p>防止多个线程同时执行临界区代码</p></li> <li><p>实现独占访问(如单例模式)</p></li> </ol></td> <td><ol type="1"> <li><p>处理异常事件(如程序崩溃、除零错误)</p></li> <li><p>进程间紧急通知(如终止信号 SIGTERM)</p></li> <li><p>定时任务触发(如 SIGALRM)</p></li> </ol></td> </tr> <tr class="odd"> <td><strong>同步方式</strong></td> <td>异步。写队列成功后会即刻返回,无需等待对方处理消息。如果队列满了,在中断上下文或者使用O_NONBLOCK时不会阻塞,其余时候都会阻塞</td> <td>同步(获取信号量失败时阻塞,直到被唤醒)</td> <td>同步(获取锁失败时阻塞,直到锁释放)</td> <td>异步(信号到达时打断程序正常执行,触发信号处理函数)</td> </tr> <tr class="even"> <td><strong>资源占用</strong></td> <td>较高(需存储消息数据,内核维护队列结构)</td> <td>较低(仅维护一个计数器)</td> <td>较低(维护锁状态和等待队列)</td> <td>极低(仅传递信号值,内核触发回调)</td> </tr> <tr class="odd"> <td><strong>典型操作</strong></td> <td>创建队列、发送消息(mq_send)、接收消息(mq_receive)、删除队列</td> <td>初始化(sem_init)、P 操作(sem_wait,减计数)、V 操作(sem_post,增计数)</td> <td>初始化(pthread_mutex_init)、加锁(pthread_mutex_lock)、解锁(pthread_mutex_unlock)</td> <td>注册处理函数(signal/sigaction)、发送信号(kill/pthread_kill)、忽略信号</td> </tr> <tr class="even"> <td><strong>关键点</strong></td> <td>消息可缓存,支持按类型 / 优先级接收,适合数据量较大的通信</td> <td>计数器初始值决定最大并发数(如初始值为 3,表示允许 3 个线程同时访问)</td> <td>具有 “所有权”(只有加锁的线程能解锁),支持递归锁(同一线程可重复加锁)</td> <td>信号处理函数需异步安全(避免使用 printf 等非线程安全函数),部分信号无法捕获(如 SIGKILL)</td> </tr> <tr class="odd"> <td><strong>主要区别</strong></td> <td>以 “数据传递” 为核心,解决进程间通信问题</td> <td>以 “资源计数” 为核心,解决并发控制问题</td> <td>以 “独占访问” 为核心,解决互斥问题</td> <td>以 “事件通知” 为核心,解决异步响应问题</td> </tr> </tbody> </table>
消息队列的使用
要使用消息队列,需要先创建或打开它,然后就可以往里面写入数据、读出数据了:

API
openvela的消息队列类似于“文件”,使用它时就像“open/read/write/close”一样有如下主要接口函数“mq_open/mq_send/mq_receive/mq_close”,对于发送、接收的功能,额外添加2个函数“mq_timedsend/mq_timedreceive”。
列表如下:
| 函数名 | 功能描述 |
| mq_open | 创建或打开一个消息队列 |
| mq_close | 关闭消息队列描述符 |
| mq_unlink | 删除消息队列(名称与内核对象解绑) |
| mq_send | 向消息队列发送消息 |
| mq_timedsend | 在指定超时时间内发送消息 |
| mq_receive | 从消息队列接收消息(获取优先级最高、最早的消息) |
| mq_timedreceive | 在指定超时时间内接收消息 |
| mq_getattr | 获取消息队列的属性 |
| mq_setattr | 设置消息队列的属性(仅mq_flags字段可修改) |
| mq_notify | 为消息队列注册一个异步事件通知。当消息队列接收到一条新消息时,系统会向注册的任务发送一个信号。 |
打开/关闭
mq_open/mq_close/mq_unlink函数管理消息队列的创建、打开、关闭和删除。
mq_open()用于创建或打开消息队列。此函数打开一个已存在的消息队列,或根据 oflags 参数创建一个新的队列。成功调用后,它返回一个消息队列描述符 (mqd_t),供后续函数使用。
函数原型如下:
mqd_t mq_open(FAR const char *mq_name, int oflags, ...)
| 参数 | 描述 |
| mq_name | 指向消息队列名称的字符串,例如 "/my_queue"。 |
| oflags | 操作标志位,可使用按位或(|)组合。 |
oflags 的常用值包括:
-
访问模式(三选一):
-
O_RDONLY:以只读方式打开。
-
O_WRONLY:以只写方式打开。
-
O_RDWR:以读写方式打开。
-
-
创建标志(可选):
- O_CREAT:如果队列不存在,则创建它。
使用此标志时,mq_open 需要两个额外参数:mode_t mode 和 struct mq_attr *attr。
-
O_EXCL:与 O_CREAT 配合使用,如果队列已存在,则调用失败。
-
O_NONBLOCK:以非阻塞模式打开。影响后续的 mq_send() 和 mq_receive() 调用。
注意:mq_open函数里,可以传入属性,比如:
struct mq_attr attr;
attr.mq_maxmsg = 10; // 最大消息数
attr.mq_msgsize = 1024; // 单条消息最大长度(字节)
mqd_t mq = mq_open("/myqueue", O_CREAT \| O_RDWR, 0644, &attr);
如果不传入attr,那么消息队列默认可以容纳16个消息,每个消息大小是32字节。
mq_close() - 关闭消息队列。此函数断开调用任务与指定消息队列之间的连接。
函数原型如下:
int mq_close(mqd_t mqdes)
调用 mq_close() 并不会销毁消息队列本身,仅释放当前任务持有的描述符。其他任务仍可通过 mq_open() 访问该队列。
mq_unlink() - 删除消息队列。此函数从系统中删除一个消息队列。
函数原型如下:
int mq_unlink(FAR const char \*mq_name)
该接口会删除名字为 mq_name 的消息队列。当有一个或多个 Task 打开一个消息队列,此时调用 mq_unlink,需要等到所有引用该消息队列的 Task 都执行关闭操作后,才会删除消息队列。
注意:如果调用 mq_unlink() 时仍有任务打开了该队列,系统会将队列标记为“待删除”状态。系统会等待所有引用该队列的任务都调用 mq_close() 后,才真正释放队列资源。
传输函数
mq_send和mq_timedsend、mq_receive和mq_timedreceive负责在任务间发送和接收消息。
mq_send() / mq_timedsend() - 发送消息,函数原型如下:
int mq_send(mqd_t mqdes, const void \*msg, size_t msglen, int prio)
int mq_timedsend(mqd_t mqdes, const char \*msg, size_t msglen, int prio,
const struct timespec \*abs_timeout);
发送消息的行为特性如下:
-
队列已满:
- 如果队列已满且未设置 O_NONBLOCK:
mq_send() 会永久阻塞,直到队列有可用空间。
mq_timedsend() 会阻塞,直到 abs_timeout指定的绝对时间超时。
- 如果设置了 O_NONBLOCK,函数会立即返回错误,而不会阻塞。
<!-- -->
- 消息长度: msglen 不能超过队列属性中定义的最大消息长度 (mq_msgsize)。
mq_receive() / mq_timedreceive()接收消息。这两个函数从指定队列中移除并返回优先级最高、等待时间最长的消息。
函数原型如下:
ssize_t mq_receive(mqd_t mqdes, void \*msg, size_t msglen, int \*prio);
ssize_t mq_timedreceive(mqd_t mqdes, void *msg, size_t msglen,
int \*prio, const struct timespec \*abstime);
接收消息的行为特性如下:
-
队列为空:
- 如果队列为空且未设置 O_NONBLOCK:
mq_receive() 会永久阻塞,直到有新消息到达。
mq_timedreceive() 会阻塞,直到 abstime 指定的绝对时间超时。
- 如果设置了 O_NONBLOCK,函数会立即返回错误。
<!-- -->
-
多任务等待: 如果有多个任务在等待同一个空队列,当新消息到达时,系统会唤醒等待时间最长且优先级最高的那个任务。
-
缓冲区大小: msglen 必须大于或等于队列的最大消息长度 (mq_msgsize)。
abs_timeout结构体定义如下:
struct timespec {
time_t tv_sec; // 秒数(自UTC时间1970-01-01 00:00:00起)
long tv_nsec; // 纳秒数(范围:0 ~ 999,999,999)
};
时间类型:必须是绝对时间(而非相对时间)。例如,若希望等待 5 秒,需先通过 clock_gettime (CLOCK_REALTIME, &ts) 获取当前系统时间,再将 ts.tv_sec += 5,而非直接设置 tv_sec=5。
超时行为:若等待时间超过 abs_timeout 指定的时间,函数会返回 - 1 并设置 errno 为 ETIMEDOUT(表示 “操作超时”)。
注意事项:tv_nsec 的值不能超过 999,999,999,否则会导致时间计算错误(例如,1,000,000,000 纳秒需转换为 1 秒 + 0 纳秒)。
属性
可以使用下面函数获得、设置消息队列的属性:
int mq_getattr(mqd_t mqdes, FAR struct mq_attr \*mq_stat);
int mq_setattr(mqd_t mqdes, FAR const struct mq_attr \*mq_stat,
FAR struct mq_attr \*oldstat);
struct mq_attr 结构体包含:
| 成员 | 描述 |
| mq_flags | 队列的标志(例如 O_NONBLOCK)。 |
| mq_maxmsg | 队列可容纳的最大消息数。 |
| mq_msgsize | 每条消息的最大字节数。 |
| mq_curmsgs | 队列中当前的消息数(仅 mq_getattr() 可获取)。 |
**注意**:消息队列的mq_maxmsg、mq_msgsize是在创建时就确定了,无法通过mq_setattr来修改。
通知
mq_notify() - 注册异步通知。此函数为消息队列注册一个异步事件通知。当消息队列接收到一条新消息时,系统会向注册的任务发送一个信号。
函数原型如下:
int mq_notify(mqd_t mqdes, const struct sigevent \*notification);
工作机制:
-
当输入参数 notification 非 NULL 时,mq_notify 会在当前任务与消息队列建立通知关联。
-
当一条新消息被放入消息队列时,系统会向该任务发送 notification 中定义的信号。
-
一次性通知: 发送信号后,该注册关系会自动解除。您必须重新调用 mq_notify() 才能接收下一次通知。
-
当 notification 为 NULL 时,函数会移除已存在的通知关联。
注意:在任何时刻,只有一个任务可以成功注册对某个消息队列的通知。
使用消息队列改造音乐任务
实验源码为:
3-4-3_使用消息队列改造音乐任务\music
本节将实现:使用队列在“控制线程”和“播放线程”直接传递数据,如下图所示。

main函数里创建了一个消息队列:
g_mq = mq_open("/myaplay", O_RDWR \| O_CREAT, 0666, &mq_stat);
还创建了一个子线程:
pthread_create(&tid1, NULL, play_thread, NULL);
在主线程里根据用户输入的信息写队列:
while (1)
{
printf("enter 0~9 (q to exit): \n");
scanf("%c", &c);
if (c == 'q' \|\| c == 'Q')
return 0;
if (c \>= '0' && c \<= '9')
{
// if (g_flag == 0)
// g_char = c;
// g_flag = 1;
/* 3. write mq */
mq_send(g_mq, &c, 1, 0);
}
}
子线程读取队列,根据队列的数值决定播放哪个wav文件:
ret = mq_receive(g_mq, &g_char, 1, 0);
……
task_create("aplay", 100, 2048, aplay_main, argv);
信号量的使用
线程之间,可以使用消息队列传递数据;不需要传递数据时,可以使用信号量进行同步。所谓“同步”,就是:A没做完事情,B等待A。这就像我们使用的flag:A执行完毕才设置flag为1;B循环等待flag为1才继续往下运行。
信号量,就是一个特殊的flag,它是一个整数:用于表示资源的数量。线程A可以增加资源数量,线程B可以等待获得资源——成功获得后资源数量减一。
要使用信号量,需要先创建或打开它,然后就可以对它执行post/wait操作了:

API
以下是 POSIX 信号量接口函数的功能说明表格:
| 函数名 | 功能描述 |
| sem_init | 初始化一个未命名的信号量,设置其初始值和共享属性 |
| sem_destroy | 销毁一个由 sem_init 初始化的未命名信号量,释放相关资源 |
| sem_open | 打开或创建一个命名信号量,返回信号量描述符 |
| sem_close | 关闭一个由 sem_open 打开的命名信号量描述符 |
| sem_unlink | 删除命名信号量的名称,当所有进程关闭该信号量后实际删除 |
| sem_wait | 信号量减 1 操作(P 操作),若信号量值为 0 则阻塞等待 |
| sem_trywait | 信号量减 1 操作(非阻塞版),若信号量值为 0 则立即返回错误 |
| sem_timedwait | 信号量减 1 操作(超时版),若信号量值为 0 则阻塞至超时 |
| sem_post | 信号量加 1 操作(V 操作),若有进程等待则唤醒其中一个 |
| sem_getvalue | 获取当前信号量的值,存储到指定的整数指针中 |
这些函数分为两类:一类用于命名信号量(sem_open/sem_close/sem_unlink),可用于不同进程间的同步;另一类用于未命名信号量(sem_init/sem_destroy),主要用于同一进程内的线程同步或共享内存中的进程同步。
未命名信号量创建与销毁
sem_init被用于初始化一个未命名的信号量(用于进程内线程间同步)。sem_destroy被用来销毁此信号量。
函数原型如下:
int sem_init(sem_t \*sem, int pshared, unsigned int value);
int sem_destroy(sem_t \*sem);
sem_init参数说明:
| 参数 | 描述 |
| sem | 指向要初始化的信号量对象的指针 |
| pshared | 标识信号量的共享范围(0 表示线程间共享,非 0 表示进程间共享) |
| value | 信号量的初始值 |
参数取值详细描述
- pshared 的取值:
0:信号量用于同一进程内的线程间同步。
非 0:信号量用于不同进程间同步(需放在共享内存中)。
- value 的取值:
初始值为非负整数,通常用于控制并发访问的资源数量。
若初始值为 0,调用 sem_wait 会阻塞直到有其他线程调用 sem_post。
sem_destroy参数说明:
| 参数 | 描述 |
| sem | 指向通过 sem_init 初始化的未命名信号量对象的指针 |
sem 必须是通过 sem_init 初始化的信号量,且当前没有线程阻塞在该信号量上,否则行为未定义。销毁后再次使用该信号量需重新初始化。
命名信号量的创建与销毁
sem_open函数被用来创建或打开一个命名信号量(用于进程间同步)。sem_close 函数用于关闭已打开的命名信号量(减少引用计数,不删除信号量)。sem_unlink函数被用于删除命名信号量(当所有进程关闭该信号量后,系统会彻底释放其资源)。
函数原型如下:
sem_t \*sem_open(const char \*name, int oflag, mode_t mode, unsigned int value);
int sem_close(sem_t \*sem);
int sem_unlink(const char \*name);
sem_open参数说明:
| 参数 | 描述 |
| name | 信号量的名称(格式通常为 "/semname") |
| oflag | 打开方式标志(创建或打开已有信号量) |
| mode | 信号量的权限位(仅当创建新信号量时有效) |
| value | 信号量的初始值(仅当创建新信号量时有效) |
参数取值详细描述:
-
oflag 的常用值:
- 基础标志:
O_CREAT:若信号量不存在则创建。
O_EXCL:与 O_CREAT 同时使用,若信号量已存在则返回错误。
- 可组合标志(可选):
O_RDWR:读写权限(通常信号量操作默认需要此权限)。
-
mode 的取值(八进制数):
-
0644:所有者可读写,组用户可读,其他用户可读。
-
0666:所有者、组用户均可读写,其他用户可读。
-
-
value 的取值:信号量的初始值
sem_close参数说明:
| 参数 | 描述 |
| sem | 指向通过 sem_open 打开的信号量对象的指针 |
sem 必须是通过 sem_open 返回的有效信号量指针,关闭后再次操作该指针会导致错误。
sem_unlink参数说明:
| 参数 | 描述 |
| name | 要删除的信号量的名称(与 sem_open 中使用的名称一致) |
name 必须是已存在的命名信号量名称,格式为 "/semname",否则返回错误。
等待信号量
信号量的基础操作为等待、释放,也被称为P操作、V操作。
等待信号量(P 操作):若信号量值大于 0,则将其减 1 并返回;若值小于或等于0,则阻塞等待或返回错误。
释放信号量(V 操作):将信号量值加 1,若有线程阻塞在 sem_wait 上,则唤醒其中一个。
sem_wait会尝试获得信号量,如果不成功则会“一直等待到成功”。sem_trywait会尝试获得信号量,成功则返回0,不成功也会立刻返回错误。sem_timedwait则引入一个超时时间,如果在这段时间内成功获得信号量则返回0,如果这段时间内都无法获得信号量则返回错误。
函数原型如下:
int sem_wait(sem_t \*sem);
int sem_trywait(sem_t \*sem);
int sem_timedwait(sem_t \*sem, const struct timespec \*abs_timeout);
sem_wait、sem_trywait参数说明:
| 参数 | 描述 |
| sem | 指向已初始化的信号量对象的指针 |
sem 必须是通过 sem_init 或 sem_open 初始化的有效信号量指针,否则行为未定义。
sem_timedwait参数说明:
| 参数 | 描述 |
| sem | 指向已初始化的信号量对象的指针 |
| abs_timeout | 指向绝对时间结构体的指针,指定等待的超时时间上限 |
sem 必须是通过 sem_init 或 sem_open 初始化的有效信号量指针,否则行为未定义。
abs_timeout结构体定义如下:
struct timespec {
time_t tv_sec; // 秒数(自UTC时间1970-01-01 00:00:00起)
long tv_nsec; // 纳秒数(范围:0 ~ 999,999,999)
};
时间类型:必须是绝对时间(而非相对时间)。例如,若希望等待 5 秒,需先通过 clock_gettime (CLOCK_REALTIME, &ts) 获取当前系统时间,再将 ts.tv_sec += 5,而非直接设置 tv_sec=5。
超时行为:若等待时间超过 abs_timeout 指定的时间,函数会返回 - 1 并设置 errno 为 ETIMEDOUT(表示 “操作超时”)。
注意事项:tv_nsec 的值不能超过 999,999,999,否则会导致时间计算错误(例如,1,000,000,000 纳秒需转换为 1 秒 + 0 纳秒)。
释放信号量
sem_post函数被用于释放信号量(V 操作):将信号量值加 1,若有线程阻塞在 sem_wait 上,则唤醒其中一个。
函数原型如下:
int sem_post(sem_t \*sem);
参数说明:
| 参数 | 描述 |
| sem | 指向已初始化的信号量对象的指针 |
sem 必须是通过 sem_init 或 sem_open 初始化的有效信号量指针,否则行为未定义。
调用后信号量值会增加,即使超过初始值也允许(无上限)。
使用信号量改造LED任务
在LED任务里,它使用轮询模式获得按键状态。我们把它改造成:
-
主循环里等待信号量
-
按键中断里释放信号量
使用GPIO中断
参考代码:apps/examples/gpio/gpio_main.c
首先,提供信号处理函数,示例代码为:
void signal_handler(int signum) {
}
signal(SIGINT, signal_handler);
然后配置GPIO为中断功能,示例代码如下:
ret = ioctl(fd_button, GPIOC_SETPINTYPE, GPIO_INTERRUPT_FALLING_PIN);
最后注册GPIO信号,示例代码如下:
struct sigevent notify;
notify.sigev_notify = SIGEV_SIGNAL; // 中断函数要发出信号
notify.sigev_signo = SIGINT; // 发出这个信号
// 设置GPIO让它发出信号
ret = ioctl(fd_button, GPIOC_REGISTER, (unsigned long)¬ify);
在信号处理函数里post信号量
代码如下:
static sem_t g_sem;
void my_signal_handler(int signo)
{
/* post semphore */
sem_post(&g_sem);
}
在主循环里等待信号量
代码如下:
sem_init(&g_sem, 0, 0);
while (1)
{
if (sem_wait(&g_sem) == 0)
{
ret = ioctl(fd_led, GPIOC_WRITE, 0);
sleep(1);
ret = ioctl(fd_led, GPIOC_WRITE, 1);
}
}
上机操作
如下启动led程序后,可以通过按键K1操作LED,也可以使用kill命令给led进程发送信号来操作LED。
按下K1,LED会亮一下;发送信号,LED也会亮一下。

互斥量的使用
我们使用互斥量来保护临界资源,下面的代码保证了“同一时间”只有一个任务能运行“临界代码”,实现了“互斥操作”:

为何需要互斥操作
如下实现一个全局变量的累加函数:
static volatile usigned int g_val ;
static void add_val(void)
{
g_val++;
}
然后创建线程A,它循环调用add_val一千万次;创建线程B,循环调用add_val一千万次。理论上,g_val最终值是2千万。
上机测试结果为:
nsh> hello
exit add_thread
g_val = 10010648
原因是“add_val”函数被打断了。把“g_val++”拆分为3个步骤:
① 读内存,把g_val的值读入CPU寄存器R0
② R0里的值累加
③ 把R0的值写入内存g_val
假设当前g_val的值为1,线程A、B调度过程如下图所示:
① 线程A调用add_val,刚把g_val的读入R0,就发生了切换
② 切换的时候会保存线程A运行时的R0,即保存7
③ 线程B运行完add_val,成功把g_val从7加到了8
④ 线程A继续运行:内核把之前保存的7先恢复到R0,然后运行add_val函数的②③步,使得g_val的值为8
这个过程触发了矛盾:线程A、B都调用了add_val各一次,但是g_val只增加了1。

原因就是:“add_val”函数里g_val的累加被打断了,线程A在运行“add_val”的过程中,线程B打断了它并且也运行“add_val”。
我们可以使用信号量来保护“add_val”里对g_val的操作,代码如下:
static volatile usigned int g_val ;
static void add_val(void)
{
sem_wait(&g_sem);
g_val++;
sem_post(&g_sem);
}
上述代码保证了:“同一时间内,只有一个线程能访问g_val”,解决了本节前面提出的Bug。
本程序里把信号量的最大值限定为1时,可以使用信号量进行互斥操作:
-
线程A想访问共享资源时,要先获得信号量:如果信号量的值为1,则线程A把值减为0,返回成功。
-
线程B也想访问同一个共享资源,也要先获得信号量:这时信号量的值为0,线程B阻塞
-
线程A可以独占地访问共享资源
-
线程A用完共享资源后要释放信号量,这会唤醒线程B
-
线程B成功获得信号量,可以独占地使用共享资源
这些共享资源如果不使用互斥方法进行保护,会导致程序问题,这些共享资源就被称为临界资源。
信号量导致的优先级反转问题
上述代码还有问题。使用信号量进行互斥操作时,会导致优先级反转的问题。举例如下:
-
低优先级任务 A 获取信号量,进入临界区。
-
高优先级任务 B 启动,尝试获取同一信号量,因资源被占用而阻塞。
-
中优先级任务 C 启动,由于其优先级高于任务 A,它抢占了 CPU。
结果:任务 C(以及其他中等优先级任务)持续运行,导致任务 A 无法执行并释放信号量。这使得高优先级任务 B 被中等优先级任务 C 无限期地阻塞,仿佛任务 B 的优先级低于任务 C。这种情况下优先级反转的问题就出现了,最高优先级的任务B反而被中优先级的任务C抢占了CPU。
使用互斥量(mutex)可以解决优先级反转的问题,互斥量是特殊的、最大值为1的信号量。
注意:openvela里互斥量能解决优先级反转的前提是配置了“CONFIG_PRIORITY_INHERITANCE”或“CONFIG_PRIORITY_PROTECT”,详情请看《3.6.2 信号量和互斥量的内部机制》。
互斥量的创建/销毁
pthread_mutex_init 函数被用来初始化一个互斥锁,为后续的线程同步操作(加锁、解锁)做准备,可初始化普通互斥锁或带有特定属性的互斥锁。
pthread_mutex_destroy函数被用来销毁已初始化的互斥锁,释放互斥锁占用的资源,销毁后互斥锁不能再进行加锁、解锁操作,除非重新初始化。
函数原型如下:
int pthread_mutex_init(pthread_mutex_t \*mutex, const pthread_mutexattr_t \*attr);
int pthread_mutex_destroy(pthread_mutex_t \*mutex);
pthread_mutex_init 函数参数说明:
| 参数 | 描述 |
| mutex | 指向待初始化的互斥锁对象的指针 |
| attr | 指向互斥锁属性对象的指针,若为 NULL 则使用默认属性初始化互斥锁 |
参数取值详细描述
-
mutex 必须是有效的 pthread_mutex_t 类型指针,未初始化的互斥锁直接使用会导致行为未定义。
-
attr 的取值:
NULL:使用默认属性,初始化后的互斥锁为普通互斥锁,默认属性下线程加锁后需自身解锁,其他线程加锁会阻塞。
非 NULL:需先通过 pthread_mutexattr_init 初始化属性对象,再设置特定属性(如互斥锁类型、进程共享属性等),最后传入此参数。
常见的属性设置场景包括:
-
互斥锁类型:通过 pthread_mutexattr_settype 设置为 PTHREAD_MUTEX_NORMAL(普通锁,不检测死锁)、PTHREAD_MUTEX_ERRORCHECK(错误检测锁,检测死锁并返回错误)、PTHREAD_MUTEX_RECURSIVE(递归锁,允许同一线程多次加锁)等。
-
进程共享属性:通过 pthread_mutexattr_setpshared 设置为 PTHREAD_PROCESS_PRIVATE(默认,仅进程内线程共享)或 PTHREAD_PROCESS_SHARED(可跨进程共享,需放在共享内存中)。
pthread_mutex_destroy函数参数说明:
| 参数 | 描述 |
| mutex | 指向已初始化的互斥锁对象的指针 |
参数取值详细描述:
-
mutex 需为已初始化的有效互斥锁指针,若传入未初始化或已销毁的互斥锁指针,会导致行为未定义。
-
销毁前提:互斥锁必须处于未锁定状态,且没有线程阻塞在该互斥锁的加锁操作上,否则函数可能返回错误(如 EBUSY),且销毁操作失败。
-
销毁后状态:互斥锁对象变为未初始化状态,若需再次使用,需重新调用 pthread_mutex_init 进行初始化。
属性初始化与销毁
前面的pthread_mutex_init 函数可以传入一个attr参数,这个参数有专门的初始化函数:pthread_mutexattr_init函数。
pthread_mutexattr_init函数被用来初始化互斥锁属性对象,为后续设置互斥锁的特定属性(如类型、进程共享性等)做准备,属性对象可用于初始化多个互斥锁。
pthread_mutexattr_destroy 函数被用来销毁已初始化的互斥锁属性对象,释放属性对象占用的资源,销毁后属性对象不能再用于设置属性或初始化互斥锁,除非重新初始化。
函数原型如下:
int pthread_mutexattr_init(pthread_mutexattr_t \*attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t \*attr);
pthread_mutexattr_init函数参数说明:
| 参数 | 描述 |
| attr | 指向待初始化的互斥锁属性对象的指针 |
参数取值详细描述
-
attr 必须是有效的 pthread_mutexattr_t 类型指针,未初始化的属性对象直接用于设置属性或初始化互斥锁,会导致行为未定义。
-
初始化后状态:属性对象初始化为默认状态,默认属性对应的互斥锁为普通类型(PTHREAD_MUTEX_NORMAL)、进程内私有(PTHREAD_PROCESS_PRIVATE)。
pthread_mutexattr_destroy参数说明:
| 参数 | 描述 |
| attr | 指向已初始化的互斥锁属性对象的指针 |
参数取值详细描述
-
attr 需为已初始化的有效互斥锁属性指针,若传入未初始化或已销毁的属性指针,会导致行为未定义。
-
销毁影响:属性对象销毁后,之前通过该属性对象初始化的互斥锁不受影响,互斥锁的属性会保持初始化时的设置。
互斥量的lock操作
pthread_mutex_lock函数被用来对互斥锁进行加锁操作:若互斥锁当前未被锁定,则当前线程成功加锁并继续执行;若互斥锁已被其他线程锁定,则当前线程阻塞,直到互斥锁被解锁后再尝试加锁。
pthread_mutex_trylock函数尝试对互斥锁进行加锁操作(非阻塞):若互斥锁当前未被锁定,则当前线程成功加锁并返回成功;若互斥锁已被锁定,则直接返回错误,不阻塞当前线程。
函数原型如下:
int pthread_mutex_lock(pthread_mutex_t \*mutex);
int pthread_mutex_trylock(pthread_mutex_t \*mutex);
pthread_mutex_lock函数参数说明:
| 参数 | 描述 |
| mutex | 指向已初始化的互斥锁对象的指针 |
参数取值详细描述:
-
mutex 必须是通过 pthread_mutex_init 初始化的有效互斥锁指针,若传入未初始化或已销毁的互斥锁指针,会导致行为未定义。
-
不同类型互斥锁的加锁差异:
普通互斥锁(PTHREAD_MUTEX_NORMAL):若同一线程对已加锁的互斥锁再次加锁,会导致死锁,且函数不会返回错误。
错误检测锁(PTHREAD_MUTEX_ERRORCHECK):若同一线程对已加锁的互斥锁再次加锁,函数会返回 EDEADLK 错误,避免死锁。
递归锁(PTHREAD_MUTEX_RECURSIVE):允许同一线程多次加锁,加锁次数与解锁次数需一致,最后一次解锁后其他线程才能加锁。
pthread_mutex_trylock函数参数说明:
| 参数 | 描述 |
| mutex | 指向已初始化的互斥锁对象的指针 |
参数取值详细描述:
-
mutex 需为已初始化的有效互斥锁指针,否则行为未定义,与 pthread_mutex_lock 的 mutex 参数要求一致。
-
返回值差异:
成功加锁:返回 0。
互斥锁已被锁定:返回 EBUSY 错误,不会阻塞线程,线程可继续执行其他逻辑,后续可再次尝试加锁。
- 与 pthread_mutex_lock 的核心区别:前者非阻塞,加锁失败立即返回;后者阻塞,直到加锁成功或出现其他错误。
互斥量的unlock操作
pthread_mutex_unlock函数对互斥锁进行解锁操作:若当前线程是互斥锁的持有线程,则解锁后互斥锁变为未锁定状态,若有其他线程阻塞在该互斥锁的加锁操作上,会唤醒其中一个线程尝试加锁。
函数原型如下:
int pthread_mutex_unlock(pthread_mutex_t \*mutex);
pthread_mutex_unlock函数参数说明:
| 参数 | 描述 |
| mutex | 指向已初始化且处于锁定状态的互斥锁对象的指针 |
参数取值详细描述
-
mutex 必须是已初始化且当前被锁定的有效互斥锁指针,若传入未锁定、未初始化或已销毁的互斥锁指针,会导致行为未定义。
-
解锁限制:
普通互斥锁和错误检测锁:仅允许持有互斥锁的线程解锁,若其他线程尝试解锁,错误检测锁会返回 EPERM 错误,普通互斥锁行为未定义。
递归锁(PTHREAD_MUTEX_RECURSIVE):需由持有线程按照加锁次数依次解锁,每次解锁会减少锁的计数,当计数减为 0 时,互斥锁才真正变为未锁定状态。
信号实例
信号是在软件层次上对中断机制的模拟,在原理上说,一个任务接收到一个信号,与CPU接收到中断请求是一致的。信号是异步的,任务不必通过任何操作来等待信号的到达,它甚至不知道信号何时会到达。
信号的来源包括:
-
硬件来源,比如按键触发
-
软件来源,比如kill,raise等系统函数,比如一些非法运算操作等
实例1:GPIO中断发出信号
在《3.4.5 使用信号量改造LED任务》里面,就用到了信号:GPIO驱动程序里中断函数发出了信号,而APP里为这个信号提供了处理函数。主循环里不知道何时收到信号,信号是“异步”的。
首先,提供信号处理函数,示例代码为:
void signal_handler(int signum) {
}
signal(SIGINT, signal_handler);
然后配置GPIO为中断功能,示例代码如下:
ret = ioctl(fd_button, GPIOC_SETPINTYPE, GPIO_INTERRUPT_FALLING_PIN);
最后注册GPIO信号,示例代码如下:
struct sigevent notify;
notify.sigev_notify = SIGEV_SIGNAL; // 中断函数要发出信号
notify.sigev_signo = SIGINT; // 发出这个信号
// 设置GPIO让它发出信号
ret = ioctl(fd_button, GPIOC_REGISTER, (unsigned long)¬ify);
实例2:软件发出信号
发送信号的API函数:
/* 当前任务向指定pid的任务(线程组)发送sig信号 */
int kill(pid_t pid, int sig);
接收方先注册信号处理函数(led.c):
signal(SIGINT, my_signal_handler);
发送方创建LED进程以便得到对方的PID(music.c):
led_pid = task_create("led", 100, 2048, led_main, argv);
发送方使用kill函数发送信号(music.c):
kill(led_pid, SIGINT);
实例3:写队列时发出信号
配置内核禁止“CONFIG_DISABLE_MQUEUE_NOTIFICATION”以便使能“信号量能发送信号”,执行如下命令:
./build.sh vendor/allwinnertech/boards/r528/r528s3-velaevb1/configs/nsh/ menuconfig
根据下图进行配置:

play_thread里先注册信号处理函数(music.c):
signal(SIGUSR1, aplay_signal_handler);
然后第1次设置队列,让它能发出信号:
struct sigevent notification;
/* let mq, send signal to play_thread when get msg from mq */
notification.sigev_notify = SIGEV_SIGNAL; // 要发出信号
notification.sigev_signo = SIGUSR1; // 发出这个信号
mq_notify(g_mq, ¬ification);
以后主线程(music.c)写消息时,会向play_thread发信号SIGUSR1:
if (c \>= '0' && c \<= '9')
{
// if (g_flag == 0)
// g_char = c;
// g_flag = 1;
/* 3. write mq */
mq_send(g_mq, &c, 1, 0);
kill(led_pid, SIGINT);
}
play_thread的信号处理函数被调用,它再次使用mq_notify让队列以后还能发出信号,然后读取队列、播放音乐:
void aplay_signal_handler(int sig)
{
char file\[32\];
char \* argv\[3\] = {"/aplay", file, NULL};
// if (g_flag)
struct sigevent notification;
/* let mq, send signal to play_thread when get msg from mq */
notification.sigev_notify = SIGEV_SIGNAL; // 要发出信号
notification.sigev_signo = SIGUSR1; // 发出这个信号
mq_notify(g_mq, ¬ification);
/* 4. read mq */
printf("to recv msg \n");
int ret = mq_receive(g_mq, &g_char, 1, 0);
printf("recv msg: %c\n", g_char);