RT-thread内核之进程间通信
一、进程间通信机制
rt-thread操作系统的IPC(Inter-Process Communication,进程间同步与通信)包含有中断锁、调度器锁、信号量、互斥锁、事件、邮箱、消息队列。其中前5个主要表现为线程间同步,邮箱与消息队列表现为线程间通信。本文主要介绍它们的一些特性及使用场合。
1、中断锁
关闭中断也叫中断锁,是禁止多任务访问临界区最简单的一种方式,即使是在分时操作系统中也是如此。当中断关闭的时候,就意味着当前任务不会被其他事件打断(因为整个系统已经不再响应那些可以触发线程重新调度的外部事件),也就是当前线程不会被抢占,除非这个任务主动放弃了处理器控制权。关闭中断/恢复中断API接口由BSP实现,根据平台的不同其实现方式也大不相同。比如在stm32平台中中断锁机制通过关闭中断函数(rt_base_t rt_hw_interrupt_disable(void),这个函数用于关闭中断并返回关闭中断前的中断状态。)以及恢复中断函数(void rt_hw_interrupt_enable(rt_base_t level),恢复调用rt_hw_interrupt_disable()函数前的中断状态)实现。
警告: 由于关闭中断会导致整个系统不能响应外部中断,所以在使用关闭中断做为互斥访问临界区的手段时,首先必须需要保证关闭中断的时间非常短,例如数条机器指令。
使用中断锁来操作系统的方法可以应用于任何场合,且其他几类同步方式都是依赖于中断锁而实现的,可以说中断锁是最强大的和最高效的同步方法。只是使用中断锁最主要的问题在于,在中断关闭期间系统将不再响应任何中断,也就不能响应外部的事件。所以中断锁对系统的实时性影响非常巨大,当使用不当的时候会导致系统完全无实时性可言(可能导致系统完全偏离要求的时间需求);而使用得当,则会变成一种快速、高效的同步方式。例如,为了保证一行代码(例如赋值)的互斥运行,最快速的方法是使用中断锁而不是信号量或互斥量。
2、调度器锁
同中断锁一样把调度器锁住也能让当前运行的任务不被换出,直到调度器解锁。但和中断锁有一点不相同的是,对调度器上锁,系统依然能响应外部中断,中断服务例程依然能进行相应的响应。所以在使用调度器上锁的方式进行任务同步时,需要考虑好任务访问的临界资源是否会被中断服务例程所修改,如果可能会被修改,那么将不适合采用此种方式进行同步。在rt-therad系统中通过上锁函数(void rt_enter_critical(void),在系统锁住调度器的期间,系统依然响应中断,如果中断唤醒了的更高优先级线程,调度器并不会立刻执行它,直到调用解锁调度器函数才尝试进行下一次调度。)以及解锁函数(void rt_exit_critical(void),当系统退出临界区的时候,系统会计算当前是否有更高优先级的线程就绪,如果有比当前线程更高优先级的线程就绪,将切换到这个高优先级线程中执行;如果无更高优先级线程就绪,将继续执行当前任务。)实现调度锁机制。
注意: rt_enter_critical/rt_exit_critical可以多次嵌套调用,但每调用一次rt_enter_critical就必须相对应地调用一次rt_exit_critical退出操作,嵌套的最大深度是65535。
调度器锁能够方便地使用于一些线程与线程间同步的场合,由于轻型,它不会对系统中断响应造成负担;但它的缺陷也很明显,就是它不能被用于中断与线程间的同步或通知,并且如果执行调度器锁的时间过长,会对系统的实时性造成影响(因为使用了调度器锁后,系统将不再具备优先级的关系,直到它脱离了调度器锁的状态)。
3、信号量
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。信号量就像一把钥匙,把一段临界区给锁住,只允许有钥匙的线程进行访问:线程拿到了钥匙,才允许它进入临界区;而离开后把钥匙传递给排队在后面的等待线程,让后续线程依次进入临界区。
使用信号量会导致的另一个潜在问题是线程优先级翻转。所谓优先级翻转问题即当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。例如:有优先级为A、B和C的三个线程,优先级A> B > C。线程A,B处于挂起状态,等待某一事件触发,线程C正在运行,此时线程C开始使用某一共享资源M。在使用过程中,线程A等待的事件到来,线程A转为就绪态,因为它比线程C优先级高,所以立即执行。但是当线程A要使用共享资源M时,由于其正在被线程C使用,因此线程A被挂起切换到线程C运行。如果此时线程B等待的事件到来,则线程B转为就绪态。由于线程B的优先级比线程C高,因此线程B开始运行,直到其运行完毕,线程C才开始运行。只有当线程C释放共享资源M后,线程A才得以执行。在这种情况下,优先级发生了翻转,线程B先于线程A运行。这样便不能保证高优先级线程的响应时间。
在RT-Thread操作系统中实现的是优先级继承算法。优先级继承是通过在线程A被阻塞的期间内,将线程C的优先级提升到线程A的优先级别,从而解决优先级翻转引起的问题。这样能够防止C(间接地防止A)被B抢占。优先级继承协议是指,提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。
线程同步是信号量最简单的一类应用。例如,两个线程用来进行任务间的执行控制转移,信号量的值初始化成具备0个信号量资源实例(信号量的值初始化为0),而等待线程先直接在这个信号量上进行等待。当信号线程完成它处理的工作时,释放这个信号量,以把等待在这个信号量上的线程唤醒,让它执行下一部分工作。这类场合也可以看成把信号量用于工作完成标志:信号线程完成它自己的工作,然后通知等待线程继续下一部分工作。
锁,单一的锁常应用于多个线程间对同一临界区的访问。信号量在作为锁来使用时,通常应将信号量资源实例初始化成1(信号量的值初始化为1),代表系统默认有一个资源可用。当线程需要访问临界资源时,它需要先获得这个资源锁。当这个线程成功获得资源锁时,其他打算访问临界区的线程将被挂起在该信号量上,这是因为其他线程在试图获取这个锁时,这个锁已经被锁上(信号量值减1变为0)。当获得信号量的线程处理完毕,退出临界区时,它将会释放信号量并把锁解开,而挂起在锁上的第一个等待线程将被唤醒从而获得临界区的访问权。因为信号量的值始终在1和0之间变动,所以这类锁也叫做二值信号量。
信号量也能够方便的应用于中断与线程间的同步,例如一个中断触发,中断服务例程需要通知线程进行相应的数据处理。这个时候可以设置信号量的初始值是0,线程在试图持有这个信号量时,由于信号量的初始值是0,线程直接在这个信号量上挂起直到信号量被释放。 当中断触发时,先进行与硬件相关的动作,例如从硬件的I/O口中读取相应的数据,并确认中断以清除中断源,然后释放一个信号量来唤醒相应的线程以做后续的数据处理。警告: 中断与线程间的互斥不能采用信号量(锁)的方式,而应采用中断锁。
资源计数适合于线程间速度不匹配的场合,这个时候信号量可以做为前一线程工作完成的计数,而当调度到后一线程时,它可以以一种连续的方式一次处理数个事件。例如,生产者与消费者问题中,生产者可以对信号进行多次释放,而后消费者被调度到时能够一次处理多个资源。注意: 一般资源计数类型多是混合方式的线程间同步,因为对于单个的资源处理依然存在线程的多重访问,这就需要对一个单独的资源进行访问、处理,并进行锁方式的互斥操作。
4、互斥量
互斥量又叫相互排斥的信号量,是一种特殊的二值性信号量。它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性。互斥量的状态只有两种,开锁或闭锁(两种状态值)。当有线程持有它时,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起。这个特性与一般的二值信号量有很大的不同,在信号量中,因为已经不存在实例,线程递归持有会发生主动挂起(最终形成死锁)。
警告: 在获得互斥量后,请尽快释放互斥量,并且在持有互斥量的过程中,不得另行更改持有互斥量线程的优先级。
互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量永远都处于开锁的状态,而被线程持有的时候则立刻转为闭锁的状态。互斥量更适合于:
• 线程多次持有(获取)互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题;
• 可能会由于多线程同步而造成优先级翻转的情况;
另外需要切记的是互斥量不能在中断服务例程中使用。信号量则可用于中断与线程同步。
5、事件
事件主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步。即一个线程可等待多个事件的触发:可以是其中任意一个事件唤醒线程进行事件处理的操作;也可以是几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件,这种多个事件的集合可以用一个32位无符号整型变量来表示,变量的每一位代表一个事件,线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件集。事件的“逻辑或”也称为是独立型同步,指的是线程与任何事件之一发生同步;事件“逻辑与”也称为是关联型同步,指的是线程与若干事件都发生同步。
RT-Thread定义的事件有以下特点:
• 事件只与线程相关,事件间相互独立:每个线程拥有32个事件标志,采用一个32 bit无符号整型数进行记录,每一个bit代表一个事件。若干个事件构成一个事件集;
• 事件仅用于同步,不提供数据传输功能;
• 事件无排队性,即多次向线程发送同一事件(如果线程还未来得及读走),其效果等同于只发送一次。
在RT-Thread实现中,每个线程都拥有一个事件信息标记,它有三个属性,分别是RT_EVENT_FLAG_AND(逻辑与),RT_EVENT_FLAG_OR(逻辑或)以及RT_EVENT_FLAG_CLEAR(清除标记)。当线程等待事件同步时,可以通过32个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。
事件可使用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。线程或中断服务例程发送一个事件给事件对象,而后等待的线程被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作在事件未清除前,是不可累计的,而信号量的释放动作是累计的。 事件另外一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。同时按照线程等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。这个特性也是信号量等所不具备的,信号量只能识别单一的释放动作,而不能同时等待多种类型的释放。各个事件类型可分别发送或一起发送给事件对象,而事件对象可以等待多个线程,它们仅对它们感兴趣的事件进行关注。当有它们感兴趣的事件发生时,线程就将被唤醒并进行后续的处理动作。
6、邮箱
邮箱服务是实时操作系统中一种典型的任务间通信方法,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的4字节内容(针对32位处理系统,指针的大小即为4个字节,所以一封邮件恰好能够容纳一个指针)。典型的邮箱也称作交换消息,线程或中断服务例程把一封4字节长度的邮件发送到邮箱中。而一个或多个线程可以从邮箱中接收这些邮件进行处理。
RT-Thread操作系统采用的邮箱通信机制有点类似于传统意义上的管道,用于线程间通讯。非阻塞方式的邮件发送过程能够安全的应用于中断服务中,是线程,中断服务,定时器向线程发送消息的有效手段。通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。当邮箱中不存在邮件且超时时间不为0时,邮件收取过程将变成阻塞方式。所以在这类情况下,只能由线程进行邮件的收取。
RT-Thread操作系统的邮箱中可存放固定条数的邮件,邮箱容量在创建/初始化邮箱时设定,每个邮件大小为4字节。当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中。在一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择是否等待挂起或直接返回-RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送的过程。在一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或设置超时时间。当设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回-RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的4个字节邮件到接收线程中。
邮箱是一种简单的线程间消息传递方式,在RT-Thread操作系统的实现中能够一次传递4字节邮件,并且邮箱具备一定的存储功能,能够缓存一定数量的邮件数(邮件数由创建、初始化邮箱时指定的容量决定)。邮箱中一封邮件的最大长度是4字节,所以邮箱能够用于不超过4字节的消息传递,当传送的消息长度大于这个数目时就不能再采用邮箱的方式。 最重要的是,在32位系统上4字节的内容恰好适合放置一个指针,所以邮箱也适合那种仅传递指针的情况。
7、消息队列
消息队列是另一种常用的线程间通讯方式,它能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。通过消息队列服务,线程或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个线程可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常应将先进入消息队列的消息先传给线程,也就是说,线程先得到的是最先进入消息队列的消息,即先进先出原则(FIFO)。
RT-Thread操作系统的消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的msg_queue_head和msg_queue_tail;有些消息框可能是空的,它们通过msg_queue_free形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。
消息队列可以应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及中断服务例程中发送给线程的消息(中断服务例程不可能接收消息)。消息队列和邮箱的明显不同是消息的长度并不限定在4个字节以内,另外消息队列也包括了一个发送紧急消息的函数接口。但是当创建的是一个所有消息的最大长度是4字节的消息队列时,消息队列对象将蜕化成邮箱。
在一般的系统设计中会经常遇到要发送同步消息的问题,这个时候就可以根据当时的状态选择相应的实现:两个线程间可以采用[消息队列+信号量或邮箱]的形式实现。 发送线程通过消息发送的形式发送相应的消息给消息队列,发送完毕后希望获得接收线程的收到确认。邮箱做为确认标志,代表着接收线程能够通知一些状态值给发送线程;而信号量作为确认标志只能够单一的通知发送线程,消息已经确认接收。
二、IPC控制块:在include/rtdef.h中
/** * IPC flags and control command definitions */ #define RT_IPC_FLAG_FIFO 0x00 /**< FIFOed IPC. @ref IPC. */ #define RT_IPC_FLAG_PRIO 0x01 /**< PRIOed IPC. @ref IPC. */ #define RT_IPC_CMD_UNKNOWN 0x00 /**< unknown IPC command */ #define RT_IPC_CMD_RESET 0x01 /**< reset IPC object */ #define RT_WAITING_FOREVER -1 /**< Block forever until get resource. */ #define RT_WAITING_NO 0 /**< Non-block. */ /** * Base structure of IPC object */ struct rt_ipc_object { struct rt_object parent; /**< inherit from rt_object *///可知其派生自内核对象 rt_list_t suspend_thread; /**< threads pended on this resource *///线程挂起链表 };
三、IPC内联函数:在src/ipc.c中
rt_inline rt_err_t rt_ipc_object_init(struct rt_ipc_object *ipc) { /* init ipc object */ rt_list_init(&(ipc->suspend_thread)); //初始化线程挂起链表 return RT_EOK; }
/** * This function will suspend a thread to a specified list. IPC object or some * double-queue object (mailbox etc.) contains this kind of list. * * @param list the IPC suspended thread list * @param thread the thread object to be suspended * @param flag the IPC object flag, * which shall be RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO. * * @return the operation status, RT_EOK on successful */ rt_inline rt_err_t rt_ipc_list_suspend(rt_list_t *list, struct rt_thread *thread, rt_uint8_t flag) { /* suspend thread */ rt_thread_suspend(thread);//挂起线程 switch (flag) { case RT_IPC_FLAG_FIFO: //FIFO方式 rt_list_insert_before(list, &(thread->tlist));//直接放入队列末尾 break; case RT_IPC_FLAG_PRIO: //线程优先级方式 { struct rt_list_node *n; struct rt_thread *sthread; /* find a suitable position */ for (n = list->next; n != list; n = n->next)//遍历信号量的挂起链表 { sthread = rt_list_entry(n, struct rt_thread, tlist); /* find out */ if (thread->current_priority < sthread->current_priority)//按优先级找到合适位置 { /* insert this thread before the sthread */ rt_list_insert_before(&(sthread->tlist), &(thread->tlist));//将线程加入到链表中 break; } } /* * not found a suitable position, * append to the end of suspend_thread list */ if (n == list) rt_list_insert_before(list, &(thread->tlist));//没有找到合适位置,则放到末尾 } break; } return RT_EOK; }
调用rt_ipc_list_suspend将当前线程挂起,这个挂起是指将当前线程加入到信号量的挂起链表中,这里有一个flag参数,即sem->parent.parent.flag(在信号量初始化时设置),其值有两种RT_IPC_FLAG_FIFO,RT_IPC_FLAG_FIFO,前者表示按FIFO的方式放入挂起链表,后者是根据线程本身的优先级等级来决定放入到挂起链表的位置,由于每次释放一个信号量,只会从信号量挂起链表上唤醒第一个线程,因此,挂起线程链表上的位置就决定了当信号到达时挂起的线程的唤醒顺序。
/** * This function will resume the first thread in the list of a IPC object: * - remove the thread from suspend queue of IPC object * - put the thread into system ready queue * * @param list the thread list * * @return the operation status, RT_EOK on successful */ rt_inline rt_err_t rt_ipc_list_resume(rt_list_t *list) { struct rt_thread *thread; /* get thread entry */ thread = rt_list_entry(list->next, struct rt_thread, tlist);//获取线程 RT_DEBUG_LOG(RT_DEBUG_IPC, ("resume thread:%s\n", thread->name)); /* resume it */ rt_thread_resume(thread);//唤醒此线程 return RT_EOK; } 函数rt_ipc_list_resume只会唤醒信号量中第一个挂起的线程。正常唤醒挂起线程时(如获取信号量,互斥量等)不会修改线程的error值,即error原持原值RT_EOK不变.
/** * This function will resume all suspended threads in a list, including * suspend list of IPC object and private list of mailbox etc. * * @param list of the threads to resume * * @return the operation status, RT_EOK on successful */ rt_inline rt_err_t rt_ipc_list_resume_all(rt_list_t *list) { struct rt_thread *thread; register rt_ubase_t temp; /* wakeup all suspend threads */ while (!rt_list_isempty(list)) //遍历线程挂起链表 { /* disable interrupt */ temp = rt_hw_interrupt_disable();//关中断 /* get next suspend thread */ thread = rt_list_entry(list->next, struct rt_thread, tlist);//获得线程 /* set error code to RT_ERROR */ thread->error = -RT_ERROR; //设置线程的错误码为-RT_ERROR /* * resume thread * In rt_thread_resume function, it will remove current thread from * suspend list */ rt_thread_resume(thread); //唤醒此线程,表明为异常唤醒 /* enable interrupt */ rt_hw_interrupt_enable(temp); //开中断 } return RT_EOK; } 将挂起链表中的所有线程都唤醒。需要注意地是,唤醒的线程的error值将会被设置为-RT_ERROR,以此标志此线程是被异常唤醒,并不是正常获取到ipc内核对象(如信号量,互斥量等)而被唤醒,这在take函数中将会以线程的error值来进行判断