目录
信号(signal)机制是UNIX系统中最为古老的进程之间的通信机制。它用于在一个进程或多个进程之间传递异步信号。信号可以由各种异步事件产生,例如键盘中断等。Shell也可以使用信号将作业控制命令传递给它的子进程。
Linux系统中定义了一系列的信号, 这些信号可以由内核产生,也可以由系统中的其他进程产生只要这些进程有足够的权限。
用kill -l命令可以察看系统定义的信号列表
大写字母是信号的名称,实际上是宏。
我们一般只学习前31个信号。
- SIGABRT: 调用abort()函数时产生此信号,进程异常终止。
- SIGALRM:超过用alarm()函数设置的时间时产生此信号。
- SIGBUS:指示一个实现定义的硬件故障。
- SIGCHLD: 在一个进程终止或停止时,SIGCHLD信号被送给其父进程。如果希望从父进程中了解其子进程的状态改变,则应捕捉此信号。信号捕捉函数中通常要调用wait()函数以取得子进程ID和其终止状态。
- SIGCONT: 此作业控制信号送给需要继续运行的处于停止状态的进程。如果接收到此信号的进程处于停止状态,则操作系统的默认动作是使该停止的进程继续运行,否则默认动作是忽略此信号。
- SIGEMT: 指示一个实现定义的硬件故障。
- SIGFPE: 此信号表示一个算术运算异常,例如除以0,浮点溢出等。
- SIGHUP: 如果终端界面检测到一个连接断开, 则将此信号送给与该终端相关的进程。
- SIGILL: 此信号指示进程已执行一条非法硬件指令。
- SIGINT: 当用户按中断键(一般采用 Delete或Ctrl+C)时,终端驱动程序产生这个信号并将信号送给前台进程组中的每一个进程。 当一个进程在运行时失控,特别是它正在屏幕上产生大量不需要的输出时,常用此信号终止它。
- SIGIO: 此信号指示一个异步IO事件。
- SIGIOT: 这指示一个实现定义的硬件故障。
- SIGPIPE: 如果在读进程时已终止写管道,则产生此信号。
- SIGQUIT: 当用户在终端上按退出键(一般采用CtrI+C) 时,产生此信号,并送至前台进程组中的所有进程。
- SIGSEGV: 指示进程进行了一次无效的存储访问。
- SIGSTOP: 这是一个作业控制信号,它停止一个进程。
- SIGSYS: 指示一个无效的系统调用。由于某种未知原因,某个进程执行了一条系统调用命令,但是调用命令所用的参数无效。
- SIGTERM: 这是由kill命令发送的系统默认终止信号。
- SIGTRAP: 指示一个实现定义的硬件故障。
- SIGTSTP: 交互停止信号,当用户在终端上按挂起键(一般采用Ctrl+Z) 时,终端驱动程序产生此信号。
- SIGTTIN:当一个后台进程组进程试图读其控制终端时,终端驱动程序产生此信号。
- SIGTTOU: 当一个后台进程组进程试图写其控制终端时产生此信号。
- SIGURG: 此信号通知进程已经发生一个紧急情况。在网络连接上,接到非规定波特率的数据时,此信号可选择地产生。
进程使用位图来管理信号。所谓发送信号,本质上是写入信号,直接修改特定进程的信号位图中的特定比特位(0->1)、数据内核结构只能由OS进行修改,所以无论有多少种信号产生的方式,最终都必须让OS来完成最后的发送过程。
用户按下Ctrl+C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程 前台进程因为收到信号,进而引起进程退出。
Ctrl+C相当于我们的2号信号SIGINT。
Ctrl+ \ 相当于我们的3号信号SIGQUIT。
信号截取函数signal()
signal()函数用于截取系统的信号,对此信号挂接用户自己的处理函数。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal()函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。 第1个参数signum是一个整型数, 第2个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。用一般语言来描述就是要向信号处理程序传送一个整型参数,而它却无返回值。当调用signal设置信号处理程序时,第2个参数是指向该函数(也就是信号处理程序)的指针。signal的返回值指向以前信号处理程序的指针。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
//自定义方法
void handler(int signo)
{
cout<<"get a signal:"<<signo<<endl;
}
int main()
{
signal(2,handler);//当2号信号产生的时候自动调用handler
signal(3,handler);
while(true)
{
cout<<"我是一个进程,pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
进程可以屏蔽掉大多数的信号,除了SIGSTOP 和SIGKILL。SIGSTOP信号使一个正在运行的进程暂停,而信号SIGKLL则便正在运行的进程退出。进程可以选择系统的默认方式处理信号,也可以选择自己的方式处理产生的信号。信号之间不存在相对的优先权,系统也无法处理同时产生的多个同种的信号,也就是说,进程不能分辨它收到的是1个或者是42个SIGCONT信号。
信号的产生
通过终端按键产生信号
我们平时在输入的时候,计算机怎么知道我从键盘输入了数据呢?
键盘通过硬件中断的方式,通知系统,键盘已经被按下。
Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。 Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步。
调用系统函数向进程发信号
向进程发送信号函数kill()和raise()
在挂接信号处理函数后,可以等待系统信号的到来。同时,用户可以自己构建信号发送到目标进程中。此类函数有kill()和 raise()函数
#include <signal.h>
int kill(pid_t pid, int sig);
int raise(int sig) ;
mykill.cc
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int count = 0;
void Usage(std::string proc)
{
std::cout << "\tUsage: \n\t";
std::cout << proc << " 信号编号 目标进程\n"
<< std::endl;
}
// ./mykill 9 1234
int main(int argc, char *argv[])
{
int signo = atoi(argv[1]);
int target_id = atoi(argv[2]);
int n = kill(target_id, signo);
if(n != 0)
{
std::cerr << errno << " : " << strerror(errno) << std::endl;
exit(2);
}
}
kil()函数向进程号为pid的进程发送信号,信号值为sig。当pid为0时,向当前系统的所有进程发送信号sig,即“群发”的意思。raise()函数在当前进程中自举一个信号 sig,即向当前进程发送信号。注意,Ikill的名称虽然是“杀死“的意思,但是它并不是杀死某个进程,而是向某个进程发送信号,这个信号除了SIGSTOP和SIGKILL,一般不会使进程显式地退出。
abort函数使当前进程接收到信号而异常终止。
#include <stdlib>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
由软件条件产生信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
一秒内对count进行累加
int count = 0;
void myhandler(int signo)
{
std::cout << "get a signal: " << signo << " count: " << count << std::endl;
exit(0);
}
int main(int argc, char *argv[])
{
signal(SIGALRM, myhandler);
alarm(1);
while(true) count++;
}
发送了14号信号。
OS用当前时间和闹钟设置的时间作比较,超过了当前时间,就给存储在数据结构中的进程发送信号。
硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU(内存管理单元)会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
除零会导致CUP中状态寄存器由0被置为1,触发硬件异常,异常后被操作系统识别后,操作系统会向出现异常的进程发送8号信号。
野指针问题也会让操作系统向进程发送信号。
在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
OS可以将该进程在异常的时候,核心代码部分进行核心转储,将内存中进程的相关数据,全部dump到磁盘中,一般核心转储文件在云服务器上确实看不到,云服务器默认是关闭这个功能的。
核心转储可以在发生异常时,方便进行调试。
使用ulimit - a 查看目前资源限制的设定
阻塞信号
实际执行信号的处理动作称为信号递达(Delivery) 信号从产生到递达之间的状态,称为信号未决(Pending)。 进程可以选择阻塞 (Block )某个信号。 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.。注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号在内核中的表示示意图
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
在上图的例子中
- SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前 不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX。允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当 前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数
#include <signal>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
sigprocmask()
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
成功返回0,若出错返回-1 。
如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号 屏蔽字备份到oldset里,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
sigpending()
#include <signal.h>
int sigpending(sigset_t *set);
#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <signal.h>
using namespace std;
static void PrintPending(const sigset_t &pending)
{
cout << "当前进程的pending位图: ";
for(int signo = 1; signo <= 31; signo++)
{
if(sigismember(&pending, signo)) cout << "1";
else cout << "0";
}
cout << "\n";
}
static void handler(int signo)
{
cout << "对特定信号:"<< signo << "执行捕捉动作" << endl;
int cnt = 30;
while(cnt)
{
cnt--;
sigset_t pending;
sigemptyset(&pending); // 不是必须的
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
}
int main()
{
//设置对2号信号的的自定义捕捉
signal(2, handler);
int cnt = 0;
//1. 屏蔽2号信号
sigset_t set, oset;
// 1.1 初始化
sigemptyset(&set);
sigemptyset(&oset);
// 1.2 将2号信号添加到set中
sigaddset(&set, SIGINT/*2*/);
sigaddset(&set, 3/*2*/);
// 1.3 将新的信号屏蔽字设置进程
sigprocmask(SIG_BLOCK, &set, &oset);
//2. while获取进程的pending信号集合,并01打印
while(true)
{
// 2.1 先获取pending信号集
sigset_t pending;
sigemptyset(&pending); // 不是必须的
int n = sigpending(&pending);
assert(n == 0);
(void)n; //保证不会出现编译是的warning
// 2.2 打印,方便我们查看
PrintPending(pending);
// 2.3 休眠一下
sleep(1);
// 2.4 10s之后,恢复对所有信号的block动作
if(cnt++ == 10)
{
cout << "解除对2号信号的屏蔽" << endl; //先打印
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
}
while(true);
}
捕捉信号
信号的产生的异步的,收到信号的时候当前进程可能正在做更重要的事情。当处在合适的时候才会处理信号。什么时候是合适的时候呢?当进程从内核态切换回用户态的时候,进程会在OS的指导下,进行信号的检测与处理。
用户态:执行用户写的代码的时候,进程所处的状态
内核态:执行OS的代码的时候,进程所处的状态
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了。
sigaction()函数
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo 是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oldact指针非空,则通过oldact传出该信号原来的处理动作。act和oldact指向sigaction结构体。
- 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动 作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回 值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信 号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
static void PrintPending(const sigset_t &pending)
{
cout << "当前进程的pending位图: ";
for(int signo = 1; signo <= 31; signo++)
{
if(sigismember(&pending, signo)) cout << "1";
else cout << "0";
}
cout << "\n";
}
static void handler(int signo)
{
cout << "对特定信号:"<< signo << "执行捕捉动作" << endl;
int cnt = 30;
while(cnt)
{
cnt--;
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
}
int main()
{
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
sigaction(2, &act, &oldact);
while(true)
{
cout << "pid:" << getpid() << endl;
sleep(1);
}
}
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。