目录
一、信号
1、基本概念
2、用户处理信号的方式
3、查看信号
4、可靠信号和不可靠信号
5、信号种类
6、终止进程信号的区别
二、进程对信号的处理
1、signal()函数
2、sigaction()函数
3、代码演示
4、运行结果
三、实战演练
四、补充
1、alarm()函数
2、wait()函数
3、僵尸进程和孤儿进程
一、信号
1、基本概念
信号是Linux系统中用于进程之间通信或者操作的机制,它给进程提供一种异步的软件中断(信号可以在任何时候发送给某一进程,而无须知道该进程的状态)。如果该进程并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
2、用户处理信号的方式
进程接受到信号后,有三种处理方式:
(1)忽略:忽略某个信号,对该信号不做任何处理,就像从未发生过。
(2)捕捉:类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来进行处理。
(3)默认/缺省:Linux对每种信号都规定了默认操作,通常是终止该进程。
注意:有两个信号比较特殊,需要特殊记一下——SIGKILL 和 SIGSTOP,这是两个不能捕捉的信号或忽略的信号。不能被忽略的原因是:他们向超级用户提供了使进程终止或停止的可靠方法。大概讲一下两个信号的区别,SIGKILL这是一个 “我不管您在做什么,立刻停止”的信号。假如您发送SIGKILL信号给进程,Linux就将进程停止在那里。SIGSTOP 停止进程的执行,但是该进程还未结束, 只是暂停执行.。
3、查看信号
我们可以使用如下的命令查看当前系统支持的信号,需要注意的是不同的系统支持的信号是不一样的:
查看信号的命令:kill -l
给大家看一下我系统所支持的信号:
注意:信号本质上是 int 类型的数字编号。内核针对每个信号,都给其定义了一个唯一的整数编号,从数字 1 开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏), 信号名字与信号编号乃是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样,所 以在程序当中一般都使用信号的符号名(也就是宏定义)。这些信号在头文件中定义,每个信号都是以 SIGxxx 为开头。
4、可靠信号和不可靠信号
Linux信号机制基本上是从UNIX系统中继承过来的。早期UNIX系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是“不可靠信号”的来源,它的主要问题就是信号可能丢失。随着时间发展,实践证明了有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已经有许多应用,不好再做改动,最后只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。
信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。对于目前Linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。
5、信号种类
经过上面的学习,我们已经知道了如何查看信号,那么每个信号都代表着什么意思呢?该信号的默认动作又是什么呢?让我们继续往下学习吧!(红色信号为常见信号,其余信号了解即可)
信号编号 | 信号名 | 信号说明 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 在终端的控制进程结束时发出 | 程序终止 |
2 | SIGINT | CTRL+c按键终止程序运行的信号 | 程序终止 |
3 | SIGQUIT | CTRL+\按键输入时产生的信号 | 程序终止 |
4 | SIGILL | 非法的指令 | 程序终止 |
5 | SIGTRAP | 跟踪自陷,由断点指令或其它trap指令产生 | 建立CORE文件 |
6 | SIGABRT | 当调用abort函数时会产生当前信号 | 程序终止 |
7 | SIGBUS | 运行非本CPU相关编译器编译的程序 | 程序终止 |
8 | SIGFPE | 算术异常时产生 | 建立CORE文件 |
9 | SIGKILL | 强制杀死程序序号,任何程序都不可以捕捉该信号 | 程序终止,不可被捕捉 |
10 | SIGUSR1 | 用户自定义信号1,不会自动产生,只能使用kill函数或者命令给指定的进程发送当前信号 | 程序终止 |
11 | SIGSEGV | 段错误系统给程序发送的信号 | 程序终止 |
12 | SIGUSR2 | 用户自定义信号2,不会自动产生,只能使用kill函数或者命令给指定的进程发送当前信号 | 程序终止 |
13 | SIGPIPE | 管道破裂信号 | 程序终止 |
14 | SIGALRM | 当alarm函数设置的时间到达时,会产生当前信号 | 程序终止 |
15 | SIGTERM | kill命令默认发送的信号,默认动作是终止信号 | 程序终止 |
16 | SIGSTKFLT | 数学协处理器的栈异常 | 程序终止 |
17 | SIGCHLD | 子进程退出信号 | 忽略该信号 |
18 | SIGCONT | 当产生当前信号后,当前停止的进程会恢复运行 | 停止的进程恢复运行 |
19 | SIGSTOP | 停止进程的执行 | 停止进程 |
20 | SIGTSTP | CTRL+z按键输入时产生的信号,但该信号可以被处理和忽略 | 停止进程 |
21 | SIGTTIN | 后台进程读终端 | 停止进程 |
22 | SIGTTOU | 后台进程写终端 | 停止进程 |
23 | SIGURG | 有"紧急"数据或out-of-band数据到达socket时产生 | 忽略该信号 |
24 | SIGXCPU | 超出CPU限制 | 程序终止 |
25 | SIGXFSZ | 文件长度过长 | 程序终止 |
26 | SIGVTALRM | 虚拟定时器超时 | 程序终止 |
27 | SIGPROF | 统计分布图用计时器到时 | 程序终止 |
28 | SIGWINCH | 终端窗口尺寸发生变化 | 忽略该信号 |
29 | SIGIO | 异步IO | 程序终止 |
30 | SIGPWR | 电力故障 | 程序终止 |
31 | SIGSYS | 无效系统调用 | 程序终止 |
6、终止进程信号的区别
经过上面的学习,我们知道终止进程的信号有三种,即SIGINT、SIGKILL、SIGTERM,三者都是结束/终止进程运行.但三者之间却有区别。让我们一起来看看吧!
(1)SIGINT
产生方式: 键盘Ctrl+C
产生结果: 只对当前前台进程,和他的所在的进程组的每个进程都发送SIGINT信号,之后这些进程会执行信号处理程序再终止。
(2)SIGTERM
产生方式: 和任何控制字符无关,用kill函数发送
本质: 相当于 kill pid
产生结果: 当前进程会收到信号,而其子进程不会收到.如果当前进程被kill(即收到SIGTERM),则其子进程的父进程将为init,即pid为1的进程。与SIGKILL的不同,SIGTERM可以被阻塞,忽略,捕获,也就是说可以进行信号处理程序,那么这样就可以让进程很好的终止,允许清理和关闭文件。
(3)SIGKILL
产生方式: 和任何控制字符无关,用kill函数发送
本质: 相当于kill -9 pid
产生结果: 当前进程收到该信号,注意该信号是无法被捕获的,也就是说进程无法执行信号处理程序,会直接发送默认行为,也就是直接退出。这也就是为什么kill -9 pid一定能杀死程序的原因。 故这也造成了进程被结束前无法清理或者关闭资源等行为。
二、进程对信号的处理
Linux下有signal()和sigaction()两种信号安装的函数,让我们分别来看看:
1、signal()函数
函数原型如下:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数描述:
signal函数用来在进程中指定当一个信号到达进程后该做什么处理,信号处理函数的handler有两个默认值,分别是SIG_IGN表示忽略行为和SIG_DFL表示默认行为。而且signal函数是阻塞的,比如当进程正在执行SIGUSR1信号的处理函数,此时又来一个SIGUSR1信号,signal会等到当前信号处理函数处理完后才继续处理后来的SIGUSR1。
2、sigaction()函数
函数原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
stuct sigaction
{
void (*)(int) sa_handle; //信号处理函数
sigset_t sa_mask; //信号屏蔽集
int sa_flags;
}
参数说明:
(1)第一个参数 signum:信号值。
(2)第二个参数act:信号的处理参数。
(3)第三个参数oldact:保存信号上次安装时的处理参数。
补充:
(1)信号阻塞:和signal函数类似,当正处于某个信号的处理函数中时,这个信号再次到达会被阻塞,待信号处理函数完成之后再处理。
(2)sa_mask:信号屏蔽集,所谓屏蔽并不是忽略,屏蔽的时间段是在信号处理函数执行期间,一旦处理函数执行完毕将会重新唤醒此信号。
(3)sa_flag:通常取值为0,则表示默认行为。
3、代码演示
上面已经讲解了signal()和sigaction()两个函数的用法,接下来我们用一个代码来实际操作一下吧!
代码如下:
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>int g_signal = 0;void signal_stop(int signum)
{if( SIGTERM == signum ){printf("SIGTERM signal detected\n");}else if( SIGALRM == signum ){printf("SIGALRM signal detected\n");g_signal = 1;}}void signal_user(int signum)
{if( SIGUSR1 == signum ){printf("SIGUSR1 signal detected\n");}else if( SIGUSR2 == signum ){printf("SIGUSR2 signal detected\n");}g_signal = 1;
}void signal_code(int signum)
{if( SIGBUS == signum ){printf("SIGBUS signal detected\n");}else if( SIGILL == signum ){printf("SIGILL signal detected\n");}else if( SIGSEGV == signum ){printf("SIGSEGV signal detected\n");}exit(-1);}int main(int argc,char *argv[])
{char *ptr = NULL;struct sigaction sigact,sigign;/*Use signal() install signal*/signal(SIGTERM,signal_stop);signal(SIGALRM,signal_stop);signal(SIGBUS,signal_code);signal(SIGILL,signal_code);signal(SIGSEGV,signal_code);/*Use sigaction() install signal*/
/*Initialize the catch signal structure.*/sigemptyset( &sigact.sa_mask );sigact.sa_flags = 0;sigact.sa_handler = signal_user;/*Setup the ignore signal*/sigemptyset( &sigign.sa_mask );sigign.sa_flags = 0;sigign.sa_handler = SIG_IGN;sigaction(SIGINT,&sigign,0); /*ignore SIGINT signal by CTRL+C*/sigaction(SIGUSR1,&sigact,0); /*catch SIGUSR1*/sigaction(SIGUSR2,&sigact,0); /*catch SIGUSR2*/printf("Program start running for 20 seconds...\n");alarm(20);while( !g_signal ){;}printf("Program start stop running...\n");printf("Invalid pointer operator will raise SIGSEGV signal\n");*ptr = 'h'; return 0;}
4、运行结果
大家是否理解这个运行结果呢?如图可以看出,进程一共接受到了两种信号,分别是SIGALRM和SIGSEGV。为什么呢?接收到SIGALRM是因为我们在程序中调用了alarm()函数;接收到SIGSEGV是因为代码最下面我们用了*ptr = 'h',而这条语句是一个指针错误。
大家应该注意到代码中有一行是sigaction(SIGINT,&sigign,0);并且sigign.sa_handler = SIG_IGN;这就代表着键盘CTRL+c输入的SIGINT信号会被忽略掉,那我们实际操作一下,看看是不是如我们所想呢?
正如我们所想,键盘CTRL+c输入的SIGINT信号因为已经被忽略掉,所以不能在终止进程了。
三、实战演练
题目:
我们知道,父进程在创建子进程之后,究竟是父进程还是子进程先运行没有规定,这由操作系统的进程调度策略决定,而如果在某些情况下我们需要确保父子进程运行的先后顺序,则可以使用信号来实现进程间的同步。
要求:写一个程序,实现父子进程之间使用信号进行同步。如果父进程先执行则进入到循环休眠等待状态,直到子进程给他发送信号之后才能跳出循环继续运行,确保子进程先执行它的任务。同样,子进程在执行完毕之后,就等待父进程给他发送信号之后才能退出,而父进程则通过调用wait()系统调用等待子进程退出后,父进程再退出。
参考代码如下:
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>int g_child_stop = 0;
int g_parent_run = 0;void sig_child(int signum)
{if(SIGUSR1 == signum){g_child_stop = 1;}
}void sig_parent(int signum)
{if(SIGUSR2 == signum){g_parent_run = 1;}
}int main(int argc,char *argv[])
{int pid;int status;signal(SIGUSR1,sig_child);signal(SIGUSR2,sig_parent);if((pid=fork()) < 0){printf("Create child process failure:%s\n",strerror(errno));return -1;}else if(pid == 0){/*Child process can do something first here.*/printf("Child process start runing!\n");/*when child process have done,then tell parent process to start running*/printf("Child process send parent a signal to tell parent process to run!\n");kill(getppid(),SIGUSR2);/*Waiting the stopping signal sent by parent process*/while( !g_child_stop ){sleep(1);}/*Child process have received the stopping signal*/printf("child process receive signal from parent and exit now!\n");return 0;}/*Only parent process run the codes beneath*//*Parents hangs up until receive signal from child*/while( !g_parent_run ){sleep(1);}/*Parent process have received the running signal from child process*//*Parent process can do something here*/printf("Parent start running now!\n");/*Parent process send a signal to tell child process to exit*/kill(pid,SIGUSR1);/*parent wait child process exit*/wait(&status);printf("Parent wait child process die and exit now!\n");return 0;}
运行结果如下:
四、补充
最后,针对上文提到的一些知识,进行一些简单的补充:
1、alarm()函数
函数原型如下:
unsigned int alarm(unsigned int seconds)
(1)功能:
在进程中设置一个定时器,在seconds秒之后,将会发送SIGALRM信号给当前的进程,故而alarm函数也被称为闹钟函数。(如果在seconds秒内再次调用了alarm函数设置了新的闹钟,那么之前设置的秒数将会被新的闹钟时间所取代)
(2)参数:
定时时间,单位为秒。
(3)返回值:
如果该alarm函数是进程中第一次调用,则返回0,如果不是第一次调用,则返回上一次调用alarm函数剩余的时间。
2、wait()函数
函数原型如下:
#include <sys/types.h>
#include <wait.h>int wait(int * status)
(1)函数功能:
父进程一旦调用wait函数就立即开始阻塞,然后wait会分析当前进程的某个子进程是否已经退出,如果让它找到了这样一个退出的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回,如果没有找到,就一直阻塞,直至找到一个结束的子进程或接收到了一个指定的信号为止。
【注意: 当父进程忘记调用wait()等待已终止的子进程,子进程就会进入一种没有父进程的状态,此时子进程就是zombie(僵尸)进程。】
(2)参数status:
用来保存被收集进程退出时的状态,它是一个指向int类型的指针,如果我们对这个子进程如何死掉的不在意,只想这把这个被僵尸进程消灭掉,就把这个参数置为NULL。如果status的值不是NULL,wait把子进程的退出状态取出并存入其中,这是一个整数值(int)。
3、僵尸进程和孤儿进程
(1)孤儿进程:
父进程先于子进程结束,当父进程退出时,系统会让pid为1的进程接管子进程。所以孤儿进程的pid都是1 。
(2)僵尸进程:
子进程先于父进程结束,子进程成了僵尸(zombie)进程,并且子进程会一直保持这样的状态直至重启,此时内核只会保留进程的一些必要信息以备父进程所需,此时子进程始终占有资源,同时也减少了系统可以创建的最大进程数。
本篇文章用到了许多进程的知识,如果大家对进程不是很了解,可以看一下这篇文章《APUE学习之多进程编程》。我会坚持使用博客来整理自己所学知识,同时也希望能够帮助到大家,如果有哪些错误或者疑问,也欢迎大家在评论区一起讨论!