目录
预备知识
信号和信号量没有任何关系
1、进程怎么认识信号的? 因为有人教,记住了。
可以识别信号 知道信号的处理方法
进程必须能识别并能够处理信号,即使信号没有产生,也要具备处理信号的能力---信号处理能力,属于进程内置功能的一部分
2、即使现在没有产生信号,但是我知道信号产生之后,我该干什么。
3、信号产生了,我们并不立即处理这个信号,因为我们现在有更重要的事。所以在信号产生之后,在信号到来和处理信号之间的时间窗口内,在时间窗口内,你必须记住信号到来。
当进程真的收到了一个具体的信号的时候,进程可能并不不会立即处理这个信号,等到合适的时候才会处理
一个进程必须当信号产生,到信号开始被处理,就一定会有时间窗口,进程具有临时保存哪些信号已经发生了的能力。
./myprocess进程运行起来 为什么ctrl+c就能杀掉前台进程呢?
./myprocess & 这样进程就会变成后台进程,ctrl+c就不能杀掉了Linux中,一次登录中,一个终端会配一个bash,每一个登录只允许一个进程是前台进程,允许多个进程是后台进程。谁是前台谁就能拿到键盘的输入。键盘输入首先是被前台进程收到的。 ctrl+c 的本质是被进程解释为收到信号---2号信号。
进程收到2号信号的默认动作就是终止自己。
没有0号信号 没有32、33号信号 一共有62个信号 1到31号信号称作普通信号 34到64称作实时信号。实时信号一旦产生了就必须立即处理。(我们一般不考虑)
信号的处理方式:
1、默认动作 (内置的动作) 2、忽略 3、自定义动作(信号的捕捉)
#include <signal.h>
typedef void(*sighandler_t)(int);参数类型是int 返回值是void 的函数指针类型
sighandler_t signal(int signum, sighandler_t handler); 设置特定进程的自定义属性 第二个参数就是上面的指针类型
只有后面遇到了signum信号,才会触发handler函数(函数的方法才会被调用)
第一个参数signum-----信号编号
第二个参数handler-----当前进程对信号执行自定义捕捉动作(修改特定进程对信号的处理动作)
总结:这个函数可以对指定信号采取捕捉动作,singal函数就像一个注册方法,设置对2号信号进行自定义捕捉,并不是调signal的时候该方法(handler)就被调了,只是告诉系统和进程我要更改2号进程的处理方法,以前是默认现在是自定义(内置方法),如果说我们调用了signal但是代码或者进程后续并没有收到信号,那么handler方法是不会被调用的。
当捕捉到信号之后,将默认动作变为了自定义动作。(ctrl+c就不会退出了)
键盘数据是如何输入给内核的,ctrl+c又是如何变成信号的
键盘被按下,肯定是OS先知道的! OS怎么知道键盘上有数据了??
linux下一切皆文件,因此键盘也是文件。读取键盘数据就是将键盘数据读取到键盘文件的缓冲区中。那么OS如何知道键盘中有数据了??
数据层面上,CPU是不和硬件打交道的。但是在控制层面,CPU是要能读取外设的。 cpu上有很多针脚,针脚是集成在主板上的,而每一种设备也是插在主板上的,键盘在物理上是可以直接和cpu相连的,cpu虽然不从键盘读数据但是键盘可以给cpu发送一个硬件中断(每一种中断都有中断号) 设备向cpu发送中断,cpu就要记录中断号。cpu内有寄存器,寄存器可以保存数据,但是这个寄存器凭什么可以保存数据呢? 给针脚发送中断的过程就是给cpu发送高低电平的过程,保存数据其实就是充电放电的过程。 OS内部会维护一张中断向量表(其实是一个数组) 里面存的是方法(直接访问外设的方法(主要是磁盘、显示器、磁盘))和地址。
中断号到cpu当中(cpu拿到中断号),触发了中断,OS就会立马识别到这个中断号(,就知道有设备就绪了),OS以中断号为索引在中断向量表中去找对应的方法,找到之后执行这个方法,这个方法才是将数据从外设拷贝到内存当中的方法。
当键盘的数据是ctrl+c的组合键呢? OS拿到数据会先对数据进行判断,控制(ctrl+c 转化成2号信号发送给进程)
键盘是基于硬件中断来进行工作的。
OS在内存里,OS怎么知道哪个外设好了、或者数据已经读完了
OS必须得随时知道每一种设备的状态,因为OS是硬件的管理者
系统是通过软硬件结合的方式来做的,硬件设备会在就绪之后通过中断单元向cpu的针脚发送硬件中断,这样cpu就能获取对应设备的中断号,cpu就会去OS提供的中断向量表中找终端号对应的中断处理方法。 这样就不需要OS不断对外设进行检测。
一、信号的产生
信号的产生和我们自己的代码运行是异步的。 (我们执行自己的代码信号可能随时会到来)
信号属于软中断(信号是仿照硬件中断实现的软件中断)
1、键盘组合键产生信号:
ctrl + c:二号信号 xtrl+\ : 三号信号
进程要接收键盘信号,进程首先得是前台进程
不是所有的信号都可以被signal捕捉的,eg:9号信号(杀掉进程信号) 19号信号(暂停信号) 2、kill命令:kill -signo pid
3、系统调用
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig) //给指定进程发送信号
第一个参数:pid
第二个参数:sig----信号编号
4、异常
进程出异常本质上时收到了信号。那么,进程收到异常信号一定会退出吗??答案是:不一定!!因为异常可以被我们捕捉,所以不一定会退出,但是异常退出了,肯定是执行了信号异常处理方法。
为什么进行除零或者野指针问题会让进程崩溃? 因为给进程发信号了 为什么会给进程发信号??因为OS识别到了系统中有除零这些错误,就会给进程发信号。 OS是如何知道发生了除零操作的??? 当有一个进程,进程的代码中有一行是除0操作,cpu中有一个eip寄存器,可以知道代码运行到哪一行了,还有一个状态寄存器,这个状态寄存器有一个溢出标志位,当识别到除零错误之后,状态寄存器由0变为1。因为OS是硬件的管理者所以它必须要知道cpu的状态,它就知道了发生了除零错误。(cpu寄存器中的数据,是进程的上下文,虽然因为这个进程中的错误我们修改了CPU内部的寄存器状态,但是这并不影响其他进程,因为这些cpu中的寄存器数据是我这个进程的上下文) OS是如何知道发生了野指针问题??? MMU内存管理单元, 集成在cpu内部,cpu读到的都是虚拟地址,会结合页表、虚拟地址、MMU三个,就可以将虚拟转化为物理。野指针其实就是虚拟到物理地址转化失败,任何的寻址都先得转化成功之后再进行。 cpu中有一个寄存器,是当虚拟地址到物理地址转化失败之后,将虚拟地址存在寄存器中。 转化失败之后也会出现硬件报错,报错也会被OS识别到。
5、软件条件产生信号
异常只会由硬件产生吗??
不一定。 比如存在一个管道,管道有读端和写端,当读端关闭,写端还一直往管道写数据时,OS就会发送13号信号SIGPIPE,这就属于软件异常,软件问题引起的异常。
闹钟
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数是设定的时间 返回值是 如果在设定时间之前反应,返回值就是剩余的时间
int main()
{
int n = alarm(5);//设定5秒 如果进程的运行时间不足5s 那么也就不会收到这个信号
while(1)
{
cout << "proc is running..." << endl;
sleep(1);
}
return 0;
}
void handler(int signo)
{
cout << "...get a sig, number: " << signo << endl;
alarm(5); //设置闹钟 但现在不会响 函数直接返回 从下面函数的while开始执行
//等5s之后,闹钟响 被捕捉 再设置
}
int main()
{
signal(SIGALRM, handler);//捕捉信号 自定义行为
int n = alarm(5);//设定5秒 如果进程的运行时间不足5s 那么也就不会收到这个信号
while(1)
{
cout << "proc is running..." << endl;
sleep(1);
}
return 0;
}
闹钟每隔5s响一次
void work()
{
cout << "print log..." << endl;
}
void handler(int signo)
{
work();
//cout << "...get a sig, number: " << signo << endl;
alarm(5);
}
int main()
{
signal(SIGALRM, handler);//捕捉信号 自定义行为
int n = alarm(5);//设定5秒 如果进程的运行时间不足5s 那么也就不会收到这个信号
while(1)
{
cout << "proc is running..." << endl;
sleep(1);
}
return 0;
}
定时执行任务
core dump标志位表示信号终止的方式:是以term方式还是core方式
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 50;
while(cnt)
{
cout << "i am a child process, pid: " << getpid() << " cnt: "<< cnt << endl;
sleep(1);
cnt--;
}
exit(0);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
//rid 退出状态(次低八位) 终止信号(低7位) core dump标志位(低八位)---将右移七位,将终止信号移走,将其放到最低位
cout << "child quit info, rid: " << rid << " exit code: " <<
((status>>8)&0xFF) << " exit signal: " << (status&0x7F) << " core dump: " << ((status>>7)&1) << endl;//(status>>7)&1 1是整数,32个比特位,最后一位是1,按位与之后将除过最后一位的全都清零只保留最后一位(最后一位是1就是1是0就是0)
}
}
现象:
打开系统的core dump 功能,一旦进程出异常,OS会将进程在内存中的运行信息,给dump(转储)到进程的当前目录(磁盘)形成core dump文件:核心转储(core dump)
为什么要进行核心转储:运行时错误,代码是在哪行出错,为什么出错。所以core dump是方便事后调试的(先运行,再调试)
为什么在云服务器上这个core dump 是被关闭的:因为服务器是在挂掉之后立即重启的,这样就会core dump 就会保存core dump 文件,但是如果有刚启动就挂掉的,就会立马形成该文件,服务器又被重新启动 又形成文件,每core dump 一次就会形成一个临时文件,这样磁盘就会被占满,可能导致OS挂掉,因此一般默认是被关掉的。
二、信号的保存
1、信号的发送与保存
什么是信号的发送
对于普通信号而言,对于进程而言,自己有没有收到哪一个信号是给进程的PCB发。
task_struct{
int signal; //0000 0000 0000 0000 0000 0000 0000 0010 32个比特位 普通信号,位图管理信号
}
1、比特位的内容是0还是1表示是否收到
2、比特位的位置(第几个),表示信号的编号 例如上面改的是第一个(我们可以理解为下标从0开始)
3、所谓的“发信号”,本质上就是OS去修改task_struct的信号位图对应的比特位,其实可理解为写信号。 一个os要给一个进程发信号,只需要找到pcb,找到对应的字段,将对应的字段由0置1,信号发送完成。(OS是进程的管理者,只有他有资格去修改task_struct内部的属性!) 但是!OS系统不能将进程直接杀掉,只能发信号,因为OS害怕进程正在执行重要的事情,万一直接杀掉有数据没有保存就会出问题。
信号的保存:
进程收到信号可能并不会立即处理。信号不会被处理就要有一个时间窗口。在这个时间窗口以内信号已经产生但还没有被处理。 (普通信号比较简单,因此采用位图来保存。)
普通信号(1~31号信号) 比如发了10次2号信号,就只会保存一次,执行一次。
34~64号信号被称为实时信号。实时信号发过来了就必须立即处理。实时信号发了10次就需要处理10次。每产生一个信号就有一个信号队列(实时信号用对列来进行管理)
1、信号要被处理:忽略 默认 自定义 实际执行信号处理的动作被称作信号的递达。
在内核中表示:
2、信号从产生到递达之间的状态(信号在被保存),称为信号未决(pending)。----和位图相关
每一个信号都要有一个hander表,这个表的元素是函数指针类型
typedef void(*handler_t)(int);
hander_t handler[31]; (函数指针数组)
每个指针指向一个方法(OS提供的默认的方法)
3、进程可以选择阻塞(block)某个信号
系统会收到各种各样的信号,我们可以选择屏蔽某种信号,只有解除屏蔽了才能递达。
屏蔽是一种状态,和是否产生没有关系。没有产生也可以屏蔽。
block也是位图表和pending一样。比特位的位置决定信号的编号,比特位的内容为0表示不屏蔽,为1表示屏蔽。
block表用来记录特定信号是否被屏蔽
pending表记录当前进程是否收到了信号包括收到了哪些信号(没有被处理的信号)
handler表表示每种信号对应的处理方式
4、被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。(当某个信号被阻塞,还是可以给进程发送信号的,只不过信号被屏蔽了罢了。)
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2、sigset_t
pending表、block表、handler表都是OS的内核数据结构,OS不相信用户,他不会让用户直接去修改这三张表,因此用户要修改这三张表时就需要系统调用接口。要获取pending表和block表就注定了要在用户空间和内核空间、内核空间和用户空间进行来回的数据拷贝,就要在接口的设计上设计输入输出型参数,
3、signprocmask
调用sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
#include<signal.h>
int sigprocmask(int how, const sigset_t *set,sigset_t *oset);
我们可以通过三个参数达到去信号屏蔽字(block信号集)的设置
若成功返回1,出错返回-1
how参数的可选值:
SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask = mask | set
或运算相当于在原来的基础上新增。(将两个不同的比特位进行按位或就是比如2号和3号都置1的效果)
SIG_UNBLOCK:mask = mask&~set 先把传入的信号集按位取反,为0的变为1,为1的变0,例如当二号信号2号位为1其他全为0,取反之后2号位为0,其他为1 然后&之后就是去掉在set中设置的信号(在block表里去掉我们传入的所有信号)eg:我们已经对1、2、3号做屏蔽了,我们不想对2号再屏蔽就把2号信号利用这个接口传进去就可以被屏蔽。
SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set。
将set传入的参数设置进内核的进程pcb当中的block表里。相当于覆盖式的重新设置对信号进行屏蔽的字段。
const sigset_t *set:
输入型参数,自己写代码时可以自己提前对位图结构做设置,然后将设置好的信号集通过sigprocmask传进来,哪个进程调这个函数 就会将这个set按照how对这个集合里添加的信号要么新增要么去掉要么重新设置。
sigset_t *oset:
输出型参数,可以保存被修改之前的block表,即保存的是上一次的。
4、sigpending
#include <signal.h>
int sigpending(sigset_t *set)
参数是输出型参数,是把调用进程对应的pending表以位图的形式带出来。方便未来做检查。
返回值 0成功 -1失败
三、信号的处理
信号是在什么时候被处理的??
前提是已经知道自己收到了信号,那么进程就要在合适的时候查一查自己对应的pending位图、block位图和handler表,而这三个都属于内核结构,其他人无法查阅。那么进程就必须处在内核状态才可以对信号做处理。
当进程从内核态返回到用户态的时候,进行信号的检测和处理!!!
可以理解为:返回意味着已经将重要的事情做完了,顺手将信号进行处理
我们自己的代码有自己写的、库的、操作系统的代码 代码组成比较复杂
一般执行自己写的代码和库的代码是在用户态 调用系统调用的时候就会将身份由用户转化为内核,返回时再将身份做变换。
9和19号信号不可被捕捉,不可被处理。
重谈地址空间:
task_struct指向当前进程所对应的地址空间
页表进行虚拟到物理的转化
用户级页表有几份?有几个进程就有几份用户级页表-----因为进程具有独立性
内核级页表有几份?只有1份,每个进程看到的3~4GB的东西都是一样的!在整个系统中,进程再怎么切换,3~4GB的空间内容是不变的!!进程视角:我们调用系统中的方法,就是在自己的地址空间中进行执行的
操作系统视角:任何一个时刻,都有进程在执行。我们想执行操作系统的代码,就可以随时执行。
操作系统的本质:基于时钟中断的死循环。
CPU中有一个ecs寄存器,它的低两位 00(0 代表内核态) 01 10 11(3 代表用户态)
要想实现用户态和内核态之间的转变就需要改变寄存器的值
cr3寄存器指向页表
内核态:允许访问操作系统的代码和数据
用户态: 只能访问用户自己的代码和数据
由上图可以看出进行了四次内核态与用户态的转化
当信号的处理动作是自定义动作时,就需要回到用户态去执行代码。(因为OS不允许,害怕你进行危险操作。)
从用户态到内核态:陷入内核。 不是只有当调用系统调用的时候才会发生。
当我们的代码里面没有系统调用,我们ctrl+c也可以杀掉进程,那它是怎么陷入内核的???
进程是会被调度的 只要进程一直在跑,进程是会被cpu调度的,当时间片消耗完毕,进程就会被从cpu上剥离下来,也必然会将进程二次拿到cpu上去运行,那么就是OS将pcb、地址空间、页表恢复到cpu上,这肯定是在内核态的,然后执行我们的代码肯定是在用户态上执行的,因此肯定就会存在内核态到用户态的转变。 不要认为只有系统调用才会有用户到内核,在代码的周期里会有无数次的内核与用户态的转化。
信号的检测和处理是在由内核态返回用户态时进行的。
四、内核如何实现信号的捕捉
1、signal
2、sigaction
#include<signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact)struct sigaction{
void (*sa_handler)(int); -------处理方法
void (*。。。。。。
sigset_t sa_mask;
。。。
。。。
}
第二个参数:输入型参数 捕捉一个信号,传入的是一个结构体 结构体名和函数名是一样的
第三个参数:输出型参数
sa_mask字段:
正在处理2号信号,2号会自动被屏蔽。如果我还想屏蔽更多信号呢??
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,1);//在屏蔽2号的同时将1、3、4同时屏蔽
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
//问题一:pending位图,什么时候从1->0
//我们知道当收到信号,信号在pending表里就被置1了
问题二:信号被处理的时候,对应的信号也会被添加到block表中,防止信号捕捉被嵌套调用
void PrintPending()
{
sigset_t set;
sigpending(&set); //获取
for(int signo = 1; signo <= 31; signo++)
{
if(sigismember(&set, signo)) cout << "1"; //打印
else cout << "0";
}
cout << "\n";
}
void handler(int signo)
{
PrintPending();
cout << "catch a signal, signal number : " << signo << endl;
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
sigaction(2, &act, &oact);//修改handler表 的2号下标的函数指针,该向指向handler方法就好了
while(true) //当没有2号信号产生的时候 进程一直运行 当产生2号信号,我们对2号信号进行自定义捕捉
{
cout << "I am a process: " << getpid() << endl;
sleep(1);
}
return 0;
}
实验现象: 先将pending位图由1变为0 再调用的handler处理方法
执行信号捕捉方法之前,先清0,再调用
可重入函数
头插新节点核心代码:
node1->next = head;
head = node1;
现象:insert函数被main和handler执行流重复进入
造成问题:节点丢失,内存泄漏
如果一个函数,被重复进入的情况下,出错了,或者可能出错,这就叫作不可重入函数!否则叫做可重入函数。目前,我们学到的大部分函数都是不可重入函数。因为,c++中的stl容器里面有大量的new或则malloc这样的操作,用容器会自动扩容,这些容器在stl库中是使用链表来管理的。容器里面有链式结构,即只要内存块上扩容,就会用大量的指针的变化,所以stl容器使用的时候都是不可重入的。
volatile
在优化的条件下,会在cpu和内存之间形成一个寄存器屏障。 把内存的值放到cpu中之后,就只会一直从cpu中取值,对内存的值进行修改并不会影响之前放入到cpu中的值。(即因为优化,导致内存不可见了)
volatile int flag = 0; 核心作用:防止编译器的过度优化,保持内存的可见性!!(告诉编译器,好好的从内存里面读,不要进行过度优化)
mysignal:mysignal.cc
g++ -o $@ $^ -O3 -std=c++11 //-O3是最高级别的优化
.PHOONY:clean
rm -f mysignal
volatile int flag = 0; //添加volatile关键字 防止flag被过度优化
void handler(int signo)
{
cout << "catch a signal: " << signo << endl;
flag = 1;
}
int main()
{
signal(2, handler);
//在优化条件下,flag变量可能被直接优化到cpu内的寄存器中
while(!flag); //flag 0, !flag 真 //while条件 是一种运算 有两种运算 算数运算、逻辑运算 flag没有被修改
cout << "process quit normal" << endl; //当捕捉到信号之后这句就会被打印
return 0;
}
SIGCHLD信号
子进程退出的时候,不是静悄悄的退出。子进程在退出的时候,会主动的向父进程发送SIGCHLD(17)号信号。
子进程在进行等待的时候,我们可以采用基于信号的方式进行等待。等待子进程的好处:1、获取子进程的退出状态,释放子进程的僵尸 2、虽然不知道父子谁先运行,但我们清楚,一定是父进程最后退出!