APUE学习之信号(Signal)

目录

一、信号

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

给大家看一下我系统所支持的信号:

APUE学习之信号(Signal)_第1张图片

        注意:信号本质上是 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   

 

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   

 

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 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 

 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学习之多进程编程》。我会坚持使用博客来整理自己所学知识,同时也希望能够帮助到大家,如果有哪些错误或者疑问,也欢迎大家在评论区一起讨论!

你可能感兴趣的:(APUE,学习,Linux环境编程)