[Linux环境编程] 信号的基本概念与操作函数
一、基本概念
1、中断的基本概念
中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。
  而在Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异常)。
   硬中断:来自硬件设备的中断
   软中断:来自其它程序的中断
2、信号的基本概念
信号是软件中断,提供了一种处理异步事件的方法,可以把他看作是进程与进程、内核与进程通信的一种方式。
3、信号的分类
信号分为不可靠信号与可靠信号,编号由1~31、34~64(31、32空缺)。
在早期Unix版本中信号都是不可靠的,其编号在1~31之间,这些信号是建立在早期的信号机制上的,其信号可能会丢失。为了尽量避免这种情况,一个事件发生可能会产生多次信号。不可靠信号不支持排除,在接收信号的时候可能会丢失,如果一个发给一个进程多次,它可能只接收到一次,其它的可能就丢失了。 早期版本的Linux中,进程在处理这种信号的时候,哪怕设置的信号处理函数,当信号处理函数执行完毕后,会再次恢复成默认的信号处理方式。
之后经过信号机机制的更改,设计了34~64编号的可靠信号。可靠信号相比不可靠信号而言,它支持排除,并且不会丢失。即使可靠信号被递送到当前要阻塞的信号集,被屏蔽的可靠信号也会按照顺序“排队”,处于未决状态而不是丢失。被解除限制后会按照顺序依次接收。
如果将不可靠信号比作是“女朋友要你送礼物”的话(发出多次信号,你最终可能只会执行一次),那么可靠信号就是“老板给你安排任务”(你可以暂时不做,但最终还是要有序完成)。其中无论是可靠信号还是不可靠信号都是通过一些同样的信号处理函数进行处理,并无区别。
不可靠信号如下:
| 信号 | 解释 | 产生条件 | 默认动作 | 
|---|---|---|---|
| SIGHUP(1) | 连接断开信号 | 如果终端接口检测一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程) | 终止 | 
| SIGINT(2) | 终端中断符信号 | 用户按中断键(Ctrl+C),产生此信号,并送至前台进程组的所有进程 | 终止 | 
| SIGQUIT(3) | 终端退出符信号 | 用户按退出键(Ctrl+),产生此信号,并送至前台进程组的所有进程 | 终止+core | 
| SIGILL(4) | 非法硬件指令信号 | 进程执行了一条非法硬件指令 | 终止+core | 
| SIGTRAP(5) | 硬件故障信号 | 指示一个实现定义的硬件故障。常用于调试 | 终止+core | 
| SIGABRT(6) | 异常终止信号 | 调用abort函数,产生此信号 | 终止+core | 
| SIGBUS(7) | 总线错误信号 | 指示一个实现定义的硬件故障,常用于内存故障 | 终止+core | 
| SIGFPE(8) | 算术异常信号 | 表示一个算术运算异常,例如除以0、浮点溢出等 | 终止+core | 
| SIGKILL(9) | 终止信号 | 不能被捕获或忽略。常用于杀死进程 | 终止 | 
| SIGUSR1(10) | 用户定义信号 | 用户定义信号,用于应用程序 | 终止 | 
| SIGSEGV(11) | 段错误信号 | 试图访问未分配的内存,或向没有写权限的内存写入数据 | 终止+core | 
| SIGUSR2(12) | 用户定义信号 | 用户定义信号,用于应用程序 | 终止 | 
| SIGPIPE(13) | 管道异常信号 | 写管道时读进程已终止,或写SOCK_STREAM类型套接字时连接已断开,均产生此信号 | 终止 | 
| SIGALRM(14) | 闹钟信号 | 以alarm函数设置的计时器到期,或以setitimer函数设置的间隔时间到期,均产生此信号 | 终止 | 
| SIGTERM(15) | 终止信号 | 由kill命令发送的系统默认终止信号 | 终止 | 
| SIGSTKFLT(16) | 数协器栈故障信号 | 表示数学协处理器发生栈故障 | 终止 | 
| SIGCHLD(17) | 子进程状态改变信号 | 在一个进程终止或停止时,将此信号发送给其父进程 | 忽略 | 
| SIGCONT(18) | 使停止的进程继续 | 向处于停止状态的进程发送此信号,令其继续运行 | 继续/忽略 | 
| SIGSTOP(19) | 停止信号 | 不能被捕获或忽略。停止一个进程 | 停止进程 | 
| SIGTSTP(20) | 终端停止符信号 | 用户按停止键(Ctrl+Z),产生此信号,并送至前台进程组的所有进程 | 停止进程 | 
| SIGTTIN(21) | 后台读控制终端信号 | 后台进程组中的进程试图读其控制终端,产生此信号 | 停止 | 
| SIGTTOU(22) | 后台写控制终端信号 | 后台进程组中的进程试图写其控制终端,产生此信号 | 停止 | 
| SIGURG(23) | 紧急情况信号 | 有紧急情况发生,或从网络上接收到带外数据,产生此信号 | 忽略 | 
| SIGXCPU(24) | 超过CPU限制信号 | 进程超过了其软CPU时间限制,产生此信号 | 终止+core | 
| SIGXFSZ(25) | 超过文件长度限制信号 | 进程超过了其软文件长度限制,产生此信号 | 终止+core | 
| SIGVTALRM(26) | 虚拟闹钟信号 | 以setitimer函数设置的虚拟间隔时间到期,产生此信号 | 终止 | 
| SIGPROF(27) | 虚拟梗概闹钟信号 | 以setitimer函数设置的虚拟梗概统计间隔时间到期,产生此信号 | 终止 | 
| SIGWINCH(28) | 终端窗口大小改变信号 | 以ioctl函数更改窗口大小,产生此信号 | 忽略 | 
| SIGIO(29) | 异步I/O信号 | 指示一个异步I/O事件 | 终止 | 
| SIGPWR(30) | 电源失效信号 | 电源失效,产生此信号 | 终止 | 
| SIGSYS(31) | 非法系统调用异常 | 指示一个无效的系统调用 | 终止+core | 
 4、信号的来源
信号的来源主要分为硬件来源与软件来源。
    硬件来源:
     键盘:Ctrl+c 终端中断信号、Ctrl+z 终端暂停信号、Ctrl+/终端退出信号
     驱动:硬件设备被激活、使用、失效
     内存:非法访问内存
  软件来源:
     命令:kill -信号 进程号、killall -信号 程序名(普通用户只能给自己的进程发信号,超级用户可以能任意进程发送信号)
     函数:kill/raise/alarm/setitimer/sigqueue
 5、信号的处理方式
  忽略信号:大多数信号都是这种处理方式。
  捕捉信号:当某种信号产生时,用户可以捕捉该信号,去调用一个用户自行设计的函数,以达到用户所期望的处理效果。
  默认操作:系统对每个信号都有默认操作,种类有:忽略、继续、终止、终止+core、停止进程。要注意的是,系统对大多数信号的默认操作是终止该进程
值得注意的是,SIGKILL(9)与SIGSTOP(19)无法被忽略和捕捉,因为他们向内核和超级用户提供了终止或停止进程的可靠方法。
   core文件是一种二进制文件,需要一些高度工具才能解析出来。在Ubuntu系统下,默认不产生core,若要生成core文件则需要使用命令设置:ulimit -c unlimited。以下是core文件的处理方式:
   1、执行gcc -g code.c 生成带调试信息的可执行文件;
  2、运行可执行文件产生core文件;
   3、执行gdb ./a.out core 后程序会停止在产生错误的位置。
6、信号集
信号集是一种数据类型,一共由128个二进制位组成,每个二进制位表示一个信号,代表的是一系列信号的集合,一般用于信号的屏蔽。
值得注意的是,由于C语言编译程序将不赋初值的局部变量和外部变量都初始化为0,而这是否与给定系统上信号集的实现相对应却并不清楚,所以在所有应用程序使用信号集之前要对该信号集调用sigemptyset(清空信号集)或sigfillset(填充信号集)一次。
7、子进程的信号处理
因为子进程在被fork创建时会复制父进程的数据空间、堆和栈,信号捕捉函数的地址在子进程中是有意义的,所以通过fork创建的子进程会继承父进程的信号处理方式。而通过vfork+exec创建的子进程所运行的新程序会将原先从父进程复制来的数据覆盖掉,因此在子进程中会父进程将原先设置为要捕捉的信号都更改为默认动作。
二、信号的捕获和处理
1、信号捕获 signal
1        #include <signal.h>
2 
3        typedef void (*sighandler_t)(int);
4 
5        sighandler_t signal(int signum, sighandler_t handler); 
其功能是向内核注册一个信号处理函数,捕捉指定信号触发指定函数。
   signum:信号的编号,可以直接写数字,也可以使用系统提供的宏(SIG_IGN 忽略信号、SIG_DFL 恢复信号默认的处理方式)
   handler:函数指针,即指定函数的函数名
   返回值  :之前信号的处理方式
在有些早期Unix系统中,向内核注册的信号处理函数在执行一次后会被恢复成默认的处理方式,如果想继续使用该信号处理函数,就得在每次的处理函数结束时再次注册。
2、信号发送 kill & raise
1        #include <sys/types.h>
2        #include <signal.h>
3 
4        int kill(pid_t pid, int sig);
5 
6 
7        #include <signal.h>
8 
9        int raise(int sig); 
   int kill(pid_t pid, int sig);
   功能:向指定的进程发送信号
   pid:1、pid大于 0时,pid为接收信号的进程号。
      2、pid等于 0时,信号将发送给所有与调用该函数的用户属同一个使用组的进程。
      3、pid等于-1时,信号将发送给所有调用该函数的进程有权给其发送信号的进程,除了进程1(init)。
      4、pid小于-1时,信号将发送给以pid绝对值为组ID的所有进程。
   sig:将要发送的信号,0表示空信号,不会向进程发送信号,但是会测试是否能向pid发送信号,这样可以检测一个进程是否存在,返回-1表示进程不存在,errno为ESRCH。但这种测试并非原子操作,所以这种测试并无多大价值。
   返回值:-1,说明进程不存在
   int raise(int sig);
   功能:向自己发送信号 
3、闹钟与休眠 alarm & pause
1        #include <unistd.h>
2 
3        unsigned int alarm(unsigned int seconds);
4 
5 
6        #include <unistd.h>
7 
8        int pause(void); 
alarm函数是一个闹钟函数,当时间超过所设置的seconds时会向程序发送一个SIGARLM信号。返回值为0或以前设置的闹钟时间的余留秒数。需要注意的是,SIGALRM信号的默认处理方式是直接退出。
pause函数从功能上来讲它相当于没有时间限制的sleep函数,进程调用了pause函数后会进程睡眠状态,直到有不被忽略的信号把它叫醒。当信号来临后,先执行信号处理函数,信号处理函数结束后pause再返回。pause函数要么不返回(一直睡眠),要么返回-1,并且修改全局变量errno的值。
4、信号集处理函数
 1        #include <signal.h>
 2 
 3        int sigemptyset(sigset_t *set); 
 4 
 5        int sigfillset(sigset_t *set);
 6 
 7        int sigaddset(sigset_t *set, int signum);
 8 
 9        int sigdelset(sigset_t *set, int signum);
10 
11        int sigismember(const sigset_t *set, int signum); 
清空/填充信号集:sigemptyset/sigfillset、添加/删除信号集:sigdelset/sigdelset、查询信号集成员中是否包含指定信号:sigismember
5、信号屏蔽 sigprocmack
每个进程都有一个信号掩码(signal mask),它就是一个信号集,里面包含了进程所屏蔽的信号。
 1        #include <signal.h>
 2 
 3        int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
 4 
 5 
 6        SIG_BLOCK
 7               The set of blocked signals is the union of the current  set  and
 8               the set argument.
 9 
10        SIG_UNBLOCK
11               The  signals  in set are removed from the current set of blocked
12               signals.  It is permissible to attempt to unblock a signal which
13               is not blocked.
14 
15        SIG_SETMASK
16               The set of blocked signals is set to the argument set. 
   功能:设置进程的信号掩码(信号屏蔽码)
   how:修改信号掩码的方式
     SIG_BLOCK:向信号掩码中添加信号
     SIG_UNBLOCK:从信号掩码中删除信号
     SIG_SETMASK:用新的信号集替换旧的信号掩码
   newset:新添加、删除、替换的信号集,也可以为空
   oldset:获取旧的信号掩码
   当newset为空时,就是在备份信号掩码
   当进程执行一些敏感操作时不希望被打扰,例如需要原子操作时,此时需要向屏蔽信号。屏蔽信号的目的不是为了不接收信号,而是延时接收,当处理完要做的事情后,应该把屏蔽的信号还原。当信号屏蔽时发生的信号会记录一次,这个信号设置为末决状态,当信号屏蔽结束后,会再发送一次。
   不可靠信号在信号屏蔽期间无论信号发生多少次,信号解除屏蔽后,只发送一次。
   可靠信号在信号屏蔽期间发生的信号会排队记录,在信号解除屏蔽后逐个处理。
   在执行处理函数时,会默认把当前处理的信号屏蔽掉,执行完成后再恢复。
6、获取未决信号 sigpending
1      #include <signal.h>
2
3      int sigpending(sigset_t *set); 
功能:获取末决状态的信号
7、信号处理 sigaction
 1        #include <signal.h>
 2 
 3        int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
 4        
 5 
 6         The sigaction structure is defined as something like:
 7 
 8            struct sigaction {
 9                void     (*sa_handler)(int); // 信号处理函数指针
10                void     (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数指针 需要使用sigqueue发送信号
11                sigset_t   sa_mask; // 信号屏蔽码
12                int        sa_flags;
13                void     (*sa_restorer)(void);
14            }; 
  功能:设置或获取信号处理方式
   SA_NOCLDSTOP:忽略SIGCHLD信号
   SA_NODEFER/SA_NOMASK:在处理信号时不屏蔽信号
   SA_RESETHAND:处理完信号后,恢复系统默认处理方式
   SA_RESTART:当信号处理函数中断的系统调用,则重启系统调用。
   SA_SIGINFO:用sa_sigaction处理信号
8、信号排队 sigqueue
1        #include <signal.h>
2 
3        int sigqueue(pid_t pid, int sig, const union sigval value);
4 
5 
6            union sigval {
7                int   sival_int;
8                void *sival_ptr;
9            }; 
sigqueue函数只能把信号发给单个进程,可以使用value参数向信号处理程序传递整数和指针,除此之外,sigqueue函数与kill函数相似。需要留意的是,信号不能无限排队,其个数受到QUEUE_MAX的限制。
来源:oschina
链接:https://my.oschina.net/u/4275712/blog/3883759