Linux下的进程间通信(三)
信号(signal)
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。
信号分为不可靠信号和可靠信号。Linux下的不可靠信号问题主要指的是信号可能丢失;可靠信号客服了信号丢失的问题。
信号又可分为实时信号和非实时信号。非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。
进程对信号的响应分为三种:(1)忽略信号,即不对信号做任何处理,但有两个信号不能忽略:SIGKILL和SIGSTOP;(2)捕捉信号,定义信号处理函数,当信号发生时,执行信号的操作函数;(3)执行缺省操作,Linux对每种信号都定义了默认操作。
1. 函数
l int kill(pid_t pid, int signo)
说明:向其它进程发送信号
头文件:sys/types.h,signal.h
参数:pid>0,进程ID为pid的进程接收信号;pid=0,同一个进程组的进程接收信号;pid<0 and pid != -1,进程组ID为-pid的所有进程接收;pid=-1,除发送进程外,所有进程ID大于1的进程接收。
signo是信号值,当为0时(即空信号),实际不发送任何信号
返回值:如果pid>0时,成功返回0,否则返回-1。
l int raise(int signo)
说明:向进程本身发送信号
头文件:signal.h
参数:signo发送的信号值
返回值:如果成功返回0,否则返回-1
l int sigqueue(pid_t pid, int signo, const union sigval val)
说明:是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。它传递了更多信息,但只能发送个指定进程,而不能是进程组。在发送非实时信号时,不支持排队,即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。
头文件:signal.h
参数:pid指接收信号的进程ID,signo为要发送的信号,val指定了信号传递的参数sigval_t:
typedef union sigval{
int sival_int;
void *sival_ptr;
}sigval_t;
返回值:成功返回0,否则返回-1。
l unsigned int alarm(unsigned int seconds)
说明:专门为SIGALRM信号而设,在指定的实际secondes秒后,将向进程本身发送SIGALRM信号,又称为闹钟信号。进程调用alarm后,任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。
头文件:unistd.h
参数:seconds为等待的时间,秒为单位
返回值:如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
l int setitimer (int which, const struct itimerval *value, struct itimerval *ovalue)
说明:setitimer()比alarm功能强大,支持3种类型的定时器:
ITIMER_REAL: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;
ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;
ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;
头文件:sys/time.h
参数:which指明三种类型的一种,value为一个时间的实例
返回值:成功返回0;否则返回-1。
l void abort(void)
说明:向进程本身发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。
头文件:stdlib.h
参数:无
返回值:无
2. 安装信号
如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。
linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现, 是库函数。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。
l typedef void (*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler)
说明:看得出来sighandler_t是个函数指针,指向signal函数,它用来映射信号和处理函数的关系
头文件:signal.h
参数:signum指明信号,handler指明signum对应的处理函数
返回值:调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。
l int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact))
头文件:signal.h
参数:signum指明信号,act为一个结构体sigaction的对象,包含了对指定信号的处理、信号所传递的消息、信号处理函数执行过程中应屏蔽掉哪些函数等。oldact表示在设置新动作之前的sigaction
3. 信号集及信号集操作函数
信号集是一种数据结构:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t
信号集有其自己的操作函数:
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum)
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;
sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;
sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;
sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;
sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。
4. 信号阻塞与信号未决
每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));
int sigpending(sigset_t *set));
int sigsuspend(const sigset_t *mask));
sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:
参数how |
进程当前信号集 |
SIG_BLOCK |
在进程当前阻塞信号集中添加set指向信号集中的信号 |
SIG_UNBLOCK |
如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞 |
SIG_SETMASK |
更新进程阻塞信号集为set指向的信号集 |
sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。
sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。
5. 信号生命周期
从信号发送到信号处理函数的执行完毕
对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:信号诞生;信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。相邻两个事件的时间间隔构成信号生命周期的一个阶段。
下面阐述四个事件的实际意义:
- 信号"诞生"。信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()或sigqueue()等)。
- 信号在目标进程中"注册";进程的task_struct结构中有关于本进程中未决信号的数据成员:
struct sigpending pending:
struct sigpending{
struct sigqueue *head, **tail;
sigset_t signal;
};
第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:
struct sigqueue{
struct sigqueue *next;
siginfo_t info;
}
信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。 只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。
注:
当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册);
当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构(一个非实时信号诞生后,(1)、如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失;(2)、如果进程的未决信号中没有相同信号,则在进程中注册自己)。
- 信号在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不应该在进程的未决信号集中删除该信号(信号注销完毕)。
进程在执行信号相应处理函数之前,首先要把信号在进程中注销。 - 信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。
注:
1)信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)。
2)在信号被注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到同一信号多次,则对实时信号来说,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。
5. 代码实例:
(1) 信号发送与接收
#include <iostream.h> #include <signal.h> #include <sys/types.h> #include <unistd.h> using namespace std; void new_op(int, siginfo_t*, void*); int main(int argc, char *argv[]) { struct sigaction act; int sig = 5; int i = 0; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO; act.sa_sigaction = new_op; if(sigaction(sig, &act, NULL) < 0) //install the signal { cout << "install signal error" << endl; } while(1) { sleep(2); cout<<"wait for the signal"<<endl; raise(sig); i++; if(i > 5) break; } return 0; } void new_op(int signo, siginfo_t *info, void *myact) { cout<<"receive signal "<<signo<<endl; sleep(5); }
(2) 不同进程间传递整型参数:把1中的信号发送和接收放在两个程序中,并且在发送过程中传递整型参数。
signal_receive.cpp
#include <signal.h> #include <sys/types.h> #include <unistd.h> #include <iostream.h> using namespace std; void new_op(int, siginfo_t*, void*); int main(int argc, char *argv[]) { struct sigaction act; int sig; pid_t pid; pid = getpid(); sig = atoi(argv[1]); sigemptyset(&act.sa_mask); act.sa_sigaction = new_op; act.sa_flags = SA_SIGINFO; if(sigaction(sig, &act, NULL) < 0) { printf("install signal error\n"); return -1; } while(1) { sleep(2); printf("wait for the signal"); } } void new_op(int signo, siginfo_t *info, void *myact) { printf("the int value is %d\n", info->si_int); sleep(5); }
signal_send.cpp
#include <signal.h> #include <sys/time.h> #include <unistd.h> #include <sys/types.h> #include <iostream.h> using namespace std; int main(int argc, char *argv[]) { pid_t pid; int sig; union sigval myval; sig = atoi(argv[1]); pid = (pid_t)atoi(argv[2]); myval.sival_int = 8; if(sigqueue(pid, sig, myval) == -1) printf("send error\n"); sleep(2); }
(3) 信号阻塞及信号集操作
#include <signal.h> #include <unistd.h> #include <iostream.h> #include <sys/types.h> using namespace std; void new_op(int, siginfo_t*, void*); int main(int argc, char *argv[]) { sigset_t new_mask, old_mask, pending_mask; struct sigaction act; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO; act.sa_sigaction = new_op; if(sigaction(SIGRTMIN + 10, &act, NULL) < 0) { printf("install signal error\n"); return -1; } sigemptyset(&new_mask); sigaddset(&new_mask, SIGRTMIN + 10); if(sigprocmask(SIG_BLOCK, &new_mask, &old_mask) < 0) { printf("block signal %d error\n", SIGRTMIN + 10); return -1; } sleep(10); printf("now begin to get pending mask and unblock %d\n", SIGRTMIN + 10); if(sigpending(&pending_mask) < 0) { printf("get pending mask error\n"); return -1; } int res = 0; if((res = sigismember(&pending_mask, SIGRTMIN + 10)) == 1) printf("signal %d is pending\n", SIGRTMIN + 10); if(sigprocmask(SIG_SETMASK, &old_mask, NULL) < 0) { printf("unblock signal error\n"); return -1; } sleep(10); } void new_op(int signo, siginfo_t *info, void *myact) { printf("receive signal %d\n", signo); }
以后台形式运行该程序,然后在不同的时间执行kill –s 44 pid(pid为后台执行进程的id),看不同的输出结果,就能明白其中的意义了