1 进程间通信简介
进程间通信(InterProcess Communication, IPC)就是在不同进程之间传播或交换信息。交换信息的方式简单的有很多,比如说通过全局变量或函数调用,又或者是两个进程通过磁盘上的普通文件交换信息,亦或是通过“注册表”或其它数据库中的某些表项和记录交换信息。广义上来说,这些都是进程间通信的手段,但是一般都不把它们算作“进程间通信”。因为这样的通信手段效率太低,实时性不高。尤其当网络上需要多个进程相互对话时,这些方案几乎成为累赘。再加上网络设计的一个重要目标是保证进程间不互相干涉,否则系统可能被挂起或自锁,因此进程间必须使用简介有效的方法进行通信。这方面,Linux具有非常显著的解决方案。
1.1 进程间通信的一些基本概念
- 进程阻塞:当一个进程在执行某些操作的条件得不到满足时,就自动放弃CPU资源而进入休眠状态,以等待条件的满足。当操作条件满足时,系统就将控制权返还给该进程继续进行未完的操作。
- 共享资源:因为计算机的内存、存储器等资源是有限的,无法为每一个进程都分配一份单独的资源,所以系统将这些资源在各个进程间协调使用,称为共享资源。
- 锁定:当某个进程在使用共享资源时,可能需要防止别的进程对该资源的使用,比如,一个进程在对某个文件进行读操作时,如果别的进程也在此时向文件中写入内容,就可能导致进程读入错误的数据。为此,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 信号列表
信号 | 含义 |
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
2.2 信号处理机制
2.2.1 内核对信号的基本处理方法
- 在一些系统中,当一个进程处理完终端信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次使用signal()系统调用。这可能会使得进程在调用signal()之前又得到该信号而导致退出。在BSD系统中,内核不再清除该地址。但不清除该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢出。为了避免出现上述情况,在BSD中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。
- 如果要捕捉的信号发生于进程正在一个系统调用中时,并且该进程睡眠在可中断的优先级上,这时该信号引起进程做一次longjmp()调用,跳出睡眠状态,返回用户态并执行信号处理例程。当从信号处理例程返回时,进程就像从系统调用返回一样,但返回了一个错误代码,指出该次系统调用曾经被中断。这里需注意的是,BSD系统中内核可以自动地重新开始系统调用。
- 若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不做longjmp()调用,一般是继续睡眠。但用户感觉不到进程曾经被唤醒,而是像没有产生过信号一样。
- 内核对子进程终止(SIGCLD)信号的处理方法与其它信号有所区别。当进程检查出收到了一个子进程终止的信号时,默认情况下,该进程就像没有收到该信号似的,如果父进程执行了系统调用wait,进程将从系统调用wait中醒来并返回wait调用,执行一系列wait调用的后续操作(找出僵尸子进程,释放子进程的进程表项),然后从wait中返回。SIGCLD信号的作用是幻想一个睡眠在可被中断优先级上的进程。如果该进程捕捉了这个信号,就像普通信号处理一样转到处理例程。如果进程忽略该信号,那么系统调用wait的动作就有所不同,因为SIGCLD的作用仅仅是唤醒一个睡眠在可被中断优先级上的进程,那么执行wait调用的父进程被唤醒继续执行wait调用的后续操作,然后等待其它的子进程。
2.2.2 setjmp和longjmp的作用
#include <setjmp.h>int setjmp(jmp_buf envbuf);void longjmp(jmp_buf envbuf, int val);
#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函数
通过系统调用signal()用于接收一个指定类型的信号,并可以指定相应的方法,原型如下:
#include <signal.h>或者POSIX定义,具体参考联机手册。
void (*signal (int signum, void (*handler) (int)))(int)
#include <signal.h>若成功返回以前的信号处理配置,若失败返回SIG_ERR。
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。参数handler描述了与信号关联的动作,它可以取以下三种值:
- 一个返回值为整数的函数地址。
int func(int sig);sig是传递给它的唯一参数。执行了signal()调用后,进程只要接收到类型为sig的信号,不管其正在执行程序的哪一部分,就立即执行func()函数。当func()函数执行结束后,控制权返回进程被中断的那一点继续执行。当指定函数地址时,我们称此为捕捉此信号,并称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)。
- SIG_IGN
- SIG_DFL
#include <signal.h>这些常数可用于表示指向函数的指针,该函数需要一个整型参数,而且无返回值。signal的第二个参数及其返回值就可用它们表示。这些常数所使用的三个值不一定要是-1、0和1,但它们必须是三个值而绝不能是任一可说明函数的地址。
#define SIG_ERR ( void ( * ) ( ) ) -1
#define SIG_DFL ( void ( * ) ( ) ) 0
#define SIG_IGN ( void ( * ) ( ) ) 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函数(一个健壮的信号接口)
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也是一个函数指针,用于指向原型为:
- iSignNum:传入的信号。
- pSignInfo:与该信号相关的一些信息,它是个结构体。
- pReserved:保留,未使用。
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指针有效。 |
#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;
}
编译并运行:
Hello World!
^COUCH! - I got signal 2
Hello World!
^COUCH! - I got signal 2
Hello World!
Hello World!
^\退出 (核心已转储)
$
#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;
}
编译并运行:
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”:
quit(用户输入)
Get: quit
$
2.3.1.3 信号集
#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);
#include <siganl.h>int sigismember (sigset_t *set, int signo);
2.3.2 信号的发送
2.3.2.1 kill函数
#include <sys/types.h>#include <signal.h>int kill (pid_t pid, int sig);
pid取值 | 含义 |
pid > 0 | 将信号发送给进程号为pid的进程 |
pid = 0 | 将信号发送给目前进程相同进程组的所有进程 |
pid < 0 && pid != -1 | 向进程组ID为pid绝对值的进程组中的所有进程发送信号 |
pid = -1 | 除发送进程自身外,向所有进程ID大于1的进程发送信号(POSIX.1 未定义此种情况) |
#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;
}
编译并运行:
send signal to child process (6311)
Hi I am child process!
child process receive signal 6
$
2.3.2.2 raise函数
#include <sys/types.h>#include <signal.h>int raise (int sig);
#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);
}
编译并运行:
Hello, I am Regan!
退出 (核心已转储)
$
2.3.2.3 sigqueue函数
#include <signal.h>#include <unistd.h>int sigqueue (pid_t pid, int sig, const union sigval val);
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系统调用的功能要灵活和强大许多。
#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);
}
编译并运行:
Receive signal number:10
Recevei message: I am Regan
$
由打印结果可以看出,进程成功接收自身发生的信号10(SIGUSR1)和信号的附加参数——字符串数据“I am Regan”。
2.3.2.4 alarm函数
#include <unistd.h>unsigned int alarm (unsigned int seconds);
#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;
}
编译并运行:
sleep 1 ...
sleep 2 ...
sleep 3 ...
sleep 4 ...
sleep 5 ...
Hello, I am Regan!
sleep 6 ...
$
#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);
}
alarm application starting
waiting for alarm to go off
Ding!
Done!
$
#include <unistd.h>int pause (void);
2.3.2.5 setitimer函数
#include <sys/time.h>int setitimer (int which, const struct itimerval *value, struct itimerval *oldvalue);
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,将返回计时器原有值。
which取值 | 定时器类型 | 发生信号 |
ITIMER_REAL(真实计时器) | 设定绝对时间,即根据系统的时间 | SIGALRM |
ITIMER_VIRTUAL(虚拟计时器) | 设定程序执行时间,只有在用户模式下才可跟踪时间 | SIGVTALRM |
ITIMER_PROF(实用计时器) | 从用户进程开始后开始计时 | SIGPROF |
#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);
}
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
$
#include <sys/time.h>int getitmer (int which, struct itimerval *value);
2.3.2.6 abort函数
#include <stdlib.h>void abort (void);
2.3.3 信号的阻塞
2.3.3.1 sigprocmask函数
#include <signal.h>int sigprocmask (int how, const sigset_t *set, sigset_t *oldset);
取值 | 含义 |
SIG_BLOCK | 把参数set中的信号添加到信号屏蔽字中 |
SIG_SETMASK | 把信号屏蔽字设置为参数set中的信号 |
SIG_UNBLOCK | 从信号屏蔽字中删除参数set中的信号 |
#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);
}
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
$
2.3.3.2 sigpending函数
#include <signal.h>int sigpending (sigset_t *set);
2.3.3.3 sigsuspend函数
#include <signal.h>int sigsuspend (const sigset_t *sigmask);
#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编程从初学到精通》。