云计算百科
云计算领域专业知识百科平台

常见嵌入式操作系统中的线程(任务)间通信机制

常见嵌入式操作系统中的线程(任务)间通信机制


目录

  • 1. 概述:线程间通信解决什么问题
  • 2. 机制全景与选型维度
  • 3. 数据传递类机制
    • 3.1 消息队列(Message Queue)
    • 3.2 邮箱(Mailbox)
    • 3.3 管道与字节流(Pipe / Stream Buffer)
    • 3.4 变长缓冲队列与环形缓冲(Ring Buffer)
  • 4. 事件通知类机制
    • 4.1 信号量(通知用法)
    • 4.2 事件标志组(Event Flags)
    • 4.3 任务通知与内嵌任务队列
    • 4.4 信号与异步回调(Signal)
  • 5. 资源共享类机制
  • 6. Linux 扩展机制
  • 7. 跨系统接口对比总表
  • 8. ISR(中断上下文)与任务通信的特殊性
  • 9. 选型建议与典型场景
  • 10. 常见误区与注意事项

1. 概述:线程间通信解决什么问题

在多线程(任务)系统中,每个线程拥有独立的栈和调度上下文,但共享同一地址空间(无 MMU 的 RTOS)或各自独立的地址空间(Linux 进程)。线程之间要协作完成一项功能,必然需要交换信息,这就是线程间通信(Inter-Thread/Task Communication,常归入 IPC)。

需要先厘清三个经常被混为一谈的概念:

概念目的典型手段
通信(Communication) 传递「数据」或「事件内容」 消息队列、邮箱、管道
同步(Synchronization) 协调「执行的先后顺序」 信号量、事件标志组、任务通知
互斥(Mutual Exclusion) 保护「共享资源不被同时访问」 互斥锁、临界区、自旋锁

三者在工程实践中是交织使用的:消息队列本身既是通信也是同步(队列为空时接收方自然阻塞等待);共享内存必须配合互斥锁才安全;事件标志组只做同步不带数据,但往往配合共享缓冲使用。本文聚焦「通信」与「同步」中承担信息传递职责的机制;互斥机制属于「并发与竞争保护」范畴,另有专文讨论,本文仅在资源共享类机制中涉及其配合关系。

一个几乎所有 RTOS 内核对象共享的底层结构值得先记住:

内核对象 = 数据区(缓冲/位掩码/计数值) + 等待列表(等待该对象的阻塞任务队列,通常按优先级排序)

无论是队列、信号量还是事件组,当条件不满足时,任务被挂到该对象的等待列表上让出 CPU;当条件满足(消息入队、位置位、信号量释放)时,内核从等待列表中唤醒最高优先级任务,必要时触发任务调度。理解这一点,后面所有机制的行为(阻塞、超时、唤醒顺序、优先级继承与否)都能推导出来。


2. 机制全景与选型维度

在这里插入图片描述

按通信目的,常见机制可分为四大类:

(1)数据传递类 —— 通信双方要交换具体的数据内容:

  • 消息队列(Message Queue):多对多、按值拷贝、FIFO 排队缓冲,是最通用的机制;
  • 邮箱(Mailbox):一对一为主,常传指针或定长小消息,缓冲浅(语义因内核差异大);
  • 管道 / 字节流(Pipe / Stream Buffer):无消息边界的字节流,适合串口数据、日志流;
  • 环形缓冲 / 变长缓冲队列:字节级或变长消息的轻量缓冲。

(2)事件通知类 —— 只传递「发生了什么」,不带或只带极少量数据:

  • 信号量(通知用法):可计数的事件通知(give/take);
  • 事件标志组(Event Flags):32 个 bit 表达多种事件,支持 AND/OR 复合等待;
  • 任务通知(Task Notification)/ 内嵌任务队列:直接挂在任务控制块上的定向通知,最快最省 RAM;
  • 信号 / 回调(Signal):异步打断式通知(POSIX 信号、软中断信号)。

(3)资源共享类 —— 多线程共同读写同一块数据:

  • 共享内存 / 全局变量:零拷贝、效率最高,但必须配合互斥锁、临界区等保护手段;
  • 内存池 / 内存块 + 队列传递指针:「数据放池里、指针走队列」的经典组合。

(4)Linux 扩展类 —— 嵌入式 Linux 下进程与线程通用的 IPC:

  • POSIX 消息队列、Unix Domain Socket、netlink、eventfd/signalfd、pipe/FIFO、shm/mmap、DBus/Binder 等。

选型时常用的评估维度:

维度要回答的问题
数据量与形式 只通知一个事件,还是要传几十上百字节?定长还是变长?
通信拓扑 一对一、多对一、多对多?是否需要广播?
缓冲深度 生产快于消费时,允许积压多少?满了怎么办(阻塞/丢弃/覆盖)?
时效性 能否容忍排队延迟?是否要紧急消息插队?
拷贝开销 按值拷贝(安全、解耦)还是传指针(零拷贝、需管理生命周期)?
上下文 发送方/接收方是否在 ISR 中?ISR 中只能调用 FromISR / no-wait 接口
资源占用 内核对象本身消耗多少 RAM?创建多少个合适?

3. 数据传递类机制

3.1 消息队列(Message Queue)

机制原理

消息队列是最通用的线程间通信机制:发送方把一条消息按值拷贝进内核维护的队列,接收方按 FIFO 顺序取出。RTOS 中的队列通常是固定槽位数 × 固定消息尺寸的环形缓冲(便于确定性内存管理),Linux/POSIX 消息队列同样限定最大消息数和单消息最大长度。

在这里插入图片描述

核心行为特征:

  • 解耦:发送方与接收方互不知道对方存在,可任意增减生产者/消费者;
  • 缓冲:生产消费速率不匹配时由队列吸收抖动;
  • 双向阻塞:队列空 → 接收方可阻塞(带超时);队列满 → 发送方可阻塞(带超时)或返回失败;
  • 按值拷贝:消息入队后发送方的缓冲可立即复用,无生命周期管理负担(代价是 memcpy 开销);
  • ISR 可用:中断里可以用 FromISR / no-wait 变体发送消息,是「ISR → 任务递延处理」的标准做法。

各系统接口与实现解析

系统内核对象创建发送接收消息形式
Linux(POSIX mq) mqd_t mq_open() mq_send() / mq_timedsend() mq_receive() / mq_timedreceive() 按值,变长(≤ mq_msgsize)
Linux(SysV) msgid msgget() msgsnd() msgrcv() 按值,变长,支持按类型选收
FreeRTOS QueueHandle_t xQueueCreate() xQueueSend() / xQueueSendToFront() / xQueueOverwrite();ISR 用 xQueueSendFromISR() xQueueReceive() / xQueuePeek() 按值,定长
μC/OS-II OS_EVENT(Q) OSQCreate() OSQPost() / OSQPostFront() / OSQPostOpt() OSQPend() / OSQAccept() 传指针(void*),不拷贝
μC/OS-III OS_Q OSQCreate() OSQPost() OSQPend() 传指针 + 携带消息大小
RT-Thread rt_mq_t rt_mq_create() rt_mq_send() / rt_mq_send_wait() / rt_mq_urgent()(插队头) rt_mq_recv() 按值,定长
Zephyr struct k_msgq K_MSGQ_DEFINE() / k_msgq_alloc_init() k_msgq_put() k_msgq_get() / k_msgq_peek() 按值,定长;另有 k_fifo(传指针)
ThreadX TX_QUEUE tx_queue_create() tx_queue_send() / tx_queue_front_send() tx_queue_receive() 按值,1~16 个 32 位字定长
NuttX POSIX mq mq_open() mq_send() mq_receive() 按值,定长上限
LiteOS 队列 ID LOS_QueueCreate() LOS_QueueWrite() / LOS_QueueWriteCopy() LOS_QueueRead() / LOS_QueueReadCopy() 指针模式或拷贝模式可选
AliOS Things kqueue_t krhino_queue_create()(aos 层 aos_queue_new()) krhino_queue_send() / krhino_queue_back_send() krhino_queue_recv() 按值,定长(环形缓冲)

实现层面的几个重要差异点:

  • 按值拷贝 vs 传指针:FreeRTOS、RT-Thread、Zephyr k_msgq、ThreadX、POSIX mq 是按值拷贝,发送后缓冲即可复用;μC/OS-II/III 的队列只传 void* 指针,数据本体由发送方保证有效(常与内存分区 OSMem* 搭配)。LiteOS 两种模式都提供(Write 传指针 / WriteCopy 拷贝)。
  • 插队能力:RT-Thread rt_mq_urgent()、FreeRTOS xQueueSendToFront()、ThreadX tx_queue_front_send()、μC/OS-II OSQPostFront() 支持把消息放到队首,用于紧急事件。
  • 「覆盖」语义:FreeRTOS 对长度为 1 的队列提供 xQueueOverwrite()——队列已满时直接覆盖旧值,等价于"最新值邮箱",适合传递最新采样值这类「丢旧保新」场景。
  • 优先级唤醒:以上系统的等待列表都按优先级排序(Linux POSIX mq 唤醒规则视实现),队列非空时唤醒最高优先级等待者。
  • 删除语义:删除队列时(如 RT-Thread rt_mq_delete、μC/OS-III OSQDel),挂起的任务会被唤醒并收到错误码,需妥善处理。
  • 最小示例(RT-Thread 风格,其他系统结构类似):

    static rt_mq_t mq;
    struct msg { rt_uint32_t type; rt_uint32_t value; };

    void producer(void *p) {
    struct msg m = { .type = 1, .value = 100 };
    rt_mq_send(mq, &m, sizeof(m)); /* 按值拷贝入队 */
    }
    void consumer(void *p) {
    struct msg m;
    if (rt_mq_recv(mq, &m, sizeof(m), RT_WAITING_FOREVER) == RT_EOK)
    handle(&m);
    }


    3.2 邮箱(Mailbox)

    机制原理

    「邮箱」在嵌入式领域的语义因内核差异极大,使用前先查手册定义。大体分为两种形态:

    • 单格邮箱:只能存一封邮件(通常一个指针),新邮件覆盖或失败——μC/OS-II 的 Mbox;
    • 排队邮箱:可存多封定长邮件,本质是「消息尺寸很小的队列」——RT-Thread 的 mb(邮件固定 4 字节,通常存指针)、Zephyr 的 k_mbox(支持发送方指定接收线程的握手式投递)。

    在这里插入图片描述

    邮箱的典型用法是**「轻量通知 + 指向共享数据块」**:数据本体放在内存池或全局缓冲中,邮箱里只传指针,兼顾零拷贝与线程安全。

    各系统接口与实现解析

    系统对象关键接口语义
    μC/OS-II OS_EVENT(Mbox) OSMboxCreate() / OSMboxPost() / OSMboxPostOpt()(可广播) / OSMboxPend() / OSMboxAccept() 单格,存 void*;PostOpt 可向所有等待者广播
    μC/OS-III —— 已取消独立 Mbox 对象,推荐用深度为 1 的 OS_Q 或内嵌任务消息队列 ——
    RT-Thread rt_mailbox_t rt_mb_create() / rt_mb_send() / rt_mb_send_wait() / rt_mb_recv() / rt_mb_urgent() 多封排队,每封固定 4 字节(指针大小)
    Zephyr struct k_mbox k_mbox_init() / k_mbox_put() / k_mbox_async_put() / k_mbox_get() / k_mbox_data_get() 支持指定目标线程的投递-接收握手(发送方可阻塞到对方取走)
    FreeRTOS —— 无独立邮箱;用长度 1 队列 + xQueueOverwrite() 模拟「最新值邮箱」 ——
    ThreadX —— 无独立邮箱;用深度 1 的 TX_QUEUE ——
    Linux / NuttX / LiteOS / AliOS —— 无独立邮箱概念,用消息队列实现 ——

    实现要点:

    • μC/OS-II Mbox 的 OSMboxPostOpt(…, OS_POST_OPT_BROADCAST) 是少见的广播能力:一条消息同时唤醒所有等待该邮箱的任务;
    • Zephyr k_mbox 是同步式投递:k_mbox_put() 默认阻塞到接收方取走消息(rendezvous 语义),适合「必须确认对方收到」的场景;k_mbox_async_put() 则交给系统工作队列异步投递;
    • RT-Thread 邮箱满了,rt_mb_send() 立即返回失败,而 rt_mb_send_wait() 允许发送方限时阻塞——这是它与 μC/OS 单格邮箱「满了只能失败」的关键区别。

    3.3 管道与字节流(Pipe / Stream Buffer)

    机制原理

    消息队列传递的是「有边界的一条条消息」,而管道传递的是无边界的字节流——像文件一样 read/write,读者不感知写入的分包。它天然适合串口收发、日志输出、Shell 数据转发等流式场景。Linux 的管道是进程间 IPC 的元老;RTOS 中则以「流缓冲」形式出现。

    各系统接口与实现解析

    系统机制关键接口特点
    Linux 匿名管道 pipe() / pipe2();命名管道 mkfifo() read() / write() / poll() 半双工字节流,内核缓冲默认 64 KiB;FIFO 可在无亲缘进程间使用;全双工需求用 socketpair()
    FreeRTOS Stream Buffer / Message Buffer xStreamBufferCreate() / xStreamBufferSend() / xStreamBufferReceive();Message Buffer 版本在流上加长度头 单写者、单读者(多写需外加互斥);极轻量、可 ISR 使用;触发级别可配
    Zephyr struct k_pipe k_pipe_init() / k_pipe_alloc_init() / k_pipe_put() / k_pipe_get() 多线程安全的字节流管道,支持超时与「至少读够 N 字节」
    RT-Thread 环形缓冲 + 设备框架 rt_ringbuffer_init() / rt_ringbuffer_put() / rt_ringbuffer_get();DFS 下另有 pipe 设备 ringbuffer 是无锁前提下的单生产单消费缓冲;串口设备内部即基于它
    NuttX POSIX pipe / FIFO pipe() / mkfifo() / read() / write() 完整 POSIX 语义
    Linux 通用说明 —— —— 线程间也能用管道(同进程内更常用 socketpair 或自建队列),但开销比纯内存队列大
    μC/OS-II/III、ThreadX、LiteOS —— 无内核级管道,用「环形缓冲 + 信号量」自行组合 见 3.4

    行为要点:

    • 阻塞语义:Linux 管道写满则写者阻塞(或 O_NONBLOCK 返回 EAGAIN),读空则读者阻塞;
    • 消息边界:字节流没有边界,要传「包」需自定协议(长度头/分隔符)——FreeRTOS 的 Message Buffer 正是为此把长度头打进流里;
    • 触发级别:FreeRTOS Stream Buffer 可设置「缓冲达到 N 字节才唤醒读者」,避免逐字节唤醒的抖动。

    3.4 变长缓冲队列与环形缓冲(Ring Buffer)

    机制原理

    消息队列要求定长消息,但实际业务常要传变长数据(一条 AT 指令、一帧协议)。两条常见路线:

  • 变长消息缓冲队列:消息连长度头一起存进字节环形区,取走时按长度还原——AliOS Things 的 buf_queue;
  • 环形缓冲(Ring/FIFO Buffer)+ 信号量:裸的字节环形区,「数据有没有、有多少」由信号量或计数变量通知——这是没有管道机制的系统上的通用做法。
  • 各系统接口与实现解析

    系统机制关键接口特点
    AliOS Things kbuf_queue_t krhino_buf_queue_create() / krhino_buf_queue_send() / krhino_buf_queue_recv() 变长消息按值存入环形区,内核管理分包
    Zephyr struct k_fifo / k_lifo / k_ringbuf k_fifo_put() / k_fifo_get();ring_buf_put() / ring_buf_get() k_fifo 传的是链表节点指针(零拷贝、要求节点头部预留内核字段);ring_buf 是裸字节环形区(默认无同步,需自配信号量;另有 ring_buf_item_* 带简单同步)
    RT-Thread rt_ringbuffer rt_ringbuffer_put() / rt_ringbuffer_get() 单生产单消费无锁;多线程需自配互斥/信号量
    FreeRTOS Stream Buffer(见 3.3) —— 本质即带同步的环形缓冲
    LiteOS 软件定时器/队列之外无专用对象 —— 用队列指针模式 + 内存池实现变长数据传递
    μC/OS-II/III、ThreadX 无专用对象 —— 组合:内存池(OSMemGet / tx_block_allocate)申请块 → 队列传指针 → 用完归还

    「内存池 + 队列传指针」组合是 RTOS 里传递大块/变长数据的经典范式,务必掌握:

    发送方:MemPool.alloc() → 填充数据 → Queue.send(指针)
    接收方:Queue.recv(指针) → 使用数据 → MemPool.free()

    相比按值拷贝省一次 memcpy,但把缓冲区生命周期管理交给了应用:谁释放、泄漏了怎么办、ISR 里能否 alloc,都要设计清楚。


    4. 事件通知类机制

    4.1 信号量(通知用法)

    机制原理

    信号量有两个用法:计数资源(竞争保护范畴)和事件通知(本文范畴)。作为通知机制时:

    • 二值信号量(Binary Semaphore)= 「事件发生了」的一次性门铃;
    • 计数信号量 = 「事件发生了 N 次」的可累积计数(这是它相对事件标志组的核心优势——能计数)。

    发送方 give/post,接收方 take/pend(带超时)。它不带任何数据,是成本最低的通信原语,也是 ISR → 任务唤醒的最常用手段。

    各系统接口与实现解析

    系统创建释放(give)获取(take)ISR 变体
    FreeRTOS xSemaphoreCreateBinary() / xSemaphoreCreateCounting() xSemaphoreGive() xSemaphoreTake() xSemaphoreGiveFromISR()
    μC/OS-II OSSemCreate() OSSemPost()(OS_POST_OPT_BROADCAST 可广播) OSSemPend() / OSSemAccept() 直接调 Post(μC/OS 无独立 FromISR 命名,ISR 中调非阻塞版)
    μC/OS-III OSSemCreate() OSSemPost()(支持广播 opt) OSSemPend() 同上
    RT-Thread rt_sem_create() rt_sem_release() / rt_sem_release()(连续) rt_sem_take() / rt_sem_trytake() ISR 中直接 rt_sem_release()(该 API 本身 ISR 安全)
    Zephyr K_SEM_DEFINE() / k_sem_init() k_sem_give() k_sem_take() k_sem_give() ISR 安全
    ThreadX tx_semaphore_create() tx_semaphore_put() tx_semaphore_get() tx_semaphore_put() ISR 安全(tx_semaphore_ceiling_put 有上限版)
    NuttX sem_init() sem_post() sem_wait() / sem_timedwait() sem_post() 是异步信号安全的
    LiteOS LOS_SemCreate() / LOS_BinarySemCreate() LOS_SemPost() LOS_SemPend() ISR 中可 LOS_SemPost()
    AliOS Things krhino_sem_create() krhino_sem_give() / krhino_sem_give_all()(广播) krhino_sem_take() give 系列 ISR 安全
    Linux(线程间) sem_init(pshared=0) sem_post() sem_wait() / sem_timedwait() 用户态无 ISR 概念;信号处理函数中 sem_post() 安全

    要点:二值信号量会丢事件——连续 give 两次只等效一次(计数值封顶 1);要计数就用计数信号量。这决定了「每来一次中断都要被处理一次」的场景必须用计数型或队列,不能用二值型。


    4.2 事件标志组(Event Flags)

    机制原理

    事件标志组用**一组 bit(通常 32 位)**表达多种事件:任意任务/ISR 把对应位置 1,等待方声明「我关心哪几位、要全部满足(AND)还是任一满足(OR)」,条件不满足则阻塞。它是「多路事件汇聚到一个任务」的标准方案(如通信任务同时等待:收到数据 | 发送完成 | 出错 | 退出请求)。

    在这里插入图片描述

    核心特性:

    • 复合等待:AND/OR 两种逻辑,一个任务可同时等待多种事件的任意组合;
    • 多等待者:同一标志组可被多个任务以不同位组合同时等待;
    • 消费清零:可选「唤醒后自动清除已满足的位」(如 FreeRTOS xClearOnExit、RT-Thread RT_EVENT_FLAG_CLEAR),否则需手动清位,处理不当会反复唤醒;
    • 不排队:同一位连续置位两次 = 一次。事件标志不能计数,要计数用信号量/队列。

    各系统接口与实现解析

    系统对象置位等待特点
    FreeRTOS EventGroupHandle_t xEventGroupSetBits() / …FromISR() / xEventGroupClearBits() xEventGroupWaitBits(bits, clearOnExit, waitForAll, timeout);另有 xEventGroupSync() 做任务间会合(rendezvous) tick 数即位数(24 或 32 位)
    μC/OS-II OS_FLAG_GRP OSFlagPost() OSFlagPend()(WAIT_SET/CLR + ALL/ANY + consume opt)/ OSFlagAccept() 位宽 8/16/32 可配
    μC/OS-III OS_FLAG_GRP OSFlagPost() OSFlagPend() / OSFlagPendGetFlagsRdy() 同 II,接口统一为 err 风格
    RT-Thread rt_event_t rt_event_send() rt_event_recv(set, option=AND/OR+RT_EVENT_FLAG_CLEAR, timeout, &recved) 32 位固定
    Zephyr struct k_event k_event_post()(保留其他位)/ k_event_set()(覆盖) k_event_wait(events, reset, timeout) / k_event_wait_all() 32 位;post 是累积式置位
    ThreadX TX_EVENT_FLAGS_GROUP tx_event_flags_set(TX_OR / TX_AND / *_CLEAR) tx_event_flags_get(TX_OR / TX_AND / *_CLEAR, timeout) set 本身就能选 AND/OR/清位语义,非常灵活
    NuttX —— 无原生事件组对象 用多个信号量或 sigwaitinfo 位掩码近似 POSIX 没有对应原语
    LiteOS EVENT_CB_S LOS_EventWrite() / LOS_EventSet() LOS_EventRead(mask, mode=AND/OR + CLEAR, timeout) 25 个可用事件位
    AliOS Things kevent_t krhino_event_set() krhino_event_get(flags, opt=AND/OR+CLEAR, timeout, &actual) 32 位
    Linux —— 无内核事件组;用户态可用 eventfd 位掩码、条件变量+标志位模拟 —— ——

    实现注意(FreeRTOS 特例):xEventGroupSetBitsFromISR() 不是直接在 ISR 里置位,而是投递给守护任务(timer service task)代为置位——因此 ISR 置位到任务被唤醒之间存在一次队列传递延迟,对时延敏感的场合优先用任务通知。


    4.3 任务通知与内嵌任务队列

    机制原理

    队列、事件组都是独立内核对象(要额外 RAM、要创建句柄)。而「任务通知」直接把通信载体嵌进任务控制块(TCB):每个任务自带一个 32 位通知值和状态,发送方指定目标任务直接改写其通知值并唤醒它。

    优势:零额外对象、定向投递、速度最快(FreeRTOS 官方数据:比二值信号量快约 45%、省 RAM);劣势:只能一对一(通知属于目标任务),且通知值只有一个(32 位可兼作事件位图或计数)。

    各系统对应机制

    系统机制关键接口说明
    FreeRTOS Task Notification xTaskNotifyGive() / vTaskNotifyGiveFromISR()(计数用法);xTaskNotify() / xTaskNotifyFromISR()(设值/设位用法);接收:ulTaskNotifyTake() / xTaskNotifyWait() 可模拟二值/计数信号量、事件组、轻量邮箱
    μC/OS-III 内嵌任务信号量 + 内嵌任务消息队列 OSTaskSemPost() / OSTaskSemPend();OSTaskQPost() / OSTaskQPend() 每个 TCB 内嵌一个信号量和一个消息队列,是 III 相对 II 的重要增强
    μC/OS-II —— 无,只能用独立信号量/队列 ——
    Zephyr k_poll_signal k_poll_signal_init() / k_poll_signal_raise() / 线程用 k_poll() 等待一组 signal/event/sem/fifo k_poll 类似 Linux poll:一次等待多个对象的任意就绪,是 Zephyr 的多路复用方案
    RT-Thread —— 无内嵌任务通知;用信号量/事件组代替 ——
    ThreadX —— 无;用 tx_semaphore_* ——
    NuttX POSIX 信号(见 4.4) sigqueue() 可携带一个 sigval 定向发给线程 接近「带一个值的任务通知」
    LiteOS / AliOS —— 无独立任务通知;用事件/信号量 ——

    工程经验:ISR → 特定任务 的唤醒链路上,FreeRTOS 任务通知、μC/OS-III 内嵌任务信号量是第一选择;只有在需要多对一汇聚或一对多广播时才退回到信号量/事件组。


    4.4 信号与异步回调(Signal)

    机制原理

    前面所有机制都是同步等待——接收方主动调用等待接口。信号(Signal)则是异步打断:内核(或另一线程)向目标线程投递一个信号,目标线程执行到合适时机时被强制转入信号处理函数。它是 POSIX 的传统机制,在嵌入式 RTOS 中只有部分系统支持。

    各系统支持情况

    系统支持情况关键接口
    Linux 完整 POSIX 信号 sigaction() 注册处理函数;kill() / tkill() / pthread_kill() 定向发送;sigwait() 同步等待;signalfd() 把信号变成可 poll 的文件描述符(推荐的多线程用法)
    NuttX 完整 POSIX 信号(含 sigqueue() 带值实时信号) sigaction() / sigqueue() / sigwaitinfo()
    RT-Thread 提供 POSIX 风格软中断信号组件 rt_signal_install() / rt_signal_kill()(发信号) / rt_signal_wait()(同步等待,类似 sigwait)
    Zephyr 无 POSIX signal;k_poll_signal 提供「对象级信号」(见 4.3) ——
    FreeRTOS / μC/OS / ThreadX / LiteOS / AliOS 无信号概念 以回调注册(如定时器回调、驱动回调)实现异步通知

    要点:信号处理函数运行在被打断线程的上下文里,可调用函数受严格限制(异步信号安全函数集);多线程程序更推荐 sigwait / signalfd 把异步信号转成同步事件处理。


    5. 资源共享类机制

    机制原理

    在无 MMU 的 RTOS(FreeRTOS、μC/OS、RT-Thread 非 smart 版、Zephyr 默认、ThreadX、LiteOS-M、AliOS、NuttX flat build)中,所有任务共享同一物理地址空间——任何全局变量、静态缓冲区天然就是「共享内存」,通信可以简单到「写一个全局结构体」。但正因为谁都能碰,必须配合互斥机制防止撕裂访问:

    • 互斥锁(FreeRTOS xSemaphoreCreateMutex、RT-Thread rt_mutex_*、Zephyr k_mutex_*、ThreadX tx_mutex_*、μC/OS OSMutex*、LiteOS LOS_Mux*、AliOS krhino_mutex_*)——支持优先级继承;
    • 临界区(关中断 taskENTER_CRITICAL / rt_enter_critical / k_sched_lock)——只适合极短操作;
    • 读写顺序约定 + 原子变量(无锁场景)。

    Linux / RT-Smart / NuttX kernel build 等有 MMU 的环境:线程间仍共享进程地址空间(全局变量直接用);进程间才需要显式共享内存:

    机制接口说明
    POSIX 共享内存 shm_open() + mmap() + ftruncate() 现代推荐,配合 sem_open 命名信号量做同步
    System V 共享内存 shmget() / shmat() / shmdt() 传统接口,key 管理较绕
    文件映射 mmap(MAP_SHARED) 共享内容可持久化到文件

    内存池是共享资源类通信的基础设施,常与队列搭配(见 3.4):RT-Thread rt_mp_create/rt_mp_alloc、ThreadX tx_block_pool_create/tx_block_allocate、Zephyr k_mem_slab_alloc / k_heap_alloc、μC/OS-III OSMemGet/OSMemPut、LiteOS LOS_Membox*、AliOS krhino_mblk_pool_*、FreeRTOS 则以静态分配 + heap_1~5 策略提供。


    6. Linux 扩展机制

    嵌入式 Linux(网关、IPC 摄像头、座舱域控等场景)下,线程间通信往往与进程间通信共用一套设施,值得单独梳理:

    机制接口线程间适用性特点
    POSIX 消息队列 mq_open/mq_send/mq_receive 内核维护、按值、带优先级(mq_receive 先收最高优先级消息),可跨进程
    SysV 消息队列 msgget/msgsnd/msgrcv 支持按 mtype 选择性接收(单队列多路复用)
    Unix Domain Socket socket(AF_UNIX, SOCK_DGRAM/STREAM) / socketpair() 全双工、可传文件描述符(SCM_RIGHTS)、可 poll/epoll 多路复用,工程上最常用
    netlink socket(AF_NETLINK, …) 用户态↔内核 与内核子系统通信的标准通道(uevent、路由、自定义)
    管道 / FIFO pipe() / mkfifo() 半双工字节流(见 3.3)
    eventfd eventfd() + read()/write() 64 位计数事件,可并入 epoll,是「可 poll 的信号量」,线程池唤醒标配
    signalfd signalfd() 把信号转成 fd,统一进事件循环
    POSIX 共享内存 shm_open() + mmap() 进程间 线程间直接用全局内存即可
    条件变量 pthread_cond_wait/signal/broadcast ✔(线程间主力) 与 pthread_mutex 配对,用户态自建消息队列的标准同步件
    D-Bus / Binder 库级 IPC 服务化架构(Android Binder、嵌入式 D-Bus),带接口描述与权限模型

    工程经验:Linux 用户态多线程程序中,「自建队列 + pthread_mutex + pthread_cond」 和 「eventfd + 线程安全环形队列」 是线程间通信的两大主流范式;需要跨进程或多路复用时则转向 Unix Socket + epoll。


    7. 跨系统接口对比总表

    下表汇总十个系统对各通信机制的原生支持情况(● 原生支持 / ◐ 需组合或变体实现 / — 不支持):

    机制LinuxFreeRTOSμC/OS-IIμC/OS-IIIRT-ThreadZephyrThreadXNuttXLiteOSAliOS
    消息队列(按值) ● mq/msg ● Queue ◐ 传指针 ◐ 传指针 ● rt_mq ● k_msgq ● tx_queue ● mq ● 队列(双模式) ● queue
    紧急消息插队 ◐ 优先级 ● ToFront ● PostFront ● urgent ● front_send ◐ 优先级 ◐ back_send
    邮箱 ◐ 队列模拟 ◐ 长度1队列+覆盖 ● Mbox 单格 ◐ 用 Q 代替 ● rt_mb 多封 ● k_mbox 握手 ◐ 深度1队列
    事件标志组 ◐ eventfd/cond ● EventGroup ● FlagGrp ● FlagGrp ● rt_event ● k_event ● event_flags ● Event ● event
    任务通知/内嵌队列 ◐ sigqueue ● TaskNotify ● TaskSem/TaskQ ◐ k_poll_signal ◐ sigqueue
    计数/二值信号量 ● sem ● k_sem
    信号(异步) ● signal ● rt_signal ◐ k_poll_signal ● signal
    管道/字节流 ● pipe ● StreamBuffer ◐ 自建 ◐ 自建 ◐ ringbuffer ● k_pipe ● pipe
    变长缓冲队列 ● MessageBuffer ◐ ring_buf ● buf_queue
    共享内存 ● shm/mmap ● 全局地址空间 ●(smart 另有进程隔离) ●(flat)/ shm(kernel build)
    Unix Socket / netlink ◐ lwip socket ◐ 网络栈 ● socket

    8. ISR(中断上下文)与任务通信的特殊性

    ISR 与任务间的通信是最常用也最易踩坑的场景,规则可归纳为四条:

  • ISR 中只能用 ISR 安全接口:FreeRTOS 必须用 …FromISR() 后缀 API;RT-Thread/Zephyr/ThreadX/LiteOS/AliOS 的 release/give/send 类 API 本身 ISR 安全但绝不能带阻塞超时(RT_WAITING_NO / K_NO_WAIT / TX_NO_WAIT);μC/OS 在 ISR 中调非阻塞 Post。
  • ISR 中绝不等待:中断上下文没有「被挂起」的概念,调用阻塞型接收(xQueueReceive、rt_mq_recv 带超时)会直接断言或行为未定义。ISR 只发不收(个别系统允许 no-wait 尝试收)。
  • 延迟处理(deferred processing)是标准模式:ISR 里只做最急迫的事(清中断标志、取硬件 FIFO 数据),把「通知任务干活」通过队列/信号量/任务通知发出去;Linux 对应 bottom half(softirq/tasklet/workqueue)。
  • 唤醒与调度的衔接:FreeRTOS 的 FromISR API 通过 xHigherPriorityTaskWoken 出参告知是否唤醒了更高优先级任务,ISR 出口据此调 portYIELD_FROM_ISR();RT-Thread 在 rt_interrupt_leave() 统一检查调度;Linux sem_post() 后靠调度点自然唤醒。遗漏这一步会让「高优先级任务已被唤醒却延迟到下个 tick 才运行」。

  • 9. 选型建议与典型场景

    场景推荐机制理由
    ISR 通知单一任务(如 DMA 完成) 任务通知 / 内嵌任务信号量(FreeRTOS TaskNotify、μC/OS-III TaskSem);无此机制用二值信号量 定向、最快、零额外对象
    ISR 递延数据给任务(串口、ADC 采样块) 消息队列(按值)或「内存池+队列传指针」 带缓冲、可累积、天然解耦
    每帧数据都要处理、不许丢帧 计数信号量 + 环形缓冲,或足够深的消息队列 二值信号量/事件标志会丢次数
    只关心最新值(传感器采样刷新) 长度 1 队列 + xQueueOverwrite / 全局变量 + 读写保护 「丢旧保新」语义
    一个任务等待多路事件(通信任务主循环) 事件标志组(AND/OR 等待);Zephyr 用 k_poll;Linux 用 poll/epoll + eventfd 单点阻塞、多源唤醒
    任务间传大块/变长数据 内存池 + 队列传指针(零拷贝);AliOS buf_queue(变长按值);Linux 用 socket 或 shm 避免大 memcpy
    多对多事件分发(总线式) 消息队列 + 订阅分发任务;Linux 用 D-Bus / Unix Socket 组 队列本身不广播,需分发者模式
    严格一对一交付确认(命令-应答) Zephyr k_mbox_put(握手式);一般系统用「请求队列 + 应答队列」双队列 需要 rendezvous 语义
    跨核 / AMP 核间通信 共享内存 + mailbox 硬件中断(OpenAMP/rpmsg 框架) 超出单内核对象范畴,属平台层

    几条通用原则:

  • 能通知就不传数据,能传指针就不拷贝——按实时性需求逐层降级;
  • 先确认「满了/空了怎么办」再选机制——阻塞、超时、丢弃、覆盖,四种策略对应不同 API 选项;
  • 队列深度按「生产峰值速率 × 最长消费延迟」估算,并预留过载监控(水位统计);
  • 删除内核对象前先确保无任务挂在其等待列表上,否则唤醒返回错误码的路径必须处理。

  • 10. 常见误区与注意事项

  • 「事件标志组可以当队列用」——错误。事件位不排队、不计数,同一位两次置位等效一次;要计数用信号量,要保序用队列。
  • 「二值信号量 = 消息队列(长度1)」——近似但不同:队列带数据且有「覆盖/插队」选项,信号量只是计数。
  • μC/OS-II/III 的队列传的是指针不是数据——新手常把局部变量地址 OSQPost 出去,函数返回后即悬垂。必须配合内存池或静态缓冲。
  • ISR 里调用阻塞接口——硬故障第一大来源;发送一律 no-wait/FromISR。
  • 共享内存不加保护——「我只是读一下」也会读到撕裂状态(多字节变量的非原子读写);32 位对齐单字写在 Cortex-M 上原子,但结构体从不原子。
  • FreeRTOS xEventGroupSetBitsFromISR 的延迟——它经守护任务中转,不是即时置位;时延敏感用任务通知。
  • 邮箱语义张冠李戴——μC/OS 的单格 Mbox、RT-Thread 的多封 mb、Zephyr 的握手 mbox 是三种东西,移植代码时尤其注意。
  • Linux 线程间滥用管道/socket——同进程线程间优先共享内存 + mutex/cond 或 eventfd;管道 socket 走内核拷贝,留给跨进程或需要 poll 多路复用的场合。
  • 忘记等待超时参数——RT_WAITING_FOREVER 语义下对端异常会造成永久挂死,生产代码建议一律带超时并处理超时分支。

  • 附录:各系统内核对象命名速查

    系统队列邮箱事件组信号量互斥锁内存池
    FreeRTOS xQueue* —(长度1队列) xEventGroup* xSemaphore* xSemaphoreCreateMutex pvPortMalloc / 静态分配
    μC/OS-II OSQ* OSMbox* OSFlag* OSSem* OSMutex* OSMem*
    μC/OS-III OSQ* / OSTaskQ* OSFlag* OSSem* / OSTaskSem* OSMutex* OSMem*
    RT-Thread rt_mq_* rt_mb_* rt_event_* rt_sem_* rt_mutex_* rt_mp_*
    Zephyr k_msgq_* / k_fifo_* k_mbox_* k_event_* k_sem_* k_mutex_* k_mem_slab_* / k_heap_*
    ThreadX tx_queue_* —(深度1队列) tx_event_flags_* tx_semaphore_* tx_mutex_* tx_block_pool_* / tx_byte_pool_*
    NuttX mq_* sem_* pthread_mutex_* malloc / 静态
    LiteOS LOS_Queue* LOS_Event* LOS_Sem* LOS_Mux* LOS_Membox* / LOS_Mem*
    AliOS Things krhino_queue_* krhino_event_* krhino_sem_* krhino_mutex_* krhino_mblk_pool_* / krhino_mm_*
    Linux mq_* / msg* —(eventfd 近似) sem_* pthread_mutex_* malloc / shm_open
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 常见嵌入式操作系统中的线程(任务)间通信机制
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!