辽宁省住房和城乡建设厅官方网站,wordpress调用指定分类的文章,站长工具权重,湘潭网站建设 很好磐石网络#x1f320; 作者#xff1a;阿亮joy. #x1f386;专栏#xff1a;《学会Linux》 #x1f387; 座右铭#xff1a;每个优秀的人都有一段沉默的时光#xff0c;那段时光是付出了很多努力却得不到结果的日子#xff0c;我们把它叫做扎根 目录#x1f449;信号入门 作者阿亮joy. 专栏《学会Linux》 座右铭每个优秀的人都有一段沉默的时光那段时光是付出了很多努力却得不到结果的日子我们把它叫做扎根 目录信号入门生活中的信号Linux信号常见信号信号产生通过终端按键产生信号核心转储调用系统函数向进程发信号由软件条件产生信号硬件异常产生信号阻塞信号信号其他相关常见概念信号在内核中的表示sigset_t信号集操作函数sigpendingsigprocmask捕捉信号内核如何实现信号的捕捉sigaction可重入函数volatileSIGCHLD信号总结信号入门 信号是一种软件中断信号在 Linux 操作系统 中提供了一种处理异步事件的方法可以很好地在多个进程之间进行同步和简单的数据交互。注信号和信号是两个东西没有关系信号只是用来通知某个进程发生了什么事情但并不给该进程传递任何数据。 生活中的信号 在生活中我们会收到很多信号比如红绿灯、闹钟、转向灯和狼烟等等。那我们为什么会知道这些生活中的信号呢其实是我们曾经学习过有关这些生活信号的知识并且记住了对应场景下的信号。有关信号的推论如下 当这些信号产生时我们就能够识别这些信号并且执行相应的动作。当特定信号没有产生时我们依旧知道应该如何处理这个信号。当我们收到信号时我们可能不会立即处理这个信号。当我们无法立即处理信号的时候信号也一定要先被临时地记住。 Linux信号 什么是 Linux 信号Linux 信号本质是一种通知机制用户或操作系统通过发送一定的信号通知进程某些时间已经发生了进程可以在后续进行信号处理。 进程要处理信号那么进程必须具备信号识别的能力收到信号加上相对应的信号处理动作。为什么进程能够识别信号呢进程能够识别信号肯定是设计操作系统的程序员将常见的信号及信号处理动作内置到进程的代码和属性中。信号产生是随机的当信号产生时进程可能正在处理某些任务。所以信号可能不是立即被进程处理的。信号会被临时地记录下来方便进程后续进行处理。那进程会在什么时候处理信号呢合适的时候。一般而言信号的产生相对于进程而言是异步的。异步指两个或两个以上的对象或事件不同时存在或发生或多个相关事物的发生无需等待其前一事物的完成。同步指两个或两个以上随时间变化的量在变化过程中保持一定的相对关系。注信号也有确定的信号比如定下闹钟的时间时那么闹钟一定会在那个时间点响起来。信号处理的常见方式 默认进程自带的处理动作该动作是程序员写好的逻辑忽略忽略也是信号处理的一种方式自定义动作捕捉信号 常见信号
kill -l #该命令可以查看常见的信号
man 7 signal #查看信号的相关描述Linux 内核支持 62 种不同的信号这些信号都有一个名字这些名字都以三个字符 SIG 开头。在头文件siganl.h中你能够这些信号都被定义为正整数称为信息编号。其中编号 1 到 31 的信号称为普通信号编号 34 到 64 的信号称为实时信号实时信号对处理的要求比较高。
普通信号和实时信号的关系就像分时操作系统和实时操作系统的关系类似分时操作系统是基于时间片轮转调度的而实时操作系统要求要有严格的时序可以认为是一个队列。将一个任务放入该队列中那么操作系统就尽量快地将该任务处理完。日常生活中使用最多的就是分时操作系统而实时操作系统常见于特殊的行业如军工领域和自动驾驶领域等等。 组合键转化成信号 Ctrl C 的本质就是给进程发送了 2 信号进程接收到 2 号信号后的默认处理动作是结束进程。 那如何理解组合键变成信号呢其实键盘的工作方式是通过中断方式进行的。键盘是槽位的每个槽位都会对应一个编号。因为有键盘驱动操作系统是能够识别这些编号的。只要按下了一些键操作系统立马就能够识别到。那么当你按下组合键操作系统也是可以识别到的。操作系统既然都识别到了你按下了组合键那么操作系统给特定的进程发送信号也就是轻而易举的事情了。
既然进程要接收操作系统发送过来的信号那么进程必须要具有保存信号的相关数据结构而该数据结构就是位图unsigned int使用比特位信息就可以表示操作系统是否有给进程发送信号。比如最低位比特位为 1则说明操作系统给该进程发送了 1 号信号反之则操作系统没有给该进程发送 1 号信号。注该位图结构保存在进程的内核数据结构 task_struct 中只有操作系统才能修改 task_struct。信号产生的方式有很多种但其发送的本质就是操作系统向目标进程写信号操作系统修改 task_struct 中的位图结构完成信号发送的过程。
那么组合键能够转化成信号也就很好理解了。当你按下组合键 Ctrl C 时操作系统识别到该组合键并解释该组合键然后查找到在前台运行的进程最后操作系统将 Ctrl C 对应的信号写入到进程内部的位图结构中就完成了信号发送。现在进程已经将操作系统发给它的信号记录下来了进程就会在合适的时候处理该信号。 注意 Ctrl C 产生的信号只能发给前台进程。一个命令后面加个 可以放到后台运行这样 shell 不必等待进程结束就可以接受新的命令启动新的进程。shell 可以同时运行一个前台进程和任意多个后台进程只有前台进程才能接到像 Ctrl C 这种组合键产生的信号。前台进程在运行过程中用户随时可能按下 Ctrl C 而产生一个信号也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止所以信号相对于进程的控制流程来说是异步(Asynchronous) 的。 信号产生
通过终端按键产生信号
在上面的内容已经提及到按下组合键 Ctrl C 可以前台进程发送 2 号信号那我们可以通过 signal 函数来验证一下。 signal 函数的原型如下 使用 signal 函数后当进程接收到 signum 信号时进程会调用 handler 函数handler 是回调函数handler 是 函数指针类型该函数的返回值是 void参数是 int并将 signum 传递给 handler 函数其实 signal 函数相当于可以自定义捕捉某个信号。signal 函数的返回值是对于 signum 信号的旧的处理方法。 #include iostream
#include signal.h
#include unistd.husing namespace std;void catchSignal(int signal)
{cout 捕捉到了一个信号: signal endl;
}int main()
{// signal(2, catchSignal); // 这种写法也可以// catchSignal是自定义捕捉signal(SIGINT, catchSignal); // 特定信号的处理动作一般只有一个while(true){cout 我是一个进程,我的pid是: getpid() endl;sleep(2);}return 0;
}注signal 函数仅仅是修改进程对特定信号的后续处理动作并不是直接调用对应的处理动作。而是当进程接收到特定信号时才会去调用对应的处理动作。如果后续没有产生 SIGINT 信号catchSignal 函数就不会被调用signal 函数往往放在最前面先注册特定信号的处理方法。
现在就无法通过 Ctrl C2 号信号终止该进程了那么我们可以通过 Ctrl \ 3 号信号终止该进程。如果你也将 3 号信号也自定义捕捉了那么可以发生 8 号信号浮点数异常来终止进程。 核心转储 首先解释什么是核心转储Core Dump。当一个进程要异常终止时可以选择把进程的用户空间内存数据全部保存到磁盘上文件名通常是 core这种行为就叫做核心转储Core Dump。 进程异常终止通常是因为有 Bug比如非法内存访问导致段错误事后可以用调试器检查 core 文件以查清错误原因这叫做 Post-mortem Debug事后调试。一个进程允许产生多大的 core 文件取决于进程的 Resource Limit这个信息保存在 PCB 中。默认是不允许产生 core 文件的因为 core 文件中可能包含用户密码等敏感信息不安全。在开发调试阶段可以用 ulimit 命令改变这个限制允许产生 core 文件。 首先用 ulimit 命令改变 shell 进程的 Resource Limit允许 core 文件最大为1024K$ ulimit -c 1024。 一般而言云服务器生产环境的核心转储功能是关闭的。程序员写代码的环境称为开发环境测试人员的环境是测试环境测试 Realease 版本产品上线后用户可以使用的环境就成为生产环境有对应的服务器。我们所购买的云服务器是集开发、测试、发布、部署于一体的机器。 打开云服务器的核心转储功能后我们来验证一下是否真的会产生 core 文件。 注只有核心转储才会生成 core 文件。 core 文件是以进程 ID 作为后缀通常该文件是比较大的。生产环境一般会关闭核心转储功能是为了防止生成大量的 core 文件占用磁盘空间。如果磁盘中充满大量的 core 文件可能会导致服务器无法重启或操作系统挂掉。
通过生成的 core 文件来进行 Debug 验证进程等待中的 core dump 标记位
#include iostream
#include signal.h
#include unistd.h
#include sys/wait.h
#include cassertusing namespace std;// 验证进程等待中的core dump标记位
int main()
{int id fork();// 子进程if(id 0){sleep(2);int a 100;a / 0;exit(0);}int status 0;int ret waitpid(id, status, 0);assert(ret ! -1);(void)ret;cout 父进程: getpid() 子进程: id exit signal: \ (status 0x7F) is core: ((status 7) 1) endl;// 父进程return 0;
}将核心转储功能关闭就不会生成 core 文件core dump 标记位始终为 0当进程不是收到核心转储信号终止进程的也不会生成 core 文件core dump 的标记位也始终为 0。 调用系统函数向进程发信号
通过系统调用实现 mykill 命令 系统调用 kill 函数可以想指定的进程发送指定的信号。 // mykill.cc
#include iostream
#include signal.h
#include unistd.h
#include sys/types.h
#include cstring
#include stdlib.husing namespace std;static void Usage(string proc)
{cout Usage:\r\n\t proc -SignalNumber ProcessID endl;
}// 通过系统调用向进程发送信号(设计mykill命令)
// ./mykill -2 pid
int main(int argc, char* argv[])
{if(argc ! 3){Usage(argv[0]);exit(1);}int signal atoi(argv[1] 1);int id atoi(argv[2]);kill(id, signal);return 0;
}raise 函数可以给调用该函数的进程发信号raise(sig)等价于kill(getpid(), sig)。 #include iostream
#include signal.h
#include unistd.husing namespace std;int main()
{cout 我正在运行中... endl;sleep(2);raise(8);return 0;
}abort 函数给调用该函数的进程发送6号信SIGABRT终止进程6 号信号会引起核心转储通常用来终止进程。就像 exit 函数一样abort 函数总是会成功的所以没有返回值。 #include iostream
#include unistd.h
#include stdlib.husing namespace std;int main()
{cout 我正在运行中... endl;sleep(2);abort();return 0;
}如何理解通过系统调用向进程发信号用户调用系统接口执行操作系统对应的系统调用代码操作系统提取参数或设置特定的数值信号编号和进程 ID操作系统向目标进程写信号修改对应进程的位图结构进程后续处理信号执行相应的处理动作。 由软件条件产生信号
学习管道的时候我们说过当管道读端关闭写端一直在写操作系统会自动终止对应的写端进程。操作系统是通过发送 13 号信号SIGPIPE来终止写端进程的
那现在我们来按照一下步骤来验证一下 创建匿名管道 让父进程进行读取子进程进行写入 父子进程通行一段时间该步骤可以省略 让父进程先关闭读端子进程只有一直写入就行 父进程通过 waitpid 等待子进程拿到子进程的退出信息
#include iostream
#include unistd.h
#include signal.h
#include sys/wait.h
#include sys/types.h
#include cassert
#include string
#include cstringusing namespace std;int main()
{// 创建匿名管道int pipefd[2] {0};int n pipe(pipefd);assert(n ! -1);(void)n;cout 创建匿名管道成功 endl;// 创建子进程int id fork();if(id 0){// 子进程// 关闭子进程的读端close(pipefd[0]);char send_buffer[128] {\0};string s 我是子进程,我正在给你发消息;int count 0;while(1){// 构造变化的字符串snprintf(send_buffer, sizeof(send_buffer), %s id:%d %d, s.c_str(), getpid(), count);write(pipefd[1], send_buffer, strlen(send_buffer));sleep(1);}}// 父进程// 关闭父进程的写端close(pipefd[1]);char read_buffer[128] {\0};int count 0;while(1){ssize_t s read(pipefd[0], read_buffer, sizeof(read_buffer) - 1);if(s 0){read_buffer[s] \0;count;cout 父进程: getpid() 收到消息 read_buffer endl;}else{cout 写端已经关闭,读取结束 endl;break;}// 循环5次,关闭读端if(count 5){close(pipefd[0]);cout 父进程的读端已关闭 endl;break;}}// 获取子进程的退出信息int status 0;int ret waitpid(id, status, 0);assert(ret ! -1);cout 等待子进程成功 子进程id: id signal: (status 0x7F) endl; return 0;
} 父进程的读端已经关闭子进程的写端再进行写入也没有任何的意义那么操作系统就向子进程发送 13 号信号SIGPIPE。像管道的读端关闭写端还在写的这样情况其实就是不符合软件条件管道通信的条件管道也是一种软件那么操作系统就会向不符合软件条件的进程发送特定的信号终止进程。 alarm 函数可以设定一个闹钟也就是告诉操作系统在 seconds 秒后给当前进程发送 14 号信号SIGALRM该信号的默认处理动作是终止当前进程。 这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方某人要小睡一觉设定闹钟为 30 分钟之后响20 分钟后被人吵醒了还想多睡一会儿。于是重新设定闹钟为 15 分钟之后响以前设定的闹钟时间还余下的时间就是 10 分钟。如果 seconds 值为 0表示取消以前设定的闹钟函数的返回值仍然是以前设定的闹钟时间还余下的秒数。 #include iostream
#include unistd.husing namespace std;int main()
{alarm(1);int count 0;// 验证1s内,count会进行多少次// cout 网络 IOwhile(true){cout count: count endl;}return 0;
}通过上图可以看到count 一定被加加了 7w 次这次数是比较少的其实是由 cout 和网络传输数据慢导致的。如果想单纯看看计算的算力可以通过下面的程序。
#include iostream
#include unistd.h
#include signal.husing namespace std;unsigned int count 0;void catchSignal(int signal)
{cout count: count endl;
}int main()
{signal(SIGALRM, catchSignal);alarm(1);while(true){count;}return 0;
}注设定了一个闹钟这个闹钟一旦被处罚就会自动被移除。
下面的代码可以做到每隔一秒就发送 SIGALRM
#include iostream
#include unistd.h
#include signal.husing namespace std;unsigned int count 0;void catchSignal(int signal)
{cout count: count endl;alarm(1);
}int main()
{signal(SIGALRM, catchSignal);alarm(1);while(true){count;}return 0;
}以上的代码就简单地实现了定时器的功能每隔一秒钟做指定的一件事。
#include iostream
#include unistd.h
#include signal.h
#include sys/wait.h
#include sys/types.h
#include functional
#include vectorusing namespace std;unsigned int count 0;
typedef functionvoid () func; // func为函数类型
vectorfunc callBacks;void showCount()
{cout count: count endl;
}
void showLog()
{cout 这个是日志功能 endl;
}
void logUser()
{// 创建子进程执行who命令if(fork() 0){execl(/usr/bin/who, who, nullptr);exit(1);}wait(nullptr);
}void catchSignal(int signal)
{for(auto func : callBacks){func();}cout ------------------------- endl;alarm(1);
}int main()
{signal(SIGALRM, catchSignal);alarm(1);callBacks.push_back(showCount);callBacks.push_back(showLog);callBacks.push_back(logUser);while(true) count;return 0;
} 如何理解软件条件给进程发送信号操作系统先识别到某种软件条件触发或者不满足然后操作系统构建信号发送给指定的进程。注闹钟也是结构体操作系统通过特定的数据结构来管理闹钟。当闹钟超时了操作系统就会给闹钟结构体中存储的进程 id 发送 SIGALRM 信号。 硬件异常产生信号
#include iostream
#include unistd.h
#include signal.husing namespace std;void handler(int signum)
{sleep(1);cout 收到了一个信号: signum endl;
}int main()
{signal(SIGFPE, handler);int a 100;a / 0;while(1) sleep(1);return 0;
}将程序运行起来就会发现程序在死循环打印语句。那为什么会这样呢如何理解除零呢进行计算的是 CPU 这个硬件CPU 内部是有寄存器的其中有一个寄存器是状态寄存器。该寄存器不进行数值保存它只用来保存 CPU 本次计算的状态其结构也是位图有着对应的状态标记位溢出标记位。当状态寄存器的溢出标记位为 0操作系统就将计算结果写回到内存中而当溢出标记位为 1时操作系统就会意识到有除零错误溢出问题操作系统会找到当前哪个进程在运行向该进程发送 SIGFPE 信号进程会在合适的时候处理该信号。
当出现硬件异常时进程不一定会退出一般默认是退出但是我们即使不退出我们也做不了什么那为什么上面的程序会死循环呢虽然我们捕捉了 SIGFPE 信号也处理了该信号但是寄存器中的异常一直没有被解决寄存器中的数据是进程的上下文当进行进程切换的时候寄存器的数据也被保存下来了。当该进程被调度时操作系统又立马就识别到该进程出现了异常所以就一直给进程发送 SIGFPE 信号那么就出现了死循环打印的现象。 #include iostream
#include unistd.h
#include signal.husing namespace std;void handler(int signum)
{sleep(1);cout 收到了一个信号: signum endl;
}int main()
{// SIGSEGV 段错误(11号信号)signal(SIGSEGV, handler);int a 100;a / 0;while(1) sleep(1);return 0;
}如何理解野指针或者越界访问问题呢 无论是野指针还是越界访问都必须通过地址来找到目标位置语言层面上的地址全部都是虚拟地址。当对某个数据进行访问时首先要将虚拟地址转化成物理地址虚拟地址通过页表和 MMUMemory Manager Unit 内存管理单元硬件来转换成物理地址当野指针或越界访问时使用的地址都是非法地址那么 MMU 进行转化的时候就一定会报错。只有 MMU 报错操作系统就能识别当前进程出现了硬件异常将该硬件异常转化成对应的信号发送给进程。出现死循环的原因和除零错误出现死循环的原因类似 小总结所有的信号都有它的来源但最终全部都是被操作系统识别、解释并发送给对应的进程的。
阻塞信号
信号其他相关常见概念 实际执行信号的处理动作称为信号递达Delivery信号处理动作有默认、忽略、自定义捕捉。信号从产生到递达之间的状态,称为信号未决Pending也就是进程收到了一个信号但该信号还未被处理信号被保存在位图Pending 位图中。进程可以选择阻塞 Block 某个信号。被阻塞的信号产生时将保持在未决状态直到进程解除对此信号的阻塞才执行递达的动作。注意阻塞和忽略是不同的只要信号被阻塞就不会递达而忽略是在递达之后可选的一种处理动作。 信号在内核中的表示
为了表示信号递达、未决和阻塞三个概念那么操作系统就要用一定的结构去表示它们。操作系统就使用了三张表来表示这三个概念如下图所示 其中 pending 表就是保存信号的位图结构unsigned int1 表示收到了信号0 表示没有收到信号handler 表是函数指针数组数组的下标就是信号编号数组中存的是信号的处理动作block 表也是位图结构1 表示该信号被阻塞0 表示该信号未被阻塞。
信号处理的过程操作系统给目标进程就是修改 pending 位图这样信号就完成发送了。进程在合适的时候处理信号遍历 pending 位图看哪些比特位为 1。当发现比特位为 1 时就去看对应的 block 位图上的比特位是否为 1。如果是 1则说明该信号被阻塞着进程不会去处理该信号也不会将 pending 位图的比特位从 1 改成 0而如果是 0则说明该信号没有被阻塞进程可以处理该信号处理完成后还需要将 pending 位图上的比特位从 1 改成 0表示该信号已经处理完成。
如果在进程解除对某信号的阻塞之前这种信号产生过多次将如何处理POSIX.1 允许系统递送该信号一次或多次。Linux 是这样实现的普通信号在递达之前产生多次只计一次而实时信号在递达之前产生多次可以依次放在一个队列里本篇博客不讨论实时信号。 sigset_t 编程语言都会给我们提高 .h 或者 .hpp 和语言本身的定义类型操作系统也会给我们提供 .h 和操作系统自定义的类型像 pid_t 和 key_t 等。如果要访问硬件那么语言类的头文件也会包含对应的系统调用接口将系统调用封装起来给我们使用。sigset_t 也是操作系统自定义的类型该类型是位图结构用以表示上图的 pending 表和 block 表。用户不能直接通过位操作来修改位图unsigned int需要使用操作系统提供的方法来修改位图。用户可以直接使用 sigset_t 类型和使用内置类型和自定义类型没有任何差别。每个信号只有一个比特位的未决标志非 0 即 1不记录该信号产生了多少次阻塞标志也是这样表示的。因此未决和阻塞标志可以用相同的数据类型 sigset_t 来表示。sigset_t 称为信号集这个类型可以表示每个信号的有效或无效状态在阻塞信号集中有效和无效的含义是该信号是否被阻塞而在未决信号集中有效和无效的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字Signal Mask这里的屏蔽应该理解为阻塞而不是忽略。 信号集操作函数
sigset_t 类型对于每种信号用一个比特位表示有效或无效状态至于这个类型内部如何存储这些比特位则依赖于系统实现从使用者的角度是不必关心的使用者只能调用以下函数来操作 sigset_ t 变量而不应该对它的内部数据做任何解释比如用 printf 直接打印 sigset_t 变量是没有意义的。
#include signal.h
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 sigismemberconst sigset_t *set, int signo);函数 sigemptyset 初始化 set 所指向的信号集使其中所有信号的对应比特位清零表示该信号集不包含任何有效信号。函数 sigfillset 初始化 set 所指向的信号集使其中所有信号的对应比特位置 1表示该信号集的有效信号包括系统支持的所有信号。sigaddset 函数将 signo 信号对应的比特位置为 1sigdelset 函数将 signo 信号对应的比特位置为 0。sigismember 函数可以判断 signo 信号是否在信号集中如果 signo 信号在信号集中返回 1如果不在返回 0出错则返回 -1。注意在使用 sigset_ t 类型的变量之前一定要调用sigemptyset 或 sigfillset 函数做初始化使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。 sigpending sigpending 函数通过输出型参数 set 获取当前进程的未决信号集调用成功返回 0出错则返回 -1。 sigprocmask sigprocmask 函数可以帮助我们读取或更改进程的信号屏蔽字阻塞信号集调用成功返回 0出错则返回 -1。 如果 oldset 是非空指针则读取进程的当前信号屏蔽字通过 oldset 参数传出。如果 set 是非空指针则更改进程的信号屏蔽字参数 how 指示如何更改。如果 oldset 和 set 都是非空指针则先将原来的信号屏蔽字备份到 oldset 里然后根据 set 和 how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask下表说明了 how 参数的可选值。 如果我们对所有的信号都进行了信号捕捉那我们是不是就写了一个不会被异常终止或者用户杀掉的进程呢我们通过代码来验证一下
#include iostream
#include signal.h
#include unistd.husing namespace std;void catchSig(int signum)
{cout 捕捉到了一个信号: signum endl;
}int main()
{for(int i 1; i 31; i)signal(i, catchSig);while(1) sleep(1);return 0;
}操作系统的设计者也考虑到了上述的情况所以就让 9 号信号无法捕捉9 号信号是管理员信号。 如果我们将 2 号信号 block 掉并且不断地获取并打印当前进程的 pending 信号集。如果我们突然发送一个 2 号信号我们应该就能看到 pending 信号集中 2 号信号的比特位由 0 变成 1。
#include iostream
#include signal.h
#include unistd.h
#include cassertusing namespace std;static void showPending(sigset_t pending)
{for(int signal 31; signal 1; --signal){if(sigismember(pending, signal))cout 1;elsecout 0;}cout endl ---------------- endl;
}int main()
{// 定义并初始化信号集sigset_t set, oldset;sigemptyset(set);sigemptyset(oldset);// 将2号信号添加到信号屏蔽集中sigaddset(set, 2);// 将信号屏蔽集设置到当前进程的PCB中// 默认情况下,进程不会对任何信号进行blockint n sigprocmask(SIG_BLOCK, set, oldset);assert(n 0); // assert本质是一个宏(void)n;cout block 2 号信号成功...... endl;// 重复打印当前进程的pending信号集sigset_t pending;sigemptyset(pending);while(true){// 获取当前进程的pending信号集sigpending(pending);// 打印pending信号集showPending(pending);sleep(2);}return 0;
}给当前进程发送 2 号信号时可以看到该进程的 pending 表中 2 号信号的比特位由 0 变成 1。如果我们在若干秒后解除对 2 号信号的 block那么 2 号信号就会被递达而终止当前进程。如果我们对 2 号信号进程捕捉那么进程也就不会被终止了。
没有捕捉 2 号信号
#include iostream
#include signal.h
#include unistd.h
#include cassertusing namespace std;static void showPending(sigset_t pending)
{for(int signal 31; signal 1; --signal){if(sigismember(pending, signal))cout 1;elsecout 0;}cout endl ---------------- endl;
}int main()
{// 定义并初始化信号集sigset_t set, oldset;sigemptyset(set);sigemptyset(oldset);// 将2号信号添加到信号屏蔽集中sigaddset(set, 2);// 将信号屏蔽集设置到当前进程的PCB中// 默认情况下,进程不会对任何信号进行blockint n sigprocmask(SIG_BLOCK, set, oldset);assert(n 0); // assert本质是一个宏(void)n;cout block 2 号信号成功...... id: getpid() endl;// 重复打印当前进程的pending信号集sigset_t pending;sigemptyset(pending);int count 0;while(true){// 获取当前进程的pending信号集sigpending(pending);// 打印pending信号集showPending(pending);sleep(2);count;if(count 6){cout 解除对 2 号信号的 block endl;// 默认情况下解除对于2号信号的block的时候,2号信号确实会递达// 但是2号信号的默认处理动作是终止进程n sigprocmask(SIG_SETMASK, oldset, nullptr);assert(n 0);(void)n;}}return 0;
} #include iostream
#include signal.h
#include unistd.h
#include cassertusing namespace std;static void showPending(sigset_t pending)
{for(int signal 31; signal 1; --signal){if(sigismember(pending, signal))cout 1;elsecout 0;}cout endl ---------------- endl;
}void catchSig(int signum)
{cout 捕捉到了一个信号: signum endl;
}int main()
{signal(2, catchSig);// 定义并初始化信号集sigset_t set, oldset;sigemptyset(set);sigemptyset(oldset);// 将2号信号添加到信号屏蔽集中sigaddset(set, 2);// 将信号屏蔽集设置到当前进程的PCB中// 默认情况下,进程不会对任何信号进行blockint n sigprocmask(SIG_BLOCK, set, oldset);assert(n 0); // assert本质是一个宏(void)n;cout block 2 号信号成功...... id: getpid() endl;// 重复打印当前进程的pending信号集sigset_t pending;sigemptyset(pending);int count 0;while(true){// 获取当前进程的pending信号集sigpending(pending);// 打印pending信号集showPending(pending);sleep(2);count;if(count 5){cout 解除对 2 号信号的 block endl;n sigprocmask(SIG_SETMASK, oldset, nullptr);assert(n 0);(void)n;}}return 0;
}打印解除 block 语句和捕捉的顺序就是一个打印的顺序问题。所有的信号发送方式都是修改 pending 位图的过程我们只需要通过 sigpending 接口来获取 pending 位图即可。 如果我们将所有的信号都进行 block我们是不是就写了一个不会被异常终止或者用户杀掉的进程呢我们也通过代码来验证一下
#include iostream
#include signal.h
#include unistd.h
#include cassertusing namespace std;static void showPending(sigset_t pending)
{for(int signal 31; signal 1; --signal){if(sigismember(pending, signal))cout 1;elsecout 0;}cout endl ---------------- endl;
}static void blockSig(int sig)
{sigset_t set;sigemptyset(set);sigaddset(set, sig);int n sigprocmask(SIG_BLOCK, set, nullptr);assert(n 0);(void)n;
}int main()
{// block所有信号for(int i 1; i 31; i){blockSig(i);}sigset_t pending;while(1){sigpending(pending);showPending(pending);sleep(1);}return 0;
}自动给进程发送信号的脚本语言
#!/bin/bashi1
id$(pidof mysignal)
while [ $i -le 31 ]
dokill -$i $idecho kill -$i $idlet isleep 1
done#!/bin/bashi1
id$(pidof mysignal)
while [ $i -le 31 ]
doif [ $i -eq 9 ];thenlet icontinuefiif [ $i -eq 19 ];thenlet icontinuefikill -$i $idecho kill -$i $idlet isleep 1
done注 9 号和 19 号信号是无法被 block 的20 号信号的默认处理动作是忽略。 捕捉信号
内核如何实现信号的捕捉
在上面提及到信号产生之后进程可能无法立即处理进程需要在合适的时候去处理信号。那这个合适的时候是什么呢带着这个问题我们来探究一下信号处理的整个流程
信号相关的数据字段是在进程的 PCB 内部PCB 内部属于内核范畴普通用户无法对信号进行检测和处理。那么要对信号进行处理就需要在内核状态。当执行系统调用或被系统调度时进程所处的状态就是内核态不执行操作系统的代码时进程所处的状态就是用户态。现在我们已经知道需要在内核态下进行信号处理那究竟具体是什么时候呢结论在内核态中从内核态返回用户态的时候进行信号的检测和处理如何进入内核态呢进行系统调用或产生异常等。汇编指令int 8080 是中断编号可以进程进入内核态也就是将代码的执行权限从普通用户转交给操作系统让操作系统去执行注汇编指令int 80内置在系统调用函数中。 sigaction sigaction 函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回 0出错则返回 -1。signum 是指定的信号编号。若 act 不为空则根据 act 修改该信号的处理动作。若 oldact 不为空则通过 oldact 传出该信号原来的处理动作。act 和 oldact 指向 sigaction 结构体。 将 sa_handler 赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号赋值为常数 SIG_DFL 表示执行系统默认动作赋值为一个函数指针表示用自定义函数捕捉信号或者说向内核注册了一个信号处理函数。该函数返回值为 void参数为 int通过参数可以得知当前信号的编号这样就可以用同一个函数处理多种信号。显然这也是一个回调函数不是被 main 函数调用而是被系统所调用。sa_sigaction 是实时信号的处理方法不需要关心。 Makefile
mysignal:mysignal.ccg $^ -o $ -stdc11 -fpermissive
.PHONY:cleanrm -f mysignal#include iostream
#include unistd.h
#include signal.husing namespace std;void handler(int signum)
{ cout 捕捉到了一个信号: signum endl;
}int main()
{// signal(2, SIG_IGN);// 一下内核数据结构变量是在用户栈定义的// 需要将它们设置进进程的内核中struct sigaction act, oldact;act.sa_flags 0; // 实时信号的标记位sigemptyset(act.sa_mask);act.sa_handler handler;// 设置进当前进程的PCB中sigaction(SIGINT, act, oldact);cout default action: (int)(oldact.sa_handler) endl;while(true) sleep(1);return 0;
}处理信号、执行自定义动作的时候如果在处理信号期间又来了同样的信号操作系统该如何处理呢Linux 的设计方案是在任何时候操作系统只能处理一层信号不允许出现信号正在处理又来信号再被处理的情况。操作系统无法决定信号什么时候来但可以决定什么时候去处理信号。接下来要一起探讨的是为什么要有信号屏蔽字 block
当某个信号的处理函数被调用时内核自动将当前信号加入进程的信号屏蔽字当信号处理函数返回时自动恢复原来的信号屏蔽字这样就保证了在处理某个信号时如果这种信号再次产生那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时除了当前信号被自动屏蔽之外还希望自动屏蔽另外一些信号则用 sa_mask 字段说明这些需要额外屏蔽的信号当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags 字段包含一些选项本篇博客的代码都把 sa_flags 设0sa_sigaction 是实时信号的处理函数本章不详细解释这两个字段有兴趣的伙伴可以再了解一下。
#include iostream
#include unistd.h
#include signal.husing namespace std;void showPending(sigset_t* pending)
{for(int i 31; i 1; --i){if(sigismember(pending, i)) cout 1;else cout 0;}cout endl ---------------- endl;
}void handler(int signum)
{ cout 捕捉到了一个信号: signum endl;cout 捕捉到了一个信号: signum endl;cout 捕捉到了一个信号: signum endl;// 验证2号信号被捕捉期间,再次发送2号信号不会去处理sigset_t pending;int c 6;while(1){sigpending(pending);showPending(pending);--c;if(!c) break;sleep(1);}
}int main()
{// 一下内核数据结构变量是在用户栈定义的// 需要将它们设置进进程的内核中struct sigaction act, oldact;act.sa_flags 0; // 实时信号的标记位sigemptyset(act.sa_mask);act.sa_handler handler;// 设置进当前进程的PCB中sigaction(SIGINT, act, oldact);cout default action: (int)(oldact.sa_handler) endl;while(true) sleep(1);return 0;
}处理 2 信号的同时屏蔽 3 ~ 7 号信号
#!/bin/bashi2
id$(pidof mysignal)
while [ $i -le 7 ]
dokill -$i $idecho kill -$i $idlet isleep 1
done#include iostream
#include unistd.h
#include signal.husing namespace std;void showPending(sigset_t* pending)
{for(int i 31; i 1; --i){if(sigismember(pending, i)) cout 1;else cout 0;}cout endl ---------------- endl;
}void handler(int signum)
{ cout 捕捉到了一个信号: signum endl;cout 捕捉到了一个信号: signum endl;cout 捕捉到了一个信号: signum endl;// 验证2号信号被捕捉期间,再次发送2号信号不会去处理sigset_t pending;int c 7;while(1){sigpending(pending);showPending(pending);--c;if(!c) break;sleep(1);}
}int main()
{// 一下内核数据结构变量是在用户栈定义的// 需要将它们设置进进程的内核中cout id: getpid() endl;struct sigaction act, oldact;act.sa_flags 0; // 实时信号的标记位sigemptyset(act.sa_mask);act.sa_handler handler;// 处理2号信号期间,3 4 5 6 7号信号也被blocksigaddset(act.sa_mask, 3);sigaddset(act.sa_mask, 4);sigaddset(act.sa_mask, 5);sigaddset(act.sa_mask, 6);sigaddset(act.sa_mask, 7);// 设置进当前进程的PCB中sigaction(SIGINT, act, oldact);cout default action: (int)(oldact.sa_handler) endl;while(true) sleep(1);return 0;
}可重入函数 信号捕捉并没有创建新的进程或线程。 main 函数调用 insert 函数向一个链表 head 中插入节点node1插入操作分为两步。刚做完第一步的时候因为硬件中断使进程切换到内核再次回用户态之前检查到有信号待处理于是切换到 sighandler 函数sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2插入操作的两步都做完之后从 sighandler 返回内核态再次回到用户态就从 main 函数调用的 insert 函数中继续往下执行先前做第一步之后被打断现在继续做完第二步。结果是main 函数和 sighandler 先后向链表中插入两个节点而最后只有一个节点真正插入链表中了。像上例这样insert 函数被不同的控制流程调用有可能在第一次调用还没返回时就再次进入该函数这称为重入。insert 函数访问一个全局链表有可能因为重入而造成错乱像这样的函数称为不可重入函数。反之如果一个函数只访问自己的局部变量或参数则称为可重入(Reentrant) 函数。想一下为什么两个不同的控制流程调用同一个函数问它的同一个局部变量或参数就不会造成错乱可重入和不可重入是函数的一种特征目前我们用的函数90% 是不可重入的。 如果一个函数符合以下条件之一则是不可重入的 调用了 malloc 或 free因为 malloc 也是用全局链表来管理堆的。调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。 volatile 该关键字在 C 语言当中我们已经有所涉猎今天我们站在信号的角度重新理解一下。 #include iostream
#include unistd.h
#include signal.husing namespace std;int flag 0;void changeFlag(int signum)
{(void)signum;cout flag: flag endl;flag 1;cout flag: flag endl;
}int main()
{signal(2, changeFlag);while(!flag);cout 进程正常退出后: flag endl;
}编译器有时候会自动地给我们进行代码优化 我们看到即使对 flag 进行了修改也没有办法结束进程。这是为什么呢正常情况下每次循环通过 flag 进行检测时都需要到内存中去数据但是编译优化编译的时候已经进行了优化后编译器认为 main 函数里的代码没有对 flag 进行修改所以为了提高效率第一次过后就不去内存中取数据了而是直接读取寄存器中的值来进行循环检测。而实际情况是内存中 flag 的值早就被改成了 1 了所以就出现上图的情况了。 编译器优化会让 CPU 无法看到内存而关键字 volatile 就是为了保持内存的可见性每次都取内存中取数据。 SIGCHLD信号 进程一章讲过用 wait 和 waitpid 函数清理僵尸进程父进程可以阻塞等待子进程结束也可以非阻塞地查询是否有子进程结束等待清理也就是轮询的方式。采用第一种方式父进程阻塞了就不能处理自己的工作了采用第二种方式父进程在处理自己的工作的同时还要记得时不时地轮询一 下程序实现复杂。 其实子进程在终止时会给父进程发 SIGCHLD 信号该信号的默认处理动作是忽略父进程可以自定义 SIGCHLD 信号的处理函数这样父进程只需专心处理自己的工作不必关心子进程了子进程终止时会通知父进程父进程在信号处理函数中调用 wait 清理子进程即可。 事实上由于 UNIX 的历史原因要想不产生僵尸进程还有另外一种办法父进程调用 sigaction 或 signal 将 SIGCHLD 的处理动作置为 SIG_IGN这样 fork 出来的子进程在终止时会自动清理掉不会产生僵尸进程也不会通知父进程。系统默认的忽略动作和用户用 sigaction 函数自定义的忽略通常是没有区别的但这是一个特例。此方法对于 Linux 可用但不保证在其它 UNIX 系统上都可用。 #include iostream
#include unistd.h
#include signal.husing namespace std;void handler(int signum)
{cout 子进程退出: signum father: getpid() endl;
}// 证明:子进程退出,会向父进程发送信号
int main()
{signal(SIGCHLD, handler);if(fork() 0){cout child pid: getpid() endl;sleep(1);exit(0);}while(true) sleep(1);}自动等待子进程
#include iostream
#include signal.h
#include sys/wait.h
#include sys/types.h
#include unistd.husing namespace std;void handler(int sig)
{pid_t id;// -1表示等待任意一个子进程while ((id waitpid(-1, nullptr, WNOHANG)) 0){printf(wait child success: %d\n, id);}printf(child is quit! %d\n, getpid());
}int main()
{signal(SIGCHLD, handler);if (fork() 0){ // childprintf(child : %d\n, getpid());sleep(3);exit(1);}while (1){printf(father proc is doing some thing!\n);sleep(1);}return 0;
}#include iostream
#include signal.h
#include sys/wait.h
#include sys/types.h
#include unistd.husing namespace std;// 如果我们不想等待子进程,并且我们还想让子进程退出之后,自动释放僵尸子进程
int main()
{// OS 默认就是忽略的signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略if(fork() 0){cout child: getpid() endl;sleep(5);exit(0);}while(true){cout parent: getpid() 执行我自己的任务! endl;sleep(1);}
}总结 本篇博客主要讲解了什么是信号、信号如何产生、阻塞信号、捕捉信号、可重入函数以及 volatile 关键字和 SIGCHLD 信号等。那么以上就是本篇博客的全部内容了如果大家觉得有收获的话可以点个三连支持一下谢谢大家❣️