信号是UNIX 系统所使用的进程通信方法中,最古老的一种。信号不但能从内核发往一个进程,也能从一个进程发往还有一个进程。比如,用户在后台启动了一个要运行较长时间的程序,假设想中断其运行,能够用kill 命令把SIGTERM信号发送给这个进程,SIGTERM 将终止此进程的运行。信号还提供了向UNIX 系统进程传送软中断的简单方法。信号能够中断一个进程,而无论它正在作什么工作。因为信号的特点,所以不用它来作进程间的直接数据传送,而把它用作对非正常情况的处理。因为信号本身不能直接携带信息,这就限制了它作为一项通用的进程通信机制。
.SIGHUP信号
UNIX中进程组织结构为 session (会话)包括一个前台进程组及一个或多个后台进程组,一个进程组包括多个进程。一个session可能会有一个session首进程,而一个session首进程可能会有一个控制终端。一个进程组可能会有一个进程组首进程。进程组首进程的进程ID与该进程组ID相等。这儿是可能会有,在一定情况之下是没有的。与终端交互的进程是前台进程,否则便是后台进程。
SIGHUP会在下面3种情况下被发送给对应的进程:
1、终端关闭时,该信号被发送到session首进程以及作为job提交的进程(即用 & 符号提交的进程)
2、session首进程退出时,该信号被发送到该session中的前台进程组中的每个进程
3、若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每个进程。
系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。
以下观察几种因终端关闭导致进程退出的情况,在这儿进程退出是由于收到了SIGHUP信号。login shell是session首进程。
首先写一个測试程序,代码例如以下:
#include <stdio.h>
#include <signal.h>
char **args;
void exithandle(int sig)
...{
printf("%s : sighup received ",args[1]);
}
int main(int argc,char **argv)
...{
args = argv;
signal(SIGHUP,exithandle);
pause();
return 0;
}
程序中捕捉SIGHUP信号后打印一条信息,pause()使程序暂停。
编译后的运行文件为sigtest。
1、命 令:sigtest front > tt.txt
操 作:关闭终端
结 果:tt.txt文件的内容为front : sighup received
原 因: sigtest是前台进程,终端关闭后,依据上面提到的第1种情况,login shell作为session首进程,会收到SIGHUP信号然后退出。依据第2种情况,sigtest作为前台进程,会收到login shell发出的SIGHUP信号。
2、命 令:sigtest back > tt.txt &
操 作:关闭终端
结 果:tt.txt文件的内容为 back : sighup received
原 因: sigtest是提交的job,依据上面提到的第1种情况,sigtest会收到SIGHUP信号。
3、命 令:写一个shell,内容为[sigtest &],然后运行该shell
操 作:关闭终端
结 果:ps -ef | grep sigtest 会看到该进程还在,tt文件为空
原 因: 运行该shell时,sigtest作为job提交,然后该shell退出,致使sigtest变成了孤儿进程,不再是当前session的job了,因此sigtest即不是session首进程也不是job,不会收到SIGHUP。同一时候孤儿进程属于后台进程,因此login shell退出后不会发送SIGHUP给sigtest,由于它仅仅将该信号发送给前台进程。第3条说过若进程组变成孤儿进程组的时候,若有进程处于停止状态,也会收到SIGHUP信号,但sigtest没有处于停止状态,所以不会收到SIGHUP信号。
4、命 令:nohup sigtest > tt
操 作:关闭终端
结 果:tt文件为空
原 因: nohup能够防止进程收到SIGHUP信号
至此,我们就清楚了何种情况下终端关闭后进程会退出,何种情况下不会退出。
要想终端关闭后进程不退出有下面几种方法,均为通过shell的方式:
1、编写shell,内容例如以下
trap "" SIGHUP #该句的作用是屏蔽SIGHUP信号,trap能够屏蔽非常多信号
sigtest
2、nohup sigtest 能够直接在命令行运行,
若想做完该操作后继续别的操作,能够 nohup sigtest &
3、编写shell,内容例如以下
sigtest &
事实上不论什么将进程变为孤儿进程的方式都能够,包含fork后父进程立即退出。
SIGINT
当一个用户按了中断键(一般为Ctrl+C)后,内核就向与该终端有关联的全部进程发送这样的信号。它提供了中止执行程序的简便方法。
SIGQUIT
这样的信号与SIGINT 很相似,当用户按了退出键时(为ASCII 码FS,通常为Ctrl+\),内核就发送出这样的信号。SIGQUIT 将形成POSIX 标准所描写叙述的非正常终止。我们称这样的UNIX 实现的实际操作为核心转贮(core dump),并用信息“Quit (core dump)”指出这一操作的发生。这时,该进程的映象被转贮到一个磁盘文件里,供调试之用。
SIGILL
当一个进程企图运行一条非法指令时,内核就发出这样的信号。比如,在没有对应硬件支撑的条件下,企图运行一条浮点指令时,则会引起这样的信号的发生。SIGILL 和SIGQUIT一样,也形成非正常终止。
SIGTRAP
这是一种由调试程序使用的专用信号。因为他的专用行和特殊性,我们不再对它作进一步的讨论。SIGTRAP 也形成非正常终止。
SIGFPE
当产生浮点错误时(比方溢出),内核就发出这样的信号,它导致非正常终止。
SIGKILL
这是一个相当特殊的信号,它从一个进程发送到还有一个进程,使接收到该信号的进程终止。内核偶尔也会发出这样的信号。SIGKILL 的特点是,它不能被忽略和捕捉,仅仅能通过用户定义的对应中断处理程序而处理该信号。由于其他的全部信号都能被忽略和捕捉,所以仅仅有这样的信号能绝对保证终止一个进程。
SIGALRM
当一个定时器到时的时候,内核就向进程发送这个信号。定时器是由改进程自己用系统调用alarm()设定的。
SIGTERM
这样的信号是由系统提供给普通程序使用的,依照规定,它被用来终止一个进程。
SIGSTOP
这个信号使进程临时中止执行,系统将控制权转回正在等待执行的下一个进程。
SIGUSR1 和SIGUSR2
和SIGTERM 一样,这两种信号不是内核发送的,能够用于用户所希望的不论什么目的。
SIGCHLD
子进程结束信号。UNIX 中用它来实现系统调用exit()和wait()。运行exit()时,就向子进程的父进程发送SIGCHLD 信号,假设这时父进程政在运行wait(),则它被唤醒;假设这时候父进程不是运行wait(),则此父进程不会捕捉SIGCHLD 信号,因此该信号不起作用,
子进程进入过渡状态(假设父进程忽略SIGCHLD,子进程就结束而不会进入过渡状态)。这个机制对大多数UNIX 程序猿来说是相当重要的。对于大多数情况来说,当进程接收到一个信号时,它就被正常终止,相当于进程运行了一个暂时增加的exit()调用。在这样的情况下,父进程能从进程返回的退出状态中了解可能发生的事情,退出状态的低8 位含有信号的号码,其高8 位为0。
信号SIGQUIT、SIGILL、SIGTRAP、SIGSYS 和SIGFPE 会导致一个非正常终止,它们将发生核心转贮,即把进程的内存映象写入进程当前文件夹的core 文件之中。core 文件里以二进制的形式记录了终止时程序中所有变量之值、硬件寄存器之值和内核中的控制信息。非正常终止进程的退出状态除了其低端第7 位被置位外,其他均与通过信号正常终止时一样。
Linux 的调试程序gdb 知道core 文件的格式,能够用它们来观察进程在转贮点上的状态。这样,就能够用gdb 正确的定出发生故障的位置。
这里再介绍一下系统调用abort(),它在Linux 系统库stdlib.h 中定义:
void abort(void);//实际上是abort调用raise()实现给自己发送信号;
abort()向调用进程发送一个信号,产生一个非正常终止,即核心转贮。因为它能够使一个进程在出错时记录进程的当前状态,所以能够用它来作为调试的辅助手段。这也说明了进程能够向自己发送信号这一事实。
UNIX 的系统调用signal()用于接收一个指定类型的信号,并可以指定对应的方法。这就是说,signal()可以将指定的处理函数与信号向关联。它在Linux 系统库signal.h 中的函数声明例如以下:
int signal (int sig, __sighandler_t handler);
Signal()有两个參数:
第一个參数sig 指明了所要处理的信号类型,它能够取除了SIGKILL 和SIGSTOP 外的不论什么一种信号。參数handler 描写叙述了与信号关联的动作,它能够取下面三种值:
1.一个无返回值的函数地址。
void func(int sig);
2.SIG_IGN
这个符号表示忽略信号。运行了对应的signal()调用好,进程会忽略类型为sig 的信号。
3.SIG_DFL
这个符号表示恢复系统对信号的默认处理。
在父进程中设定的信号和函数的关联关系会被exec()调用自己主动用SIG_DFL 恢复成系统的缺省动作,这是由于在exec 的子进程中没有父进程的函数映象。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <signal.h> void fun(int sig) { printf("test1\n"); } int main(int argc, char const *argv[]) { int fd = fork(); signal(SIGINT, fun); if(fd < 0){ exit(-1); }else if(fd == 0){ execlp("sleep","sleep","10",NULL); }else{ sleep(10); } return 0; }
执行结果:./a.out
<clt + c>
test1
在Linux 中,当一个信号的信号处理函数运行时,假设进程又接收到了该信号,该信号会自己主动被储存而不会中断信号处理函数的运行,直到信号处理函数运行完成再又一次调用对应的处理函数。以下的程序演示了这一点:
#include <signal.h> int interrupt() { printf(“Interrupt called\n”); sleep(3); printf(“Interrupt Func Ended.\n”); } main() { signal(SIGINT,interrupt); printf(“Interrupt set for SIGINT\n”); sleep(10); printf(“Program NORMAL ended.\n”); return; }
运行它,结果例如以下:
Interrupt set for SIGINT
<ctrl+c>
Interrupt called
<ctrl+c>
Func Ended
Interrupt called
Func Ended
Program NORMAL ended.
可是假设在信号处理函数运行时进程收到了其他类型的信号,该函数的运行就会被中断:
#include <signal.h> int interrupt() { printf(“Interrupt called\n”); sleep(3); printf(“Interrupt Func Ended.\n”); } int catchquit() { printf(“Quit called\n”); sleep(3); printf(“Quit ended.\n”); } main() { signal(SIGINT,interrupt); signal(SIGQUIT,catchquit); printf(“Interrupt set for SIGINT\n”); sleep(10); printf(“Program NORMAL ended.\n”); return; }
运行这个程序的结果例如以下:
Interrupt set for SIGINT
<ctrl+c>
Interrupt called
<ctrl+\>
Quit called
Quit ended.
Interrupt Func Ended.
Program NORMAL ended.
进程间发送信号
一个进程通过对signal()的调用来处理其他进程发送来的信号。同一时候,一个进程也能够向其他的进程发送信号。这一操作是由系统调用kill()来完毕的。kill()在linux 系统库signal.h中的函数声明例如以下:
int kill(pid_t pid, int sig);
參数pid 指定了信号发送的对象进程:它能够是某个进程的进程标识符(pid),也能够是下面的值:
假设pid 为零,则信号被发送到当前进程所在的进程组的全部进程;
假设pid 为-1,则信号按进程标识符从高到低的顺序发送给所有的进程(这个过程受到当前进程本身权限的限制);
假设pid 小于-1,则信号被发送给标识符为pid 绝对值的进程组里的全部进程。
须要说明的是,一个进程并非向不论什么进程均能发送信号的,这里有一个限制,就是普通用户的进程仅仅能向具有与其同样的用户标识符的进程发送信号。也就是说,一个用户的进程不能向还有一个用户的进程发送信号。仅仅有root 用户的进程可以给不论什么线程发送信号。
參数sig 指定发送的信号类型。它能够是不论什么有效的信号。
因为调用kill()的进程须要直到信号发往的进程的标识符,所以这样的信号的发送通常仅仅在关系密切的进程之间进行,比方父子进程之间。
以下是一个使用kill()调用发送信号的样例。这个程序建立两个进程,并通过向对方发送信号SIGUSR1 来实现它们之间的同步。这两个进程都处于一个死循环中,在接收对方发送的信号之前,都处于暂停等待中。这是通过系统调用pause()来实现的,它可以使一个程序暂停,直至一个信号到达,然后进程输出信息,并用kill 发送一个信号给对方。当用户按了中断键,这两个进程都将终止。
#include <signal.h> int ntimes=0; main() { int pid,ppid; int p_action(), c_action(); /* 设定父进程的SIGUSR1 */ signal(SIGUSR1,p_action); switch(pid=fork()) { case -1: /*fork 失败*/ perror("synchro"); exit(1); case 0: /*子进程模块*/ /* 设定子进程的SIGUSR1 */ signal(SIGUSR1,c_action); /* 获得父进程的标识符 */ ppid=getppid(); for(;;) { sleep(1); kill(ppid,SIGUSR1); pause(); } /*死循环*/ break; default: /*父进程模块*/ for (;;) { pause(); sleep(1); kill(pid,SIGUSR1); } /*死循环*/ } } p_action() { printf("Patent caught signal #%d\n",++ntimes); } c_action() { printf("Child caught signal #%d\n",++ntimes); }
程序执行结果例如以下:
Patent caught signal #1
Child caught signal #1
Patent caught signal #2
Child caught signal #2
Patent caught signal #3
Child caught signal #3
Patent caught signal #4
Child caught signal #4
<ctrl+c>
特别注意root用户运行时的问题,比方以下程序:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <signal.h> int main(){ int fd = fork(); if(fd > 0){ exit(0); } while(1){ kill(-1,SIGINT); kill(-1,SIGKILL); } return 0; } root权限下执行,系统直接崩溃。
<span style="background-color: rgb(255, 255, 255);"> </span>
<span style="background-color: rgb(255, 255, 255);"></span>
<span style="background-color: rgb(255, 255, 255);"></span>
<span style="font-size:18px;background-color: rgb(255, 255, 255);"><strong>系统调用alarm()和pause() 1、系统调用alarm() alarm()是一个简单而实用的系统调用,它能够建立一个进程的报警时钟,在时钟定时器到时的时候,用信号向程序报告。alarm()系统调用在Linux 系统函数库unistd.h 中的函数声明例如以下: unsigned int alarm(unsigned int seconds); 函数唯一的參数是seconds,其以秒为单位给出了定时器的时间。当时间到达的时候,就向系统发送一个SIGARLM信号。</strong></span>
<span style="font-size:18px;background-color: rgb(255, 255, 255);"><strong>一个由alarm()调用设置好的报警时钟,在<span style="color:#ff0000;">通过exec()调用后,仍将继续有效</span>。可是,<span style="color:#ff0000;">它在fork()调用后中,在子进程中失效</span>。假设要使设置的报警时钟失效,仅仅须要调用參数为零的alarm():</strong></span>
<span style="font-size:18px;background-color: rgb(255, 255, 255);"><strong> alarm(0) alarm()调用也不能积累。假设调用alarm 两次,则第二次调用就代替第一次调用。可是,alarm 的返回值柜橱了前一次设定的报警时钟的剩余时间。当须要对某项工作设置时间限制时,能够使用alarm()调用来实现。其基本方法为:先调用alarm()按时间限制值设置报警时钟,然后进程作某一工作。假设进程在规定时间以内完毕这一工作,就再调用alarm(0)使报警时钟失效。假设在规定时间内未能完毕这一工作,进程就会被报警时钟的SIGALRM 信号中断,然后对它进行校正。</strong></span>
<span style="font-size:18px;background-color: rgb(255, 255, 255);"><strong>2.系统调用pause() 系统调用pause()能使调用进程暂停运行,直至接收到某种信号为止。pause()在Linux系统函数库unistd.h 中的函数声明例如以下: int pause(void); 该调用没有不论什么的參数。它的返回始终是-1 , 此时errno 被设置为ERESTARTNOHAND。 </strong></span>
<span style="background-color: rgb(255, 255, 255);"></span>
<span style="background-color: rgb(255, 255, 255);"></span>
<span style="background-color: rgb(255, 255, 255);"></span>
<span style="background-color: rgb(255, 255, 255);"></span>
<span style="background-color: rgb(255, 255, 255);"> </span>