信号:信号是进程之间事件异步通知的一种方式,是一个软中断
作用:操作系统通过信号告诉进程发生了某个事件,打断进程当前的操作,去处理这个事件

信号在我们生活中无处不在,例如交通上的红绿灯,学校上下课的铃声,而且肯定是一个信号对应一个事件,并且我们能够识别这个信号

在操作系统中的信号也是如此,在linux下我们可以通过kill -l来查看信号的种类

但是我们会发现,没有32和33号信号,所以信号总数共有62种
1~31号信号是从unix借鉴而来的,每个信号都有具体对应的系统事件,但是它们是非可靠信号,也就是有可能会使信号丢失----事件丢失
34~64号信号是后期扩充的,它们都没有具体对应的事件,起的名字也比较粗糙,但是它们是可靠信号,也就是不会使信号丢失,肯定能传达到进程

信号的生命周期:产生->在进程中注册->在进程中注销->处理

信号的产生

信号的产生环境有两种,分别是硬件产生软件产生

硬件产生

在linux上,我们可以使用ctrl+cctrl+zctrl+\来产生硬件信号,操作系统再通过硬件信号转换成数字信号传递到进程,从而达到中断进程的作用

软件产生

kill -signum pid命令,中间的signum就是我们的信号值,例如-9对应的事件是SIGKILL,就是无论处于什么状态,都要去处理这个信号。例如我们的一个进程当处于暂停态时,如果不加上-9,默认是15号信号SIGTERM终止进程,是杀不死这个进程的,因为处于暂停态,此时进程不会处理任何事情。
kill杀死一个进程的原理:给进程发送一个终止信号,进程处理信号的方式就是退出进程
代码实现
接口kill(pid_t pid, int sig) 参数内容(进程id,信号值)给指定进程发送指定信号


接口raise(int sig)给自己发送指定信号
接口abort() 给进程自身发送SIGABRT信号,通常用于异常通知
接口alarm(int seconds) seconds秒之后给进程自己发送SIGALRM信号----俗称定时器

在进程中注册信号

在进程中注册信号,就是让进程知道自己收到了信号
在进程pcb中有struct sigpending,里面有struct sigset_t,在这个结构体中,只有一个数组成员,这个数组是用于实现一个位图,一共有64个元素。在没有收到信号之前,位图元素全部归0,000000...当进程收到信号时,会在这个位图中对应的位置置1,例如给进程发送3号信号,位图变为000100...,通过修改位图,从而注册信号。这个位图也称为是未决信号集合----收到但是没有处理的信号集合。位图数组中只有0/1,只能表示是否收到了某个信号,但是不能表示收到了多少个同样的信号;信号的注册其实不仅会修改位图,还会为信号组织一个sigqueue节点添加到pcb的sigqueue链表中。

1~31号非可靠信号的注册方式:若信号注册的时候,位图为0,则会创建一个sigqueue节点并修改位图为1,如果下次进来同样的信号,此时位图为1,此时就会将进来的信号丢掉
34~64号可靠信号的注册方式:不管位图当前是否为0,都会创建一个节点,添加到链表中,并修改位图置为1,此时加到链表就可以知道同样信号来的次数

在进程中注销信号

注销信号的原因是保证信号只会被处理一次,因此在处理信号之前要注销信号,在pcb中删除当前信号的信息。注销后会立即去处理刚注销的信号

1~31号非可靠信号的注销方式:因为非可靠信号只会有一个节点,因此删除节点后,位图直接置0
34~64号可靠信号的注销方式:因为可靠信号有可能同个信号被注册多次,有多个节点。因此删除节点后,需要判断是否存在相同节点,若没有相同节点才能将位图置0

信号的处理

信号表示一个事件的到来,处理事件就是完成功能,在c语言中完成一个功能的最小模块是函数。每个信号都会有对应自己事件的处理函数,当信号到来的时候,就会去执行这个处理函数,当处理函数执行完,也就表示该事件被处理完了

信号的处理方式

  1. 默认处理方式:操作系统中原定义好的每个信号的处理方式
  2. 忽略处理方式:处理方式就是忽略,不做任何处理
  3. 自定义处理方式:自己定义事件回调函数,修改内核中信号回调函数指针的指向,当信号到来就会调用我们自定义的回调函数了

通过pending位图中置为1的信号值,去handler信号处理数组中找对应的回调函数指针,然后进去执行这个指针指向的函数完成对事件的处理。

接口

//定义了一个名字叫sighandler_t的函数指针类型
typedef void (*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler)

参数内容(signum:信号值;handler :SIG_DFL-默认处理方式;SIG_IGN-忽略处理方式;用户自己定义一个没有返回值,有一个int型参数的函数地址)

忽略处理方式演示

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>int main()
{signal(SIGINT,SIG_IGN);while (1){printf("Hello WhiteShirtI\n");sleep(10);}return 0;
}

SIGINT信号的处理方式改为忽略处理方式,当程序运行时按下ctrl+c就无法终止程序

自定义处理函数演示

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>//自定义的处理函数
void sigcb(int signo)
{printf(" recv a signal no:%d\n",signo);
}int main()
{signal(SIGINT,sigcb);signal(SIGQUIT,sigcb);while (1){printf("Hello WhiteShirtI\n");sleep(10);}return 0;
}

SIGINT信号的处理方式改为自定义处理方式,当程序运行时按下ctrl+c就会去执行我们的信号处理函数。注意:只有当信号到来才会调用sigcb这个函数,并且通过参数传入当前触发回调函数的信号值。信号到来会打断当前进程的操作,当信号处理完后就继续执行进程,所以会出现每次发送信号处理完后都会打印一次数据

自定义处理方式的信号捕捉流程

信号的阻塞

信号的阻塞并不是不接受信号,信号可以正常注册,只是标识了哪些信号暂时不处理

在pcb中有一个block位图,也叫阻塞信号集合,如果存在这个位图中的信号来了,即使添加到了pending位图中,也会暂停处理

阻塞流程

  1. 自定义信号处理函数
  2. 将信号置为阻塞
  3. 在解除阻塞之前给进程发送信号
  4. 解除阻塞,查看信号的处理情况

如何阻塞一个信号

接口int sigprocmask(int how, sigset_t *set, sigset_t *old)
参数内容(how和set和oldhow有三个参数:SIG_BLOCK表示将set集合中的信号添加到内核中的block阻塞信号集合中,使用old保存原来的阻塞信息以便于还原,set | block;
SIG_UNBLOCK表示将set集合中的信号从内核中的block阻塞信号集合中移除,对set集合中的信号解除阻塞~set & blcok;
SIG_SETMASK表示将内核中的block信号集合内容设置为set集合中的信息。阻塞集合中的信号,block=set)

接口int sigemptyset(sigset_t *set) 清空set信号集合,初始化set集合

接口int sigaddset(sigset_t *set, int signum) 向set集合中添加指定的信号

接口int sigfillset(sigset_t *set) 将所有信号添加到set集合中

接口int sigdelset(sigset_t *set, int signum) 从set集合中移除指定的信号

接口int sigismember(const sigset_t *set, int signum) 判断指定信号是否在set集合中

代码测试
mask.c

//mask.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>void sigcb(int signo)
{printf("recv a signal:%d\n",signo);
}int main()
{signal(SIGINT, sigcb);signal(SIGRTMIN+4, sigcb);sigset_t set;//清空集合,防止未知数据造成影响sigemptyset(&set);//向集合中添加所有信号sigfillset(&set);//阻塞set集合中的所有信号sigprocmask(SIG_BLOCK, &set, NULL);printf("press enter continue\n");//等待一个回车,如果不按回车就一直卡在这里getchar();//对set集合中的信号解除阻塞sigprocmask(SIG_UNBLOCK, &set, NULL);while (1){sleep(1);}return 0;
}

运行mask程序,我们给进程发送了SIGINT和SIGRTMIN+4都被阻塞了,ctrl+c对应的信号也被阻塞了


当我们按下回车,解除信号,进程就会去执行我们发送的两个信号SIGINT和SIGRTMIN+4,我们会发现,可靠信号SIGRTMIN+4是收到多少信号就执行多少次处理函数,而非可靠信号是在阻塞时无论发送多少次,最后都只执行一次处理函数,发送了信号丢失事件。这就是非可靠信号和可靠信号的区别

但是我们要知道有两个特殊的信号是无法被阻塞的,SIGKILL-9SIGSTOP-19 这两个信号不可被阻塞,不可被
忽略,不可被自定义处理。




哪种情况下,进程无法杀死
1、僵尸进程
2、信号被阻塞或者自定义处理或者被忽略处理
3、进程是停止状态

示例
1、在命名管道中,如果管道所有读端被关闭,则继续写入会触发异常并退出,其实是触发了信号SIGPIPE-13,如果我们不想让程序退出,我们可以将该信号对应的处理函数改变成我们自定义的处理函数,这样子进程就不会退出,而是进程写入会触发信号就可以执行我们信号对应的信号处理函数了

2、僵尸进程,在子进程退出后会向父进程发送SIGCHLD-17信号通知父进程,让它知道子进程的状态改变了。但是因为SIGCHLD信号默认的处理方式是忽略处理,因此如果父进程不进行进程等待,信号就会被忽略处理,父进程就不知道子进程退出,子进程就成为僵尸进程。但是父进程进行进程等待,就会使父进程发生阻塞。如果我们想让父进程等待又不阻塞,我们就可以自定义SIGCHLD信号的处理方式,在自定义回调函数中调用waitpid,处理僵尸进程,只要当子进程退出时就会调用回调函数,这时父进程就不会发生阻塞了

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>void sigcb(int signo)
{int pid = waitpid(-1, NULL, 0);printf("child prcess %d exited\n", pid);
}int main()
{//发送SIGCHLD信号signal(SIGCHLD, sigcb);pid_t pid = fork();if (pid == 0){sleep(5);exit(0);}while (1){printf("Hello WhiteShirtI\n");sleep(1);}return 0;
}


这样子父进程就没有阻塞了,但是SIGCHLD信号是一个非可靠信号,如果有多个子进程同时退出,有可能造成信号丢失。这时候我们可以在回调函数中进行循环非阻塞等待,将所有僵尸进程全部处理掉

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>void sigcb(int signo)
{int pid;//循环非阻塞等待while ((pid = waitpid(-1, NULL, WNOHANG)) > 0){printf("child prcess %d exited\n", pid);}printf("所有的僵尸进程都被处理完毕\n");
}int main()
{//发送SIGCHLD信号signal(SIGCHLD, sigcb);pid_t pid = fork();if (pid == 0){sleep(5);exit(0);}pid = fork();if (pid == 0){sleep(5);exit(0);}while (1){printf("Hello WhiteShirtI\n");sleep(1);}return 0;
}

知识扩展

关键字volatile

volatile作用:用于修饰一个变量,保持变量的内存可见性,防止编译器的过度优化

cpu处理一个数据的过程是从内存中将数据加载到寄存器上进行处理
在gcc编译器中,在编译程序的时候,如果使用了代码优化 -Olevel选项,发现某个变量使用频率非常高,为了提高效率,则直接将变量的值设置为某个寄存器的值,以后访问的时候直接从寄存器访问,则减少了内存访问的过程,提高了访问效率
但是有时候这种优化有时候会造成代码的逻辑混乱,例如以下程序
我们想通过ctrl+c来改变循环的条件,让该循环结束

//volatile.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>long long a = 1;void sigcb(int signo)
{a = 0;printf("a = %d\n", a);
}int main()
{//当按下ctrl+c时会调用自定义回调函数signal(SIGINT, sigcb);while (a){};printf("exited a = %d\n", a);return 0;
}

gcc -O2优化编译volatile.c生成volatile程序,再执行volatile程序,当我们给进程发送信号,也就是按下ctrl+c去改变a的值时,循环并没有停下来,还继续运行中。

这时候volatile就是解决这种优化过度的问题,我们在定义变量a前添加关键字volatile,让cpu不管该变量的使用频率有多高,每次都重新到内存中获取数据

//volatile.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>volatile long long a = 1;void sigcb(int signo)
{a = 0;printf("a = %d\n", a);
}int main()
{//当按下ctrl+c时会调用自定义回调函数signal(SIGINT, sigcb);while (a){};printf("exited a = %d\n", a);return 0;
}

我们会发现,按下ctrl+c,a的值改变为0后,程序就跳出循环打印后边的数据,从而结束程序。

函数的可重入与不可重入

函数的重入:在多个执行流程中,同时进入一个函数并运行

例如下面程序,我们按下ctrl+c会去执行我们信号处理函数,主控流程也会去执行下一条语句,而这个执行的处理函数和我们主控流程要执行的函数是同一个函数,这就叫做函数的重入

#include <stdio.h>
#include <unistd.h>int a = 1;
int b = 1;int test()
{a++;b++;return a + b;
}void sigcn(int signo)
{printf("signal sum:%d\n", test());
}int main()
{   signal(SIGINT, sigcb);printf("main sum:%d\n",test());return 0;
}

函数的可重入:指的是函数重入之后,不会造成数据二义或者逻辑混乱
函数的不可重入:指的是函数重入之后,有可能造成数据二义或者逻辑混乱

让上面程序的test()函数睡上3秒

#include <stdio.h>
#include <unistd.h>int a = 1;
int b = 1;int test()
{a++;sleep(3);b++;return a + b;
}void sigcn(int signo)
{printf("signal sum:%d\n", test());
}int main()
{   signal(SIGINT, sigcb);printf("main sum:%d\n",test());return 0;
}

运行rentry程序,不发送信号,也就是不按下ctrl+c,程序正确打印

运行rentry程序,发送信号,也就是按下ctrl+c,程序输出后的结果是混乱的,所以上面的test函数是不可重入函数。


函数是否可重入的判断基准

这个函数中是否对全局变量进行了非原子的操作

操作的原子性:操作一次完成,中间不会被打断
原子操作:要操作就直接一次性完成,如果不是一次性完成就不做

函数可重入

  1. 一个函数若没有对全局变量进行操作,则是可重入函数,因为每个函数的调用都会有自己独立的函数栈
  2. 一个函数若对全局变量进行操作,但是操作是原子性的,则是可重入函数

Linux 进程信号详细总结相关推荐

  1. 【Linux】第八讲:Linux进程信号详解(一)_ 认识信号 | 产生信号

    「前言」文章是关于Linux进程信号方面的知识,本文的内容是Linux进程信号第一讲,讲解会比较细,下面开始! 「归属专栏」Linux系统编程 「笔者」枫叶先生(fy) 「座右铭」前行路上修真我 「枫 ...

  2. linux 查看进程的信号,Linux 进程信号查看与控制

    Linux 进程信号查看与控制 1) SIGHUP 本信号在用户终端连接 (正常或非正常) 结束时发出 通常是在终端的控制进程结束时 通知同一 session 内的各个作业 这时它们与控制终端不再关联 ...

  3. Linux进程信号(产生、保存、处理)/可重入函数概念/volatile理解/SIGCHLD信号

    首先区分一下Linux信号跟进程间通信中的信号量,它们的关系就犹如老婆跟老婆饼一样,没有一毛钱的关系. 信号的概念 信号的概念:信号是进程之间事件异步通知的一种方式,属于软中断.比如:红绿灯是一种信号 ...

  4. Linux进程的详细内容

    1 进程 1.1 程序的顺序执行与并发执行 程序的顺序执行: 程序的各操作步骤之间依序执行,程序与程序之间串行执行,称为顺序执行.顺序执行时单道程序系统中的程序的运行方式. 特点: 顺序性:一个操作结 ...

  5. Linux 进程信号:信号的概念、生命周期、产生流程、阻塞

    信号的概念 信号的生命周期 信号的阻塞 信号的概念 信号 信号是一个软中断.操作系统通过信号通知某个进程发生了某件事件,然后中断这个进程当前操作,让它优先去处理这个事件. 我们在linux下常用的ki ...

  6. Linux进程信号之阻塞信号

    前两天写了信号的基本概念以及如何去产生信号,欢迎大家戳博客链接:https://blog.csdn.net/apt1203JN/article/details/79955014 先来了解一下信号的三种 ...

  7. Linux 进程信号

    目录 什么是信号 生活中的角度 技术中的角度 PS 信号概念 kiil -l 查看所有信号 对于信号的分析 关于信号的产生(信号产生前) 1. 通过终端按键产生信号 2. 调用系统函数向进程发信号 3 ...

  8. Linux进程信号——信号的产生

    文章目录 1.信号的概念 2.通过Ctrl c分析信号 2.1Ctrl c的作用 2.2 signal接口 2.3证明ctrl +c本质是信号 2.4总结 3.信号处理的常见方式 4.信号的产生方式 ...

  9. Linux——进程信号(总结)

最新文章

  1. mysql dba系统学习(19)配置mysql+lvs+keeplived实现Mysql读操作的负载均衡
  2. 走过求职的季节(2)-十月 龙卷风
  3. mysql 权限管理 目录
  4. Git之深入解析如何使用Git调试项目源码中的问题
  5. iOS开发之本地通知UILocalNotification
  6. Docker Inspect
  7. Centos Missing Library: QtWebKit.so.4
  8. html 图片 保持长宽比,实现图片在页面中宽高一直保持16:9比例的方法
  9. Foundation框架: 5.常用结构体知识补充
  10. PowerDesiGner数据库设计
  11. 人脸识别研究任务及开源项目调研
  12. ASP.NET MVC中使用Autofac实现简单依赖注入
  13. 《天天数学》连载01:一月一日
  14. 三角形 JAVA 代码
  15. java 不变类_Immutable-不变模式与不变类-一版
  16. 微信小程序icon图标使用详解
  17. ROS——RPLIDAR A1 SDK详解
  18. D3基础 | 条形图
  19. Region Proposal by Guided Anchoring阅读笔记
  20. PHP 富文本内容中图片路径追加域名

热门文章

  1. mysql表分区数量限制_MySQL分区表的局限和限制详解
  2. oracle or索引失效,以下Oracle错误意味着什么:无效的列索引
  3. c语言制作图片软件,大佬们,小菜鸟想问一问用vc编译器做简易画图软件
  4. java 安全发布对象_Java安全的发布对象
  5. java ajax异步验证,【求助】真的不会做了。。关于AJAX异步验证的问题。。
  6. SQL SERVER中XML命名空间
  7. 玩转 SpringBoot 2 快速整合 Filter 注解版
  8. 基于JAVA+SpringBoot+Vue+Mybatis+MYSQL的在线音乐网站
  9. 基于JAVA+SpringBoot+Vue+Mybatis+MYSQL的汽车销售管理系统
  10. Spring Boot的学习之路(02):和你一起阅读Spring Boot官网