目录
一、信号基础
信号是事件发生时对进程的通知机制,也可以把它称为软件中断。
信号的目的是用来通信的。
信号本质上是 int 类型数字编号,信号有如下宏定义:
/* Signals. */
#define SIGHUP 1 /* Hangup (POSIX). */
#define SIGINT 2 /* Interrupt (ANSI). */
#define SIGQUIT 3 /* Quit (POSIX). */
#define SIGILL 4 /* Illegal instruction (ANSI). */
#define SIGTRAP 5 /* Trace trap (POSIX). */
#define SIGABRT 6 /* Abort (ANSI). */
#define SIGIOT 6 /* IOT trap (4.2 BSD). */
#define SIGBUS 7 /* BUS error (4.2 BSD). */
#define SIGFPE 8 /* Floating-point exception (ANSI). */
#define SIGKILL 9 /* Kill, unblockable (POSIX). */
#define SIGUSR1 10 /* User-defined signal 1 (POSIX). */
#define SIGSEGV 11 /* Segmentation violation (ANSI). */
#define SIGUSR2 12 /* User-defined signal 2 (POSIX). */
#define SIGPIPE 13 /* Broken pipe (POSIX). */
#define SIGALRM 14 /* Alarm clock (POSIX). */
#define SIGTERM 15 /* Termination (ANSI). */
#define SIGSTKFLT 16 /* Stack fault. */
#define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
#define SIGCHLD 17 /* Child status has changed (POSIX). */
#define SIGCONT 18 /* Continue (POSIX). */
#define SIGSTOP 19 /* Stop, unblockable (POSIX). */
#define SIGTSTP 20 /* Keyboard stop (POSIX). */
#define SIGTTIN 21 /* Background read from tty (POSIX). */
#define SIGTTOU 22 /* Background write to tty (POSIX). */
#define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */
#define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */
#define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */
#define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */
#define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */
#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
#define SIGPOLL SIGIO /* Pollable event occurred (System V). */
#define SIGIO 29 /* I/O now possible (4.2 BSD). */
#define SIGPWR 30 /* Power failure restart (System V). */
#define SIGSYS 31 /* Bad system call. */
#define SIGUNUSED 31
以下是一些常用信号的编号、描述以及进程收到该信号时默认进行的操作。
信号名称 | 编号 | 描述 | 系统默认操作 |
SIGINT | 2 | 终端中断符 | term |
SIGQUIT | 3 | 终端退出符 | term+core |
SIGILL | 4 | 非法硬件指令 | term+core |
SIGABRT | 6 | 异常终止(abort) | term+core |
SIGBUS | 7 | 内存访问错误 | term+core |
SIGFPE | 8 | 算术异常 | term+core |
SIGKILL | 9 | 终极终止信号 | term |
SIGUSR1 | 10 | 用户自定义信号 1 | term |
SIGSEGV | 11 | 无效的内存引用 | term+core |
SIGUSR2 | 12 | 用户自定义信号 2 | term |
SIGPIPE | 13 | 管道关闭 | term |
SIGALRM | 14 | 定时器超时(alarm) | term |
SIGTERM | 15 | 终止进程 | term |
SIGCHLD/SIGCLD | 17 | 子进程终止或停止 | ignore |
SIGCONT | 18 | 使停止状态的进程继续运行 | cont |
SIGSTOP | 19 | 停止进程 | stop |
SIGTSTP | 20 | 终端停止符 | stop |
SIGXCPU | 24 | 超过 CPU 限制 | term+core |
SIGVTALRM | 26 | 虚拟定时器超时 | term |
SIGWINCH | 28 | 终端窗口尺寸发生变化 | ignore |
SIGPOLL/SIGIO | 29 | 异步 I/O | term/ignore |
SIGSYS | 31 | 无效系统调用 | term+core |
二、进程对信号进行处理
① signal函数
signal()函数是 Linux 系统下设置信号处理方式最简单的接口, 可将信号的处理方式设置为捕获信号、 忽略信号以及系统默认操作, 此函数原型如下所示:
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
signum: 此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
handler: sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处
理函数;参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设
置为 SIG_IGN 或 SIG_DFL, SIG_IGN 表示此进程需要忽略该信号, SIG_DFL 则表示设置为系统默认操作。
函数测试:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
printf("Received signal: %d\n", sig);
}
int main(void)
{
sig_t ret = NULL;
ret = signal(SIGINT, (sig_t)sig_handler); //绑定信号处理函数
if (SIG_ERR == ret) {
perror("signal error");
exit(-1);
}
/* 死循环 */
while(1);
exit(0);
}
通过 signal()函数将 SIGINT信号绑定到了一个用户自定的处理函数上sig_handler, 当进程收到 SIGINT 信号后会执行该函数然后运行 printf 打印语句。当运行程序之后,程序会卡在死循环处,此时在终端按下中断符 CTRL + C,系统便会给前台进程组中的每一个进程发送SIGINT 信号,我们测试程序便会收到该信号。
运行结果如下:
当一个进程调用 fork()创建子进程时,其子进程将会继承父进程的信号处理方式。
② sigaction()函数
sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
函数参数和返回值含义如下:
signum: 需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
act: act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式;如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
oldact: oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构。如果参数oldact 不为 NULL, 则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL。
返回值: 成功返回 0;失败将返回-1,并设置 errno。
struct sigaction 结构体
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler:指定信号处理函数,与 signal()函数的 handler 参数相同。
sa_sigaction:也用于指定信号处理函数,这是一个替代的信号处理函数,他提供了更多的参数,可以通
过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取,稍后介绍该数据结构; sa_handler 和sa_sigaction 是互斥的,不能同时设置, 对于标准信号来说, 使用 sa_handler 就可以了,可通过
SA_SIGINFO 标志进行选择。
sa_mask: 参数 sa_mask 定义了一组信号, 当进程在执行由 sa_handler 所定义的信号处理函数之前,会
先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号
从信号掩码字段中删除,实现信号屏蔽。
sa_restorer:该成员已过时,不要再使用了。
sa_flags: 参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志
(多个标志使用位或" | "组合):
下面编写一段代码利用sigaction函数实现与signal函数相同的功能:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
printf("Received signal: %d\n", sig);
}
int main(void)
{
struct sigaction sig; //定义一个sigaction变量
int ret;
sig.sa_handler = sig_handler;
sig.sa_flags = 0; //初始化
ret = sigaction(SIGINT, &sig, NULL);
if (-1 == ret) {
perror("sigaction error");
exit(-1);
}
/* 死循环 */
while(1);
exit(0);
}
实验结果如下:
可以看出要利用sigaction函数实现与signal函数相同的功能只需要实现3步骤:
① 定义一个sigaction变量
② 设置变量的sa_handler、sa_flags
③ 执行sigaction函数
三、向进程发送信号
① kill()函数
kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程, 其函数原型如下所示:
使用该函数需要包含头文件<sys/types.h>和<signal.h>。
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid: 参数 pid 为正数的情况下,用于指定接收此信号的进程 pid;除此之外,参数 pid 也可设置为 0 或
-1 以及小于-1 等不同值,稍后给说明。
sig: 参数 sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但任执
行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。
返回值: 成功返回 0;失败将返回-1,并设置 errno。
参数 pid 不同取值含义:
如果 pid 为正,则信号 sig 将发送到 pid 指定的进程。
如果 pid 等于 0,则将 sig 发送到当前进程的进程组中的每个进程。
如果 pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
如果 pid 小于-1,则将 sig 发送到 ID 为-pid 的进程组中的每个进程。
函数测试:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc,char *argv[])
{
int pid;
/* 判断传参个数 */
if (2 > argc)
exit(-1);
/* 将传入的字符串转为整形数字 */
pid = atoi(argv[1]);
printf("pid: %d\n", pid);
/* 向 pid 指定的进程发送信号 */
if (-1 == kill(pid, SIGINT)) {
perror("kill error");
exit(-1);
}
exit(0);
}
通过此函数向一个已经打开的进程发送SIGINT信号,可通过外部参数传入指定进程的pid号。
实验结果如下:
② raise()函数
raise()函数可用于实现向进程自身发送信号, raise()函数原型如下所示(此函数为 C库函数):
#include <signal.h>
int raise(int sig);
函数参数和返回值含义如下:
sig: 需要发送的信号。
返回值: 成功返回 0;失败将返回非零值。
raise(sig)函数等于kill(getpid(),sig)。
四、Alarm()和Pause()函数
① Alarm()函数
使用 alarm()函数可以设置一个定时器(闹钟) ,当定时器定时时间到时,内核会向进程发送 SIGALRM信号,其函数原型如下所示:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
seconds: 设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟。
返回值: 如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作
为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0。
测试函数:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
static void sig_handler(int sig)
{
puts("Alarm timeout");
exit(0);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
int second;
/* 检验传参个数 */
if (2 > argc)
exit(-1);
/* 为 SIGALRM 信号绑定处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (sigaction(SIGALRM, &sig, NULL) == -1) {
perror("sigaction error");
exit(-1);
}
/* 启动 alarm 定时器 */
second = atoi(argv[1]);
printf("定时时长: %d 秒\n", second);
alarm(second);
/* 循环 */
while(1);
exit(0);
}
运行结果如下:
程序调用alarm()函数设置了一个5s的定时闹钟,定时时间结束内核回向该进程发送一个SIGALRM信号,再SIGALRM信号处理函数中打印字符“Alarm timeout",并返回0。
② pause()函数
pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时, pause()才返回,在这种情况下, pause()返回-1,并且将 errno 设置为 EINTR。其函数原型如下所示:
#include <unistd.h>
int pause(void);
测试函数:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
puts("Alarm timeout");
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
int second;
/* 检验传参个数 */
if (2 > argc)
exit(-1);
/* 为 SIGALRM 信号绑定处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (sigaction(SIGALRM, &sig, NULL) == -1) {
perror("sigaction error");
exit(-1);
}
/* 启动 alarm 定时器 */
second = atoi(argv[1]);
printf("定时时长: %d 秒\n", second);
alarm(second);
/* 进入休眠状态 */
pause();
puts("休眠结束");
exit(0);
}
运行结果如下:
五、信号掩码
内核为每一个进程维护了一个信号掩码(其实就是一个信号集) ,即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理, 那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。
① 当应用程序调用 signal()或 sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中, 这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;sigaction()函数需要设置 SA_NODEFER 标志;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
② 还可以使用 sigprocmask()系统调用,随时可以显式地向信号掩码中添加/移除信号。
sigprocmask()函数原型如下所示:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how: 参数 how 指定了调用函数时的一些行为。
set: 将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为
NULL,则表示无需对当前信号掩码作出改动。
oldset: 如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,
存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码。
返回值: 成功返回 0;失败将返回-1,并设置 errno。
SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信
号掩码设置为当前值与 set 的并集。
SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除。
SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集。
测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("执行信号处理函数...\n");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t sig_set;
/* 注册信号处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGINT, &sig, NULL))
exit(-1);
/* 信号集初始化 */
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL))
exit(-1);
/* 向自己发送信号 */
raise(SIGINT);
/* 休眠 2 秒 */
sleep(2);
printf("休眠结束\n");
/* 从信号掩码中移除添加的信号 */
if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
exit(-1);
exit(0);
}
运行结果如下:
执行test程序首先绑定SIGINT信号的处理函数,然后向进程的信号掩码中添加SIGINT信号,此时调用raise(SIGINT),向进程自己发送SIGINT信号,如果信号掩码没有生效会直接打印执行信号处理函数,由于成功添加信号掩码,会在进程睡眠2s后移除信号掩码后再执行信号处理函数。