进程间通信简介(一)——信号

1 进程间通信简介

        进程间通信(InterProcess Communication, IPC)就是在不同进程之间传播或交换信息。交换信息的方式简单的有很多,比如说通过全局变量或函数调用,又或者是两个进程通过磁盘上的普通文件交换信息,亦或是通过“注册表”或其它数据库中的某些表项和记录交换信息。广义上来说,这些都是进程间通信的手段,但是一般都不把它们算作“进程间通信”。因为这样的通信手段效率太低,实时性不高。尤其当网络上需要多个进程相互对话时,这些方案几乎成为累赘。再加上网络设计的一个重要目标是保证进程间不互相干涉,否则系统可能被挂起或自锁,因此进程间必须使用简介有效的方法进行通信。这方面,Linux具有非常显著的解决方案。

1.1 进程间通信的一些基本概念

  1. 进程阻塞:当一个进程在执行某些操作的条件得不到满足时,就自动放弃CPU资源而进入休眠状态,以等待条件的满足。当操作条件满足时,系统就将控制权返还给该进程继续进行未完的操作。
  2. 共享资源:因为计算机的内存、存储器等资源是有限的,无法为每一个进程都分配一份单独的资源,所以系统将这些资源在各个进程间协调使用,称为共享资源。
  3. 锁定:当某个进程在使用共享资源时,可能需要防止别的进程对该资源的使用,比如,一个进程在对某个文件进行读操作时,如果别的进程也在此时向文件中写入内容,就可能导致进程读入错误的数据。为此,Linux提供一些方法来保证共享资源在被某个进程使用时,别的进程无法使用。这就叫做共享资源的锁定。

1.2 进程通信的方法概述

        Linux的进程间通信的方法有管道、消息队列、信号量、共享内存、套接口等。其中,管道又分为命名管道和无名管道。消息队列、信号量、共享内存通称为系统(POSIX和System V系统)IPC。管道、消息队列、信号量和共享内存用于本地进程间通信,而套接口用于远程进程通信。

  • 管道(Pipe)及命名管道(named pipe):管道可用于具有亲缘关系进程间的通信,命名管道克服了管道没有名字的限制,因此,出具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
  • 消息(Message)队列(报文队列):消息队列是消息的链接表,包括POSIX消息队列System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号量承载信息量少,管道只能承载无格式字节流及缓冲区大小受限等缺点。
  • 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式,是针对其它通信机制运行效率较低而设计的,往往与其它通信机制,如信号量,结合使用来达到进程间的同步及互斥。
  • 信号量(semaphore):主要作为进程间及同一进程不同线程之间的同步手段。
  • 套接口(Socket):也称套接字,是更为一般的进程间通信机制,可用于不同机器之间(远程)的进程间通信。起初是由UNIX系统的BSD分支开发出来的,但现在一般可以移植到其它类UNIX系统上:Linux和System V的变种都支持套接字。

      某种意义上,信号也可以归结为进程间的通信方式之一,但是信号和信号量是由区别的,设置可以说二者截然不同。信号(signal)是一种处理异步事件的方法,信号是由硬件或软件触发,再由操作系统内核发送给应用程序的中断形式,POSIX定义了一系列的信号集。信号量(semaphore)是一种实现进程间同步、互斥的机制。信号量是POSIX进程间通信的工具,在它上面定义了一系列操作原语,简单地讲它可以在进程间进行通信。

2 信号

2.1 信号的基本概念

        信号是一种进程间通信的方法,应用于异步事件的处理。信号的实质是一种软中断,它被发送给一个正在被执行的进程以通知该进程有某一事件发生了。由于信号的特点,所以不用它来作进程间的直接数据传送,而把它用作对非正常情况的处理。

2.1.1 信号的含义

        软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill()发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

        收到信号的进程对各种信号有不同的处理方法。大致分为三类:第一类是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理;第二类方法是忽略某个信号,对该信号不进行任何处理,就像从未发生过一样;第三类方法是对该信号的处理保留系统的默认值,这种默认操作大多数是使得进程终止。进程通过系统调用signal()来指定进程对某个信号的处理行为。

        在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。

        每一个信号都有一个名字,这些名字都以字符SIG开头。在头文件signal.h中这些信号都被定义为正整数,称为信号编号。没有编号为0的信号,kill()函数对编号0有特殊的应用,POSIX.1将此种信号编号值称为空信号。

2.1.2 信号的分类

      按发出信号的原因简单分类:
  • 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。
  • 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其它各种硬件错误。
  • 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。
  • 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
  • 在用户态的进程发出的信号。如进程调用系统调用kill向其它进程发送信号。
  • 与终端交互相关的信号。如用户关闭一个终端,或按“break”键等情况。
  • 跟踪进程执行的信号。

2.1.3 信号列表

        shell下输入“kill -l”可以查看系统的信号列表,或者输入“man 7 signal”可以查看更加详细的说明。常用的信号着重显示。
表2-1-3-1 信号简介(部分)
信号含义
SIGHUP本信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一会话期(Session)内的各个作业,这时它们与控制终端不再关联。登录Linux时,系统会自动分配给登录用户一个控制终端。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于同一个会话。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。此外,对于与终端脱离关系的守护进程,这个信号用户通知它重新读取配置文件。
SIGINT程序终止(或中断,interrupt)信号,在用户键入INTR字符(通常是Ctrl+c或Delete键)时发出,用于通知前台进程组终止进程。
SIGQUIT和SIGINT类似,但由QUIT字符(通常是Ctrl+\)来控制。进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号。
SIGILL执行了非法指令。通常是因为可执行文件本身出现错误,或者试图执行数据段,堆栈溢出时也有可能产生这个信号。
SIGTRAP由断点指令或其它陷阱(trap)指令产生,由调试器(debugger)使用,比如跟踪陷阱信号。
SIGABRT调用abort函数时产生的信号,将会使进程非正常结束。
SIGBUS非法地址,包括内存地址对齐(alignment)出错。比如访问一个4个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
SIGFPE在发生致命的算术运算错误时发出,不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。
SIGKILL用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可以尝试发送此信号。
SIGUSR1留给用户使用,可由用户在应用程序中自行定义。
SIGSEGV试图访问未分配给登录用户的内存区,或试图向没有写权限的内存地址写数据。
SIGUSR2留给用户使用,可由用户在应用程序中自行定义。
SIGPIPE管道破裂信号,当对一个读进程已经运行结束的管道执行写操作时产生。这种情况通常发生在进程间通信时,比如采用管道(FIFO)通信的两个进程,读管道还没有打开或者意外终止就向管道写时,写进程会收到SIGPIPE信号。此外比如使用套接字(Socket)通信的两个进程,写进程在写Socket的时候,读进程已经终止。
SIGALRM时钟定时信号,计算的是实际的时间或时钟时间。由alarm函数设定的时间段终止时,会产生该信号。
SIGTERM程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令“kill”默认产生这个信号。如果进程终止不了,才会尝试SIGKILL。
SIGSTKFLT堆栈错误。
SIGCHLD子进程结束时,父进程会收到这个信号。如果父进程没有处理这个信号,也没有等待子进程,子进程虽然终止,但还是会在内核进程表中占有表项,这时的子进程被称为僵尸进程,这种情况应该尽量避免。也就是说,父进程忽略SIGCHLD信号,或者捕捉它,或者等待它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管。
SIGCONT让一个停止(stopped)的进程继续执行。此信号不能被阻塞,可以用一个信号处理程序来让程序在由停止状态变为继续执行时完成特定的工作。例如,重新显示提示符。
SIGSTOP停止(stopped)进程的执行。注意它和terminate及interrupt的区别:该进程还未结束,只是暂停执行。此信号不能被阻塞、处理或忽略。
SIGTSTP停止进程的运行,但该信号可以被处理和忽略。用户键入SUSP字符时(通常是Ctrl+z)发出这个信号。
SIGTTIN当后台作业要从用户终端读取数据时,该作业中的所有进程会收到SIGTTIN信号,默认时这些进程会停止执行。
SIGTTOU类似于SIGTTIN,但在写终端(或修改终端模式)时收到。
SIGURG套接字上出现紧急情况时产生此信号,比如紧急数据。
SIGXCPU超过CPU时间资源限制时产生的信号。这个限制可以由getrlimit/setrlimit来读取/改变。
SIGXFSZ当进程企图扩大文件以至于超过文件大小资源限制时产生此信号。
SIGVTALRM虚拟时钟信号,类似于SIGALRM,但是计算的是该进程占用的CPU时间。
SIGPROF类似于SIGALRM/SIGVTALRM,但包括该进程使用的CPU时间以及系统调用的时间。
SIGWINCH窗口大小改变时发出的信号。
SIGIO文件描述符准备就绪,表示可以开始进行输入/输出操作。
SIGPWR电源失效信号(Power failure)。
SIGSYS非法的系统调用。
  • 在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL、SIGSTOP
  • 不能恢复至默认动作的信号有:SIGILL、SIGTRAP
  • 默认会导致进程流产生的信号有:SIGABRT、SIGBUS、SIGFPE、SIGILL、SIGIOT、SIGQUIT、SIGSEGV、SIGTRAP、SIGXCPU、SIGXFSZ
  • 默认会导致进程退出的信号有:SIGALRM、SIGHUP、SIGINT、SIGKILL、SIGPIPE、SIGPOLL、SIGPROF、SIGSYS、SIGTERM、SIGUSR1、SIGUSR2、SIGVTALRM
  • 默认会导致进程停止的信号有:SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU
  • 默认进程忽略的信号有:SIGCHLD、SIGPWR、SIGURG、SIGWINCH
        此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。信号SIGIOT与SIGABRT是同一个信号。另外,同一个信号在不同的系统中值可能不一样,所以建议最好使用为信号定义的名字,而不要直接使用信号的值。

2.2 信号处理机制

2.2.1 内核对信号的基本处理方法

        内核给一个进程发送软中断信号的方法是,在进程所在的进程表项的信号域设置对应于该信号的位(内核通过在进程的 struct task_struct 结构中的信号域中设置相应的位来实现向一个进程发送信号)。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级之上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。
        内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
        内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。前面说过,处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户自定义的使用系统调用signal()注册的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就像从来没有收到该信号似的而继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数,而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的额原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。
        在信号的处理方法中有几点特别要引起注意:
  • 在一些系统中,当一个进程处理完终端信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次使用signal()系统调用。这可能会使得进程在调用signal()之前又得到该信号而导致退出。在BSD系统中,内核不再清除该地址。但不清除该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢出。为了避免出现上述情况,在BSD中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。
  • 如果要捕捉的信号发生于进程正在一个系统调用中时,并且该进程睡眠在可中断的优先级上,这时该信号引起进程做一次longjmp()调用,跳出睡眠状态,返回用户态并执行信号处理例程。当从信号处理例程返回时,进程就像从系统调用返回一样,但返回了一个错误代码,指出该次系统调用曾经被中断。这里需注意的是,BSD系统中内核可以自动地重新开始系统调用。
  • 若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不做longjmp()调用,一般是继续睡眠。但用户感觉不到进程曾经被唤醒,而是像没有产生过信号一样。
  • 内核对子进程终止(SIGCLD)信号的处理方法与其它信号有所区别。当进程检查出收到了一个子进程终止的信号时,默认情况下,该进程就像没有收到该信号似的,如果父进程执行了系统调用wait,进程将从系统调用wait中醒来并返回wait调用,执行一系列wait调用的后续操作(找出僵尸子进程,释放子进程的进程表项),然后从wait中返回。SIGCLD信号的作用是幻想一个睡眠在可被中断优先级上的进程。如果该进程捕捉了这个信号,就像普通信号处理一样转到处理例程。如果进程忽略该信号,那么系统调用wait的动作就有所不同,因为SIGCLD的作用仅仅是唤醒一个睡眠在可被中断优先级上的进程,那么执行wait调用的父进程被唤醒继续执行wait调用的后续操作,然后等待其它的子进程。
        如果一个进程调用signal系统调用,并设置了SIGCLD的处理方法,并且该进程有子进程处于僵尸状态,则内核将向该进程发一个SIGCLD信号。

2.2.2 setjmp和longjmp的作用

        在介绍信号的时候,我们看到多个地方要求进程在检查收到信号后,从原来的系统调用中直接返回,而不是等到该调用完成。这种进程突然改变其上下文的情况,就是使用setjmp和longjmp的结果。setjmp将保存的上下文存入用户区,并继续在旧的上下文中执行。这就是说,进程执行一个系统调用,当英文资源或其他原因要去睡眠时,内核为进程做了依次setjmp,如果在睡眠中被信号唤醒,进程不能再进入睡眠时,内核为进程调用longjmp,该操作是内核为进程将原先setjmp调用保存在进程用户区的上下文恢复称现在的上下文,这样就使得进程可以恢复等待资源前的状态,而且内核为setjmp返回1,使得进程知道该次系统调用失败。这就是它们的作用。声明如下:
#include <setjmp.h>
int setjmp(jmp_buf envbuf);
void longjmp(jmp_buf envbuf, int val);
        setjmp函数用缓冲区envbuf保存系统堆栈的内容,以便后续的longjmp函数使用,setjmp函数初次启用时返回0值。longjmp函数中的参数envbuf是由setjmp函数所保存的堆栈环境,参数val设置setjmp函数的返回值。longjmp函数本身没有返回值,它执行后跳转到保存envbuf参数的setjmp函数调用,并由setjmp函数调用返回,此时setjmp函数的返回值就是val。调用longjmp函数时不能使用setjmp函数返回0,如果val为0,则setjmp函数返回1。longjmp函数从来不返回,因为它调用后就跳转到setjmp函数保存的堆栈处,恢复堆栈开始执行,所以longjmp函数不会返回。setjmp函数与longjmp函数总是组合起来使用,而且有很多需要注意的细节,比如二者必须有严格的先后执行顺序(先调用setjmp函数,后调用longjmp函数),longjmp的调用是有一定的域范围要求,setjmp和longjmp不能很好地支持C++中面向对象的语义等等。
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>

jmp_buf position;

int main()
{
	void goback();
	...
	...
	/* 保存当前的堆栈环境 */
	setjmp(position);
	signal(SIGINT, goback);
	domenu();
	...
	...
}

void goback()
{
	fprintf(stderr, "\nInterrupted\n");
	/* 跳转回被保存的断点 */
	longjmp(position, 1);
}

2.3 信号的操作实现

2.3.1 信号的处理

2.3.1.1 signal函数
        要对一个信号进行处理,就需要给出此信号发生时系统所调用的处理函数,可以为一个特定的信号(除去无法捕捉的SIGKILL和SIGSTOP信号)注册相应的处理函数。如果正在运行的程序源代码里注册了针对某一特定信号的处理程序,不论当时程序执行到何处,一旦进程接收到该信号,相应的调用就会发生。
        通过系统调用signal()用于接收一个指定类型的信号,并可以指定相应的方法,原型如下:
#include <signal.h>
void (*signal (int signum, void (*handler) (int)))(int)
        或者POSIX定义,具体参考联机手册。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
        若成功返回以前的信号处理配置,若失败返回SIG_ERR。
        第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。参数handler描述了与信号关联的动作,它可以取以下三种值:
  • 一个返回值为整数的函数地址。
        此函数必须在signal()被调用前声明,handler为这个函数的名字。当接收到一个类型为sig的信号时,就执行handler所指定的函数,这个函数应有如下形式的定义:
int func(int sig);
        sig是传递给它的唯一参数。执行了signal()调用后,进程只要接收到类型为sig的信号,不管其正在执行程序的哪一部分,就立即执行func()函数。当func()函数执行结束后,控制权返回进程被中断的那一点继续执行。当指定函数地址时,我们称此为捕捉此信号,并称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)。
  • SIG_IGN
        这个符号表示忽略信号。执行了相应的signal()调用,进程会忽略类型为sig的信号。
  • SIG_DFL
        这个符号表示恢复系统对信号的默认处理。
#include <signal.h>
#define SIG_ERR ( void ( * ) ( ) ) -1
#define SIG_DFL ( void ( * ) ( ) ) 0
#define SIG_IGN ( void ( * ) ( ) ) 1
        这些常数可用于表示指向函数的指针,该函数需要一个整型参数,而且无返回值。signal的第二个参数及其返回值就可用它们表示。这些常数所使用的三个值不一定要是-1、0和1,但它们必须是三个值而绝不能是任一可说明函数的地址。
        下面演示一下捕捉终端键入“Ctrl+c”时产生的SIGINT信号。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void SignHandler(int signnum)
{
	printf("Capture signal number:%d\n", signnum);
}

int main(void)
{
	signal(SIGINT, SignHandler);
	while (1)
		sleep(1);

	return 0;
}
        编译并运行:
$ ./catch_sigint
^CCapture signal number:2
^CCapture signal number:2
^CCapture signal number:2
^\退出 (核心已转储)
$
        注意,这里显示的^C是键入了“Ctrl+c”,被打印到了控制台上,同样,^\是键入了“Ctrl+\”。结果可以看出,在键入“Ctrl+c”后并不会终止程序的运行,因为“Ctrl+c”产生的SIGINT信号已经被进程中注册的SignHandler函数捕获了。
        接下来演示忽略终端键入“Ctrl+c”时产生的SIGINT信号。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(void)
{
	signal(SIGINT, SIG_IGN);
	while (1)
		sleep(1);
	return 0;
}
        编译并运行:
$ ./ignore_sigint
^C^C^C^C^C^\退出 (核心已转储)
$
        运行结果可以看出,我们连续键入了五个“Ctrl+c”但是程序都没有任何反应,这是因为产生的SIGINT信号已经被忽略。
        接下来演示接受信号的默认处理方式,接受默认处理就相当于没有写信号处理程序。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(void)
{
	signal(SIGINT, SIG_DFL);
	while (1)
		sleep(1);
	return 0;
}
        编译并运行:
$ ./default_sigint
^C
$
        运行结果可以看出,在键入一个“Ctrl+c”的时侯,程序就执行退出,跟我们平常的操作结果是一样的,正式大家熟悉的系统默认设置。
        接下来演示多个信号的处理。
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void sigroutine(int dunno)
{
	switch (dunno)
	{
		case 1: printf("Capture SIGHUP signal, the signal number is %d\n", dunno); break;
		case 2: printf("Capture SIGINT signal, the signal number is %d\n", dunno); break;
		case 3: printf("Capture SIGQUIT signal, the signal number is %d\n", dunno); break;
	}
	return;
}

int main(void)
{
	printf("process ID is %d\n", getpid());
	if (signal(SIGHUP, sigroutine) == SIG_ERR)
	{
		printf("Couldn't register signal handler for SIGHUP!\n");
	}
	if (signal(SIGINT, sigroutine) == SIG_ERR)
	{
		printf("Couldn't register signal handler for SIGINT!\n");
	}
	if (signal(SIGQUIT, sigroutine) == SIG_ERR)
	{
		printf("Couldn't register signal handler for SIGQUIT!\n");
	}
	while (1)
		sleep(1);
	return 0;
}
        编译并运行:
$ ./signals
process ID is 3694
^CCapture SIGINT signal, the signal number is 2(键入“Ctrl+c”)
^\Capture SIGQUIT signal, the signal number is 3(键入“Ctrl+\”)
^Z(键入“Ctrl+z”,进程置于后台)
[1]+  已停止               ./signals
$ bg
[1]+ ./signals &
$ kill -HUP 3694(向进程发送SIGHUP信号)
Capture SIGHUP signal, the signal number is 1
$ kill -9 3694(向进程发送SIGKILL信号,终止进程)
        括号里面代表应该进行的操作,结果很明显,不再过多解释。
2.3.1.2 sigaction函数(一个健壮的信号接口)
        Linux还提供了另外一种功能更为强大的信号处理机制——sigaction系统调用。sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作,此函数可以完全替代signal函数,原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
        若成功返回0,若失败返回-1。如果给出的信号无效或者试图对一个不允许被捕捉或忽略的信号进行捕捉或忽略,错误变量errno将被设置为EINVAL。
        参数signum为需要捕捉的信号。参数act是一个结构体,里面包含信号处理函数的地址、处理方式等信息。参数oldact是一个传出参数,sigaction函数调用成功后,oldact里面包含以前对signum信号的处理方式的信息。粗略来讲,sigaction函数的过程就是:sigaction函数设置与信号signum关联的动作,如果oldact不是空指针,sigaction函数将把原先对该信号的动作写到它指向的位置,如果act为空指针,则sigaction函数就不需要再做其它设置了,否则将在该参数中设置对指定信号的动作。
        结构体struct sigaction的原型:
struct sigaction
{
	void (*sa_handler) (int);				/* 老类型的信号处理函数指针 */
	void (*sa_sigaction) (int, siginfo_t *, void *);	/* 新类型的信号处理函数指针 */
	sigset_t sa_mask;					/* 将要被阻塞的信号集合 */
	int sa_flags;						/* 信号处理方式掩码 */
	void (*sa_restorer) (void)				/* 保留,不使用 */
};
        struct sigaction结构体中:字段sa_handler是一个函数指针,用于指向原型为void handler (int)的信号处理函数地址,即老类型的信号处理函数,也可以将sa_handler字段设置为特殊值SIG_IGN和SIG_DFL。字段sa_sigaction也是一个函数指针,用于指向原型为:
void handler (int iSignNum, siginfo_t *pSignInfo, void *pReserved);
        的信号处理函数,即新类型的信号处理函数。该函数的三个参数含义为:
  • iSignNum:传入的信号。
  • pSignInfo:与该信号相关的一些信息,它是个结构体。
  • pReserved:保留,未使用。
        注意,字段sa_handler和sa_sigaction应该只有一个生效,如果想采用老的信号处理机制就应该让sa_handler指向正确的信号处理函数,否则应该让sa_sigaction指向正确的信号处理函数,并且让字段sa_flags包含SA_SIGINFO选项。
        字段sa_mask是一个包含信号集合的结构体,该结构体内的信号表示在进程信号处理时,将要被阻塞的信号。针对sigset_t结构体,有一组专门的函数对它进行处理,稍后介绍。
        字段sa_flags指示了信号处理函数的不同选项,具体参数见表2-3-1-2-1。在实际使用时,通常是通过或预算串接不同的参数而实现所需的选项设置,将其赋值为0则表示选用所有的默认选项。
表2-3-1-2-1 sa_flags的取值及含义
sa_flags取值含义
SA_NOCLDSTOP用于指定信号SIGCHLD,当子进程被中断时,不产生此信号,当且仅当子进程结束时产生该信号。
SA_NOCLDWAIT对信号SIGCHLD,当调用进程的子进程终止时,不创建僵尸进程。若调用进程在后面调用wait,则阻塞到它所有子进程都终止,此时返回-1,errno设置为ECHILD。
SA_NODEFER在处理信号时,如果又发生了其它的信号,即立即进入其它信号的处理,等其它信号处理完毕后,再继续处理当前的信号,即递归地处理。如果sa_flags包含了该选项,则结构体sigaction的sa_mask将无效。
SA_NOMASK同SA_NODEFER功能相似。
SA_RESETHAND处理完毕要捕捉的信号后,将自动撤销信号处理函数的注册,即必须再重新注册信号处理函数,才能继续处理接下来产生的信号。该选项不符合一般的信号处理流程,现已被废弃。
SA_ONESHOT同SA_RESETHAND功能相似。
SA_RESTART如果在发生信号时,程序正阻塞在某个系统调用,例如调用read函数,则在处理完毕信号后,接着从阻塞的系统返回。该选项符合普通的程序处理流程,一般应该设置该选项。
SA_SIGINFO指示结构体的信号处理函数指针是哪个有效,如果sa_flags包含该选项,则sa_sigaction指针有效,否则是sa_handler指针有效。
        当一个信号被捕获时,SA_RESETHAND标志可以用来自动清除它的信号处理函数。
        程序中使用的许多系统调用都是可中断的。也就是说,当接收到一个信号时,它们就返回一个错误并将errno设置为EINTR,表明函数是因为一个信号而返回的。使用了信号的应用程序需要特别注意这一行为。如果sigaction调用中的sa_flags字段设置了SA_RESTART标志,那么在信号处理函数执行完后,函数将被重启而不是被信号中断。
        一般的做法是,信号处理函数正在执行时,新接收到的信号将在该处理函数的执行期间被添加到进程的信号屏蔽字中。这防止了同一信号的不断出现引起信号处理函数的再次运行。如果信号处理函数是一个不可重入的函数,在它结束对第一个信号的处理之前又让另一个信号再次调用它就有可能引起问题。但如果设置了SA_NODEFER标志,当程序接收到这个信号时就不会改变信号屏蔽字。
        信号处理函数可以在其执行期间被中断并再次被调用。当返回到第一次调用时,它能否继续正确操作是很关键的。这不仅仅是递归(调用自身)的问题,而是可重入(可以安全地进入和再次执行)的问题。Linux内核中,在同一时间负责处理多个设备的中断服务例程就需要是可重入的,因为优先级更高的中断可能会在同一段代码的执行期间“插入”进来。

        接下来演示使用sigaction来截获SIGINT信号:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void ouch(int sig)
{
	printf("OUCH! - I got signal %d\n", sig);
}

int main(void)
{
	struct sigaction act;

	act.sa_handler = ouch;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;

	sigaction(SIGINT, &act, 0);

	while (1)
	{
		printf("Hello World!\n");
		sleep(1);
	}

	return 0;
}
        编译并运行:
$ ./sigaction_ctrl
Hello World!
Hello World!
^COUCH! - I got signal 2
Hello World!
^COUCH! - I got signal 2
Hello World!
Hello World!
^\退出 (核心已转储)
$
        从运行结果可以看出,我们每次按下“Ctrl+c”(打印成了^C)的时候都会显示捕获了信号2。程序中代替Signal来设置“Ctrl+c”(SIGINT信号)的信号处理函数为ouch,它首先必须设置一个sigaction结构,在该结构中包含信号处理函数、信号屏蔽字和标志。在本例中不需要设置任何表示,并通过调用新的函数sigemptyset来创建空的信号屏蔽字。
        接下来再看一个例子:当终端没有产生SIGINT(Ctrl+c)或SIGQUIT(Ctrl+\)信号时,程序能很好地执行read函数,即读入终端输入的字符串;当SIGINT或SIGQUIT信号产生时,进程被信号中断,read出错退出了:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

int g_iSeq = 0;

void SignHandlerNew(int sig, siginfo_t *pInfo, void *pReserved)
{
	int iSeq = g_iSeq++;
	printf("%d Enter SignHandlerNew, signo: %d\n", iSeq, sig);
	sleep(3);
	printf("%d Leave SignHandlerNew, signo: %d\n", iSeq, sig);
}

int main(void)
{
	char szBuf[20];
	int iRet;
	struct sigaction act;
	act.sa_sigaction = SignHandlerNew;
	act.sa_flags = SA_SIGINFO;
	sigemptyset(&act.sa_mask);

	sigaction(SIGINT, &act, NULL);
	sigaction(SIGQUIT, &act, NULL);
	
	do
	{
		iRet = read(STDIN_FILENO, szBuf, sizeof(szBuf) - 1);
		if (iRet < 0)
		{
			perror("read failed");
			break;
		}
		szBuf[iRet] = 0;
		printf("Get: %s", szBuf);
	} while (strcmp(szBuf, "quit\n") != 0);
	return 0;
}
        编译并运行:
$ ./sigaction_test
good good study(用户输入)
Get: good good study
day day up(用户输入)
Get: day day up
^C0 Enter SignHandlerNew, signo: 2(键入“Ctrl+c”)
^\1 Enter SignHandlerNew, signo: 3(键入“Ctrl+\”)
1 Leave SignHandlerNew, signo: 3
0 Leave SignHandlerNew, signo: 2
read failed: Interrupted system call
$
        通过运行结果可以看出,当终端还没有产生SIGINT或SIGQUIT信号时,能正确进行输入并打印输入的数据,当信号产生时进程就中断了。再次运行程序,这一次不使用SIGINT和SIGQUIT信号,而是程序中规定的退出字符“quit”:
$ ./sigaction_test
quit(用户输入)
Get: quit
$
        这里不难看出,程序是正常地退出。
2.3.1.3 信号集
        头文件siganl.h定义了类型sigset_t和用来处理信号集的函数。sigaction和其它函数将用这些信号集来修改进程在接收到信号时的行为。
#include <signal.h>
int sigaddset (sigset_t *set, int signo);
int sigemptyset (sigset_t *set);
int sigfillset (sigset_t *set);
int sigdelset (sigset_t *set, int signo);
        sigemptyset将信号集初始化为空,sigfillset将信号集初始化为包含所有已定义的信号,sigaddset和sigdelset从信号集中增加或者删除给定的信号(signo)。它们在成功时返回0,失败时返回-1并设置errno。只有一个错误代码被定义,即当给定信号无效时,errno将设置为EINVAL。
        函数sigismember判断一个给定的信号是否是一个信号集的成员。如果是就返回1,不是就返回0。如果给定的信号无效,就返回-1并设置errno为EINVAL。
#include <siganl.h>
int sigismember (sigset_t *set, int signo);

2.3.2 信号的发送

2.3.2.1 kill函数
        kill函数用于向某一给定进程或进程组发送信号,原型如下:
#include <sys/types.h>
#include <signal.h>
int kill (pid_t pid, int sig);
        若成功返回0,失败返回-1。参数pid表示kill函数发送信号对象的进程或进程组号,其取值有集中情况,见下表。参数sig代表发送的信号。
表2-3-2-1 pid取值及对应含义
pid取值含义
pid > 0将信号发送给进程号为pid的进程
pid = 0将信号发送给目前进程相同进程组的所有进程
pid < 0 && pid != -1向进程组ID为pid绝对值的进程组中的所有进程发送信号
pid = -1除发送进程自身外,向所有进程ID大于1的进程发送信号(POSIX.1 未定义此种情况)
        注:对于pid < 0时的情况,哪些进程将接受信号,各种版本说法不一,具体情况请参阅内核源码kernel/signal.c即可。
        sig是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。
        kill最常用语pid > 0时的信号发送,调用成功返回0,否则返回-1。
        下面演示使用kill函数产生SIGABRT信号:
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
	pid_t pid;
	int status;
	if (!(pid = fork()))
	{
		printf("Hi I am child process!\n");
		sleep(10);			/* 让子进程睡眠,看父进程的行为 */
		printf("Hi I am child process, again!\n");
		return 0;
	}
	else
	{
		printf("send signal to child process (%d) \n", pid);
		sleep(1);
		if (kill(pid, SIGABRT) == -1)
		{
			printf("kill failed!\n");
		}
		wait(&status);
		if (WIFSIGNALED(status))
		{
			printf("child process receive signal %d\n", WTERMSIG(status));
		}
	}
	return 0;
}
        编译并运行:
$ ./kill_test
send signal to child process (6311) 
Hi I am child process!
child process receive signal 6
$
        从打印结果可以看出,当父进程将信号SIGABRT发送给子进程(子进程ID为6311)之后,子进程非正常结束,因为printf("Hi I am child process, again!\n");语句并没有执行。
2.3.2.2 raise函数
        raise函数用于向进程本身发送信号,原型如下:
#include <sys/types.h>
#include <signal.h>
int raise (int sig);
        若成功返回0,失败则返回-1。参数sig为将要发送的信号值。
        接下来演示raise函数向自身的进程发送一个SIGABRT信号。
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	printf("Hello, I am Regan!\n");
	if (raise(SIGABRT) == -1)
	{
		printf("raise failed!\n");
		exit(EXIT_FAILURE);
	}
	printf("Nice to meet you!\n");
	exit(EXIT_SUCCESS);
}
        编译并运行:
$ ./raise_test
Hello, I am Regan!
退出 (核心已转储)
$
        可以明显看出,当raise向自身进程发送了SIGABRT信号后,程序非正常终止。
2.3.2.3 sigqueue函数
        sigqueue函数是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持先前介绍的所有信号),支持信号带有参数,通常与函数sigaction配合使用,原型如下:
#include <signal.h>
#include <unistd.h>
int sigqueue (pid_t pid, int sig, const union sigval val);
        若执行成功返回0,否则返回-1。
        sigqueue的第一个参数pid是指定接收信号的进程ID,第二个参数sig确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值,定义如下:
typedef union sigval
{
	int sival_int;
	void *sival_ptr;	/* 指向要传递的信号参数 */
} sigval_t;
        sigqueue比kill传递了更多的附加信息,但sigqueue只能向一个进程发送信号,而不能发送信号给一个进程组。如果sig = 0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性及当前进程是否有权限向目标进程发送信号。在调用sigqueue时,sigval_t指定的信息会拷贝到3参数信号处理函数(3参数信号处理函数值得是信号处理函数由sigaction安装,并设定了sa_sigaction指针)的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数的信号,所以比kill系统调用的功能要灵活和强大许多。
        注:sigqueue发送非实时信号时,第三个参数包含的信息仍然能够传递信号处理函数;sigqueue发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的所有相同信号都被合并为一个信号。
        接下来演示进程给自己发送信号SIGUSR1,并且附加信息。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void sighandler(int sig, siginfo_t *info, void *context)
{
	char *msg = (char *)info->si_value.sival_ptr;
	printf("Receive signal number:%d\n", sig);
	printf("Recevei message: %s\n", msg);
}

int main(void)
{
	struct sigaction act;
	act.sa_flags = SA_SIGINFO;
	act.sa_sigaction = sighandler;
	if (sigaction(SIGUSR1, &act, NULL) == -1)
	{
		printf("sigaction failed!\n");
		exit(EXIT_FAILURE);
	}
	sigval_t val;
	char msg[] = "I am Regan";
	val.sival_ptr = msg;
	if (sigqueue(getpid(), SIGUSR1, val) == -1)
	{
		printf("sigqueue failed!\n");
		exit(EXIT_FAILURE);
	}
	sleep(3);
	exit(EXIT_SUCCESS);
}
        编译并运行:
$ ./sigqueue_test
Receive signal number:10
Recevei message: I am Regan
$
        由打印结果可以看出,进程成功接收自身发生的信号10(SIGUSR1)和信号的附加参数——字符串数据“I am Regan”。
2.3.2.4 alarm函数
        alram函数专门为SIGALRM信号而设,使系统在一定时间之后发送信号。原型如下:
#include <unistd.h>
unsigned int alarm (unsigned int seconds);
        如果调用alarm之前进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
        参数seconds指定了下一次发送信号的时间,即在当前时间的seconds秒后,向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,任何以前的alarm调用都将无效。如果参数seconds为0,那么进程内将不再包含任何闹钟时间。
        接下来演示alarm函数的用法,将alarm的时间参数设为5秒钟,5秒钟之后将调用信号的处理函数。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler()
{
        printf("Hello, I am Regan!\n");
}

int main(void)
{
        int i;
        signal(SIGALRM, handler);
        alarm(5);
        for (i = 1; i < 7; i++)
        {
                printf("sleep %d ...\n", i);
                sleep(1);
        }
        return 0;
}
        编译并运行:
$ ./alarm
sleep 1 ...
sleep 2 ...
sleep 3 ...
sleep 4 ...
sleep 5 ...
Hello, I am Regan!
sleep 6 ...
$
        由打印结果可以看出,在for循环执行了五次(大约5秒)以后,产生了SIGALRM信号,此时由signal注册新号的处理函数handler输出字符串。信号处理完毕后又返回先前程序的中断点继续执行for循环。
        接下来演示alarm函数模拟一个闹钟。
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static int alarm_fired = 0;

void ding(int sig)
{
        alarm_fired = 1;
}

int main(void)
{
        pid_t pid;

        printf("alarm application starting\n");

        switch (pid = fork())
        {
                case -1:
                        perror("fork failed");
                        exit(1);
                case 0:
                        sleep(5);
                        kill(getppid(), SIGALRM);
                        exit(0);
        }

        printf("waiting for alarm to go off\n");
        signal(SIGALRM, ding);

        pause();
        if (alarm_fired)
                printf("Ding!\n");

        printf("Done!\n");
        exit(0);
}
        编译并运行:
$ ./alarmclock
alarm application starting
waiting for alarm to go off
Ding!
Done!
$
        这个程序用到了一个新的原函数pause,它的作用很简单,就是把程序的执行挂起直到有一个信号出现位置。当程序接收到一个信号时,预设好的信号处理函数将开始运行,程序也将恢复正常的执行。pause函数的定义如下:
#include <unistd.h>
int pause (void);
        当它被一个信号中断时,将返回-1(如果下一个接收到的信号没有导致程序终止的话)并把errno设置为EINTR。当需要等待信号时,一个更常见的方法是后面介绍的sigsuspend函数。
        使用信号并挂起程序的执行是Linux程序设计中的一个重要部分,这意味着程序不需要总是在执行着。程序不必在一个循环中无休止地检查某个事件是否已经发生,相反,它可以等待事件的发生。这在只有一个CPU的多用户环境中尤其重要,进程共享着一个处理器,繁忙的等待将会对系统的性能造成极大的影响。程序中信号的使用将带来一个特殊的问题:“如果信号出现在系统调用的执行过程中会发生什么情况?”答案是相当让人不满意的“视情况而定”。一般来说,你只需要考虑慢系统调用,例如从终端读数据,如果在这个系统调用等待数据时出现一个信号,它就会返回一个错误。如果你开始在自己的程序中使用信号,就需要注意一些系统调用会因为接收到一个信号而失败,而这种错误情况可能是你在添加信号处理函数之前没有考虑到的。
        在编写程序中处理信号部分的代码时必须非常小心,因为在使用信号的程序中会出现各种各样的“竞态条件”。例如,如果想调用pause等待一个信号,可信号却出现在调用pause之前,就会使程序无限期地等待一个不会发生的事件。这些竞态条件都是一些对时间要求很苛刻的问题,所以在检查和信号相关的代码时总是要非常小心。
2.3.2.5 setitimer函数
        setitimer函数同alarm函数一样,也可以用于使系统在某一时刻发出信号,但它可以更加精确地控制程序。原型如下:
#include <sys/time.h>
int setitimer (int which, const struct itimerval *value, struct itimerval *oldvalue);
        若成功则返回0,否则返回-1。
        参数which指定定时器类型,setitimer比alarm功能强大,支持3种类型的定时器,见表2-3-2-5-1。参数value和oldvalue为指向时间参数的结构体指针,itimerval结构原型如下:
struct itimerval
{
	struct timeval it_interval;		/* 计时器重启动的间歇值 */
	struct timeval it_value;		/* 计时器安装后首先启动的初始值 */
};
        成员it_interval和it_value又是timeval类型的结构体:
struct timeval
{
	long tv_sec;			/* 时间的秒数部分 */
	long tv_usec;			/* 时间的微妙(1/1000000)部分 */
};
        setitimer将value指向的结构体设为计时器的当前值,如果oldvalue不是NULL,将返回计时器原有值。
表2-3-2-5-1 which取值及对应定时器类型
which取值定时器类型发生信号
ITIMER_REAL(真实计时器)设定绝对时间,即根据系统的时间SIGALRM
ITIMER_VIRTUAL(虚拟计时器)设定程序执行时间,只有在用户模式下才可跟踪时间SIGVTALRM
ITIMER_PROF(实用计时器)从用户进程开始后开始计时SIGPROF
        真实计时器计算的是程序运行的实际时间。虚拟计时器计算的是程序运行在用户态时所消耗 的时间(可认为是实际时间剪掉系统调用和程序睡眠所消耗的时间)。实用计时器计算的是程序处于用户态和处于内核态所消耗的时间之和。
        接下来演示setitimer函数产生SIGALRM信号:
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

static void ElsfTimer(int sig)
{
        struct timeval tp;
        struct tm *tm;
        gettimeofday(&tp, NULL);
        tm = localtime(&tp.tv_sec);
        printf(" sec=%ld \t", tp.tv_sec);
        printf(" usec=%ld \n", tp.tv_usec);
        printf("%d-%d-%d%d:%d:%d\n", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
}

static void InitTime(int tv_sec, int tv_usec)
{
        struct itimerval value;
        signal(SIGALRM, ElsfTimer);
        value.it_value.tv_sec = tv_sec;
        value.it_value.tv_usec = tv_usec;
        value.it_interval.tv_sec = tv_sec;
        value.it_interval.tv_usec = tv_usec;
        setitimer(ITIMER_REAL, &value, NULL);
}

int main(void)
{
        InitTime(5, 0);
        while (1){
        }
        exit(0);
}
        编译并运行:
$ ./setitimer_test
 sec=1445584047          usec=584597
2015-10-2315:7:27
 sec=1445584052          usec=588180
2015-10-2315:7:32
 sec=1445584057          usec=629235
2015-10-2315:7:37
 sec=1445584062          usec=591822
2015-10-2315:7:42
 sec=1445584067          usec=590302
2015-10-2315:7:47
 sec=1445584072          usec=584884
2015-10-2315:7:52
 sec=1445584077          usec=592305
2015-10-2315:7:57
^C
$
        从打印结果可以看出,程序每隔5秒便会调用信号处理函数ElsfTimer打印出当前系统的时间和日期。在ElsfTimer函数中使用了另外两个系统调用gettimeofday和localtime。gettimeofday的作用是获得以秒和微妙计时的系统的当前时间,这个时间是以UNIX操作系统的诞生之日开始计时的。localtime的作用是输出当地目前的时间和日期。
        setitimer还有一个成对的函数getitimer函数,getitimer用于获取计时器的设置,原型如下:
#include <sys/time.h>
int getitmer (int which, struct itimerval *value);
        参数which用于指定计时器的类型,见表2-3-2-5-1。value为以结构体的传出参数,用于传出该计时器的处室间隔时间和重复间隔时间。
2.3.2.6 abort函数
#include <stdlib.h>
void abort (void);
        向进程发送SIGABRT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABRT被进程设置为阻塞信号,调用abort后,SIGABRT仍然能被进程接收。该函数无返回值。

2.3.3 信号的阻塞

        在Linux的信号控制中,有时候既不希望进程在接收到信号时立刻中断进程的执行,也不希望该信号完全被忽略,而是延迟一段时间再去调用相关的信号处理函数。这种操作就是通过阻塞信号的方法来实现的。
2.3.3.1 sigprocmask函数
        进程的信号屏蔽字的设置或检查工作由函数sigprocmask来完成,信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前进程接收到。
#include <signal.h>
int sigprocmask (int how, const sigset_t *set, sigset_t *oldset);
        sigprocmask函数可以根据参数how指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set(如果它不为空)指定,而原先的信号屏蔽字将保存到信号集oldset中。参数how的取值可以是表2-3-1-3-1中的一个。
表2-3-1-3-1 how的取值
取值含义
SIG_BLOCK把参数set中的信号添加到信号屏蔽字中
SIG_SETMASK把信号屏蔽字设置为参数set中的信号
SIG_UNBLOCK从信号屏蔽字中删除参数set中的信号
        如果参数set是空指针,how的值就没有意义了,此时这个调用的唯一目的就是把当前信号屏蔽字的值保存到oldset中。如果sigprocmask成功完成将返回0,如果参数how取值无效则返回-1并设置errno为EINVAL。
        接下来演示sigprocmask函数屏蔽掉SIGINT信号。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

int g_iSeq = 0;

void SignHandlerNew(int iSignNo, siginfo_t *pInfo, void *pReserved)
{
        int iSeq = g_iSeq++;
        printf("%d Enter SignHanlderNew, signo: %d\n", iSeq, iSignNo);
        sleep(3);
        printf("%d Leave SignHandlerNew, signo: %d\n", iSeq, iSignNo);
}

int main(void)
{
        char szBuf[20];
        int iRet;
        struct sigaction act;
        act.sa_sigaction = SignHandlerNew;
        act.sa_flags = SA_SIGINFO;

        sigset_t sigSet;
        sigemptyset(&sigSet);
        sigaddset(&sigSet, SIGINT);
        sigprocmask(SIG_BLOCK, &sigSet, NULL);
        sigemptyset(&act.sa_mask);
        sigaction(SIGINT, &act, NULL);
        sigaction(SIGQUIT, &act, NULL);

        do
        {
                iRet = read(STDIN_FILENO, szBuf, sizeof(szBuf) - 1);
                if (iRet < 0)
                {
                        perror("read failed");
                        break;
                }
                szBuf[iRet] = 0;
                printf("Get: %s", szBuf);
        } while (strcmp(szBuf, "quit\n") != 0);
        exit (0);
}
        编译并运行:
$ ./block_sigint
hello, world
Get: hello, world
^C^C^C(连续键入“Ctrl+c”)
Get:
^\0 Enter SignHanlderNew, signo: 3
0 Leave SignHandlerNew, signo: 3
read failed: Interrupted system call
$
        由打印结果可以看出,当我们键入“Ctrl+c”的时候程序没有任何反应,只有键入“Ctrl+\”的时候,程序依然能够中断退出。
2.3.3.2 sigpending函数
        如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态。程序可以通过调用函数sigpending来查看它阻塞的信号中有哪些正停留在待处理状态。
#include <signal.h>
int sigpending (sigset_t *set);
        这个函数的作用是,将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中。成功时返回0,否则返回-1并设置errno以表明错误原因。如果程序需要处理信号,同时又需要控制信号处理函数的调用时间,这个函数就很有用。
2.3.3.3 sigsuspend函数
        进程可以通过调用sigsuspend函数挂起自己的执行,直到信号集中的一个信号到达为止。这时pause函数更通用的一种表现形式。
#include <signal.h>
int sigsuspend (const sigset_t *sigmask);
        sigsuspend函数将进程的屏蔽字替换为由参数sigmask给出的信号集,然后挂起程序的执行。程序将在信号处理函数执行完毕后继续执行。如果接收到的信号终止了程序,sigsuspend就不会返回,如果接收到的信号没有终止程序,sigsuspend就返回-1并将errno置为EINTR。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig)
{
	printf("go on!!\n");
}

int main()
{
	pid_t pid;
	sigset_t set, newset, oldset;
	sigemptyset(&set);
	sigemptyset(&newset);
	sigemptyset(&oldset);
	sigaddset(&newset, SIGALRM);

	switch (pid = fork())
	{
		case -1:
			perror("fork failed");
			return 1;
		case 0:
			sleep(3);
			kill(getppid(), SIGALRM);
			printf("child process free.\n");
			return 0;
	}
	signal(SIGALRM, handler);
	
	sigsuspend(&set);
	printf("OK\n");
	return 0;
}
        编译并运行:
$ ./sigsuspend_test
child process free.
go on!!
OK
$
        距离打印的第一条数据等待了三秒(子进程中的sleep作祟)。这里之所以sigsuspend和sigprocmask配合使用,是为了原子操作。如果之前没有调用sigprocmask()屏蔽SIGALRM信号,那么SIGALRM信号随时都能发生。而调用了sigprocmask()屏蔽SIGALRM信http://write.blog.csdn.net/postedit/49306281号之后,即使信号发生,也将延迟递交。直到sigsuspend()解除信号屏蔽。

        sigsuspend函数接受一个信号集指针,将信号屏蔽字设置为信号集中的值,在进程接受到一个信号之前,进程会挂起,当捕捉一个信号,首先执行信号处理程序,然后从sigsuspend返回,最后将信号屏蔽字恢复为调用sigsuspend之前的值。pause函数使调用进程挂起直到捕捉到一个信号。只有执行了一个信号处理程序并从其返回时,pause才返回。


整理自 《Linux程序设计第4版》、《Linux C编程从初学到精通》。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值