0%

信号

信号

学习《Linux高性能服务器编程》第十章信号,为了印象深刻一些,多动手多实践,所以记下这个笔记。这一篇主要记录Linux中

Linux信号概述、信号集、信号函数和一些疑惑。

Linux信号概述

发送信号

Linux 下,一个进程给其他进程发送信号的API是kill函数。其定义如下:

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

这个函数把信号sig发送给目标进程;目标进程pid参数指定,其可能的取值以及含义如表所展示:

image-20220822113352089

Linux当中信号都大于0,如果sig取值为0,则kill函数不发送任何信号。这种方法可以用来检测目标进程或进程组是否存在,但是这种方法是不可靠的(这种方法不是原子操作)。

该函数成功时返回0,失败则返回-1并设置errno。几种可能的errno如表所示。

image-20220822114203553

信号处理方式

在目标进程收到信息时,需要定义一个接收函数来处理。信号处理函数的原则如下:

1
2
3
#include <signal.h>
/* Type of a signal handler. */
typedef void (*__sighandler_t) (int);

信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。

除了用户自己定义信号处理函数之外,Linux当中还定义了信息号的两种其他处理方式:

1
2
#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命令看到所有的信号,但是我们并不关心这些所有的信号,只用重点关心SIGHUPSIGPIPESIGURGSIGALRMSIGCHLD等几个信号即可。

信号 起源 默认行为 含义
SIGHUP POSIX Term 控制终端挂起
SIGPIPE POSIX Term 往读端被关闭的管道或者socket连接中些数据
SIGURG 4.2BSD Ign socket连接上接收到紧急数据
SIGALRM POSIX Term 由alarm 或setitimer设置的实时闹钟超时引起
SIGCHLD POSIX Ign 子进程状态发生变化(退出或者暂停)

信号集

信号集函数

信号集sigset_t的定义如下

1
2
3
4
5
6
7
#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提供了如下一组函数来设置、修改、删除和查询信号集:

1
2
3
4
5
6
7
8
9
10
11
#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() 函数来独立检查和修改其信号掩码。)

下面的函数可以用于设置或查看进程的信号掩码:

1
2
3
4
#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参数指定设置进程信号掩码的方式,其可选值如表所示。

image-20220825111927286

如果set为 NULL,则进程信号掩码不变,此时我们仍然可以利用oldset参数来获得进程当前的信号掩码。

sigprocmask成功时返回0,失败则返回-1并设置errno

被挂起的信号

设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:

1
2
3
#include <signal.h>

int sigpending(sigset_t *set);

set用于保存被挂起的信号集。

sigpending成功时返回0,失败则返回-1并设置errno

进程多次接收到同一个被挂起的信号,sigpending函数也只能反映一次。并且,当我们再次使用sigprocmask使能该挂起的信号时,该信号的处理函数也只被触发一次。

信号集这几个函数举例

我们以SIGINTSIGQUIT为例,就是键盘上按下(Ctrl+C)和(Ctrl+\)为例。

通过sigprocmask设置这两个信号被挂起,然后分别按下(Ctrl+C)和(Ctrl+\),再通过sigpending查看那些进程被挂起(这个进程可以通过kill杀死)。

image-20220825181937952

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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;
}

image-20220825215756905

信号函数

处理或者说捕捉信号的函数有signalsigaction

signal系统调用

1
2
3
4
5
#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

1
#define	SIG_ERR	 ((__sighandler_t) -1)	/* Error return.  */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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;
}

image-20220825182345010

可以看的按下ctrl+c之后是杀不死这个进程的,但是可以通过“ctrl+\”或者关闭shell或者通过kill命令进行杀死。

sigaction系统函数

设置信号处理函数更为健壮的方法如下

1
2
3
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum参数指出要捕获的信号类型

act参数指定新的信号处理方式

oldact参数输出信号之前处理的方式(如果不为NULL的话)。

actoldact都是sigaction结构体类型的指针,sigaction结构体定义如下:

1
2
3
4
5
6
7
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成员用于设置程序收到信号时的行为,其可选值如表所示。

image-20220824165327097

sa_restorer成员已经过时,最好不要使用。

sigaction成功时返回0,失败则返回-1并设置errno

sigaction中有信号集,所以最好配合信号集函数一起使用。

简单是使用例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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;
}

image-20220826155323696

一些疑惑

第一个疑惑是进程在处理一个信号的过程中,能接收另一个信号吗?

答案是可以的,下面的代码接收了两个SIGINTSIGQUIT两个信号,在按下(Ctrl+C)后立刻按下(Ctrl+\),都能进行输出,说明进程在处理一个信号的过程中,能接收另一个信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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;
}

image-20220826161922094

第二个疑惑是信号在处理一个信号的过程中,会阻塞(挂起)这个信号吗?

个人感觉是阻塞了这个信号的。也就是第1个信号在处理的过程中,收到再多这个信号也是不会进行处理的,知道第2个信号处理完毕,后面的第2到n个信号当作一次信号进行处理(这里的信号指相同一种信息)。

我们以SIGINT为例,当我们按下(Ctrl+C)后,SIGINT信号的回调函数在进行处理,处理的过程中我们疯狂的按(Ctrl+C),最终后续的SIGINT信息只当作一次信息进行处理了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#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;
}

image-20220826163813060

第三个疑惑是信号被屏蔽之后,我们多次发送该信号,信号被挂起了,再“解挂”或者叫取消屏蔽情况会如何?

实际情况是,取消屏蔽后只会执行一次信号处理,后续的信号处理和普通信号处理相同。

我们以SIGINT为例,先屏蔽SIGINT这个信息,在此期间我们不停的发信息,后续取消屏蔽后,信号的回调函数被处理了一次。再后续的信息处理和普通信号类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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;
}

image-20220826165823734