APUE学习之信号(Signal)

发布时间:2024年01月25日

目录

一、信号

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、信号种类

????????经过上面的学习,我们已经知道了如何查看信号,那么每个信号都代表着什么意思呢?该信号的默认动作又是什么呢?让我们继续往下学习吧!(红色信号为常见信号,其余信号了解即可)

信号编号信号名信号说明默认动作
1SIGHUP在终端的控制进程结束时发出程序终止
2SIGINTCTRL+c按键终止程序运行的信号程序终止
3SIGQUITCTRL+\按键输入时产生的信号程序终止
4SIGILL非法的指令程序终止
5SIGTRAP跟踪自陷,由断点指令或其它trap指令产生建立CORE文件
6SIGABRT当调用abort函数时会产生当前信号程序终止
7SIGBUS运行非本CPU相关编译器编译的程序程序终止
8SIGFPE算术异常时产生建立CORE文件
9SIGKILL强制杀死程序序号,任何程序都不可以捕捉该信号程序终止,不可被捕捉
10SIGUSR1用户自定义信号1,不会自动产生,只能使用kill函数或者命令给指定的进程发送当前信号程序终止
11SIGSEGV段错误系统给程序发送的信号程序终止
12SIGUSR2用户自定义信号2,不会自动产生,只能使用kill函数或者命令给指定的进程发送当前信号程序终止
13SIGPIPE管道破裂信号程序终止
14SIGALRM当alarm函数设置的时间到达时,会产生当前信号程序终止
15SIGTERMkill命令默认发送的信号,默认动作是终止信号程序终止
16SIGSTKFLT数学协处理器的栈异常程序终止
17SIGCHLD子进程退出信号忽略该信号
18SIGCONT当产生当前信号后,当前停止的进程会恢复运行停止的进程恢复运行
19SIGSTOP停止进程的执行停止进程
20SIGTSTPCTRL+z按键输入时产生的信号,但该信号可以被处理和忽略停止进程
21SIGTTIN后台进程读终端停止进程
22SIGTTOU后台进程写终端停止进程
23SIGURG有"紧急"数据或out-of-band数据到达socket时产生忽略该信号
24SIGXCPU超出CPU限制程序终止
25SIGXFSZ文件长度过长程序终止
26SIGVTALRM虚拟定时器超时程序终止
27SIGPROF统计分布图用计时器到时程序终止
28SIGWINCH终端窗口尺寸发生变化忽略该信号
29SIGIO异步IO程序终止
30SIGPWR电力故障程序终止
31SIGSYS无效系统调用程序终止

?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、运行结果

? ? ? ? 大家是否理解这个运行结果呢?如图可以看出,进程一共接受到了两种信号,分别是SIGALRMSIGSEGV。为什么呢?接收到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学习之多进程编程》。我会坚持使用博客来整理自己所学知识,同时也希望能够帮助到大家,如果有哪些错误或者疑问,也欢迎大家在评论区一起讨论!

文章来源:https://blog.csdn.net/m0_65292176/article/details/135823186
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。