赞
踩
目录
信号(signal)机制是UNIX系统中最为古老的进程之间的通信机制。它用于在一个进程或多个进程之间传递异步信号。信号可以由各种异步事件产生,例如键盘中断等。Shell也可以使用信号将作业控制命令传递给它的子进程。
Linux系统中定义了一系列的信号, 这些信号可以由内核产生,也可以由系统中的其他进程产生只要这些进程有足够的权限。
用kill -l命令可以察看系统定义的信号列表
大写字母是信号的名称,实际上是宏。
我们一般只学习前31个信号。
进程使用位图来管理信号。所谓发送信号,本质上是写入信号,直接修改特定进程的信号位图中的特定比特位(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),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
在上图的例子中
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?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);
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);
- 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字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。