一、概述
- 信号是UNIX中所使用的进程通信的一种最古老的方法。它是在软件层次上对中断机制的一种模拟,是一种异步通信方式,用来通知进程发生了异步事件。在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
- 一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。
- 信号可以直接进行用户空间进程和内核进程之间的交互内核进程,也可以利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时侯发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
- 一个完整的信号生命周期可以分为3个重要阶段(产生(generation)、未决(pending)、递送(delivery)),这3个阶段由4个重要事件来刻画的:信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数。
- 信号分为不可靠信号和可靠信号:
① 不可靠信号处理过程:如果发现该信号已经在进程中注册,那么就忽略该信号。因此,若前一个信号还未注销又产生了相同的信号就会产生信号丢失。
② 可靠信号处理过程:信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此信号就不会丢失。 - 所有可靠信号都支持排队,而不可靠信号则都不支持排队。
二、信号产生
- 信号产生有以下几种情况:
① 用户:用户能够通过输入 CTRL+c、 Ctrl+\,或者是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号;
② 内核:当进程执行出错时,内核会给进程发送一个信号,例如非法段存取(内存访问违规)、浮点数溢出等。
③ 进程:一个进程可以通过系统调用 kill() 给另一个进程发送信号,一个进程可以通过信号和另外一个进程进行通信。
2.1 kill() /raise()
- kill()函数发送信号给进程或进程组(实际上,kill系统命令只是kil函数的一个用户接口)。它不仅可以中止进程(实际上发出 SIGKILL信号),也可以向进程发送其他信号。
- raise()函数允许进程向自身发送信号。
#include <sys/types.h>
#include <signal.h>
/* @function 向进程或进程组发送sig信号
* @param[in] pid > 0 向进程号为pid的进程发送信号
* @arg pid = 0, 往同一进程组且发送进程具有权限向其发送信号的所有进程发送信号。系统进程集中的进程除外
* @arg pid < 0, 将该信号发送给其进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程。系统进程集中的进程除外
* @arg pid = -1, 将该信号发送给发送进程有权限向它们发送信号的所有进程。系统进程集中的进程除外
* @param[in] sig 信号
* @return 成功:0, 失败:非0
* @note 系统进程集包括内核进程和init(pid为1)
*/
int kill(pid_t pid, int sig);
/* @function 向进程本身发送信号
* @param[in] sig 信号
* @return 成功:0, 失败:非0
*/
int raise(int sig);
2.2 alarm()
- alarm函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,产生 SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程。
#include <unistd.h>
/* @function 设定的闹钟时间到达,向调用进程发送SIGALRM信号
* @param[in] seconds 秒数
* @return 成功:0或上次调用alarm剩余秒数,失败:-1
* @note alarm可以多次调用,但以最后一次调用为准。
*/
unsigned int alarm(unsigned int seconds);
三、信号处理
- 用户进程对信号的响应可以有3种方式:
① 忽叙略信号:即对信号不做任何处理,但是有两个信号不能忽略,即 SIGKILL及SIGSTOP。
② 捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
③ 执行缺省操作: Linux对每种信号都规定了默认操作。- 常见信号的含义及系统默认操作,用:
kill -l
命令查看
- 常见信号的含义及系统默认操作,用:
信号名 | 含义 | 默认操作 |
---|---|---|
SIGHUP | 该信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一会话内的各个作业与控制终端不再关联 | 终止 |
SIGINT | 该信号在用户键入INTR字符(通常是CtrI+C)时发出,终端驱动程序发送此信号并送到前台进程中的每一个进程 | 终止 |
SIGQUIT | 该信号和 SIGINT类似,但由QUIT字符(通常是Ctrl+\ )来控制 | 终止 |
SIGILL | 该信号在一个进程企图执行一条非法指令时(可执行文件本身出现错误,或者试图执行数据段、堆栈溢出时)发出 | 终止 |
SIGFPE | 该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出及除数为0等其他所有的算术的错误 | 终止 |
SIGKILL | 该信号用来立即结束程序的运行,并且不能被阻塞、处理和忽略 | 终止 |
SIGALRM | 该信号当一个定时器到时的时候发出 | 终止 |
SIGSTOP | 该信号用于暂停一个进程,且不能被阻塞、处理或忽略 | 暂停进程 |
SIGTSTP | 该信号用于交互停止进程,用户可键入SUSP字符时(通常是Ctrl+Z)发出这个信号 | 停止进程 |
SIGCHLD | 子进程改变状态时,父进程会收到这个信号 | 忽略 |
3.1 signal() / pause()
- signal() 函数用于接收信号,并进行相应的处理。
- pause() 函数是用于将调用进程挂起直至捕捉到信号为止。这个函数很常用,通常可以于判断信号是否已到。
#include <signal.h>
#define SIG_DFL ((__force __sighandler_t)0) /* default signal handling */
#define SIG_IGN ((__force __sighandler_t)1) /* ignore signal */
#define SIG_ERR ((__force __sighandler_t)-1) /* error return from signal */
//__sighandler_t等同于sighandler_t
//由 signal 函数注册,注册以后,在整个进程运行过程中均有效,
//并且对不同的信号可以注册同一个信号处理函数。
//该函数只有一个整型参数,表示信号值。
typedef void (*sighandler_t)(int);
/* @function 捕获信号,并调用相关信号处理函数
* @param[in] signum 要捕获的信号
* @param[in] handler 信号处理函数,也可以为下面参数
* @arg SIG_IGN 忽略该信号(SIGSTOP、SIGKILL不能被忽略)
* @arg SIG_DFL 采用系统默认方式处理
* @return 成功:以前信号处理的配置, 失败:SIG_ERR
* @note signal不阻塞进程
*/
sighandler_t signal(int signum, sighandler_t handler);
/* @function 用于将调用进程挂起直至捕捉到信号为止。
* @return 只在捕获信号并返回信号捕获函数时返回。在这种情况下返回-1,errno设置为EINTR。
*/
int pause(void);
- 简单应用例程
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#define SIG_USR1 24
void signal_handler_func(int signum)
{
switch (signum) {
case SIG_USR1:
printf("user1 signal... \r\n");
break;
case SIGUSR2:
printf("user2 signal... \r\n");
break;
default:
printf("others signal... \r\n");
}
}
int main(int argc, char **argv)
{
if (signal(SIG_USR1, signal_handler_func) == SIG_ERR)
printf("signal user1 err \r\n");
if (signal(SIGUSR2, signal_handler_func) == SIG_ERR)
printf("signal user2 err \r\n");
printf("xxxxx\r\n");
for (;;)
pause();
return 0;
}
在 signal 处理机制下,还有许多特殊情况需要考虑:
① 注册一个信号处理函数,并且处理完毕一个信号之后,是否需要重新注册,才能够捕捉下一个信号; (不需要)
② 如果信号处理函数正在处理信号,并且还没有处理完毕时,又发生了一个同类型的信号,这时该怎么处理; (挨着执行)
③ 如果信号处理函数正在处理信号,并且还没有处理完毕时,又发生了一个不同类型的信号,这时该怎么处理; (跳转去执行另一个信号,之后再执行剩下的没有处理完的信号)
④ 如果程序阻塞在一个系统调用(如 read(…))时,发生了一个信号,这时是让系统调用返回错误再接着进入信号处理函数,还是先跳转到信号处理函数,等信号处理完毕后,系统调用再返回
3.2 信号集处理
- 使用信号集函数组处理信号时涉及一系列的函数,这些函数按照调用的先后次序可分为以下几大功能模块:创建信号集合、登记信号处理器、检测信号。
(1) 创建信号集合主要用于创建用户感兴趣的信号,其函数包括以下几个:
#include <signal.h>
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
int sigemptyset(sigset_t * set); //清空信号集合 set
int sigfillset(sigset_t * set); //将所有信号填充进 set 中
int sigaddset(sigset_t * set, int signum); //往 set 中添加信号 signum
int sigdelset(sigset_t * set, int signum); //从 set 中移除信号 signum
//上面四个函数返回值:成功:0, 失败:-1
int sigismember(const sigset_t * set, int signum); //判断 signnum 是不是包含在 set 中
//sigismember 返回值:若包含:1,不包含:0
(2) 登记信号处理器主要用于决定进程如何处理信号。这里要注意的是:信号集里的信号并不是真正可以处理的信号,只有当信号的状态处于非阻塞状态时才真正起作用。因此首先就要判断出当前阻塞能不能传递给该信号的信号集。这里首先使用 sigprocmask函数判断检测或更改信号屏蔽字,然后使用 sigaction函数用于改变进程接收到特定信号之后的行为。
- sigprocmask
- sigprocmask最重要的一个作用就是:我无法决定信号什么时候来,但我可以决定信号什么时候被响应。
#include <signal.h>
/* @function 检查或修改(或检查并修改)进程的信号屏蔽字
* @param[in] how
* @arg SIG_BLOCK,将参数 2 的信号集合添加到进程原有的阻塞信号集合中
* @arg SIG_UNBLOCK,从进程原有的阻塞信号集合移除参数 2 中包含的信号
* @arg SIG_SETMASK,重新设置进程的阻塞信号集为参数 2 的信号集
* @param[in] set 为阻塞信号集
* @param[out] oldset 是传出参数,存放进程原有的信号集,通常为 NULL
* @return 成功:0, 失败:-1
*/
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- sigaction
#include <signal.h>
/* @member sa_handler 一个函数指针, 用于指向原型为 void handler(int)的信号处理函数地址,
* 即老类型的信号处理函数(如果用这个再将 sa_flags = 0,就等同于 signal()函数)
* @member sa_sigaction 也是一个函数指针,用于指向原型为:void handler(int iSignNum, siginfo_t *pSignInfo, void *pReserved);
* 的信号处理函数,即新类型的信号处理函数该函数的三个参数含义为:
* iSignNum:传入的信号;
* pSignInfo:与该信号相关的一些信息,它是个结构体;
* pReserved:保留,现没用,通常为 NULL
* @member sa_mask 是一个包含信号集合的结构体, 该结构体内的信号表示在进行信号处理时,将要被阻塞的信号。
* @member sa_flags 是一组掩码的合成值,指示信号处理时所应该采取的一些行为
* @arg SA_RESETHAND 处理完毕要捕捉的信号后,将自动撤消信号处理函数的注册,
* 即必须再重新注册信号处理函数,才能继续处理接下来产生的信号。
* 该选项不符合一般的信号处理流程,现已经被废弃。
* @arg SA_NODEFER 在处理信号时,如果又发生了其它的信号,则立即进入其它信号
* 的处理,等其它信号处理完毕后,再继续处理当前的信号,即递
* 规地处理。如果 sa_flags 包含了该掩码,则结构体 sigaction
* 的sa_mask 将无效!(不常用)
* @arg SA_RESTART 如果在发生信号时,程序正阻塞在某个系统调用,例如调用 read()
* 函数,则在处理完毕信号后,接着从阻塞的系统返回。如果不指定该
* 参数,中断处理完毕之后, read 函数读取失败。
* @arg SA_SIGINFO 指示结构体的信号处理函数指针是哪个有效,如果 sa_flags 包含该
* 掩码,则 sa_sigaction 指针有效,否则是 sa_handler 指针有效。(常用)
*/
struct sigaction {
void (*sa_handler)(int); //是
void (*sa_sigaction)(int, siginfo_t *, void *);//新类型的信号处理函数指针
sigset_t sa_mask; //将要被阻塞的信号集合
int sa_flags; //信号处理方式掩码 (SA_SIGINFO )
void (*sa_restorer)(void); //保留,不要使用
};
/* @function 检查或修改(或检查并修改)与指定信号相关联的处理动作。
* @param[in] signum 要捕获的信号
* @param[in] act 指向结构 sigaction的一个实例的指针,指定对特定信号的处理
* @param[in] oldact 指向结构 sigaction的一个实例的指针,指定对特定信号的处理
* @return 成功:0, 失败:-1
*/
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
(3) 检测信号是信号处理的后续步骤,但不是必须的。由于内核可以在任何时刻向某一进程发出信号,因此,若该进程必须保持非中断状态且希望将某些信号阻塞,这些信号就处于“未决”状态(也就是进程不清楚它的存在)。所以,在希望保持非中断进程完成相应的任务之后就应该将这些信号解除阻塞。 sigpending函数就允许进程检测“未决”信号,并进一步决定它们作何处理。
- sigpending
sigpending函数的作用是获取被设置为SIG_BLOCK的信号集。即将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中。基本不会用到
信号集简单例程
- 实例首先把 SIGQUIT、 SIGINT两个信号加入信号集,然后将该信号集设为阻塞状态并在该状态下使程序暂停5秒。接下来再将信号集设置为非阻塞状态,再对这两个信号分别操作,其中 SIGQUIT执行默认操作,而 SIGINT执行用户自定义函数的操作。
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define SIG_USR1 24
void signal_handler_func(int signum)
{
printf("If you want to quit, please try SIGQUIT\n");
}
int main(int argc, char **argv)
{
sigset_t set, pendset;
struct sigaction action1, action2;
if (sigemptyset(&set) < 0)
perror("sigemptyset");
if (sigaddset(&set, SIGINT) < 0)
perror("sigaddset");
else {
printf("blocked\n");
sleep(5);
}
if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
perror("sigprocmask");
else
printf("unblock\n");
while(1) {
if (sigismember(&set, SIGINT)) {
sigemptyset(&action1.sa_mask);
action1.sa_handler = signal_handler_func;
sigaction(SIGINT, &action1, NULL);
} else if (sigismember(&set, SIGQUIT)) {
sigemptyset(&action2.sa_mask);
action2.sa_handler = SIG_DFL;
sigaction(SIGTERM, &action2, NULL);
}
}
}
- 该程序的运行结果:在信号处于阻塞状态时,所发出的信号对进程不起作用。需等待5秒,在信号接触阻塞状态之后,用户发出的信号才能正常运行。这里 SIGINT已按照用户自定义的函数运行