1 Linux进程间通信
linux下的IPC基本上是从Unix上的IPC继承而来的。而Unix的两大主力AT&T的Bell Lab及BSD在IPC方面的侧重点有所不同。AT&T对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;BSD则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图示:
最初Unix IPC包括 :信号、管道
System V IPC包括 :System V消息队列、System V信号灯、SystemV共享内存区
Posix IPC包括 :Posix消息队列、Posix信号灯、Posix共享内存区
由于Unix版本的多样性,IEEE开发了一个独立的Unix标准,称为计算机环境的可移植性操作系统界面(PSOIX)。现有大部分Unix版本都遵循POSIX标准,而Linux从一开始就遵循POSIX标准。linux下进程间通信的几种主要手段:信号、信号灯、管道、消息队列、共享内存、套接口。
1.1 信号-Signal
信号是在软件层次上对中断机制的一种模拟,进程收到一个信号等同于处理器收到一个中断。信号是异步的,一个进程不必等待信号的到达,事实上,进程也不知道信号什么时候到达。
下面的情况可以产生signal:
1.硬件中断,如除0(SIGFPE),非法内存访问(SIGSEV);
2.软件中断,如alarm超时(SIGALRM),读进程终止之后又向管道写数据(SIGPIPE);
1.按下特殊的组合键,例如CTRL+C(SIGINT),CTRL+Z(SIGTSTP);
3.kill函数可以对进程发送signal;
4.kill命令,实际上是对kill函数的一个封装实现;
进程可以通过三种方式来响应一个信号:
1.忽略signal。大部分signal都可以被ignore,除了SIGKILL和SIGSTOP。
2.捕捉signal。内核调用我们定义的信号处理函数,SIGKILL和SIGSTOP无法被捕捉。
3.执行缺省行为。
部分signal的缺省行为不仅终止进程,同时还会产生core dump,也就是生成一个名为core的文件,其中保存了退出时进程内存的镜像,可以用来调试。但是在下面情况,不会生成core文件:
1.当前进程不属于当前user;
2.当前进程不属于当前group;
3.用户在当前目录下无写权限;
4.core文件已存在,用户无写权限;
5.文件大小超出限制。
信号的种类:
非实时(不可靠)信号:可能丢失,不支持排队(pending);小于SIGRTMIN的信号。
实时(可靠)信号:支持排队(pending),不会丢失;SIGRTMIN和SIGRTMAX之间的信号。
信号名 |
值 |
|
信号含义 |
SIGHUP |
1 |
T |
1、终端关闭时,SIGHUP信号会被发送到session leader以及作为job提交的进程(即用&提交的进程); 2、session leader退出时,SIGHUP信号会被发送到session中前台进程组中的每一个进程; 3、若进程退出导致进程组成为孤儿进程组,且进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),SIGHUP信号和SIGCONT信号会被发送到该进程组中的每一个进程。 孤儿进程组定义为:该进程组中每个成员的父进程或者是本组的一个成员,或者是属于其它session的成员。 |
SIGINT |
2 |
T |
程序终止信号,在用户键入INTR字符(Ctrl-C)时发出,用于通知前台进程组中的所有进程,终止进程。 |
SIGQUIT |
3 |
D |
程序终止信号,在用户键入QUIT字符(Ctrl-\)时发出,用于通知前台进程组中的所有进程,终止进程。与SIGINT不同的时,进程收到SIGQUIT终止时会产生core文件。 |
SIGILL |
4 |
D |
执行了非法指令,通常是因为可执行文件本身出现错误,或者试图执行数据段,堆栈溢出时也有可能产生这个信号。 |
SIGTRAP |
5 |
D |
SIGTRAP is the signal thrown by process when a condition arises that a debugger has requested to be informed of. |
SIGABRT |
6 |
D |
调用abort( )函数产生的信号。 |
SIGIOT |
6 |
D |
与SIGABRT同义 |
SIGBUS |
7,10,10 |
D |
非法地址,包括内存地址对齐(alignment)出错。 |
SIGFPE |
8 |
D |
发生致命算术运算错误时发出,包括浮点运算错误,溢出及除数为0等所有的算术运算错误。 |
SIGKILL |
9 |
TEF |
立即结束进程。 |
SIGUSR1 |
10,30,16 |
T |
留给用户使用。 |
SIGSEGV |
11 |
D |
试图访问未分配给自己的内存,或者试图往没有写权限的内存地址写数据。 |
SIGUSR2 |
12,31,17 |
T |
留给用户使用。 |
SIGPIPE |
13 |
T |
管道破裂,通常在进程间通信时产生。比如采用FIFO(管道)通信的两个进程,读管道没打开或者已经终止就往管道写,写进程就会收到SIGPIPE信号;此外用socket通信SOCT_STREAM的两个进程,如果读进程已经终止,写进程在写socket的时候就会收到SIGPIPE信号。 |
SIGALRM |
14 |
T |
当alarm( )函数设置间隔时间超时或setitimer( )函数设置的interval timer超时,产生此信号。 |
SIGTERM |
15 |
T |
进程结束信号,与SIGKILL不同的是该信号可以被阻塞和处理。 |
SIGSTKFLT |
16,-,- |
T |
SIGSTKFLT is the signal sent to process when the coprocessor experiences a stack fault. |
SIGCHLD |
17,20,18 |
I |
子进程结束时, 父进程会收到这个信号。 |
SIGCONT |
18,19,25 |
C |
让一个停止(stopped)的进程继续执行。 |
SIGSTOP |
19,17,23 |
SEF |
停止(stopped)进程的执行。 |
SIGTSTP |
20,18,24 |
S |
停止(stopped)进程的执行,键入SUSP字符(Ctrl-Z)时发出这个信号,用于通知前台进程组中的所有进程。 |
SIGTTIN |
21,21,26 |
S |
当一个后台进程组中的进程试图读其控制终端时,终端驱动程序产生此信号。在下列例外情形下,不产生此信号,此时读操作出错返回,errno设置为EIO:(a)读进程忽略或阻塞此信号,(b)读进程所属的进程组是孤儿进程组。 |
SIGTTOU |
22,22,27 |
S |
当一个后台进程组中的进程试图写其控制终端时产生此信号。与SIGTTIN信号不同,一个进程可以选择为允许后台进程写控制终端。如果不允许后台进程写,在下列例外情形下,不产生此信号,写操作出错返回,errno设置为EIO:(a)写进程忽略或阻塞此信号,(b)写进程所属的进程组是孤儿进程组。 |
SIGURG |
23,16,21 |
I |
有紧急数据或out-of-band数据到达socket时产生SIGURG信号。 |
SIGXCPU |
24,24,30 |
D |
超过CPU时间资源限制,可由getrlimit/setrlimit来读取/改变。 |
SIGXFSZ |
25,25,31 |
D |
进程企图扩大文件,以至于超过文件大小资源限制。 |
SIGVTALRM |
26,26,28 |
T |
当setitimer( )函数设置的虚拟间隔时间(virtual interval timer)超时,产生此信号。 |
SIGPROF |
27,27,29 |
T |
当setitimer( )函数设置的梗概统计间隔时间(profiling interval timer)超过时,产生此信号。 |
SIGWINCH |
28,28,20 |
I |
窗口大小改变时发出。窗口的大小可以用ioctl( )函数得到或设置窗口的大小。如果一个进程用ioctl( )更改了窗口大小,则系统核将SIGWINCH信号送至前台进程组。 |
SIGIO |
29,23,22 |
I/T |
文件描述符准备就绪,可以开始进行输入/输出操作。对SIGIO的系统默认动作是终止或忽略,这依赖于不同版本的系统。 |
SIGPOLL |
29,-,- |
I/T |
与SIGIO同义。 |
SIGPWR |
30,29,19 |
T |
电源失效/重启动。 |
SIGSYS |
31,12,12 |
D |
非法的系统调用。由于某种未知原因,进程执行了一条系统调用指令,但其参数却是无效的。 |
SIGINFO |
-,29,- |
T |
键入STATUS字符(CTRL+T)时发出这个信号,发送给前台进程组中的所有进程。 |
-表示信号没有实现,第一个值对应i386,中间的值对应Alpha和Sparc,最后一个值对应MIPS。
T 终止进程
I 忽略信号
D 终止进程并进行内核映像转储(dump core)
S 停止进程
C 继续进程
E 信号不能被捕获
F 信号不能被忽略
内核映像转储是指将进程数据在内存的映像和进程在内核结构中存储的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。
1.1.1 可被信号中断的系统调用
进程在系统调用阻赛的时候,可能会被一个signal中断,此时系统调用将会返回错误并且errno被设置为EINTR。为了处理这个问题,对于这些系统调用需要作额外的检查和判断,如果错误返回并且errno=EINTR,需要重新调用。
Free BSD 4.2对于部分系统调用ioctl(),read( ),readv( ),write( ),writev( ),wait( )实现了自动恢复功能,也就是说当系统调用被中断的时候会自动恢复到原来的执行情况,Linux 2.4.22支持此功能,但POSIX.1不作强制要求。
1.1.2 信号的生命周期
对于一个完整的信号生命周期(从信号发送到相应的信号处理函数执行完毕)来说,可以分为三个阶段,这三个阶段由四个重要事件来刻画:信号诞生;信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。相邻两个事件的时间间隔构成信号生命周期的一个阶段。
1.信号诞生。信号诞生指的是触发信号的事件发生,如检测到硬件异常、定时器超时以及调用信号发送函数kill( )或sigqueue( )等。
2.信号在目标进程中注册。信号在进程中注册指的就是信号值加入到进程的pending信号集中。只要信号在进程的pending信号集中,表明进程已经知道这些信号的存在,只是还没来得及处理,或者该信号被进程阻塞。
注意:当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,实时信号不会丢失,又叫做可靠信号;当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做不可靠信号。
3.信号在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理。如果存在pending信号并且该信号没有被进程阻塞,则在运行相应的信号处理函数前,首先要把信号在进程中注销。注销的意思就是把信号从pending信号集中删除。
注意:在信号注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到相同的信号,同样会在进程中注册。
4.信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。
1.1.3 信号的安装
linux主要有两个函数实现信号的安装:signal( )、sigaction( )。
signal( )不支持信号传递信息,主要用来安装不可靠信号;
sigaction( )支持信号传递信息,主要用来安装可靠信号,并与sigqueue( )配合使用;
sigaction( )同样可以用来安装不可靠信号的。
1.1.3.1 signal( )
#include<signal.h>
void ( *signal( int signum, void (*handler)(int) ) )(int);
typedef void (*SIGHandler)(int)
SIGHandler signal(int signum, SIGHandler handler )
参数signum:信号值。
参数handler:信号处理函数,也可以忽略信号(SIG_IGN)或执行缺省操作(SIG_DFL)。
安装成功:返回上次调用signal( )安装信号时的handler。
安装失败:返回SIG_ERR。
如果用exec( )创建子进程,那么子进程的所有signal状态是缺省或者忽略:
1.如果父进程忽略了某个signal,那么父进程用exec创建子进程时子进程的这个signal的状态也是忽略;
2.如果父进程为某个signal注册了处理函数,那么子进程中该signal的状态会恢复成缺省状态,这是因为注册的函数地址在另外一个进程中无法被调用。
如果用fork( )创建子进程,那么子进程会继承所有父进程的signal状态。
1.1.3.2 sigaction( )
#include<signal.h>
intsigaction( int signum, const struct sigaction *newAct, struct sigaction *oldAct)
typedef void (*SIGHandler)(int)
typedef void (*SIGAction)(int, struct siginfo_t *,void * )
成功返回0,失败返回-1。
struct sigaction
{
sigset_t sa_mask;
ulong sa_flags;
// sa_handler或sa_sigaction指定信号处理函数,二者使用一个即可
SIGHandler sa_handler;
SIGAction sa_sigaction;
}
1.sa_mask:当信号处理函数被调用的时候,自动block这些signal。当信号处理函数返回时,mask会恢复到原来的值。同时,内核会自动把该signal加到mask中,防止重入。
2.sa_flags用来改变信号处理的方式:
SA_SIGINFO :打开随信号一起传递来的信息
SA_NODEFER :在信号处理函数的执行过程中,不会自动阻塞当前信号。
SA_INTERRUPT :被信号中断的系统调用不会自动重启。
SA_RESTART :默认情况下,可中断的系统调用在执行过程中收到信号,就会失败退出,并将errno设为EINTR;可以使被信号中断的系统调用在信号处理函数执行完成之后重新启动执行。
SA_RESETHAND :完成信号处理函数之后,恢复默认的信号处理SIG_DFL,并会清除SA_SIGINFO,而且在信号处理函数的执行过程中,不会阻塞当前信号,就好像同时设置了SA_NODEFER。
SA_NOCLDWAIT :在目标信号是SIGCHLD时使用,声明父进程对子进程的结束不感兴趣,可以阻止子进程成为僵尸进程。如果未设置该标志,而且也没有显式地设置SIGCHLD的处理方式为SIG_IGN,则在父进程调用wait( )之前结束的子进程都会成为僵尸进程;如果设置了该标志,不管有没有显式地设置SIGCHLD的处理方式为SIG_IGN,子进程结束之后都不会成为僵尸进程。而且,如果父进程在设置完该标志位之后,调用wait( ),则父进程将被阻塞,直到所有子进程全部结束,最后返回-1。
struct siginfo_t
{
int si_signo; // 信号值
int si_errno; // 信号产生的原因
int si_code; // 信号产生的方式
union // 联合数据结构,适应不同信号
{
int mem[MEM_SIZE]; // 确保分配足够大的空间,每个信号的数据结构都能放的下
struct // 对信号1有意义的结构
{ … }
…
…
struct // 对信号n有意义的结构
{ … }
}
}
si_code的取值:
SI_USER :signal was sent by kill( ) or raise( )
SI_QUEUE :signal was sent by sigqueue( )
SI_TIMER :signal was generated by expiration of a timer set bytimer_settime( )
SI_MESGQ :signal was generated by arrival of a message on anempty message queue
SI_ASYNCIO:signal wasgenerated by completion of an asynchronous I/O request
siginfo_t结构中的联合数据结构确保适应所有的信号,比如对于实时信号而言,实际采用下面的结构形式:
struct siginfo_t
{
int si_signo;
int si_errno;
int si_code;
sigvalsi_value;
};
si_value同样为一个联合数据结构:
union sigval
{
int sival_int;
void * sival_ptr;
}
1.1.4 信号的发送
1.1.4.1 kill( )
#include<sys/types.h>
#include<signal.h>
intkill( pid_t pid, int signum )
成功返回0,失败返回-1。
参数pid指定接收信号的进程:
pid > 0 :进程ID为pid的进程
pid = 0 :同一个进程组的所有进程
pid = -1 :除发送进程自身外,所有进程ID>1的进程
pid < -1 :进程组ID为abs(pid)的所有进程
当参数sinnum为0时,不发送任何信号,可用于检查目标进程是否存在,以及当前进程是否具有向目标进程发送信号的权限。
1.1.4.2 raise( )
#include<signal.h>
intraise( int signum )
向进程本身发送信号。
成功返回0,失败返回非0值。
1.1.4.3 abort( )
#include<stdlib.h>
voidabort( void )
等价于raise( SIGABRT )。
1.1.4.4 sigqueue( )
#include<sys/types.h>
#include<signal.h>
intsigqueue( pid_t pid, int signum, const union sigval val )
成功返回0,失败返回-1。
参数val指定随信号一起传递的信息,如下图所示:
1.1.4.5 alarm( )
#include<unistd.h>
unsignedint alarm( unsigned int seconds )
设置一个timer,到指定时间(秒)之后会expire,发送一个SIGALRM给本进程。如果调用alarm( )前,进程中已经设置了alarm,则返回前一个alarm的剩余时间(秒),否则返回0。
SIGALRM的缺省行为是中止进程。同一个进程只能有一个alarm。调用alarm函数会取消前一个alarm。如果seconds设为0,则只是取消前一个alarm。
1.1.4.6 ualarm( )
#include<unistd.h>
useconds_tualarm( useconds_t useconds, useconds_t interval );
简化版的setitimer( ),使用了ITIMER_REAL的计时方式。在指定时间(毫秒)后,向进程本身发送SIGALRM信号。参数interval指定了重复的间隔时间(毫秒)。
成功返回上次调用ualarm( )后剩余的时间(毫秒),如果之前没有调用过ualarm( )则返回0;
失败返回-1。
1.1.5 信号的等待
1.1.5.1 sigwait( )
#include<signal.h>
int sigwait( const sigset_t *set, int *signum )
挂起当前进程,直到set中的一个信号到达,并将该信号从进程的pending list中清除。
如果set中的某个信号已经pending,则立即返回,并将该信号从进程的pending list中清除。
成功返回0,失败返回-1。
1.1.5.2 pause( )
#include<signal.h>
intpause( void )
挂起当前进程直到有信号被捕获,也就是说该信号:
1.有自己的信号处理函数;或者是
2.终止当前进程。
如果信号有自己的信号处理函数,则pause( )将会在信号处理函数执行完毕之后返回。
如果信号终止当前进程,则pause( )不会返回。
由于pause( )会挂起当前进程,直到某个信号中断它的执行,所以pause( )总是返回-1,并将errno设为EINTR。
1.1.5.3 sigwaitinfo( )
#include<signal.h>
intsigwaitinfo( const sigset_t *set, siginfo_t *info )
成功返回信号值,失败返回-1。
1.1.5.4 sigtimedwait( )
#include<signal.h>
#include<time.h>
intsigtimedwait( const sigset_t *set, siginfo_t *info, const struct timespec*timeout );
如果timeout为0,立即返回。
成功返回信号值,失败返回-1。
struct timespec
{
time_t tv_sec //seconds
long tv_nsec //nanoseconds
}
1.1.6 信号函数
#include<signal.h>
intsighold( int signum ); // add signal to calling process’s signalmask
intsigrelse( int signum ); // remove signal from calling process’ssignal mask
intsigignore( int signum ); // set signal disposition to SIG_IGN.
成功返回0,失败返回-1。
intsigpause( int signum ); // remove signal from calling process’s signalmask and suspend the calling process until any signal is received. Note,sigpause( ) will restore calling process’s signal mask to orginal state beforereturning.
始终返回-1。
1.1.7 信号集
#include<signal.h>
intsigfillset( sigset_t *set ) // set中将包含Linux支持的64种信号
intsigemptyset( sigset_t *set )
int sigaddset( sigset_t *set, int signum )
int sigdelset( sigset_t *set, int signum )
成功返回0,失败返回-1。
int sigismember( const sigset_t *set, int signum )
存在返回1,不存在返回0,失败返回-1。
#include <signal.h>
int sigprocmask( int how, const sigset_t *newSet,sigset_t *oldSet );
成功返回0,失败返回-1。
参数how改变当前阻塞信号的集合:
SIG_BLOCK :在当前进程的阻塞信号集中添加set中的信号。
SIG_UNBLOCK:在当前进程的阻塞信号集中删除set中的信号。
SIG_SETMASK:更新当前进程的阻塞信号集为set。
intsigpending( sigset_t *set );
成功返回0,失败返回-1。
获得已经被发送到当前进程,却被阻塞的信号。
intsigsuspend( const sigset_t *mask );
始终返回-1,errno设为EINTR。
用于在接收到某个信号之前,临时用mask替换当前进程的阻塞信号集,并将当前进程挂起,直到收到一个信号为止;在返回之前,会将进程的阻塞信号集恢复到调用之前的设置。
可以在一个原子操作内设置mask然后pause,防止在设置mask之后pause之前,信号就被发出和捕获,从而造成进程永远等待。
1.1.8 信号编程的注意事项
考虑程序的可移植性,应该尽量采用POSIX信号函数,POSIX信号函数主要分为两类:
1.POSIX1003.1信号函数:kill( )、sigaction()、sigaddset( )、sigdelset()、sigemptyset( )、sigfillset( )、sigismember()、sigpending( )、sigprocmask()、sigsuspend( )。
2.POSIX1003.1b信号函数,在信号的实时性方面对POSIX 1003.1进行了扩展,包括以下三个函数:sigqueue( )、sigtimedwait( )、sigwaitinfo( )。
考虑程序的稳定性,应该尽量在信号处理函数中使用可重入函数。满足下列条件的函数是不可重入的:
1.使用静态数据结构;
2.调用malloc()或free( )。因为当进程正在执行malloc()动态内存分配时,信号产生从而转入到信号处理函数,但是当信号处理函数中也用到了malloc( )函数时,问题就出来了。因为malloc()函数通常维护一个所有已分配内存的链表。当信号发生时,进程可能正在修改链表指针,这时在信号处理函数中将又一次修改链表。
3.调用标准I/O函数。因为大多数的标准I/O函数都使用了global全局数据结构。
即使信号处理函数使用的都是安全函数,在进入处理函数时,首先仍然要保存errno的值,结束时再恢复。这是因为在信号处理过程中,errno的值随时可能被改变。