Linux操作系统进程通信方式:信号(Signal)

什么是信号

信号是一条小的消息,由内核或者其它进程生成并发送至目标进程,目标进程可以根据该信号来做出响应。信号可以由进程或者内核发出,例如:

  1. 用户在Bash界面通过键盘对正在执行的进程输入Ctrl+CCtrl+\等信号命令,或者执行kill命令发送信号。
  2. 进程执行出错,例如访问了一个非法的地址、除0运算,或者硬件发生故障,就会由内核向进程发送一个信号。
  3. 进程执行kill命令向目标进程发送信号。

简单地说,传送一个信号到目的进程主要有两个步骤:

  1. 发送信号:内核通过更新目标进程上下文的某个状态,传递一个信号给目标进程。
  2. 接收信号:目标进程会被内核强制以某种方式对信号的发送做出反应,它就会接收到信号。如果程序没有针对这种信号指定其处理方式,就会采用默认的处理策略,例如中止进程、忽略。

一个发出但没有接收的信号称之为待处理信号,在任何时刻,同一种类型的信号最多只会有一个待处理的信号。如果进程有一个信号值为 k k k 的信号没有处理,那么下一个发到该进程的 k k k 信号会被丢弃,应用程序无法得知在此期间有多少个类型 k k k 的信号发出。


信号的种类

每种信号对应于一种系统事件,Linux提供以下几种基本的信号种类:

[root@bogon ~]# kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

前30种都是常用的信号:

信号信号值程序默认行为产生原因
SIGHUP1终止运行终端线挂断
SIGINT2终止运行来自键盘Ctrl + C的中断
SIGQUIT3终止运行来自键盘Ctrl + \的退出
SIGILL4终止运行进程尝试执行非法机器指令
SIGTRAP5终止运行并dump进程跟踪陷阱
SIGABRT6终止运行并dump进程来自abort()函数的终止信号
SIGBUS7终止运行总线错误
SIGFPE8终止运行并dump进程算术错误,例如尝试除以0
SIGKILL9终止运行强制杀死进程,程序无法对该信号进行定制处理
SIGUSR110终止运行用户定义的信号1
SIGSEGV11终止运行并dump进程段错误,程序访问了非法的地址,一般是由于程序本身BUG导致
SIGUSR212终止运行用户定义的信号2
SIGPIPE13终止运行向一个没有读用户的管道做写入操作
SIGALRM14终止运行来自alarm()函数的定时器信号
SIGTERM15终止运行软件中止信号
SIGSTKFLT16终止运行协处理器上的栈异常
SIGCHLD17忽略一个子进程停止或者终止
SIGCONT18忽略继续进程如果该进程停止
SIGSTOP19挂起当前进程直到SIGCONT不是来自终端的停止信号
SIGTSTP20挂起当前进程直到SIGCONT来自终端的停止信号
SIGTTIN21挂起当前进程直到SIGCONT后台进程向终端读
SIGTTOU22挂起当前进程直到SIGCONT后台进程向终端写
SIGURG23忽略套接字上的紧急情况
SIGXCPU24终止运行CPU时间限制超出
SIGXFSZ25终止运行文件大小限制超出
SIGVTALRM26终止运行虚拟定时器期满
SIGPROF27终止运行剖析定时器期满
SIGWINCH28忽略窗口大小变化
SIGIO29终止运行在某个描述符上有可执行的IO操作
SIGIO30终止运行电源故障

可以看出,每个信号类型都有一个预定义的默认行为:

  1. 进程终止
  2. 进程终止并且将进程上下文dump到硬盘
  3. 进程挂起直到被SIGCONT信号唤醒
  4. 进程接收该信号,不进行任何处理。

进程组的概念

在Linux操作系统中,信号的发送是基于进程组的。每个进程都属于一个进程组,进程组在当前操作系统中通过一个唯一的正整数ID来标识,我们可以通过unistd.h中的getpgrp函数来获取当前进程的进程组ID:

#include <stdio.h>
#include <unistd.h>

int main() {
	pid_t group = getpgrp();
    fprintf(stdout, "%d\n", group);
    return 0;
}

在默认情况下,一个进程和它的父进程属于同一个进程组,一个进程可通过setpgid函数来改变自己或者其它进程的进程组:

int setpgid (pid_t pid, pid_t pgid);

该函数将进程PID为pid的进程组改为pgid。如果pid为0,就使用当前进程的PID。如果pgid为0,就使用pid指定的进程PID作为进程组的ID。也就是说,如果执行setpgid(0,0)就会创建一个进程组,并且进程组ID就是该进程的PID。
默认情况下,通过命令行界面启动的进程,进程PID和进程组的PID是一致的,例如:

#include <stdio.h>
#include <unistd.h>

int main() {
	pid_t group = getpgrp();
	pid_t pid = getpid();
    fprintf(stdout, "PID:%d GroupID:%d\n", pid, group);
    return 0;
}

通过g++编译,并在命令行界面运行:

[root@bogon Debug]# ./test-app
PID:30842 GroupID:30842

了解完进程组后,我们再来看的信号的发送:


信号的发送
1.通过bin/kill程序发送信号

Linux系统自带了bin/kill程序,用于向目标进程发送信号。例如

[root@bogon Debug]# kill -9 30842

该命令会向PID为30842的进程发送信号值为9的信号,也就是SIGKILL信号,该信号用于强制杀死进程。
如果第二个参数为负值,例如:

[root@bogon Debug]# kill -9 -30842

该命令会向进程组ID为30842下的所有进程发送SIGKILL信号。

2、使用键盘发出信号

在命令行界面中,最多只能有一个前台任务和多个后台任务。例如下面命令会尝试搜索当前目录中包含bin关键字的文件或文件夹:

[root@bogon Debug]# ls | grep "bin"

上述命令会创建一个前台任务,这个前台任务包含两个进程:ls进程和grep进程,它们隶属于同一个进程组。
如果上述命令执行期间,用户通过键盘按下Ctrl + C,就会导致内核向这个进程组发送SIGINT信号,此时ls进程和grep进程都会收到这个信号。

3、kill函数发送信号

kill函数的定义位于头文件signal.h中:

int kill(pid_t pid, int sig);

如果pid > 0,该函数会向PID为pid的进程发送信号值为sig的信号。
如果pid = 0,该函数会向当前进程隶属的进程组下所有的进程发出信号值为sig的信号,包括这个进程自己。
如果pid < 0,该函数会向进程组ID为-pid下所有的进程发出信号值为sig的信号。

4、alarm函数发送SIGALRM信号

进程可以通过alarm函数向自己发送一个SIGALRM信号,该函数定义于头文件unistd.h中:

unsigned int alarm(unsigned int secs);

该函数需要传入一个时间参数secs,单位为秒,表示在secs秒后发送一个SIGALRM信号给当前线程。如果secs为0,那么不会调度新的alarm


信号的接收处理

当内核把进程从内核模式切换到用户模式时(例如从系统调用返回的时候,或者CPU进行了一次进程上下文切换),它会检查该进程尚未被处理的信号集合。如果信号集合为空,那么内核会将控制传递给该进程的逻辑控制流的下一条指令。如果不为空,那么内核会选择集合中的某个信号,并强制进程接收该信号,该过程会触发进程采取某种行为,完成这个行为后才将控制传递回进程逻辑控制流的下一个指令。也就是说信号的处理是同步的。

我们可以通过signal函数(定义于signal.h)来定义自己的信号处理逻辑:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

sighandler_t是一个指向函数的指针,要求参数列表只有一个int参数,并且无返回值。signal函数会将信号值为signum和信号处理逻辑sighandler_t映射起来,当进程收到信号值为signum的信号后,就会调用handler处理。例如:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void sigint_handler(int signum) {
	fprintf(stdout, "Receive SIGINT signal\n");
	_exit(0);
}

int main() {
    fprintf(stdout, "PID:%d GroupID:%d\n", getpid(), getpgrp());
	signal(SIGINT, sigint_handler);
	pause();
    return 0;
}

编译上述程序,执行并按下Ctrl + C

[root@bogon Debug]# ./test-app
PID:35462 GroupID:35462
^CReceive SIGINT signal
[root@bogon Debug]#

可以发现sigint_handler函数得到了执行。pause函数会使当前进程挂起直到捕捉到了一个信号,只有执行了一个信号处理逻辑并返回时,pause函数才会返回

之前提到过进程对信号的默认处理方式,程序都可以对各个种类的信号进行定制化处理,唯独SIGSTOPSIGKILL不能进行定制化处理,它们的默认行为只能是终止进程。


信号阻塞

信号阻塞就是让内核暂时保留信号待以后发送,信号阻塞一般都是暂时的,只是为了防止信号打断一些重要的操作。需要注意信号阻塞并不是忽略信号,而是延时一段时间去调用信号处理函数。

例如下面程序中:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void sigint_handler(int signum) {
	fprintf(stdout, "Receive SIGINT signal\n");
	_exit(0);
}

int main() {
    fprintf(stdout, "PID:%d GroupID:%d\n", getpid(), getpgrp());
	signal(SIGINT, sigint_handler);

	sigset_t mask, prev;
	sigemptyset(&mask);
	sigaddset(&mask, SIGINT);
	sigprocmask(SIG_BLOCK, &mask, &prev);	
	//在此期间SIGINT被阻塞
	char str[100];
	fscanf(stdin, "%s", str);
	fprintf(stdout, "%s\n", str);
	
	sigprocmask(SIG_SETMASK, &prev, NULL);
	//在这之后SIGINT可以被处理
	pause();
    return 0;
}

gcc编译上述代码,并运行:

[root@bogon Debug]# ./app
PID:38779 GroupID:38779

此时按下Ctrl + C键,会发现并没有执行sigint_handler函数,此时我们随便输入一个字符串并按下回车:

[root@bogon Debug]# ./app
PID:38797 GroupID:38797
^Cabcdef
abcdef
Receive SIGINT signal

在输出该字符串后,会发现信号已经成功被sigint_handler函数处理了。这个示例就是信号阻塞的简单应用,其中与之密切相关的就是sigprocmask函数:

int sigprocmask (int __how, const sigset_t *__restrict __set, sigset_t *__restrict __oset)

sigprocmask函数可以修改当前阻塞的信号种类,修改行为取决于__how的值,该值取值范围有:

  • SIG_BLOCK:把__restrict __set中的信号添加到阻塞信号列表中
  • SIG_UNBLOCK:删除当前阻塞信号列表中__restrict __set中的信号
  • SIG_SETMASK:将__restrict __set设置为阻塞信号列表。

sigset_t是一个结构体,包含了一个阻塞的信号种类的集合,定义如下:

# define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))
typedef struct
  {
    unsigned long int __val[_SIGSET_NWORDS];
  } __sigset_t;

typedef __sigset_t sigset_t;

我们只需要通过sigaddset函数设置sigset_t实例。上述例子中,就将SIGINT设置到了mask中,之后的sigprocmask才真正应用mask中设定的阻塞的信号类型。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值