当前位置: 首页 > news >正文

进程信号的学习

信号

信号 :         产生           --            保存             --               处理

1.认识信号

信号VS信号量   老婆VS老婆饼     没有任何关系

1.信号:红绿灯,电话铃声
2.信号?中断我们正在做的事情,是一种事情异步通知机制
    信号是一种给进程发送的,用来进行异步通知的机制
    信号的产生,相对于进程的运行是异步的
    信号是发给进程的


我们自习,等他上厕所回来再一起讲   同步
我们继续上课,他去上厕所           异步

1.信号处理,进程在信号没有产生的时候早就知道信号该如何处理(我们点了外卖,外卖电话还没接到,接到电话我们知道我们是要去拿外卖的)
2.信号的处理并不是立即处理,而是等一会再处理,合适的时候再进行信号的处理,如果当时不适合处理信号,我们就必须把要处理的信号记录下来
3.人能识别信号,是提前被教过的,进程也是这样。OS程序员设计的进程,进程早就已经内置了对于信号的识别和处理方式
4.信号源非常多,给进程产生信号的信号源也非常多

2.信号的产生

都要OS来执行

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>void handlerSig(int sig)
{std::cout << "获得了一个信号" << sig << std::endl;
}int main()
{signal(SIGINT,handlerSig);int cnt = 0;while(true){std::cout << "hello world, " << cnt++ << " ,pid: " << getpid() << std::endl;sleep(1);}
}

查到这个进程

dm@hcss-ecs-f2ab:~/0419$ ps ajx | head -1 && ps ajx | grep testsigPPID     PID    PGID     SID TTY    TPGID STAT  UID   TIME COMMAND
1423645 1423892 1423892 1423645 pts/2  1423645 T   1000  0:00 ./testsig
1422936 1424245 1424245 1422936 pts/1  1425998 T   1000  0:00 ./testsig
1423645 1425243 1425243 1423645 pts/2  1423645 T   1000  0:00 ./testsig
1423645 1425373 1425373 1423645 pts/2  1423645 T   1000  0:00 ./testsig
1422936 1425999 1425998 1422936 pts/1  1425998 S+  1000  0:00 grep --color=auto testsig


产生信号的方式非常多

2-1.键盘产生信号

ctrl+c是给目标进程(前台进程)发送信号的。相当一部分信号的处理动作就是让自己终止

ctrl + c 发送2号信号

ctrl + \  退出

信号有哪些?kill -l 查看信号

每个信号都有⼀个编号和⼀个宏定义名称
编号34以上的是实时信号,我们不讨论实时信号

man 7 signal  查看信号的详细处理

目标进程是什么意思?前台进程和后台进程
#./XXX  -- 前台进程
#./ZZZ & -- 后台进程  (ctrl+c 不会做处理。父进程先退出的孤儿进程,要用kill 9)
命令行shell进程 -- 前台进程

后台进程无法从标准输入中获取内容
前台进程能从键盘中获取标准输入
但是都可以向标准输出上打印

原因:键盘只有一个,输入数据一定是给一个确定的进程的,前台进程必须只有一个,后台进程可以有多个
前台进程的本质就是要从键盘获取数据

jobs:查看所有后台任务


fg 任务号:特定的进程,提到前台

ctrl+z:进程切换到后台
bg 任务号:让后台进程回复运行

2-2.系统调用

kill命令是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号

kill函数

mykill.cc

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>int main(int argc,char *argv[])
{if(argc !=3){std::cout << "./mykill signumber pid" << std::endl;return 1;}int signum = std::stoi(argv[1]);pid_t target = std::stoi(argv[2]);int n = kill(target, signum);if(n == 0){std::cout << "send " << signum << " to " << target << " success ";}return 0;
}

raise函数

可以自己给自己发送任意指定信号

我们做自定义捕捉

我们会发现 9) SIGKILL 无法做自定义捕捉

abort函数

给自己发一个固定的信号,使当前进程接收到这个固定的信号而异常终止

2-3.系统调用命令向进程发信号kill

kill -SIGSEGV PID

2-4.[硬件]异常产生信号

引起硬件错误,OS给进程发信号,进程就退出了

就会崩掉 

1.除0错误

2.野指针

8) SIGFPE

SIGFPE       P1990      Core    Floating-point exception

OS识别硬件出错,发现计算机是溢出的,CPU保存的是当前进程的上下文,当前进程的task_struct识别到出错进程,得出计算机移除,发送8号信号

11) SIGSEGV   段错误

SIGSEGV      P1990      Core    Invalid memory reference

当程序试图访问一个空指针指向的内存地址时,就会发生段错误

0号地址在页表中不存在映射关系,无法映射到物理内存

MMu(拿着虚拟地址做在页表物理内存直接的转换)集成在cpu,有可能转换失败硬件报错,运行的依旧是当前进程的上下文

2-5.由软件条件产生信号

信号的软件条件是一种信号产生机制,触发因素是软件内部状态或特定的软件操作,满足特定的软件操作时,OS会向相关进程发送对应的信号,进程收到信号后执行相应处理

SIGALRM 属于一种信号,其用途是在设定的时间到达时通知进程

SIGALRM信号的编号通常是 14,当使用 alarm() 或者 setitimer() 函数设定的定时器到期时,系统就会向进程发送此信号

当一个进程向已经关闭写端的管道写入数据时,操作系统会向该进程发送 SIGPIPE 信号

调用alarm(seconds)时,设定了在seconds秒之后,OS会向当前进程发送SIGALRM信号

3.信号的保存

OS发送信号实际是修改比特位

1.实际执行信号的处理动作称为信号递达
2.信号从产生到递达之间的状态称为信号未决
3.进程可以选择阻塞某个信号
4.被阻塞/(屏蔽)的信号产生时将保存在未决状态,直到进程解除对这个信号的阻塞才会执行递达的动作
5.信号被阻塞就不会递达,忽略是在递达之后可选的一种处理动作

信号在内核中的表示示意图


task_struct有保存信号的三张表
pending 待处理信号
unsigned int pending 保存收到的信号的位图
比特位的位置:表示的是第几个信号
比特位的内容:是否收到信号
对应数值为1表示处于待处理状态


handler 
函数指针数组。信号处理函数
sighander_t hander[31]
数组下标:信号编号

block
unsigned int block; 也是位图
比特位的位置:表示的是第几个信号
比特位的内容:是否阻塞

SIGHUP(1)对应的元素值为0,表示该信号没有被阻塞
SIGINT(2) 和 SIGQUIT(3)对应的元素值为1,表示信号被进程阻塞,进程暂时不响应这两个信号

linux提供信号操作,一定是围绕着这仨张表展开的

默认情况下进程不对任何信号进行屏蔽,即全为0

代码示例

1.自定义捕捉

通过std::cout 输出接收到的信号编号

2.

3.

阻塞信号集也叫做当前进程的 信号屏蔽字(Signal Mask),


每个信号只有一个bit的未决标准,不记录信号产生的次数,阻塞标志也一样。所以未决和阻塞可以用相同的sigset_t(类似权限学习里的unmask)数据类型来存储


信号集操作函数

调用sigprocmask可以读取或更改进程的信号屏蔽字

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

成功返回1,出错返回-1

• 如果oset是非空指针,函数就会把当前的信号屏蔽字通过oset参数传给调用者
• 如果set是非空指针,就要由how参数来指示如何更改进程的信号屏蔽字
• how参数
当前信号屏蔽字为mask set里包含了一些信号
how是SIG_BLOCK时 相当于把set里的信号添加到mask中
mask = mask | set
eg:
原来屏蔽字mask阻塞了信号A,set里有信号B,执行后屏蔽字就就阻塞信号A和B

SIG_UNBLOCK时:set里是希望从当前信号屏蔽字中解除阻塞的信号
mask = mask & ~set
eg:
原来屏蔽字阻塞了信号A和B,set里有信号B,执行后屏蔽字就只阻塞信号A了

SIG_SETMASK:直接把当前信号屏蔽字设置为set所指向的值,类似 mask = set
eg:
原来mask阻塞了一堆信号,set只包含信号C,执行后就只阻塞信号C
 

sigpending

读取当前进程的未决信号集,通过set参数传出

#include <signal.h>

int sigpending(sigset_t *set);

成功返回1,出错返回-1

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <vector>void PrintPending(sigset_t &pending)
{printf("我是一个进程(%d), pending: ", getpid());for (int signo = 31; signo >= 1; signo--){if (sigismember(&pending, signo)){std::cout << "1";}else{std::cout << "0";}}std::cout << std::endl;
}
void handler(int sig)
{std::cout << "#######################" << std::endl;std::cout << "递达" << sig << "信号!" << std::endl;sigset_t pending;int m = sigpending(&pending);PrintPending(pending); // 0000 0010(处理完,2号才回被设置为0),0000 0000(执行handler方法之前,2对应的pending已经被清理了)std::cout << "#######################" << std::endl;
}
int main()
{signal(SIGINT, handler);// 1. 屏蔽2号信号sigset_t block, oblock;sigemptyset(&block);sigemptyset(&oblock);sigaddset(&block, SIGINT); // 已经对2号信号进行屏蔽了吗?没有!//for(int i = 1; i<32; i++)//sigaddset(&block, i);int n = sigprocmask(SIG_SETMASK, &block, &oblock);(void)n;// 4. 重复获取打印过程int cnt = 0;while (true){// 2. 获取pending信号集合sigset_t pending;int m = sigpending(&pending);// 3. 打印PrintPending(pending);if (cnt == 10){// 5. 恢复对2号信号的block情况std::cout << "解除对2号的屏蔽" << std::endl;sigprocmask(SIG_SETMASK, &oblock, nullptr);}sleep(1);cnt++;}return 0;
}

一开始没有信号被发送,所以所有信号位都为 0

我是一个进程(12345), pending: 00000000000000000000000000000000
我是一个进程(12345), pending: 00000000000000000000000000000000
我是一个进程(12345), pending: 00000000000000000000000000000000

按ctrl+c,由于信号被屏蔽他不会立即递达,进入挂起状态

我是一个进程(12345), pending: 00000000000000000000000000000000
我是一个进程(12345), pending: 00000000000000000000000000000000
^C我是一个进程(12345), pending: 00000000000000000000000000000010
我是一个进程(12345), pending: 00000000000000000000000000000010

程序运行10秒后,会解除信号的屏蔽,该信号会被递达,执行hander函数并且输出相应信息

我是一个进程(12345), pending: 00000000000000000000000000000010
我是一个进程(12345), pending: 00000000000000000000000000000010
解除对2号的屏蔽
#######################
递达2信号!
我是一个进程(12345), pending: 00000000000000000000000000000000
#######################

处理动作:

收到信号,合适时处理信号
1.默认处理动作(等红灯)
2.自定义信号处理动作(在马路上跳舞)
3.忽略处理(闯红灯)


2->自定义捕捉

给进程发送信号?信号产生之后并不是立即处理的,是在合适的时候处理,所以进程必须把信号记录下来

记录在struct task_struct
    {
        unsigned sigs;
    }
靠位图结构记录,32bit位。如何记录?

在进程PCB里维护一个整数。发信号的本质是向PCB位图修改 0->1代表发了信号

修改位图需要pid、信号的编号

相关位图的本质是修改内核数据。只能OS自己修改
不管信号(就是数字)怎么产生,发送信号在底层必须让os发送!!!
OS自己提供发送信号的系统调用 eg:kill   !!!

合适的时候处理信号是什么时候?进程从内核态返回到用户态的时候(会进行信号检查)

信号是默认/忽略的比自定义捕捉要简单

我们在执行自定义方法(用户写的) 必须以用户的身份执行

信号的捕捉流程

是一个进程就会被调度

中断

硬件中断:由外部设备触发的,中断系统运行流程

os怎么知道键盘上有数据?硬件中断

外设也存在寄存器

cpu有特定针脚

中断向量表就是一个函数指针数组,下标就是中断号

没有中断的时候,OS什么都不做,是暂停的

for(;;)

        pause();

时钟中断

OS,就在硬件时钟中断的驱动下,进行调度。所以OS就是基于中断进行工作的软件

时钟源,会以特定的频率向cpu

外设没有中断时,CPU自己每隔一段时间(固定的频率,固定的时间间隔)触发中断,由cpu去调度我们自己注册的schedule()方法

current -> count--;//时间片耗尽

if(current -> count == 0)

{
        schedule();

}

当前时间,计算成时间戳,知道计算机的主频就可以转换成历史总频率,总的相加,就算计算机离线我们也能知道几点了

所有的硬件异常都会被转化成中断,所以OS就会知道

软中断

cpu内部自己可以让软件触发中断吗?让cpu通过软件主动中断   ->   软中断

C/C++代码本质就是编译成为了指令集+数据

我们进行系统调用的时候,具体是怎么进入操作系统完成系统调用过程,cpu只有一个

系统调用表

每一个系统调用都要一个唯一的下标,这个下标我们叫做系统调用号

OS不提供任何系统调用接口,OS只提供系统调用号

系统调用的过程,也是在进程地址空间上进行的所有函数的调用都是地址空间之间当跳转

所谓的时间片就是计数器
 

无论进程如何调度我们总能找到操作系统

让cpu执行中断处理方法,读取键盘数据
进程调度方法
发送cpu中断,cpu周期性地执行
时钟源集成在cpu内部,外设没有中断时,每隔一段时间向我们当前cpu自己触发中断,由我们cpu自己去调度注册的中断方法  schedule()

os怎么知道硬件出异常/中断?
软件触发的中断 => 软中断
 

缺页中断

真正用的时候再申请 => 缺页中断

eg:访问了一个较大的数组中超出当前已经加载页面范围的元素

OS=>软硬件结合

内核态和用户态

1.无论进程如何调度我们总能找到OS(OS也是软件,一定也存在内存,只要找到进程就能找到对应的OS),OS系统调用方法是在进程的地址空间中执行的

2.用户和内核都在同一个[0,4GB]的地址空间上

OS为了保护自己不相信任何人,必须采用系统调用的方式进行访问

用户态:以用户身份,只能访问自己的[0,3GB]的代码和数据

内核态:以内核的身份,运行我们通过系统调用的方式,访问OS[3,4GB]的代码和数据

用户或者OS自己怎么知道当前处于用户态还是内核态?

使用代码段寄存器(cs)的最低两位来表示特权级别,00表示内核态(特权级0),11表示用户态(特权级3)用户/OS可以读取这些标志来确定当前所处的状态


自定义捕捉     
检查并改变信号处理动作

sigaction 函数

1.默认会自动阻塞(屏蔽)该信号的后续发送

2.可有效避免信号处理中的竞态条件和不可重入问题

正在执行某一个信号时,OS会把它自动屏蔽,修改block表,这个信号就不可能再递达
信号处理完成时就会解除屏蔽
 

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);//signum:信号编号
//act:用于指定新的信号的处理方式,如果为空则不改变当前处理方式
//oldact:用于保存旧的信号处理方式,如果为空则不保存

在调用信号处理函数时,除了当前信号被自动屏蔽值位,还希望屏蔽另外一些信号们可以使用信号集说明这些需要额外屏蔽的信号

struct sigaction {void     (*sa_handler)(int);         // 信号处理函数指针void     (*sa_sigaction)(int, siginfo_t *, void *); // 带额外信息的处理函数sigset_t   sa_mask;                  // 信号处理期间要阻塞的信号集int        sa_flags;                 // 控制信号处理行为的标志void     (*sa_restorer)(void);       // 已弃用,应设为 NULL
};


可重入函数:是指可以被多个执行流同时调用的函数

可重入/不可重入本质是描述函数特点

大部分函数都是不可重入的

不可重入:

1.调用了malloc/free     --   malloc也是用全局链表来管理堆的

2.调用了标准I/O库函数    --    标准I/O库的很多实现都以不可重入的方式使用全局结构

3.使用全局变量或静态变量

函数只有自己的临时变量 -- 可重入


volatile:告知编译器,被改关键字修饰的变量不允许被优化,对该变量的优化必须在真实的内存中进行操作

适用场景:适用于变量可能被程序外部元素修改的易变场景(硬件寄存器、多线程共享变量)

核心价值在于确保变量与内存的直接交互,防止被编译器过度优化,保存内存空间可见性

所有进程共享内核页表
易变关键字修饰的时候代表的是某个变量是易变的,所以CPU在访问时不能优化成寄存器变量,不能覆盖我们的内存,要保持内存可见性,每次访问该变量要从内存里读


SIGCHLD信号

默认处理动作:忽略

WNOHANG => 可以是多进程局部退出,由阻塞=>非阻塞

1. 主动回收:子进程在终止时给父进程发送SIGCHLD信号,该信号的默认处理是忽略。父进程可以自定义该信号的处理函数,父进程通过捕获SIGCHLD并调用wait()/waitpid()回收子进程资源,防止僵尸进程堆积

2. 自动回收:IGN可以实现进程退出时自动释放资源,不会产生僵尸进程也不会通知父进程

默认是lgn,我们还是设置为SIG_IGN?通常是没有区别的这是一个特例
父进程处理SIGCHLD的时候用的是SIG_DFL,是缺省的,缺省动作lgn,signal(SIGCHLD ,SIG_DFL) => signal( SIGCHLD,SIG_IGN),叫做忽略
 

相关文章:

  • 使用termius连接腾讯云服务器
  • MySQL八股(自用)
  • 【JAVA常见数据类型】
  • Android学习总结之kotlin篇(二)
  • 前端3D动画库
  • [Java实战]Spring Boot 3整合JWT实现无状态身份认证(二十四)
  • 18前端项目----Vue项目收尾优化|重要知识
  • ubuntu studio 系统详解
  • Spring Boot拦截器详解:原理、实现与应用场景
  • 计算机过程控制干燥操作实训装置JG-SX210化工单元操作实训装置
  • JavaScript 中级进阶技巧之map函数
  • 【嵌入式笔记】Modbus TCP
  • git仓库初始化
  • zabbix7.2最新版本 nginx自定义监控(三) 设置触发器
  • Anki 学习法
  • 深入浅出 IPFS 在 DApps 和 NFT 中的应用:以 Pinata 实战为例
  • 印度尼西亚数据源对接技术指南
  • vue3基础学习(上) [简单标签] (vscode)
  • 基于单片机的车灯智能控制系统设计与实现
  • 嵌入式中深入理解C语言中的指针:类型、区别及应用
  • 上海制造佳品汇大阪站即将启幕,泡泡玛特领潮出海
  • 今年有望投产里程已近3000公里,高铁冲刺谁在“狂飙”?
  • 法治课|争议中的“行人安全距离”于法无据,考量“注意义务”才更合理
  • 人才争夺战,二三线城市和一线城市拼什么?洛阳官方调研剖析
  • 联合国秘书长欢迎中美经贸高层会谈成果
  • 国羽用冠军开启奥运周期,林丹:希望洛杉矶奥运取得更好成绩