Linux下C语言开发(信号signal处理机制)

信号signal处理是Linux程序的一个特色,用信号处理来模拟操作系统的中断功能,对于系统程序员来说是最好的一个选择了。同样信号处理也是Linux编程中非常重要的部分,本文将详细介绍信号机制的基本概念、Linux对信号机制的大致实现方法、如何使用信号以及有关信号的几个系统调用。

信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称软中断。从它的命名可以看出,它的实质和使用很像中断,所有,信号可以说是进程控制的一部分。

一、信号的基本概念

本节先介绍信号的一些基本概念,然后给出一些基本的信号类型和信号对应的事件。基本概念对于理解和使用信号,对于理解信号机制都特别重要。下面就来看看什么是信号?

1、基本概念

软中断信号(signal,又简称为信号)用来通知进程发生了异常事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:

  1. 第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理;
  2. 第二种是忽略某个信号,对该信号不做任何处理,就像从未发生过一样;
  3. 第三种是对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。

进程通过调用signal来指定进程对某个信号的处理信号。

在进程表的表现中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不用的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少次。

2、信号的类型

发出信号的原因有很多,这里按发出信号的原因简单分类,以了解各种信号:

1)与进程终止相关的信号。当进程退出或子进程终止时,发出这类信号。

2)与进程例外事件相关的信号。如果进程越界或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。

3)与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。

4)与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。

5)在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。

6)与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。

7)跟踪进程执行的信号。

Linux支持的信号列表如下,运行命令 kill -l 我们可以看到Linux支持的信号列表

Linux下C语言开发(信号signal处理机制)

列表中,编号为1~31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32~63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

1)SIGHUP:本信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束的,通知同一session内的各个作用,这是它们与控制终端不再关联。

登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程和后台有对终端输出的进程将会收到SIGHUP信号,这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会终止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

2) SIGINT:程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

3) SIGQUIT:和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号。

4) SIGILL:执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

5) SIGTRAP:由断点指令或其它trap指令产生. 由debugger使用。

6) SIGABRT:调用abort函数生成的信号。

7) SIGBUS:非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

8) SIGFPE:在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

9) SIGKILL:用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

10) SIGUSR1:留给用户使用

11) SIGSEGV:试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

12) SIGUSR2:留给用户使用

13) SIGPIPE:管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

14) SIGALRM:时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

15) SIGTERM:程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

17) SIGCHLD:子进程结束时, 父进程会收到这个信号。

如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。

18) SIGCONT:让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

19) SIGSTOP:停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束,只是暂停执行. 本信号不能被阻塞, 处理或忽略.

20) SIGTSTP:停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

21) SIGTTIN:当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

22) SIGTTOU:类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

23) SIGURG:有"紧急"数据或out-of-band数据到达socket时产生.

24) SIGXCPU:超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

25) SIGXFSZ:当进程企图扩大文件以至于超过文件大小资源限制。

26) SIGVTALRM:虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

27) SIGPROF:类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

28) SIGWINCH:窗口大小改变时发出.

29) SIGIO:文件描述符准备就绪, 可以开始进行输入/输出操作.

30) SIGPWR:Powerfailure

31) SIGSYS:非法的系统调用。

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:SIGILL,SIGTRAP
默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

二、信号机制

上一节中介绍了信号的基本概念,在这一节中,我们将介绍内核如何实现信号机制。即内核如何向一个进程发送信号,进程如何接收一个信号、进程怎么控制自己对信号的反应、内核在什么时机处理和怎么样处理进程收到的信号。还要介绍一下setjmp和longjmp在信号中起到的作用。

1、内核对信号的基本处理方法

内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的实际是:一个进程在即将从内核态返回到用户态时,或者在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。

内核处理一个进程收到的信号实际是在一个进程从内核态返回用户态时,所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只用处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。内 核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。前面介绍概念的时候讲过,处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创 建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权 限)。

在信号的处理方法中有几点特别要引起注意。第一,在一些系统中,当一个进程处理完中断信号返回用户态之前,内核清除用户区中设 定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次使用signal系统调用。这可能会使得进程 在调用signal之前又得到该信号而导致退出。在BSD中,内核不再清除该地址。但不清除该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢 出。为了避免出现上述情况。在BSD系统中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。 

第二个要 引起注意的是,如果要捕捉的信号发生于进程正在一个系统调用中时,并且该进程睡眠在可中断的优先级上,这时该信号引起进程作一次longjmp,跳出睡眠 状态,返回用户态并执行信号处理例程。当从信号处理例程返回时,进程就象从系统调用返回一样,但返回了一个错误代码,指出该次系统调用曾经被中断。这要注意的是,BSD系统中内核可以自动地重新开始系统调用。 

第三个要注意的地方:若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不做longjmp,一般是继续睡眠。但用户感觉不到进程曾经被唤醒,而是象没有发生过该信号一样。 

第 四个要注意的地方:内核对子进程终止(SIGCLD)信号的处理方法与其他信号有所区别。当进程检查出收到了一个子进程终止的信号时,缺省情况下,该进程就象没有收到该信号似的,如果父进程执行了系统调用wait,进程将从系统调用wait中醒来并返回wait调用,执行一系列wait调用的后续操作(找 出僵死的子进程,释放子进程的进程表项),然后从wait中返回。SIGCLD信号的作用是唤醒一个睡眠在可被中断优先级上的进程。如果该进程捕捉了这个信号,就象普通信号处理一样转到处理例程。如果进程忽略该信号,那么系统调用wait的动作就有所不同,因为SIGCLD的作用仅仅是唤醒一个睡眠在可被 中断优先级上的进程,那么执行wait调用的父进程被唤醒继续执行wait调用的后续操作,然后等待其他的子进程。 

如果一个进程调用signal系统调用,并设置了SIGCLD的处理方法,并且该进程有子进程处于僵死状态,则内核将向该进程发一个SIGCLD信号。 
2、setjmp和longjmp的作用 
前面在介绍信号处理机制时,多次提到了setjmp和longjmp,但没有仔细说明它们的作用和实现方法。这里就此作一个简单的介绍。 
在 介绍信号的时候,我们看到多个地方要求进程在检查收到信号后,从原来的系统调用中直接返回,而不是等到该调用完成。这种进程突然改变其上下文的情况,就是使用setjmp和longjmp的结果。setjmp将保存的上下文存入用户区,并继续在旧的上下文中执行。这就是说,进程执行一个系统调用,当因为资 源或其他原因要去睡眠时,内核为进程作了一次setjmp,如果在睡眠中被信号唤醒,进程不能再进入睡眠时,内核为进程调用longjmp,该操作是内核为进程将原先setjmp调用保存在进程用户区的上下文恢复成现在的上下文,这样就使得进程可以恢复等待资源前的状态,而且内核为setjmp返回1,使 得进程知道该次系统调用失败。这就是它们的作用。

三、有关信号的系统调用

前面两节已经介绍了有关信号的大部分知识,这一节我们来了解这些系统调用。其中,系统调用signal是进程用来设定某个信号的处理方法,系统调用kill是用来发送信号给指定进程的。这两个调用可以形成信号的基本操作。后两个调用pause和alarm是通过信号实现的进程的暂停和定时器,调用alarm是通过信号通知进程定时器到时。所有在这里,我们还要介绍两个调用

1、signal系统调用

系统调用signal用来设定某个信号的处理方法,该调用声明格式如下:

void (*(signal)(int signum, void (*handler)(int)))(int);

在使用该调用的进程中加入以下头文件 #include <signal.h>

上述声明格式比较复杂,如果不清楚如何使用,也可以通过下面这种类型定义的格式来使用(POSIX的定义)

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

当这种格式在不同的系统中有不同的类型定义,所以要使用这种格式,最好还是参考一下联机手册

在调用中,参数signum指出要设置处理方法的信号,第二个参数handler是一个处理函数,或者是SIG_IGN:忽略参数signum所指的信号,或者是SIG_DFL:恢复参数signum所指的信号的处理方法为默认值。

传递给信号处理例程(信号处理方法)的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。系统调用signal返回值的指定信号signum前一次的处理例程或者错误时返回错误代码SIG_ERR。下面看一个简单的例子

创建一个signal.c文件,signal.c文件的内容如下:

Linux下C语言开发(信号signal处理机制)

保存后,输入gcc signal.c -o signal编译产生可执行文件signal,输入./signal运行signal文件

信号SIGINT检测,注意输入kill -INT 4071是需要另外打开一个终端,最后输入kill 4071杀死进程

Linux下C语言开发(信号signal处理机制)

信号SIGHUP和信号SIGQUIT检测,这里我只有通过kill发送信号检测,通过按键检测我没有成功。请多多关照,如果你们按键检测成功了,可否通过评论告知我,谢谢!

Linux下C语言开发(信号signal处理机制)

2、kill系统调用

系统调用kill用来向进程发送一个信号。该调用声明的格式如下:

int kill(pid_t pid, int sig);

在使用该调用的进程中加入以下头文件:#includ <sys/types.h>   #include <signal.h>

该系统调用可以用来向任何进程或进程组发送任何信号。如果参数pid是整数,那么该调用将信号sig发送到进程号为pid的进程。如果pid等于0,那么信号sig将发送到当前进程所属进程组里的所有进程。如果参数pid等于-1,信号sig将发送给除了进程1和自身以外的所有进程。如果参数pid小于-1,信号sig将发送给属于进程组-pid的所有进程。如果参数sig为0,将不发送信号。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码

EINVAL:指定的信号无效

ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程

ERERM:进程没有权利将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权限,或者是发出的进程的UID或EUID指定接收的进程的UID或保护用户ID(savedset-user-ID)相同。

如果参数pid小于-1,即该信号发送个一个组,则该错误表示组中有成员进程不能接收该信号。

将上面signal.c文件内容修改如下部分:

1)添加头文件#include <sys/types.h>

2)修改main函数,main函数修改如下

Linux下C语言开发(信号signal处理机制)

同样保存后,输入gcc signal.c -o signal编译产生可执行文件signal,输入./signal运行signal文件

Linux下C语言开发(信号signal处理机制)

我们也可以在另外一个.c文件中获取signal进程的pid,即我们可以通过Linux C程序来向指定的进程发送指定的信号。其主要难点就是如何来获取一致进程名来获取进程的pid,这个我可以查看我的另外一篇博客Linux下C语言开发(已知进程名得到其PID号)

3、pause系统调用

系统调用pause的作用是等待一个信号。该调用的声明格式如下:

int pause(void);

在使用该调用的进程中加入以下头文件:#include <unistd.h>

该调用使得发出调用的进程进入睡眠直到接收一个信号为止。该调用总是返回-1,并设置错误代码为EINTR(收到一个信号)。下面时一个简单的范例

创建一个pause.c文件,文件内容如下:

Linux下C语言开发(信号signal处理机制)

保存后,输入gcc pause.c -o pause编译生成二进制文件pause,并输入./pause运行pause进程

运行pause进程时:

Linux下C语言开发(信号signal处理机制)

按下Ctrl+C组合键,产生中断信号时:

Linux下C语言开发(信号signal处理机制)

这个例子中,程序开始执行,就像进入了死循环一样,就是因为进程正在等待信号,当我们按下Ctrl+C组合键时,信号被捕捉到,从而使得pause退出等待状态。

4、alarm和setitimer系统调用

系统调用alarm的功能是设置一个定时器,当定时器计时到达时将发出一个信号给进程。该调用的声明格式如下:

unsigned int alarm(unsigned int seconds);

在使用该调用的进程中加入以下头文件:#include <unistd.h>

系统调用alarm安排内核为调用进程在指定的seconds秒后发出一个SIGALRM的信号。如果指定的参数seconds为0,则不再发送SIGALRM信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间的剩余的时间,或者因为没有前一次定时调用而返回0.

注意,在使用时,alarm只是定为发送一次信号,如果要多次发送,就要多次使用alarm调用。

创建一个alarm.c文件,alarm.c文件的内容如下:

Linux下C语言开发(信号signal处理机制)

保存后,输入gcc alarm.c -o alarm编译生成二进制代码alarm。输入./pause运行之前我们已经编译好的pause进程,接着打开另外一个终端并运行alarm进程(alarm文件目录下输入./alarm)

Linux下C语言开发(信号signal处理机制)

而现在的系统中很多程序不在使用alarm调用了,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下:

int getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用这两个调用的进程中加入以下头文件:#include <sys/time.h>

该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号格进程,并使得计时器重新开始。三个计时器有参数which指定,如下所示:

ITIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。

ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。

ITIMER_PROF:当进程执行时和系统为该经常执行动作时都计时。与ITIMER_VIRTUAL是一对,该定时器经常用来统计进程在用户态和内核态话费的时间。计时达到将发送SIGPROF信号给进程。

定时器中的参数value用来致命定时器的时间,其结构如下

struct itimerval {

struct timeval it_interval;/*下一次的取值*/

struct timeval it_value;   /*本次的设定值*/

};

该结构体中timeval结构定义如下:

struct timeval {

long tv_sec;     /*秒*/

long tv_usec;  /*微秒,1秒 = 1000000微秒*/

};

在setitimer调用中,参数ovalue如果不为空,则其中保留的是上一次调用设定的值。定时器it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此反复。当it_value设定为0,计时器停止,或者当它计时到期,而it_interval为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:

EFAULT:参数value或ovalue是无效的指针。

EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或者ITIMER_PROF中的一个。

下面是关于setitimer调用的一个简单实例,在该例子中每隔一秒发出一个SIGALRM,每个0.5秒发出一个SIGVTALRM信号:

创建一个setitimer.c文件,setitimer.c文件内容如下:

Linux下C语言开发(信号signal处理机制)

保存后,输入gcc setitimer.c -o setitimer编译生成二进制代码setitimer,再次输入./setitimer运行setitimer进程,可以看到如下效果:

Linux下C语言开发(信号signal处理机制)