3.3.3 信号 (Signal) 处理 信号 (signal) 是向进程发送的软件通知, 通知进程有事件发生 引发信号的事件发生时, 信号就被生成 (generate) 了 进程根据信号采取行动时, 信号就被传递 (deliver) 了 信号的寿命 (lifetime) 就是信号的生成和传递之间的时间间隔 已经生成但还未被传递的信号被称为挂起 (pending) 的信号 在信号生成和信号传递之间可能会有相当长的时间 传递信号时, 进程必须在处理器上运行 如果在传递信号时, 进程执行了信号处理程序 (signal handler), 那么进程就捕捉 (catch) 到了这个信号 程序可以使用用户编写的函数名作为参数调用 sigaction 来安装用户自定义的信号处理程序 ; 或者以 SIG_DFL 或 SIG_IGN 作为参数调用 sigaction 函数,SIG_DFL 表示采取默认的动作,SIG_IGN 表示忽略信号, 这两个动作都不是在 捕捉 信号 如果将进程设置为忽略 (ignore) 某个信号, 那么在传递时那个信号就会被丢弃, 不会对进程产生影响 信号生成时所采取的动作取决于那个信号当前使用的信号处理程序和进程信号掩码 (process signal mask) 信号掩码中包含一个当前被阻塞信号(blocked signal) 的列表 阻塞一个信号很容易和忽略一个信号混淆起来 被阻塞的信号不会像被忽略的信号一样被丢弃 如果一个挂起信号被阻塞了, 那么当进程解除了对那个信号的阻塞时, 信号就会被传递出去 程序通过调用 sigprocmask 改变它的进程信号掩码来阻塞一个信号, 而通过调用 sigaction 将信号处理程序设置为 SIG_IGN 来忽略一个信号 注意, 信号最初是在多进程系统中引入, 而本节的重点在于讲述多线程系统中如何处理信号 本节所涉及的函数 数据类型和宏定义都需要使用 signal.h 头文件 3.3.3.1 产生信号 每个信号都有一个以 SIG 开头的符号名 信号的名字都定义在 signal.h 中, 任何一个使用了信号的 C 程序中都要包含这个文件 信号的名字都是某个大于 0 的小整数的宏定义 表 3.10 中描述了必须的 POSIX 信号, 并列出了它们的默认行为 有两个信号 SIGUSR1 和 SIGUSR2 是提供给用户使用的, 没有预先指定的用途 出现某些错误时, 会产生诸如 SIGFPE 或 SIGSEGV 这样的信号, 其它的信号是由 alarm 这样特殊的调用产生的 信号 描述 默认行为 SIGABRT 进程放弃 与实现有关 SIGALRM 报警时钟 为正常终止 SIGBUS 访问了内存对象中的未定义 与实现有关 部分 SIGCHLD 子进程被终止 停止或继续 忽略 SIGCONT 如果进程被停止了, 本信号使 继续 进程继续执行 SIGFPE 算术计算中出现了被零除的 与实现有关 错误 SIGHUP 在控制终端或进程上挂起或 非正常终止
终止 SIGILL 无效的硬件指令 与实现有关 SIGINT 交互终端提示信号 ( 通常是 非正常终止 Ctrl-C) SIGKILL 终止 ( 不能被捕获或忽略 ) 非正常终止 SIGPIPE 向一个没有读程序的管道写 非正常终止 入 SIGQUIT 交互终端终止 : 信息转储 ( 通 与实现有关 常为 Ctrl-L) SIGSEGV 无效的内存引用 与实现有关 SIGSTOP 执行停止 ( 不能被捕捉或忽 停止 略 ) SITTERM 终止 非正常终止 SIGTSTP 终端停止 停止 SIGTTIN 后台进程试图进行读操作 停止 SIGTTOU 后台进程试图进行写操作 停止 SIGURG 在套接字上有高带宽数据 忽略 SIGUSR1 用户定义的信号 1 非正常终止 SIGUSR2 用户定义的信号 2 非正常终止 表 3.10 POSIX 信号 调用 kill 函数可以向指定进程发送指定的信号, 调用 raise 函数可以使进程向自己发送指定的信号, 调用 alarm 函数可以使进程向自己在经过指定的秒数之后发送 SIGALRM 信号 由于这些函数不属于本节的主题, 在此不详细介绍, 感兴趣的读者可以自己查询这些函数的相关资料 3.3.3.2 对信号掩码和信号集进行操作 如前所述, 进程可以通过阻塞信号暂时地阻止信号的传递 在传递之前, 被阻塞的信号不会 影响进程的行为 进程的信号掩码 (signal mask) 给出了一个信号集合, 对哪些信号进行阻 塞需要通过信号掩码进行设置 信号掩码的类型为 sigset_t 信号集由下面的五个函数来操作 每个函数的第一个参数都是一个指向 sigset_t 的指针 sigaddset 负责将 signo 加入信号集, 而 sigdelset 将 signo 从信号集中删除 sigemptyset 函数对一个 sigset_t 对象进行初始化, 使其不包含任何信号 sigfillset 也可用来对一个 sigset_t 对象进行初始化, 使其包含所有的信号 sigismember 负责报告 signo 是否在 sigset_t 中 这五个函数的形式为 : int sigaddset(sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo); int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigismember(const sigset_t *set, int signo);
对于进程来说, 可以通过调用 sigprocmask 函数来检查或修改它的进程信号掩码, 由于该函 数不属于本节的主题, 在此不详细介绍, 感兴趣的读者请自己查阅相关资料 3.3.3.3 信号的捕捉 忽略和等待 sigaction 函数允许调用程序检查或指定与特定信号相关的动作, 即处理信号的行为, 或忽 略信号 由于该函数不属于本节的主题, 在此不详细介绍, 感兴趣的读者请自己查阅相关资 料 关于等待信号, 信号机制提供了一种不需要忙等 (busywaiting) 的等待事件的机制 忙等是指连续地使用 CPU 来检测事件的发生, 而更有效的方式是将进程或线程挂起直到所等待的事件发生为止, 这样其它的进程或线程就可以更有效地使用 CPU 了 POSIX 中的 pause, sigsuspend 和 sigwait 函数提供了三种机制, 用来挂起进程, 直到信号发生为止 pause 函数将调用线程挂起, 直到传递了一个信号为止, 这个信号的动作或者是执行用户定 义的处理程序, 或者是终止进程 如果信号的动作是终止进程,pause 就不返回 如果信号 被进程捕捉,pause 就会在信号处理程序返回之后返回 它的形式为 : int pause(void); pause 函数总是返回 -1 如果被信号中断,pause 就将 errno 设置为 EINTR pause 函数面临的一个主要问题是, 它将错过在调用之前程序收到的信号 为了解决这个问 题, 程序必须不间断地执行两项操作 解除对信号的阻塞, 并启动 pause 也就是说, 这 两项操作应该是原子的 sigsuspend 函数即用来解决这个问题 它的形式为 : int sigsuspend(const sigset_t *sigmask); sigsuspend 函数用参数 sigmask 指向的信号掩码来阻塞相应的信号, 并将进程或线程挂起, 直到进程或线程捕捉到相应的信号 sigsuspend 函数在信号处理程序返回之后返回, 并将 sigmask 指向的信号掩码设置为调用 sigsuspend 之前的信号阻塞状态 sigsuspend 函数总是返回 -1 如果被信号中断,sigsuspend 就将 errno 设置为 EINTR sigwait 函数的形式为 : int sigwait(const sigset_t *restrict sigmask, int *restrict signo); sigwait 函数会将调用的线程挂起, 直到 sigmask 指定的信号集中的信号被挂起, 然后从挂起的信号集合中删除那个信号 当 sigwait 返回时, 会把从挂起的信号集合中删除的那个信号的信号码存储在 signo 指向的地址中 如果成功,sigwait 返回 0, 如果不成功,sigwait 返回 -1 并设置 errno 要注意的是,sigmask 指定的信号集中的信号必须在 sigwait 调用之前被阻塞, 并且不能被 忽略的, 如果为这些信号指定处理函数, 这些处理函数也不会被调用
3.3.3.4 多线程程序中的信号 进程中的所有线程都共享进程中的信号处理程序, 但每个线程可以有它自己的信号掩码 由于线程的操作可以异步于信号, 所以线程与信号的交互会比较复杂 对于线程而言, 有三种信号的类型, 其中异步信号是指传递给某些解除了对该信号的阻塞的线程的信号, 同步信号是指传递给引发该信号的线程的信号, 定向的信号是指由 pthread_kill 函数发送给指定线程的信号 SIGFPE( 浮点异常 ) 这样的错误信号就是同步于引发它们的线程的, 因为引发这些信号的线程将等待信号处理程序完成之后才能继续执行, 而其它信号因为不与特定的线程相关, 所以它们是异步的 如果有几个线程都解除了对同一个异步信号的阻塞, 当有信号到达时, 线程运行系统就从中挑选一个来处理信号 pthread_kill 函数将指定的信号发送到指定的线程 它的形式为 : int pthread_kill(pthread_t thread, int sig); 如果成功,pthread_kill 就返回 0 如果不成功,pthread_kill 就返回一个非零的错误码 下表列出了 pthread_kill 可能发生的错误和对应得错误码 错误 原因 EINVAL sig 是无效的或不被支持的信号码 ESRCH 没有线程对应于指定的 ID 表 3. 11 pthread_kill 可能发生的错误和对应得错误码 尽管 pthread_kill 将信号发送到指定的线程, 但处理信号的行为可能会影响整个进程 例 如, 将 SIGKILL 信号发送给指定线程, 将使整个进程终止 因为 SIGKILL 信号不能被捕捉, 阻塞或忽略, 而它的行为就是将进程终止 虽然信号处理程序是进程范围的, 但是每个线程可以有它自己不同的信号掩码 线程可以用 pthread_sigmask 函数来检查或设置它的信号掩码, 这个函数是 sigprocmask 在线程化程序中的推广 当进程中有多个线程时, 就不应该使用 sigprocmask 函数了, 但在创建其它线程之前, 也可以被主线程调用, 这样可以初始化子线程的信号掩码, 因为当子线程被创建时将继承父线程的信号掩码 这个函数的形式为 : int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset); 参数 how 指定修改信号掩码的方式,how 的值如果为 SIG_SETMASK 会使线程信号掩码被 set 取代, 如果为 SIG_BLOCK 会使线程阻塞 set 中的信号, 如果为 SIG_UNBLOCK 会使线程解除对 set 中的信号的阻塞 set 为指定的信号集, 如果为 NULL, 则不进行修改 oset 保存 pthread_sigmask 调用之前的信号掩码 在多线程的进程中进行信号处理的一种推荐策略是 : 为信号处理使用特定的线程 主线程在 创建线程之前阻塞所有的信号, 这样, 所有的线程都将信号阻塞了 然后, 专门用来处理信 号的线程对那些需要处理的信号执行 sigwait, 这样指定的信号都将被这个信号处理线程处
理 3.3.3.5 多线程程序中应用信号的实例 下面是一个简单的信号应用实例 类似于前面的多线程实例, 主线程在创建子线程之后, 等待子线程运行结束, 然后继续运行 不同之处在于, 主线程没有调用 pthread_join 来等待子线程运行结束, 而是等待子线程发送的信号 子线程则在运行结束前向主线程发送信号, 来通知主线程 这样, 利用信号机制, 实现了线程的同步 代码 3. 6 多线程程序中应用信号的实例 #include <stdio.h> #include <pthread.h> #include <signal.h> void *threadfunc(void *pvoid) { pthread_t* main_tid = (pthread_t*)pvoid; printf("child thread says:hello!world!\n"); pthread_kill(*main_tid, SIGUSR1); } int main() { sigset_t sigs; int sig; pthread_t tid; pthread_t main_tid; sigemptyset(&sigs); sigaddset(&sigs, SIGUSR1); sigprocmask(sig_block, &sigs, NULL); main_tid = pthread_self(); pthread_create(&tid, NULL, &threadfunc, &main_tid); sigwait(&sigs, &sig); printf("main thread says:hello!world!\n"); } return 0;