当前位置:Linux教程 - Linux文化 - OSKit的线程机制 第三章

OSKit的线程机制 第三章


第三章 线程通信分析


众所周知,在Linux中,进程为了能在同一项任务上协调工作,彼此之间必须能够进行通信。例如,在一个shell管道中,第一个进程的输出必须传输到第二个进程,这样沿着管道传递下去。因此在需要通信的进程之间,应该使用一种结构较好的通信方式。

Linux支持许多不同形式的进程间通信机制,在特定的情况下,它们各有自己的优缺点,如利用管道进行通信的最大缺点就是,只有成为系统调用pipe的进程的子孙后代的进程才可以使用管道,因此,相互无关的进程不能通过管道通信。

OSKit的开发人员们充分考虑了各种通信方式的利弊,扬长避短,采用了如下的两种线程间通信方式:消息队列和信号量。虽然这还有很多不足之处,但较之System V以有所改进。


3.1 线程间通信

3.1.1消息队列

OSKit提供的消息队列机制于UNIX十分相似,一个和多个线程可向消息队列写入消息,而一个或多个线程可从消息队列中读取消息,这种线程间通信的机制就像客户机/服务器一样,客户机向服务器发送请求消息,服务器读取消息并执行相应的请求。

OSKit将消息描述成在内核地址空间的一个内部链表,每一个消息队列由一个IPC标识号唯一的标识。

3.1.2信号

尽管大多数线程间通信是计划好的,但是同时还需要处理不可预知的通信问题。例如,用户使用文本编辑器要求列出一个大文件的全部内容,但随即他认识到该操作并不重要,这是就需要一种方法来中止编辑器的工作,比如说,我们可以按DEL键来实现。按DEL键实际上是向编辑器发送一个信号,编辑器收到此信号便立即停止打印文件的内容。信号还可以用来报告硬件捕获到的特定的中断,如非法指令和浮点运算溢出、超时也通过信号实现的。

在OSKit中,信号是一种重要的线程间通信机制,线程通过信号知道系统中正在出现的事件。信号的一个特点就是它的异步性,这表示线程在它的执行期间的任何时候都可以接受到信号,甚至可能当线程正在执行系统调用的时候接到信号,因此,线程必须随时为相应信号作好准备。

下面我们再谈谈信号的生成与交付,当引起信号的事件首次出现时,就认为信号为线程而生成,也就是说信号被发送到线程。这类事件的例子,包括:时间片到时,终端激活,硬件故障的检测,kill( )的调用。在某些情况下,一个事件为多个可以为多个线程生成信号。每个线程采用样应的动作来响应系统定义的每种信号(见信号量表3.1)。

在信号的生成与交付之间,该信号被挂起,但在用户一级是检测不到这个间隔的。每个线程都拥有一个信号掩码(sig_mask),它规定了当前被阻塞而不能交付给线程的信号集。线程的信号掩码是从父线程继承而来的。我前面提到sigaction( ),sigprocmask( )和sigsuspend( )函数,都是对信号掩码的操作。

信号的动作与信号有关的动作分为三种:SIG_DFL,SIG_IGN或指向函数的指针。在最开始,进入main( )之前,所有信号都将被置成SIG_DFL或SIG_IGN。

(1)SIG_DFL信号专用的默认动作:
  (a)如果默认动作是暂停线程,则该线程的执行被暂时挂起。当线程暂停期间,发送给线程的任何附加信号都不交付,直到该线程开始执行,但是SIGKILL除外。
  (b)把挂起信号的信号动作设置成SIG_DFL,且其默认动作是忽略信号 (SIGCHLD)。

(2)SIG_IGN忽略信号
  (a)该信号的交付对线程没有影响
  (b)系统不允许把SIGKILL或SIGTOP信号的动作设置为SIG_DFL

(3)指向函数的指针--捕获信号
  (a)信号一经交付,接收线程就在指定地址上执行信号捕获程序。在信号捕 获函数返回后,接受线程必须在被中断点恢复执行。
  (b)用C语言函数调用的方法进入信号捕捉程序:
    void func (signo)
    int signo;
    func( )是指定的信号捕捉函数,signo是正被交付信号的编码
  (c)如果SIGFPE,SIGILL或SIGSEGV信号不是由C标准定义的kill( )或raise( )函数所生成,则从信号SIGFPE,SIGILL,SIGSEGV的信号捕获函数正常返回后线程的行为是未定义的。
  (d)系统不允许线程捕获SIGKILL和SIGSTOP信号。
  (e)如果线程为SIGCHLD信号建立信号捕获函数,而该线程有未被等待的以终止的子线程时,没有规定是否要生成SIGCHLD信号来指明那个子线程。

每一种信号都被OSKit给予了一个符号名,对于32位的i386平台而言,一个字32位,因而信号有32种。下面的表给出了常用的符号名、描述和它们的信号值。

符号名  信号值 描述                是否符合POSIX
SIGHUP  1   在控制终端上检测到挂断或控制线程死亡  是
SIGINT  2   交互注意信号              是
SIGQUIT  3   交互中止信号              是
SIGILL  4   检测到非法硬件的指令          是
SIGTRAP  5   从陷阱中回朔              否
SIGABRT  6   异常终止信号              是
SIGEMT  7   EMT 指令                否
SIGFPE  8   不正确的算术操作信号          是
SIGKILL  9   终止信号                是
SIGBUS  10   总线错误                否
SIGSEGV  11   检测到非法的内存调用          是
SIGSYS  12   系统call的错误参数           否
SIGPIPE  13   在无读者的管道上写           是
SIGALRM  14   报时信号                是
SIGTERM  15   终止信号                是
SIGURG  16   IO信道紧急信号             否
SIGSTOP  17   暂停信号                是
SIGTSTP  18   交互暂停信号              是
SIGCONT  19   如果暂停则继续             是
SIGCHLD  20   子线程终止或暂停            是
SIGTTIN  21   后台线程组一成员试图从控制终端上读出  是
SIGTTOU  22   后台线程组的成员试图写到控制终端上   是
SIGIO   23   允许I/O信号               否
SIGXCPU  24   超出CPU时限               否
SIGXFSZ  25   超出文件大小限制            否
SIGVTALRM 26   虚时间警报器              否
SIGPROF  27   侧面时间警报器             否
SIGWINCH 28   窗口大小的更改             否
SIGINFO  29   消息请求                否
SIGUSR1  30   保留作为用户自定义的信号1        是
SIGUSR2  31   保留作为用户自定义的信号        是

请求按默认的规则进行信号处理
SIG_DFL (void (*)(int)) 0
请求忽略信号
SIG_IGN (void (*)(int)) 1
注意: 信号队列中最多允许有64个信号


3.2 pthreads/pthread_ipc.c

此源码文件包括了一套完整的消息队列型线程通信机制,消息队列是线程通信的主要手段,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现信号队列,从而进行线程通信的。

3.2.1 消息发送:
说明:当一个线程要向另一个线程发送消息的时候,OSKit使用下面定义的函数来实现一个线程向另一个线程传递消息。
oskit_error_t oskit_ipc_send ( pthread_t dst,void *msg,
        oskit_size_t msg_size, oskit_s32_t timeout )
  dst:目标地址              *msg:指向消息的指针
  msg_size:消息大小(32位无符号整形)    timeout:超时

3.2.2 send的算法
发送一个同步消息,直到接收者发送终止消息或超时才结束
为避免死锁,给没个人加一个锁,并关中断。
assert_interrupts_enabled( );
disable_interrupts( );
pthread_lock (&target->waitlock);
通过条件,检测目标线程是否在等待接受:
条件: a:目标线程的等待标志 & THREAD_WS_IPCRECV_WAIT(线程接收等待位)
    b:目标线程号存在
    c:目标线程等于任意一个线程
    d:消息大小不为0
条件成立: a && ( b || c) && d == 1 才可以发送
   if (target->waitflags & THREAD_WS_IPCRECV_WAIT 
     &&(target->ipc_state.tid == pthread->tid||target->ipc_state.tid== ANYTID))
   { if (msg_size)
     { Memcpy(target->ipc_state.msg,
        msg,MIN(msg_size,target->ipc_state.msg_size));
        if (msg_size > target->ipc_state.msg_size)
           err = OSKIT_ERANGE;
     }
   }
把数据的大小和发送者的ID告诉接受线程:
target->ipc_state.msg_size = msg_size;
target->ipc_state.tid = pthread->tid;
清除接受位:
target->waitflags &= ~THREAD_WS_IPCRECV_WAIT;
pthread_unlock ( &target->waitlock );
注意: flagA & = ~flagB 实际上就是将flagB中等于1的位在flagB中清0
发送队列:
queue_enter ( &target->ipc_state.senders,pthread,pthread_thread_t*,
ipc_state.senders_chain)
&target->ipc_state.senders:发送线程的ID pthread:
pthread_thread_t *:指向消息的指针是pthread_thread_t类型
ipc_state.senders_chain:发送队列的指针
队列间参数传递:
pthread->ipc_state.msg = msg; /* 消息 */
pthread->ipc_state.msg_size = msg_size; /* 消息的长度 */
pthread->ipc_state.tid = dst; /* 消息的目的地址 */
pthread->waitflags = THREAD_WS_IPCSEND_WAIT;
/* 消息目的等待位置1 */

3.2.3 消息接收
说明:当一个线程要从其消息队列中开始接受消息的时候,OSKit提供了下面的函数供线程调用。
oskit_error_t oskit_ipc_recv ( pthread_t src,void *msg,
     oskit_size_t msg_size, oskit_size_t *actual,oskit_s32_t timeout )
  src: 发送线程的ID *msg: 指向消息的指针
  msg_size: 消息大小(32位无符号整形)
  actual: 实际消息的大小 timeout: 超时

3.2.4 receive的算法
等待从发送线程来的消息
通过条件,检测是否接受消息

a:有等待发送的线程
b:线程等待位&THREAD_WS_IPCSEND_WAIT(发送等待标志)
c:发送线程存在
条件成立: a&&(b||c)
   pthread_lock(&source->waitlock);
   if (source->waitflags & THREAD_WS_IPCSEND_WAIT &&
          source->ipc_state.tid == pthread->tid) {
      *actual = MIN(msg_size, source->ipc_state.msg_size);
      if (*actual) {
          memcpy(msg, source->ipc_state.msg, *act0ual);
          if (source->ipc_state.msg_size > msg_size)
              err = OSKIT_ERANGE;
              }
   }
如果没有等待发送的进程,则让接受进程等待
当发送队列为空且timeout=0时解锁,如果发送了但没接受,则重新初始化发送进程,重新发送。


3.3 pthreads/pthread_signal.c

此源码文件包括了一套完整的信号量型线程通信机制,信号量是线程通信的另一个重要的手段,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现信号量,从而进行线程通信的。

3.3.1 检测并更改阻塞的信号
说明:当一个信号被发送出来的时候,并不一定马上能得到相应,一般来说,它要被阻塞一段时间,但系统也是就不管这些阻塞信号了,系统会定时执行此线程,用以检测阻塞的到底是什么信号,它的含义是什么,然后根据需求作出决定,可以将信号阻塞或者响应。
int pthread_sigmask ( int how, const sigset_t *set, sigset_t *oset )
  how: SIG_BLOCK SIG_UNBLOCK SIG_SETMASK
  set: 如果是空指针,则指向下一个信号
  oset:如果是空指针,则指向上一个信号记录

3.3.2杀线程信号
说明:OSKit以及所有的操作系统,都包含这个函数,因为它是操作系统所必须具备的基本功能,也就是说,当一个线程出于无响应状态很长时间,或者因为其他的什么原因线程不能得到执行,则调用它,将该线程杀死。
int pthread_kill(pthread_t tid, int signo)

3.3.3 关线程锁
说明:当线程接受到了消息后的响应过程中,有些时候是不希望被其他的线程打断的,所以OSKit提供了此函数调用,用来将线程锁起来,也可以理解为将线程保护了起来。
int pthread_kill_locked ( pthread_thread_t *pthread, int signo )

3.3.4 在目标线程的信号等待队列中加一个信号
说明:在信号发送和接受的过程中,线程间通讯并不一定是同步的,此时,OSKit提供了一个消息等待队列,将不能及时得到响应的消息用指针链接成为一个类似与链表的数据结构。当接受线程要响应信号的时候可以直接从消息队列里提取,这样作既可以提高通信效率,有可以增加通信的稳定性。
int sigaddset ( &pthread->sigpending, signo )

3.3.5 信号完成的动作
说明:在用信号方式通信的时候,线程间传递的信号并不是要做的动作,而是动作的代码,而什么信号对应什么动作,还要有此汉说来进行解析,这样作是为了规范线程间通信,同时还可以节约空间,节省发送和接受的时间,从而大大提高系统的效率。
int sigaction ( int sig, const struct sigaction *act, struct sigaction *oact )
  sig:指定信号 act:指向与指定信号向联系的规定信号动作的结构
  oact:指向存储先前与该信号相联系的动作的结构

3.3.6 测试或改变(或两者兼有)主调进程的信号掩码
说明:所谓掩码就是信号的屏蔽码,比如说,当一个信号被发送的时候,它被自动加载到接收线程的信号掩码中,如果此时又向该线程发送了同样的信号,则阻塞该信号,知道前一个信号响应完毕。
OSKit采用下面的函数完成的对信息掩码的测试和改变。
int sigprocmask ( int how, const sigset_t *set, sigset_t *oset )
  how: 指明改变信号集的方式
  SIG_BLOCK: 结果是当前集与实参set所指向的信号集的联合
  SIG_UNBLOCK: 结果集是当前集与实参set所指向的信号集的补集的交
  SIG_SETMASK: 结果集是实参set所指向的信号集
  set: 指向要用来改变当前被阻塞的信号集 oset: 存储当前的掩码

3.3.7 杀线程
说明:此函数和前面介绍过的杀线程信号并不完全一样,源线程是通过看是否有向目的线程发信号的权限,如果有的话就杀掉它
int kill ( pid_t pid, int signo )
  pid: 目标线程 signo: 所要发的信号

3.3.8 等待信号的线程队列
说明:由于线程间通信不一定同步的原因,调用此函数创建信号等待队列是十分有必要的。
int sigqueue ( pid_t pid, int signo, const union sigval value )

3.3.9 等待内部信号
说明:OSKit将信号分为内部信号和外部信号,这是十分有意义的,因为很多情况下,它们的处理方式是不一样的,有的外部消息可以不加理睬,但一般来说,内部消息是一定要及时响应的,系统通过调用下面的函数实现了等待一个内部信号,一旦等到,马上响应。
oskit_error_t oskit_sigwait_internal ( const sigset_t *set,siginfo_t
        *info,const oskit_timespec_t *timeout )
  sigset_t: 信号装置,无符号整型
  siginfo_t: 信号信息不会被外部的信号打断

3.3.10 线程等待信号
说明:在线程调度时,调度程序经常会发送此信号让执行完毕的线程等待还没执行完的线程,这是为了保持线程之间的同步。
int sigwait ( const sigset_t *set, int *sig )

3.3.11 线程等待信号(一般指内部信号)
说明:同样的,核心内的线程一样也要保持同步。
int sigwaitinfo ( const sigset_t *set, siginfo_t *info )

3.3.12 线程等待信号(有时间限制)
说明:此外为了完善系统的线程管理,OSKit还提供了这条函数,用来设定线程的等待时间,如果在规定的时间内没有信号到来的话,则不再等待。
int sigtimedwait ( const sigset_t *set,siginfo_t *info,
     const struct oskit_timespec *timeout )

3.3.13 线程等待信号,如果无信号发给它,则无限期等待
说明:但有的时候,线程要等待的信号对于该线程来说是十分重要的,所以它将无限期的等待下去,同时调度程序会将该线程阻塞起来。
int sigsuspend ( const sigset_t *sigmask)

3.3.14 从一个陷入发送信号给线程
说明:当系统内部发生陷入的时候,系统会给正在执行的线程发送信号,OSKit正是调用了此函数来实现。
void oskit_libc_sendsig ( int sig, int code, struct sigcontext *scp )

3.3.15 发出线程阻塞信号
说明:如果当前正在执行的线程由于某种突发情况被阻塞了,它将调用此函数,来通知与其相关的线程。
void oskit_deliver_pending_signals ( void )

3.3.16 不会被阻塞的信号发送
说明:在操作系统中,有许多十分重要的消息是不能被阻塞的,OSKit所提供的此条函数调用便是用来发送此种信号的。
void really_deliver_signal ( int sig, siginfo_t *info, struct sigcontext *scp )

3.3.17 信号初始化
说明:与线程类似,信号并不是从天而降,它也要先进行初始化,然后再发送给接受线程。
void pthread_init_signals ( )


3.4 pthreads/pthread_cond.c

此源码文件包括了一套完整的线程通信的条件变量,它是线程通信的重要组成部分,通过阅读下面的函数分析,将使读者了解OSKit到底是用什么函数来具体实现线程通信的条件变量。

3.4.1条件变量的初始化数据结构
说明:在OSKit的线程间通信机制中,经常用到条件变量。所谓条件变量,就是两个线程通过条件变量来达到同步。例如,A线程我们让它等待一个条件变量,B线程我们让它发送一个信号,满足A线程所等待的条件,这样就实现了A、B线程之间的同步。
int pthread_cond_init ( pthread_cond_t *c, const pthread_condattr_t *attr )
  pthread_cond_t: 条件变量 pthread_condattr_t: 条件变量属性

3.4.2撤销条件变量
说明:条件变量并不是永远发生作用的,所以有创建就得有撤销,OSKit正是调用了这个函数来实现条件变量撤销。
int pthread_cond_destroy(pthread_cond_t *c)

3.4.3 线程等待条件变量
说明:OSKit设定条件变量就是为了通过让线程等待它,从而实现线程之间的相互协调。
int pthread_cond_wait ( pthread_cond_t *c, pthread_mutex_t *m )
  pthread_mutex_t:为什么而等待

3.4.4 安全的等待
说明:等待条件变量的方式也是多种多样,此函数提供的是一种安全的等待方式,即等待条件变量的线程不会被中断。
int pthread_cond_wait_safe ( pthread_cond_t *c, pthread_mutex_t *m )
  注:不会被中断

3.4.5 等待条件变量,但有时间限制
说明:另一种等待条件变量的方式是在一定的时间限制内等待,一旦超时,线程便放弃对该条件变量的请求。
int pthread_cond_timedwait ( pthread_cond_t *c, pthread_mutex_t
             *m,oskit_timespec_t *abstime )
  struct oskit_timespec {
      oskit_time_t tv_sec; /* 秒 */
      oskit_s32_t tv_nsec; /* 纳秒 */
  };

3.4.6 等待捐赠条件
说明:在操作系统中线程之间总有一些比较有趣的调度算法,捐赠就是其中之一,在OSKit里,出于同一个等待目的的线程之间,为了总体上的最优化,某些线程可以将自己占有的资源捐赠给其他线程,让其尽早满足执行的条件。
int pthread_cond_donate_wait ( pthread_cond_t *c, pthread_mutex_t
       *m,oskit_timespec_t *abstime, pthread_t donee_tid )
  pthread_t donee_tid:被捐赠线程号

3.4.7 等待条件超时
说明:此函数是OSKit的出错信息,它是在等待条件变量超时的情况下才被调用的。
oskit_error_t pthread_condwait_timeout ( struct oskit_iunknown *listener, void *arg )

3.4.8 线程发送信号条件
说明:在线程间通信中,线程并不是什么时候都可以发送信号的,OSKit定义了下面这个函数,规定了信号发送是所要等待的条件;换句话说就是只有满足了此函数调用所设定的条件,线程之间才可进行通信。
int pthread_cond_signal ( pthread_cond_t *c )

3.4.9 线程广播条件
说明:有些时候,某些线程需要对系统中所有的线程发送同一条消息,这样就用到了OSKit提供的线程广播的函数。
int pthread_cond_broadcast ( pthread_cond_t *c )


本 章 小 结

本章从线程间的两种通信机制入手,系统地阐述了OSKit的线程通信机制。

在第一节主要描述了OSKit所用到的两种线程间通信机制,以及它为什么没有使用UNIX中的管道通信,另外,我还以表格的形式详细列出了OSKit提供的各种信号量、以及它们与POSIX标准之间的关系。第二节中以源代码中的 /pthreads/pthread_ipc.c 为例,说明了第一节中消息队列的具体实现。第三节以源代码中的 /pthreads/pthread_signal.c 为例,说明了Oskit第一节中信号量的具体实现。第四节中又以源代码/pthreads/pthread_cond.c为例,阐述了条件变量的概念以及用条件变量进行辅助通信的具体实现。