线程间通信

IPC:消息队列、信号量、互斥量、事件、邮箱

                                                             ----《RT-Thread 内核实现与应用开发实战指南—基于野火 STM32 全系列(M3/4/7)开发板》

消息队列

消息队列可以在线程与线程间、中断和线程间传送信息,实现了线程接收来自其他线程或中断的不固定长度的消息,并根据不同的接口选择传递消息是否存放在线程自己的空间。 线程能够从队列里面读取消息,当队列中的消息是空时,挂起读取线程,用户还可以指定挂起的线程时间 timeout;当队列中有新消息时,挂起的读取线程被唤醒并处理新消息, 消息队列是一种异步的通信方式。

struct rt_messagequeue {
  struct rt_ipc_object parent;//消息队列属于内核对象,会在自身结构体里面包含一个内核对象类型
的成员,通过这个成员可以将消息队列挂到系统对象容器里面

  void *msg_pool; //存放消息的消息池开始地址

  rt_uint16_t msg_size; //每条消息大小,消息队列中也就是节点的大小,单位为字节
  rt_uint16_t max_msgs; //能够容纳的最大消息数量
 
  rt_uint16_t entry; //队列中的消息索引,记录消息队列的消息个数

  void *msg_queue_head; //链表头指针,指向即将读取数据的节点
  void *msg_queue_tail; //链表尾指针,指向允许写入数据的节点
  void *msg_queue_free; //指向队列的空闲节点的指针
};
typedef struct rt_messagequeue *rt_mq_t;

通过消息队列服务,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。 同时消息队列支持优先级,也就是说在所有等待消息的线程中优先级最高的会先获得消息。

消息队列的创建、发送、接收,删除

创建:创建消息队列时先创建一个消息队列对象控制块,然后给消息队列分配一块内存空间,组织成空闲消息链表,这块内存的大小等于[消息大小+消息头(用于链表连接) ]与消息队列容量的乘积,接着再初始化消息队列,此时消息队列为空。RT-Thread 操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的msg_queue_head 和 msg_queue_tail;有些消息框可能是空的,它们通过 msg_queue_free 形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。可以设置消息队列的阻塞唤醒模式,选择是最高优先级的线程先获得资源或者FIFO。
发送:线程或者中断服务程序都可以给消息队列发送消息。当发送消息时,消息队列对象先从空闲消息链表上取下一个空闲消息块,把线程或者中断服务程序发送的消息内容复制到消息块上,然后把该消息块挂到消息队列的尾部。如果当前有线程因为访问队列而进入阻塞,现在有消息了则可以将该线程从阻塞中恢复(线程调度)。当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL)。
发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,从空闲消息链表上取下来的消息块不是挂到消息队列的队尾,而是挂到队首,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。

static void send_thread_entry(void* parameter)
{
    rt_err_t uwRet = RT_EOK;
    uint32_t send_data1 = 1;
    uint32_t send_data2 = 2;
    while (1) {/* K1 被按下 */
        if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
            /* 将数据写入(发送)到队列中,等待时间为 0 */
            uwRet = rt_mq_send(test_mq, /* 写入(发送)队列的 ID(句柄) */
            &send_data1, /* 写入(发送)的数据 */
            sizeof(send_data1)); /* 数据的长度 */
            if (RT_EOK != uwRet) {
                rt_kprintf("数据不能发送到消息队列!错误代码: %lx\n",uwRet);
            }
        }/* K1 被按下 */
        if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
            /* 将数据写入(发送)到队列中,等待时间为 0 */
            uwRet = rt_mq_send(test_mq, /* 写入(发送)队列的 ID(句柄) */
            &send_data2, /* 写入(发送)的数据 */
            sizeof(send_data2)); /* 数据的长度 */
            if (RT_EOK != uwRet) {
                rt_kprintf("数据不能发送到消息队列!错误代码: %lx\n",uwRet);
            }
        }
        rt_thread_delay(20);
    }
}

读取:读取消息时,根据 msg_queue_head 找到最先入队列中的消息节点进行读取。根据消息队列控制块中的 entry 判断队列是否有消息读取,如果消息全部空闲(entry 为 0),非阻塞的情况下,读取函数会直接返回。阻塞的情况下会引起线程挂起(持续等待或者等待指定时间)

/* 队列读取(接收),等待时间为一直等待 */
uwRet = rt_mq_recv(test_mq, /* 读取(接收)队列的 ID(句柄) */
                   &r_queue, /* 读取(接收)的数据保存位置 */
                   sizeof(r_queue), /* 读取(接收)的数据的长度 */
                   RT_WAITING_FOREVER); /* 等待时间:一直等 */
if (RT_EOK == uwRet){
    rt_kprintf("本次接收到的数据是: %d\n",r_queue);
} else{
    rt_kprintf("数据接收出错,错误代码: 0x%lx\n",uwRet);
}

删除:当消息队列不再被使用时,应该删除它以释放系统资源,一旦操作完成,消息队列将被永久性的删除。

消息队列的阻塞机制

消息队列的发送没有阻塞机制,发送消息操作的时候,为了保护数据,当且仅当空闲消息链表上有可用的空闲消息块时,发送者才能成功发送消息;当空闲消息链表上无可用消息块,说明消息队列已满,此时,发送消息的的线程或者中断程序会收到一个错误码(-RT_EFULL) ,发送消息并不带有阻塞机制的,因为发送消息的环境可能是在中断中,不允许有阻塞的情况

消息队列读取时,必须保证该线程能正常完成读写操作,而不受后来的线程干扰。一个线程对某个队列进行读操作的时候(也就是我们所说的出队),若该消息队列中没有消息,那么此时线程 有 3 个选择:1、放弃继续读取,该线程 不会进入阻塞态;2、选择等待指定时间,此时该线程会进入阻塞状态,在这段指定的时间内都是处于阻塞态, 当阻塞的这段时间该线程等到了队列的消息,那么该线程就会从阻塞态变成就绪态,如果此时该线程比当前运行的线程优先级还,那么,该线程就会得到消息并且运行;若在指定等待时间内,队列还没消息,那该线程就不等了,从阻塞态中唤醒, 返回一个没等到消息的错误代码,然后继续执行线程 A 的其他代码;3、死等,线程进入阻塞态,直到完成读取队列的消息。

 

信号量

信号量(Semaphore)是一种实现线程间通信的机制,实现线程之间同步或临界资源的互斥访问, 常用于协助一组相互竞争的线程来访问临界资源。给临界资源建立一个标志。

通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。 0: 表示没有积累下来的 release 释放信号量操作,且有可能有在此信号量上阻塞的线程。正值,表示有一个或多个 release 释放信号量操作。
用作互斥时,信号量创建后可用信号量个数应该是满的, 线程在需要使用临界资源时,先获取信号量,使其变空,这样其他线程需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。 
用作同步时,信号量在创建后被置为空, 线程 1 取信号量而阻塞, 线程 2 在某种条件发生后,释放信号量,于是线程 1 得以进入就绪态,如果线程 1 的优先级是最高的,那么就会立即切换线程,从而达到了两个线程间的同步。 同样的,在中断服务函数中释放信号量,也能达到线程与中断间的同步。

二值信号量(0或1)

struct rt_semaphore {
    struct rt_ipc_object parent; 

    rt_uint16_t value; // 信号量的值
};
typedef struct rt_semaphore *rt_sem_t;

信号量的创建、获取、释放、删除

信号量的创建, 为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数, 二值信号量的最大可用信号量个数为 1。可以设置信号量的阻塞唤醒模式,选择是最高优先级的线程先获得资源或者FIFO。

信号量的获取, 从创建的信号量资源中获取一个信号量, 获取成功返回正确。只有信号量的值大于0,才会获取成功,否则线程会等待其它线程释放该信号量, 超时时间由用户设定。当线程获取信号量失败时,线程将进入阻塞态, 系统将线程挂到该信号量的阻塞列表中。

rt_sem_take(test_sem, /* 获取信号量 */
            RT_WAITING_FOREVER); /* 等待时间:一直等 */

uwRet = rt_sem_take(test_sem, /* 获取一个计数信号量 */
                    0); /* 等待时间: 0 */
if ( RT_EOK == uwRet )
    rt_kprintf( "获取信号量成功\r\n" );

信号量的释放,信号量的值加1,如果当前有线程等待这个信号量时,那么现在进行信号量释放的时候, 将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量,并且将其从阻塞中恢复。恢复的过程是:将线程从阻塞列表中删除,添加到就绪列表中。需要线程调度

static void send_thread_entry(void* parameter)
{
    rt_err_t uwRet = RT_EOK;
    /* 线程都是一个无限循环,不能返回 */
    while (1) { //如果 KEY2 被单击
        if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
            /* 释放一个计数信号量 */
            uwRet = rt_sem_release(test_sem);
            if ( RT_EOK == uwRet )
                rt_kprintf ( "KEY2 被单击:释放 1 个停车位。 \r\n" );
            else
                rt_kprintf ( "KEY2 被单击:但已无车位可以释放! \r\n" );
        }
    rt_thread_delay(20); //每 20ms 扫描一次
    }
}

信号量的删除,根据信号量句柄直接删除的,删除之后这个信号量的所有信息都会被系统回收,并且用户无法再次使用这个信号量。所有因为访问此信号量的而阻塞的线程从阻塞态中恢复过来。

 

互斥量

互斥量又称互斥型信号量,是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。

任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被线程持有时, 该互斥量处于闭锁状态,这个线程获得互斥量的所有权。当该线程释放这个互斥量时,该互斥量处于开锁状态, 线程失去该互斥量的所有权。当一个线程持有互斥量时,其他线程将不能再对该互斥量进行开锁或持有。 持有该互斥量的线程也能够再次获得这个锁而不被挂起,这就是递归访问,这个特性与一般的二值信号量有很大的不同, 在信号量中, 由于已经不存在可用的信号量,线程递归获取信号量时会发生主动挂起(最终形成死锁) 。

如果想要用于实现同步(线程之间或者线程与中断之间), 二值信号量或许是更好的选择, 虽然互斥量也可以用于线程与线程、 线程与中断的同步,但是互斥量更多的是用于保护资源的互锁。

用于互锁的互斥量可以充当保护资源的令牌,和二值信号量一样。当一个线程希望访问某个资源时,它必须先获取令牌。当线程使用完资源后,必须还回令牌,以便其它线程可以访问该资源。 但是信号量会导致的另一个潜在问题,那就是线程优先级翻转。 而互斥量通过优先级继承算法, 可用降低优先级翻转问题产生的影响,所以,用于临界资源的保护一般建议使用互斥量。

优先级继承算法是指, 暂时提高某个占有某种资源的低优先级线程的优先级,使之与在所有等待该资源的线程中优先级最高那个线程的优先级相等,而当这个低优先级线程执行完毕释放该资源时,优先级重新回到初始设定值。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。

优先级翻转

现在有 3 个线程分别为 H 线程(High)、 M 线程(Middle)、 L 线程(Low), 3 个线程的优先级顺序为 H 线程>M 线程>L 线程。正常运行的时候 H 线程可以打断 M 线程与 L 线程, M 线程可以打断 L 线程,假设系统中有一个资源被保护了,此时该资源被 L 线程正在使用中,某一刻, H 线程需要使用该资源,但是 L 线程还没使用完, H线程则因为申请不到资源而进入阻塞态, L 线程继续使用该资源,此时已经出现了“优先级翻转”现象,高优先级线程在等着低优先级的线程执行,如果在 L 线程执行的时候刚好M 线程被唤醒了, 由于 M 线程优先级比 L 线程优先级高,那么会打断 L 线程,抢占了CPU 的使用权, 直到 M 线程执行完,再把 CUP 使用权归还给 L 线程, L 线程继续执行,等到执行完毕之后释放该资源, H 线程此时才从阻塞态解除,使用该资源。这个过程,本来是最高优先级的 H 线程,在等待了更低优先级的 L 线程与 M 线程,其阻塞的时间是 M线程运行时间+L 线程运行时间,这只是只有 3 个线程的系统,假如很多个这样子的线程打断最低优先级的线程,那这个系统最高优先级线程岂不是崩溃了,这个现象是绝对不允许出现的, 高优先级的线程必须能及时响应。所以,没有优先级继承的情况下,使用资源保护,其危害极大。

假如有优先级继承,那么,在 H 线程申请该资源的时候,由于申请不到资源会进入阻塞态,那么系统就会把当前正在使用资源的 L 线程的优先级临时提高到与 H 线程优先级相同,此时 M 线程被唤醒了,因为它的优先级比 H 线程低,所以无法打断 L 线程, 因为此时 L 线程的优先级被临时提升到 H,所以当 L 线程使用完该资源了,进行释放,那么此时 H 线程优先级最高,将接着抢占 CPU 的使用权, H 线程的阻塞时间仅仅是 L 线程的执行时间,此时的优先级的危害降到了最低。
 

互斥量更适合于
1、线程可能会多次获取互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题;
2、可能会引起优先级翻转的情况;

使用互斥量时候需要注意几点:
1、两个线程不能对同时持有同一个互斥量。如果某线程对已被持有的互斥量进行获取,则该线程会被挂起,直到持有该互斥量的线程将互斥量释放成功, 其他线程才能申请这个互斥量。
2、互斥量不能中断服务程序中使用。
3、尽量避免线程的长时间阻塞,因此在获得互斥量之后,应该尽快释放互斥量。
4、持有互斥量的过程中,不得再更改持有互斥量线程的优先级。

struct rt_mutex {
    struct rt_ipc_object parent; //

    rt_uint16_t value; //互斥量的值。 初始状态下互斥量的值为 1,因此,如果值大于 0,表示可以使用互斥量

    rt_uint8_t original_priority; //持有互斥量线程的原始优先级,用来做优先级继承的保存
    rt_uint8_t hold; //持有互斥量的线程的持有次数,用于记录线程递归调用了多少次获取互斥量

    struct rt_thread *owner; //当前持有互斥量的线程
};
typedef struct rt_mutex *rt_mutex_t;

互斥量的创建、获取、释放、删除

获取:线程对互斥量的所有权是独占的, 任意时刻互斥量只能被一个线程持有,如果互斥量处于开锁状态,那么获取该互斥量的线程将成功获得该互斥量,并拥有互斥量的使用权;如果互斥量处于闭锁状态, 获取该互斥量的线程将无法获得互斥量, 线程将被挂起, 直到持有互斥量线程释放它,而如果线程本身就持有互斥量,再去获取这个互斥量却不会被挂起,只是将该互斥量的持有值加 1。

释放:只有已持有互斥量所有权的线程才能释放它,每释放一次该互斥量,它的持有计数就减 1。当该互斥量的持有计数为零时(即持有线程已经释放所有的持有操作), 互斥量则变为开锁状态,等待在该互斥量上的线程将被唤醒。如果线程的优先级被互斥量的优先级翻转机制临时提升,那么当互斥量被释放后,线程的优先级将恢复为原本设定的优先级。

事件

事件是一种实现线程间通信的机制, 主要用于实现线程间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。即一个线程可以等待多个事件的发生: 可以是任意一个事件发生时唤醒线程进行事件处理;也可以是几个事件都发生后才唤醒线程进行事件处理。 同样,事件也可以是多个线程同步多个事件。

事件集合用 32 位无符号整型变量来表示,每一位代表一个事件, 线程通过“逻辑与”或“逻辑或” 与一个或多个事件建立关联,形成一个事件集。事件的“逻辑或”也称作是独立型同步,指的是线程感兴趣的所有事件任一件发生即可被唤醒;事件“逻辑与”也称为是关联型同步,指的是线程感兴趣的若干事件都发生时才被唤醒。多线程环境下, 线程之间往往需要同步操作,一个事件发生即是一个同步。事件可以提供一对多、多对多的同步操作。一对多同步模型:一个线程等待多个事件的触发;多对多同步模型:多个线程等待多个事件的触发。

RT-Thread 提供的事件具有如下特点:
1、事件只与线程相关联,事件相互独立,一个 32 位的事件集合(set 变量) ,用于标识该线程发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、 1 表示该事件类型已经发生),一共 32 种事件类型。
2、事件仅用于同步,不提供数据传输功能。
3、事件无排队性,即多次向线程发送同一事件(如果线程还未来得及读走), 等效于只发送一次
4、允许多个线程对同一事件进行读写操作。
5、支持事件等待超时机制。

事件可使用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。一个线程或中断服务例程发送一个事件给事件对象,而后等待的线程被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作是不可累计的,而信号量的释放动作是可累计的。事件另外一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。同时按照线程等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。这个特性也是信号量等所不具备的,信号量只能识别单一同步动作,而不能同时等待多个事件的同步。
各个事件可分别发送或一起发送给事件对象,而线程可以等待多个事件,线程仅对感兴趣的事件进行关注。当有它们感兴趣的事件发生时并且符合感兴趣的条件,线程将被唤醒并进行后续的处理动作。

struct rt_event {
    struct rt_ipc_object parent;

    rt_uint32_t set; // 事件标志位,每一位代表一个事件的发生
};
typedef struct rt_event *rt_event_t; 

事件的创建、发送、接收、删除

事件的创建,创建事件集合,即设置set为0。

时间的发送,通过参数 set 指定的事件标志来设定事件的标志位,然后遍历等待在 event 事件对象上的等待线程链表,判断是否有线程的事件激活要求与当前事件对象标志值匹配,如果有,则唤醒该线程。简单来说,就是设置我们自己定义的事件标志位为1,并且看看有没有线程在等待这个事件,有的话就唤醒它。

#define KEY1_EVENT (0x01 << 0)//设置事件掩码的位 0
#define KEY2_EVENT (0x01 << 1)//设置事件掩码的位 1
static void send_thread_entry(void* parameter)
{
    /* 线程都是一个无限循环,不能返回 */
    while (1) {//如果 KEY2 被单击
        if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
            rt_kprintf ( "KEY1 被单击\n" );
            /* 发送一个事件 1 */
            rt_event_send(test_event,KEY1_EVENT);
        }
        //如果 KEY2 被单击
        if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
            rt_kprintf ( "KEY2 被单击\n" );
            /* 发送一个事件 2 */
            rt_event_send(test_event,KEY2_EVENT);
        }
        rt_thread_delay(20); //每 20ms 扫描一次
    }
}

事件的接收,系统首先根据 set 参数和接收选项(通过 “逻辑与”、“逻辑或”等操作对感兴趣的事件进行接收)来判断它要接收的事件是否发生,如果已经发生,则根据参数 option 上是否设置有 RT_EVENT_FLAG_CLEAR 来决定是否清除事件的相应标志位,其中 recved 参数用于保存收到的事件; 如果事件没有发生,则把线程感兴趣的事件和接收选项填写到线程控制块中,然后把线程挂起在此事件对象的阻塞列表上,直到事件发生或等待时间超时

static void receive_thread_entry(void* parameter)
{
    rt_uint32_t recved;
    /* 线程都是一个无限循环,不能返回 */
    while (1) {
        /* 等待接收事件标志 */
        rt_event_recv(test_event, /* 事件对象句柄 */
                      KEY1_EVENT|KEY2_EVENT, /* 接收线程感兴趣的事件 */
                      RT_EVENT_FLAG_AND|RT_EVENT_FLAG_CLEAR,/* 接收选项 */
                      RT_WAITING_FOREVER, /* 指定超时事件,一直等 */
                      &recved); /* 指向接收到的事件 */
        if (recved == (KEY1_EVENT|KEY2_EVENT)) { /* 如果接收完成并且正确 */
            rt_kprintf ( "Key1 与 Key2 都按下\n");
            LED1_TOGGLE; //LED1 反转
        } else
            rt_kprintf ( "事件错误! \n");
    }
}

邮箱

邮箱可以在线程与线程之间、中断与线程之间进行消息的传递,邮箱相比于信号量与消息队列来说,其开销更低,效率更高。 邮箱中的每一封邮件只能容纳固定的 4 字节内容(STM32 是 32 位处理系统, 一个指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针) ,当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。

线程能够从邮箱里面读取邮件消息,当邮箱中的邮件是空时, 根据用户自定义的阻塞时间决定是否挂起读取线程;当邮箱中有新邮件时,挂起的读取线程被唤醒, 邮箱也是一种异步的通信方式。

通过邮箱,线程或中断服务函数可以将一个或多个邮件放入邮箱中。同样,一个或多个线程可以从邮箱中获得邮件消息。当有多个邮件发送到邮箱时,通常应将先进入邮箱的邮件先传给线程,也就是说,线程先得到的是最先进入邮箱的消息,即先进先出原则(FIFO),同时 RT-Thread 中的邮箱支持优先级,也就是说在所有等待邮件的线程中优先级最高的会先获得邮件。

RT-Thread 中使用邮箱实现线程异步通信工作,具有如下特性:
1、 邮件支持先进先出方式排队与优先级排队方式,支持异步读写工作方式。
2、发送与接收邮件均支持超时机制
3、一个线程能够从任意一个消息队列接收和发送邮件。
4、多个线程能够向同一个邮箱发送邮件和从中接收邮件。
5、邮箱中的每一封邮件只能容纳固定的 4 字节内容(可以存放地址)。
6、当队列使用结束后,需要通过删除邮箱以释放内存。
邮箱与消息队列很相似, 消息队列中消息的长度是可以由用户配置的,但邮箱中邮件的大小却只能是固定容纳 4 字节的内容,所以,使用邮箱的开销是很小的,因为传递的只能是 4 字节以内的内容,那么其效率会更高。

struct rt_mailbox {
    struct rt_ipc_object parent; //

    rt_uint32_t *msg_pool; //邮箱缓冲区的开始地址

    rt_uint16_t size; //邮箱缓冲区的大小,也就是邮箱的大小,它的大小决定了能存放多少封 4 字节大小的邮件

    rt_uint16_t entry; //邮箱中当前邮件的数目
    rt_uint16_t in_offset; //邮箱邮件的进偏移指针,指向空的邮件
    rt_uint16_t out_offset; 邮箱邮件的出偏移指针,如果邮箱中有邮件,则指向先进来的邮件

    rt_list_t suspend_sender_thread; //发送线程的挂起等待链表
};
typedef struct rt_mailbox *rt_mailbox_t;

邮件的创建、发送、接收、删除

邮箱的创建,创建邮箱对象时会先创建一个邮箱对象控制块,然后给邮箱分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4 字节)与邮箱容量的乘积,接着初始化接收邮件和发送邮件在邮箱中的偏移量, 接着再初始化消息队列,此时消息队列为空。

邮箱的发送,线程或者中断服务程序都可以给邮箱发送邮件,非阻塞方式的邮件发送过程能够安全的应用于中断服务中,中断服务函数、定时器向线程发送消息的有效手段,而阻塞方式的邮件发送只能应用于线程中。 当发送邮件时, 当且仅当邮箱还没满邮件的时候才能进行发送,如果邮箱已满,可以根据用户设定的等待时间进行等待, 当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送的过程, 当等待时间到了还未完成发送邮件,或者未设置等待时间, 此时发送邮件失败,发送邮件的线程或者中断程序会收到一个错误码(-RT_EFULL)。 线程发送邮件可以带阻塞,但在中断中不能采用任何带阻塞的方式发送邮件。
 

/* 定义邮箱控制块 */
static rt_mailbox_t test_mail = RT_NULL;
/************************* 全局变量声明 ****************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些全局变量。
*/
char test_str1[] = "this is a mail test 1";/* 邮箱消息 test1 */
char test_str2[] = "this is a mail test 2";/* 邮箱消息 test2 */

static void send_thread_entry(void* parameter)
{
    rt_err_t uwRet = RT_EOK;
    /* 线程都是一个无限循环,不能返回 */
    while (1) {
        //如果 KEY1 被单击
        if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
            rt_kprintf ( "KEY1 被单击\n" );
            /* 发送一个邮箱消息 1 */
            uwRet = rt_mb_send_wait(test_mail,/* 邮箱对象句柄 */
                                    (rt_uint32_t)&test_str1,/*邮件内容(地址) */
                                    10); /* 超时时间 */
            if (RT_EOK == uwRet)
                rt_kprintf ( "邮箱消息发送成功\n" );
            else
                rt_kprintf ( "邮箱消息发送失败\n" );
        }
        //如果 KEY2 被单击
        if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
            rt_kprintf ( "KEY2 被单击\n" );
            /* 发送一个邮箱消息 2 */
            uwRet = rt_mb_send_wait(test_mail,/* 邮箱对象句柄 */
                                    (rt_uint32_t)&test_str1,/* 邮件内容(地址) */
                                    10); /* 超时时间 */
            if (RT_EOK == uwRet)
                rt_kprintf ( "邮箱消息发送成功\n" );
            else
                rt_kprintf ( "邮箱消息发送失败\n" );
            }
        rt_thread_delay(20); //每 20ms 扫描一次
    }
}

邮箱的接收,接收邮件时, 根据邮箱控制块中的 entry 判断队列是否有邮件,如果邮箱的邮件非空,那么可以根据 out_offset 找到最先发送到邮箱中的邮件进行接收。在接收时如果邮箱为空,如果用户设置了等待超时时间, 系统会将当前线程挂起, 当达到设置的超时时间,邮箱依然未收到邮件时, 那么线程将被唤醒并返回-RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收线程中。 通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。

/* 定义邮箱控制块 */
static rt_mailbox_t test_mail = RT_NULL;

static void receive_thread_entry(void* parameter)
{
    rt_err_t uwRet = RT_EOK;
    char *r_str;
    /* 线程都是一个无限循环,不能返回 */
    while (1) {
        /* 等待接邮箱消息 */
        uwRet = rt_mb_recv(test_mail, /* 邮箱对象句柄 */
                           (rt_uint32_t*)&r_str, /* 接收邮箱消息 */
                           RT_WAITING_FOREVER); /* 指定超时事件,一直等 */

        if (RT_EOK == uwRet) { /* 如果接收完成并且正确 */
            rt_kprintf ( "邮箱的内容是:%s\n\n",r_str);
            LED1_TOGGLE; //LED1 反转
        } else
            rt_kprintf ( "邮箱接收错误!错误码是 0x%x\n",uwRet);
    }
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值