信号及信号来源
使用kill命令杀死进程的实质,是向目标进程发送了一个信号,当目标进程接收到这个信号后,会根据信号的处理函数,执行指
定动作。
比如:keil 杀死进程就是使用的9号信号
使用“kill-l”命令可查看系统中的信号
产生信号的五种情况:
linux系统中信号的状态:
linux 系统中信号的处理方式:
信号的默认动作:
信号的产生
系统调用
系统调用中发送信号常用的函数有kill()、raise()、abort()等,其中kill是最常用的函数,该函数的作用是给指定进程发送信号,但是否杀死进程取决于所发送信号的默认动作。kilI()存在于函数库signal.h中,其函数声明如下:
int kill(pid_t pid, int sig);
案例1:使用fork()函数创建一个子进程,在子进程中使用kilI()发送信号,杀死父进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
int main()
{
pid_t pid;
pid = fork();
if (pid == 0){
//子进程
sleep(1);
printf("child pid=%d,ppid=%d\n", getpid(), getppid());
kill(getppid(), SIGKILL);//发送信号SIGKILL给父进程
}
else if (pid > 0){
//父进程
while (1){
printf("parent pid=%d,ppid=%d\n", getpid(), getppid());
}
}
return 0;
}
为了保证父进程能接收到子进程发送的信号,在父进程执行的代码段中添加循环,保持父进程的运行;
子进程的代码段中调用了kill()函数发送SIGKILL信号给父进程,在此之前使子进程先沉睡1秒。编译案例,执行程序,执行结果如下:
当终端输出Killed时,表明子进程发送的信号SIGKILL成功杀死了父进程。
除kilI()外,
raise()、abort()和pause()也是常用的系统调用。
raise()函数的功能是发送指定信号给当前进程自身,该函数存在于函数库signal.h中,其函数声明如下:
int raise(int sig);
若raise()函数调用成功,则返回0;否则返回非0。其参数sig为要发送信号的编号,使用kilI()函数可以实现与该函数相同的功能,该函数与kilI()之间的关系如下:
raise(sig= = kill(getpid(),sig)
abort()函数的功能是给当前进程发送异常终止信号SIGABRT,终止当前进程,并生成core文件,该函数存在于函数库stdlib.h中,其函数声明如下:
void abort(void);
该函数在调用之时会先解除阻塞信号SIGABRT,然后才发送信号给自己。它不会返回任何值,可以视为百分百调用成功。
pause()函数的作用是造成进程主动挂起,等待信号唤醒。调用该函数后进程将主动放齐CPU,进入阻塞状态,直到有信号通达将其唤醒,才继续工作。pause()存在于函数库unistd.h中,其声明如下:
int pause(void);
pause()函数的参数列表为空,不一定有返回值。根据唤醒进程信号不同的默认动作,pause()函数可能有以下几种情况:
(1)若信号的默认处理动作是终止进程,则进程终止,pause()函数没有机会返回;
(2)若信号的默认处理动作是忽略,进程继续处于持起状态,pause()函数不返回;
(3)若信号的处理动作是捕捉,则调用完信号处理函数后,pause返回-1,并将errno设置为EINTR,表示“被信号中断”
由以上情况可知,pause()只有错误返回值。另外,需要注意的是,若信号被屏蔽,使用pause()函数挂起的进程无法被其唤醒。
软件条件
当满足某种软件条件时,也可以驱使内核发送信号。Linux系统中的alarm()函数就是一个典型的产生软件条件信号的信号源。
alarm()
当满足某种软件条件时,也可以驱使内核发送信号。Linux系统中的alarm()函数就是一个典型的产生软件条件信号的信号源。
alarm()函数的功能相当于计时器,驱使内核在指定秒数后发送信号到调用该函数的进程。alarm()函数存在于函数库unistd.h中,其函数声明如下
unsigned int alarm (unsigned int seconds);
alarm()函数的参数seconds用于指定计时秒数;函数的返回值根据函数的调用情况有几种不同的结果
若进程中不是第一次调用alarm(),且上一个的alarm()尚有剩余秒数,则该函数成功调用后会返回旧计时器的剩余秒数,否则返回0。例如在定时器alarm(5)启动3秒后,新定时器alarm(4)启动,那么alarm(4)的返回值为2;若3秒后第三个定时器alarm(2)启动,那么alarm(2)的返回值为0;若额外设置alarm(0),将会取消计时器。计时器采用自然定时法,无论当前进程是否处于运行态,计时器都会计时。
计时结束后,内核会发送14号信号SIGALRM到当前进程,进程受到SIGALRM信号后执行该信号的动作,若该信号被进程屏蔽,进程将无法接收到该信号。
案例7-2:在程序中设置计时器,使进程在指定秒数后终止运行。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
alarm(1); //设置计时器
while (1) //循环保证进程不退出
printf("process will finish.\n");
return 0;
}
在案例7-2中先设置了一个1秒的计时器:为了保证进程在信号到达之前保持运行,又在进程中添加while循环,使进程不断打印信息。1秒后计时器会驱使内核发送SIGALRM信号到进程,因此进程会在1秒之后结束。
编译案例7-2,执行程序,观察到屏幕不断打印“proces willfinish.”。1秒后停止打印并输出Alarmclock·表示计时器生效,使进程终止。
setitimer()
setitimer()函数也可以设置定时器。与alarm()相比,它精确到微秒,精度更高,并且可实现周期定时。该函数存在于函数库sys/time.h中,函数声明如下:
若setitimer()函数成功调用则返回0;否则返回-1并设置errno。
该函数有3个参数,其中参数 which用来设置以何种方式计时。which有3个取值,不同的值对应不同的计时方法,产生不同的信号。which取值及对应含义如下:
- 若参数为ITIMER_REAL,使用自然定时法计时,计算自然流逝的时间,计时结束递送14号信号SIGALRM。
- 若参数为ITIMER_VIRTUAL,只计算进程占用CPU 的时间,计时结束后递送26号信号SIGVTALRM。
- 若参数为ITIMER_PROF ,计算进程占用CPU 以及执行系统调用的时间,即进程在用户空间和内核空间运行时间的总和,计时结束后递送27号信号SIGPROF。
setitimer()的 第二个参数是一个传入参数,表示计时器定时时长,其本质是一个itimerval类型数据结构的指针,itimerval中有两个timerval类型的成员,这两个成员也是结构体类型。
itimerval与timeval定义如下:
成员 it_interval和 it_value分别指定间隔时间和初始定时时间。
若只指定it_value,则只实现一次定时;
若同时指定it_interval,则用来实现重复定时。
setitimer()的工作机制是,先对it_value倒计时,当 it_value计时结束时,触发信号发送条件。然后重置it_value为 it_interval,继续对it_value倒计时,如此一直循环。
setitimer()函数的第三个参数用来保存先前设置的new_value值,通常设置为NULL。
案例7-3:使用setitimer()函数实现alarm()函数。
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <error.h>
unsigned int my_alarm(unsigned int sec)
{
struct itimerval it, oldit;
int ret;
it.it_value.tv_sec = sec; //指定时间
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 0; //指定重复次数
it.it_interval.tv_usec = 0;
ret = setitimer(ITIMER_REAL, &it, &oldit);
if (ret == 1){
perror("setitimer");
exit(1);
}
return oldit.it_value.tv_sec;
}
int main()
{
my_alarm(1);
while (1)
printf("process will finish\n");
return 0;
}
alarm()只实现一次计时,因此 my_alarm()中调用的setitimer()的参数it的成员 it_interval 的值都为0;
因为alarm()只精确到秒,所以setitimer()中参数it表示微秒的成员变量it_value.tv_usec设置为0即可。
kill命令
举个例子:
先新建5个进程
信号阻塞
信号集设定函数
sigprocmask()
sigpending()
案例4:以2号信号为例,通过位操作函数sigprocmask0与siqpending0获取信号状态。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
void printset(sigset_t *ped) //pending打印函数
{
int i;
for (i = 1; i < 32; i++){
if ((sigismember(ped, i) == 1))
putchar('1');
else
putchar('0');
}
printf("\n");
}
int main()
{
sigset_t set, oldset, ped; //信号集定义
sigemptyset(&set); //初始化自定义信号集set
sigaddset(&set, SIGINT); //将2号信号SIGINT加入set
sigprocmask(SIG_BLOCK, &set, &oldset);//位操作
while (1){
sigpending(&ped);
printset(&ped);
sleep(1);
}
return 0;
}
编译该案例,执行程序,终端会不断打印进程PCB中的未决信号集。初始情况下进程未决信号集中的每一位都应为0,因此打印的信息如下:
使用kill命令或组合按键Cturl+C驱使内核发送信号SIGINT给当前进程。进程第一次接收到信号SIGINT后,sigproemask()函数被触发,此后终端打印的信息如下:
之后继续向进程发送SIGINT信号,终端打印信息不变,说明信号SIGINT被成功屏蔽。
信号捕获
signal()
案例5:为2号信号SIGINT设置自定义信号处理函数,并在信号处理函数中将函数恢复为默认值。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
void sig_int(int signo) //自定义信号处理函数
{
printf(".........catch you,SIGINT\n");
signal(SIGINT, SIG_DFL); //信号处理函数执行
}
int main()
{
signal(SIGINT, sig_int); //捕获信号SIGINT,修改信号处理函数
while (1); //等待信号递达
return 0;
}
sigaction()函数
案例6:使用sigacign()函数修改2号信号SIGINT的默认动作。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
void sig_int(int signo)
{
printf("...........catch you,SIGINT,signo=%d\n", signo);
sleep(5); //模拟信号处理函数执行时间
}
int main()
{
struct sigaction act, oldact;
act.sa_handler = sig_int; //修改信号处理函数指针
sigemptyset(&act.sa_mask); //初始化位图,表示不屏蔽任何信号
sigaddset(&act.sa_mask, SIGINT); //更改信号SIGINT的信号处理函数
act.sa_flags = 0; //设置flags,屏蔽自身所发信号
sigaction(SIGINT, &act, &oldact);
while (1);
return 0;
}
sleep()函数自实现
案例7:mysleep()函数自实现
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void sig_alrm(int signo)
{
//do something...
}
unsigned int mysleep(unsigned int seconds)
{
struct sigaction newact, oldact;
unsigned int unslept;
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact); //屏蔽信号SIGALRM
alarm(seconds); //倒计时
sigaction(SIGALRM, &oldact, NULL); //解除信号屏蔽
pause(); //挂起等待信号
return alarm(0); //返回
}
int main()
{
while (1){
mysleep(2);
printf("two seconds passed.\n");
}
return 0;
}
这里实现的mysleep()函数中使用计时器alarm()函数作为计时工具,进入睡眠状态的进程不应有其他操作,因此使用pause()函数将程序挂起;
另外为了保证进程在进入况睡状态后不被由其他进程发送的SIGALRM信号干扰,计时器启动之前应先屏蔽SIGALRM信号:在计时器计时结束后,SIGALRM信号将进程唤醒,此时进程应能接收SIGALRM信号,因此在pause()之前调用sigaction()函数解除了屏蔽;
最后返回alarm(0),因为alarm(0)默认返回0或上一个计时器的剩余秒数,所以mysleep()函数直接返回alarm(0)的返回值即可。此外,alarm(0)也是取消计时的一个安全方法。
根据程序的执行结果可知,自实现的mysleep()函数实现了sleep()函数的功能,但其实这个函数仍是存在问题的。这就是我们接下来要讲解的程序执行的时序问题时序竞态。
时序竞态
案例8:使用alarm()和sigsuspend()自实现mysleep()函数
#include <stdio.h>
#include <signal.h>
#include <stdio.h>
void sig_alrm(int signo)
{
//do something...
}
unsigned int mysleep(unsigned int seconds)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
//①为SIGALRM设置捕捉函数
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
//②设置阻塞信号集,屏蔽SIGALRM信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
//③设置计时器
alarm(seconds);
//④构造临时阻塞信号集
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
//⑤采用临时阻塞信号集suspmask替换原有阻塞信号集(不包含SIGALRM信号)
sigsuspend(&suspmask); //挂起进程,等待信号递达
unslept = alarm(0);
//⑥恢复SIGALRM原有的处理动作,呼应注释①
sigaction(SIGALRM, &oldact, NULL);
//⑦解除对SIGALRM的屏蔽,呼应注释②
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return unslept;
}
int main()
{
while (1){
mysleep(2);
printf("two seconds passed\n");
}
return 0;
}
SIGCHLD信号
案例9:使用信号捕捉函数浦获SIGCHLD信号,实现子进程的回收。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int signo) //信号处理函数
{
waitpid(0, NULL, WNOHANG);
}
int main(void)
{
pid_t pid;
int i;
for (i = 0; i < 5; i++) {
//子进程创建
if ((pid = fork()) == 0)
break;
else if (pid < 0) //容错处理
sys_err("fork");
}
if (pid == 0) {
//子进程分支
int n = 1;
while (n--) {
printf("child ID %d\n", getpid());
}
exit(i + 1);
}
else if (pid > 0) {
//父进程分支
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
while (1) {
printf("Parent ID %d\n", getpid());
sleep(1);
}
}
return 0;
}
案例10:使用信号捕提函数捷获SIGCHLD信号,实现多个子进程的回收。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int signo) //信号处理函数
{
int status;
pid_t pid;
while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
//判断子进程状态
if (WIFEXITED(status))
printf("child %d exit %d\n", pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
}
}
int main(void)
{
pid_t pid;
int i;
for (i = 0; i < 10; i++) {
if ((pid = fork()) == 0) //创建一个子进程
break;
else if (pid < 0) //容错处理
sys_err("fork");
}
if (pid == 0) {
//子进程执行流程
int n = 1;
while (n--) {
printf("child ID %d\n", getpid());
sleep(1);
}
return i + 1;
}
else if (pid > 0) {
//父进程执行流程
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL); //注册捕获函数
while (1) {
//保证父进程运行
printf("Parent ID %d\n", getpid());
sleep(1);
}
}
return 0;
}
本章主要介绍了Linux系统中信号的概念、产生方式以及信号的相关操作。通过本章的学习,读者应掌握信号的基本概念,包括信号产生条件、信号状态、处理方式和默认处理动作等,并熟练使用与信号操作相关的函数。除此之外,本章还介绍了在编程中使用信号时可能出现的时序问题以及使用信号回收子进程的方法,这也是信号学习中应着重掌握的知识。信号是Linux系统编程中非常重要的一部分知识,读者应做到在程序中熟练使用信号,达到优化程序的目的。