信号
学习《Linux高性能服务器编程》第十章信号,为了印象深刻一些,多动手多实践,所以记下这个笔记。这一篇主要记录Linux中
Linux信号概述、信号集、信号函数和一些疑惑。
Linux信号概述
发送信号
Linux 下,一个进程给其他进程发送信号的API是kill
函数。其定义如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
这个函数把信号sig
发送给目标进程;目标进程pid
参数指定,其可能的取值以及含义如表所展示:
Linux
当中信号都大于0,如果sig
取值为0,则kill
函数不发送任何信号。这种方法可以用来检测目标进程或进程组是否存在,但是这种方法是不可靠的(这种方法不是原子操作)。
该函数成功时返回0,失败则返回-1并设置errno
。几种可能的errno
如表所示。
信号处理方式
在目标进程收到信息时,需要定义一个接收函数来处理。信号处理函数的原则如下:
#include <signal.h>
/* Type of a signal handler. */
typedef void (*__sighandler_t) (int);
信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。
除了用户自己定义信号处理函数之外,Linux当中还定义了信息号的两种其他处理方式:
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
SIG_IGN
表示忽略目标信号,SIG_DFL
表示使用信号默认处理方式。信号默认处理方式有如下几种:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件(Core)、暂停进程(Stop),以及继续进程(Cont)。
Linux信号
在linux上,可以使用kill -l
命令看到所有的信号,但是我们并不关心这些所有的信号,只用重点关心SIGHUP
、SIGPIPE
、SIGURG
、SIGALRM
、SIGCHLD
等几个信号即可。
信号 | 起源 | 默认行为 | 含义 |
---|---|---|---|
SIGHUP | POSIX | Term | 控制终端挂起 |
SIGPIPE | POSIX | Term | 往读端被关闭的管道或者socket连接中些数据 |
SIGURG | 4.2BSD | Ign | socket连接上接收到紧急数据 |
SIGALRM | POSIX | Term | 由alarm 或setitimer设置的实时闹钟超时引起 |
SIGCHLD | POSIX | Ign | 子进程状态发生变化(退出或者暂停) |
信号集
信号集函数
信号集sigset_t
的定义如下
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
#endif
由该定义可见,sigset_t
实际上是一个长整型数组,数组的每个元素的每个位表示一个信号(虽然不知道为啥定义多个元素)。这种定义方式和文件描述符集fd_set
类似。Linux
提供了如下一组函数来设置、修改、删除和查询信号集:
#include <signal.h>
int sigemptyset(sigset_t *set); /* 清空信号集 */
int sigfillset(sigset_t *set); /* 在信号集中设置所有的信息 */
int sigaddset(sigset_t *set, int signum); /* 将信号signum添加到信号集中 */
int sigdelset(sigset_t *set, int signum); /* 将信号signum从到信号集中删除 */
int sigismember(const sigset_t *set, int signum); /* 测试信号signum是否在信号集中 */
进程掩码
内核会为每个进程维护一个信号掩码,即一组信号,并将阻塞其针对该进程的传递。如果将遭阻塞的信号发给某进程,那么对该信号的传递将延后,直至从进程信号掩码中移除该信号,从而解除阻塞为止。(信号掩码实际属于线程属性,在多线程进程中,每个线程都可使用 pthread_sigmask() 函数来独立检查和修改其信号掩码。)
下面的函数可以用于设置或查看进程的信号掩码:
#include <signal.h>
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
set
参数指定新的信号掩码
oldset
参数输出原来的信号掩码(不过不为NULL)
如果set
参数不为NULL,则how
参数指定设置进程信号掩码的方式,其可选值如表所示。
如果set
为 NULL,则进程信号掩码不变,此时我们仍然可以利用oldset
参数来获得进程当前的信号掩码。
sigprocmask
成功时返回0,失败则返回-1并设置errno
。
被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:
#include <signal.h>
int sigpending(sigset_t *set);
set
用于保存被挂起的信号集。
sigpending
成功时返回0,失败则返回-1并设置errno
。
进程多次接收到同一个被挂起的信号,sigpending
函数也只能反映一次。并且,当我们再次使用sigprocmask
使能该挂起的信号时,该信号的处理函数也只被触发一次。
信号集这几个函数举例:
我们以SIGINT
和SIGQUIT
为例,就是键盘上按下(Ctrl+C)和(Ctrl+\)为例。
通过sigprocmask
设置这两个信号被挂起,然后分别按下(Ctrl+C)和(Ctrl+\),再通过sigpending
查看那些进程被挂起(这个进程可以通过kill
杀死)。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void printset(sigset_t *ped)
{
int i;
for (i = 1; i < 32; i++)
{
if ((sigismember(ped, i) == 1))
{
putchar('1');
}
else
{
putchar('0');
}
}
printf("\n");
}
int main(int argc, char const *argv[])
{
sigset_t set, oldset, ped;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
sigprocmask(SIG_BLOCK, &set, &oldset);
printf("进程信号掩码:");
printset(&set);
while (1)
{
sigpending(&ped); //获取信号集
printf("被挂起的信号掩码:");
printset(&ped);
sleep(1);
}
return 0;
}
信号函数
处理或者说捕捉信号的函数有signal
和sigaction
signal系统调用
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum
参数指出要捕获的信号类型。
handler
参数是sighandler_t
类型的函数指针,用于指定信号signum
的处理函数。
signal
函数成功时返回一个函数指针,该函数指针的类型也是sighandler_t
。这个返回值是前一次调用signal
函数时传入的函数指针,或者是信号signum
对应的默认处理函数指针SIG_DEF
(如果是第一次调用signal
的话)。
signal
系统调用出错时返回SIG_ERR
,并设置errno
。
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
void do_sig(int a)
{
printf("Hi, SIGINT, how do you do !\n");
}
int main(int argc, char const *argv[])
{
// 设置SIGINT信号(ctrl+C)对应的事件
if (signal(SIGINT, do_sig) == SIG_ERR)
{
perror("signal");
exit(1);
}
while (1)
{
printf("---------------------\n");
sleep(1);
}
return 0;
}
可以看的按下ctrl+c
之后是杀不死这个进程的,但是可以通过“ctrl+\”或者关闭shell或者通过kill命令进行杀死。
sigaction系统函数
设置信号处理函数更为健壮的方法如下
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
参数指出要捕获的信号类型
act
参数指定新的信号处理方式
oldact
参数输出信号之前处理的方式(如果不为NULL的话)。
act
和oldact
都是sigaction
结构体类型的指针,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
成员指定信号处理函数。
sa_mask
成员设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。sa_mask
是信号集sigset_t
(_sigset_t
的同义词)类型,该类型指定一组信号。
sa_flags
成员用于设置程序收到信号时的行为,其可选值如表所示。
sa_restorer
成员已经过时,最好不要使用。
sigaction
成功时返回0,失败则返回-1并设置errno
。
sigaction
中有信号集,所以最好配合信号集函数一起使用。
简单是使用例子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
/*自定义的信号捕捉函数*/
void sig_int(int signo)
{
printf("catch signal SIGINT\n"); //单次打印
}
int main(int argc, char const *argv[])
{
struct sigaction act;
act.sa_handler = sig_int;
act.sa_flags = 0;
sigemptyset(&act.sa_mask); //不屏蔽任何信号
sigaction(SIGINT, &act, NULL);
while (1)
{
printf("---------------------\n");
sleep(1);
};
return 0;
}
一些疑惑
第一个疑惑是进程在处理一个信号的过程中,能接收另一个信号吗?
答案是可以的,下面的代码接收了两个SIGINT
和SIGQUIT
两个信号,在按下(Ctrl+C)后立刻按下(Ctrl+\),都能进行输出,说明进程在处理一个信号的过程中,能接收另一个信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/*自定义的信号捕捉函数*/
void sig_int(int signo)
{
printf("catch signal SIGINT\n");
sleep(10); //模拟信号处理函数执行很长时间
printf("end of SIGINT handler\n");
}
void sig_quit(int signo)
{
printf("catch signal SIGQUIT\n");
sleep(10); //模拟信号处理函数执行很长时间
printf("end of SIGQUIT handler\n");
}
int main(int argc, char const *argv[])
{
struct sigaction act1, act2;
act1.sa_handler = sig_int;
sigemptyset(&act1.sa_mask); //不屏蔽任何信号
act1.sa_flags = 0;
act2.sa_handler = sig_quit;
sigemptyset(&act2.sa_mask); //不屏蔽任何信号
act2.sa_flags = 0;
sigaction(SIGINT, &act1, NULL); //注册信号处理函数
sigaction(SIGQUIT, &act2, NULL); //注册信号处理函数
while (1)
{
printf("---------------------\n");
sleep(1);
};
return 0;
}
第二个疑惑是信号在处理一个信号的过程中,会阻塞(挂起)这个信号吗?
个人感觉是阻塞了这个信号的。也就是第1个信号在处理的过程中,收到再多这个信号也是不会进行处理的,知道第2个信号处理完毕,后面的第2到n个信号当作一次信号进行处理(这里的信号指相同一种信息)。
我们以SIGINT
为例,当我们按下(Ctrl+C)后,SIGINT
信号的回调函数在进行处理,处理的过程中我们疯狂的按(Ctrl+C),最终后续的SIGINT
信息只当作一次信息进行处理了。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/*自定义的信号捕捉函数*/
void sig_int(int signo)
{
printf("catch signal SIGINT\n");
sleep(5); //模拟信号处理函数执行很长时间
printf("end of SIGINT handler\n");
}
int main(int argc, char const *argv[])
{
struct sigaction act;
act.sa_handler = sig_int;
sigemptyset(&act.sa_mask); //不屏蔽任何信号
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL); //注册信号处理函数
while (1)
{
printf("---------------------\n");
sleep(1);
};
return 0;
}
第三个疑惑是信号被屏蔽之后,我们多次发送该信号,信号被挂起了,再“解挂”或者叫取消屏蔽情况会如何?
实际情况是,取消屏蔽后只会执行一次信号处理,后续的信号处理和普通信号处理相同。
我们以SIGINT
为例,先屏蔽SIGINT
这个信息,在此期间我们不停的发信息,后续取消屏蔽后,信号的回调函数被处理了一次。再后续的信息处理和普通信号类似。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/*自定义的信号捕捉函数*/
void sig_int(int signo)
{
printf("catch signal SIGINT\n");
}
int main(int argc, char const *argv[])
{
struct sigaction act;
act.sa_handler = sig_int;
sigemptyset(&act.sa_mask); //不屏蔽任何信号
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL); //注册信号处理函数
sigset_t set, oldset, ped;
sigemptyset(&set);
sigaddset(&set, SIGINT); // 将SIGINT进行屏蔽
sigprocmask(SIG_BLOCK, &set, &oldset);
printf("-----begin sleep 10s--\n");
sleep(10);
printf("-----end sleep 10s--\n");
sigprocmask(SIG_UNBLOCK, &set, &oldset);
while (1)
{
printf("---------------------\n");
sleep(1);
}
return 0;
}