目录
一.什么是信号
1.1系统函数
前台和后台
1.2信号概念
编辑 二.产生信号
2.1使用函数产生信号
raise
abort
2.2硬件异常产生信号
2.2.1除0错误
2.2.2野指针
编辑
2.3由软件条件产生信号
2.3.1基本alarm验证-体会I/O效率
2.3.2设置重复闹钟
2.2.3理解软件条件
2.2.4理解系统闹钟
2.4core dump标志
core dump
三.保存信号
3.1概念
3.2内核中的表示
简单的代码理解:
3.3sigset_t
3.4信号集操作函数
3.4.1sigprocmask
3.4.2sigpending
3.5代码实现
小问题:pending表的1是在递达之前置为0,还是递达之后?
编辑
四.捕捉信号
4.1信号捕捉的流程
4.2操作系统是怎么运行的
4.2.1硬件中断
4.2.2时钟中断
4.2.3死循环
4.2.4软中断
4.2.5 缺页中断?内存碎片处理?除零野指针错误?
4.3理解用户态和内核态
4.4按下crtl+c发生什么?
4.5sigaction
五.可重入函数
六.volatile
七.SIGCHLD
默认就是ign,还要我们自己设置SIG_IGN?
一.什么是信号
闹钟响了,上课铃响了,肚子饿了。这些都是给人的信号,人可以接收这些信号,从而去处理信号。
信号的三种处理方式:默认处理动作(我饿了,立刻就去吃饭),自定义处理信号(我饿了,先打完这把游戏再吃),忽略信号(我饿了,但我现在就是不吃)。
你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。信号到来 | 信号保存 | 信号处理怎么进行信号处理啊?a.默认 b.忽略 c.自定义, 后续都叫做信号捕捉。
#include<iostream>
#include<unistd.h>
int main()
{int cnt=0;while(true){std::cout<<"hello world"<<cnt++<<std::endl;sleep(1);}return 0;
}
使用crtl+c就是发送二号信号,让程序终止。
1.1系统函数
注意是函数指针:
既然我上面说crtl+c是二号信号SIGINT。这里就可以通过这个系统调用来查看我们调用的信号,这里也就是我上面说的三种方式之一的自定义(自定义捕捉)。
#include<iostream>
#include<unistd.h>
#include<signal.h>void handleSig(int sig)
{std::cout<<"获得一个信号"<<SIGINT<<std::endl;
}int main()
{signal(SIGINT,handleSig);int cnt=0;while(true){std::cout<<"hello world"<<cnt++<<std::endl;sleep(1);}return 0;
}
前台和后台
在我们的可执行文件后面加上&符号,这个进程就会被切换到后台运行。我们自己所输入的命令比如crtl+c还有ls什么的,这些命令都是针对于前台的进程的。(前台的进程只允许有一个)
注意这里的打印问题,后台的程序和前台的程序都会在标准输出中输出,这里的打印都混在一起了。进程间通信的问题,因为标准输出是一个共享的资源,需要对对标准输出进行适当的同步控制
当然也可以进行前后台的切换:
jobs:查看所有的后台程序
fg+任务号:特定的进程提到前台
crtl+z:进程切换到后台
bg+任务号: 让后台进程恢复运行
1.2信号概念
1到31是普通信号
其实就是宏:
号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

二.产生信号
给进程发送信号,信号产生之后并不是立即处理的,进程必须把信号记录下来。而记录在哪里?在task_struct里有个sigs,32位的位图来记录下信号。而PCB属于操作系统,所以修改位图实际上就是修改内核数据结构(必须要操作系统本身来修改)。所以说,不管信号怎么产生,发送信号,在底层,必须让OS发送。
上面的代码部分有一个系统调用signal可以自定义捕捉。假如我把所有的信号都给捕捉呢?
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include <sys/types.h>void handleSig(int sig)
{std::cout<<"获得一个信号"<<SIGINT<<std::endl;
}int main()
{for(int i=1;i<32;i++)signal(i,handleSig);int cnt=0;while(true){std::cout<<"hello world,"<<cnt++<<" pid:"<<getpid()<<std::endl;sleep(1);}return 0;
}
这样就会导致很多的信号都无法使用,导致我们的进程会一直运行,但是有一个特殊的9号进程,这算是底牌,它不受自定义捕捉的约束。这里就可以直接杀掉进程。
上面的许多都是用系统命令和终端按键来产生信号的,这里依然还可以使用函数
2.1使用函数产生信号
上面的kill函数实际上就是的。
raise
raise 函数可以给当前进程发送指定的信号(自己给自己发信号)。
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include <sys/types.h>void handleSig(int sig)
{std::cout<<"获得一个信号"<<sig<<std::endl;
}int main()
{for(int i=1;i<32;i++)signal(i,handleSig);for(int i=1;i<32;i++){sleep(1);if(i==9||i==19) continue;raise(i);}int cnt=0;while(true){std::cout<<"hello world,"<<cnt++<<" pid:"<<getpid()<<std::endl;sleep(1);}return 0;
}
相当于给自己发信号:
abort
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include <sys/types.h>void handleSig(int sig)
{std::cout<<"获得一个信号"<<sig<<std::endl;
}int main()
{for(int i=1;i<32;i++)signal(i,handleSig);// for(int i=1;i<32;i++)// {// sleep(1);// if(i==9) continue;// raise(i);// }int cnt=0;while(true){std::cout<<"hello world,"<<cnt++<<" pid:"<<getpid()<<std::endl;abort();sleep(1);}return 0;
}
虽然我们已经捕捉了信号,但是这个函数是强制给进程发信号,也就是说:
直接终止。
2.2硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令, CPU的运算单元会产生异常, 内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址, MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
2.2.1除0错误
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include <sys/types.h>void handleSig(int sig)
{std::cout<<"获得一个信号"<<sig<<std::endl;exit(13);
}int main()
{for(int i=1;i<32;i++)signal(i,handleSig);// for(int i=1;i<32;i++)// {// sleep(1);// if(i==9) continue;// raise(i);// }int cnt=0;while(true){std::cout<<"hello world,"<<cnt++<<" pid:"<<getpid()<<std::endl;//abort();int a=1;a/=0;sleep(1);}return 0;
}
可以看 一下信号8是什么错误
2.2.2野指针
信号11是段错误。
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
2.3由软件条件产生信号
先来看一个函数alarm
2.3.1基本alarm验证-体会I/O效率
I/O多的例子:一秒后程序会停下
#include<iostream>
#include<unistd.h>int main()
{int count=0;alarm(1);while (true){std::cout<<"count: "<<count++<<std::endl;}return 0;
}
要知道CPU是非常快的,这里才打印了5万多次。
IO少:
#include<iostream>
#include<unistd.h>
#include<signal.h>int count;
void handle(int signum)
{std::cout<<"count: "<<count++<<std::endl;exit(0);
}int main()
{signal(SIGALRM,handle);alarm(1);while (true){//std::cout<<"count: "<<count++<<std::endl;count++;}return 0;
}
这里是5亿多次。
2.3.2设置重复闹钟
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<vector>
#include<functional>//包装器
using func_t = std::function<void()>;int gcount;std::vector<func_t> gfuncs;void handle(int signum)
{// std::cout<<"count: "<<gcount<<std::endl;// exit(0);for(auto& f:gfuncs){f();}std::cout << "gcount : " << gcount << std::endl;int n = alarm(1); // 重设闹钟,会返回上⼀次闹钟的剩余时间std::cout << "剩余时间 : " << n << std::endl;
}void Push()
{gfuncs.push_back([](){std::cout<<"我是一个内核的刷新操作"<<std::endl;});gfuncs.push_back([](){std::cout<<"我是⼀个检测进程时间片的操作,如果时间⽚到了,我会切换进程"<<std::endl;});gfuncs.push_back([](){std::cout<<"我是⼀个内存管理操作,定期清理操作系统内部的内存碎片"<<std::endl;});
}int main()
{Push();//装方法signal(SIGALRM,handle);//自定义捕捉alarm(1);//一秒的闹钟while (true){pause();//这里的函数是接收信号的std::cout << "我醒来了..." << std::endl;gcount++;}return 0;
}
注意pause函数的用途:
2.2.3理解软件条件
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。
2.2.4理解系统闹钟
内核中:
struct timer_list {struct list_head entry;unsigned long expires;void (*function)(unsigned long);unsigned long data;struct tvec_t_base_s *base;
}
操作系统管理定时器,采用的是时间轮的做法,但是我们为了简单理解,可以把它在组织成为"堆结构"。
2.4core dump标志
man 7 signal:
之前在学习frok的时候,只提到了退出状态,可以通过waitpid来获取子进程的退出状态。
int main()
{int n=fork();if(n==0){//子进程sleep(1);int a = 10;a /= 0;exit(0);}int status=0;waitpid(-1,&status,0);printf("exit code: %d, exit signal: %d, core dump: %d\n", (status>>8)&0xFF,status&0x7F, (status>>7)&1);return 0;
}
因为子进程有一个除0错误,所以这里系统填充的status对应的信号是8,退出码无效,core dump也是0.
core dump
• SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
• 首先解释什么是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
core变成了1,而我们当前的目录下也多开了一个文件core。
这样可以看的更清晰:
int main()
{printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");int a=10;a/=0;printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");return 0;
}
这里运行结束后也就是会多一行core dumped。
gdb之后:core-file core
会得到我们想要的。
使用ulimit -c 0关闭:
三.保存信号
3.1概念
• 实际执行信号的处理动作称为信号递达(Delivery)(上面说到的三种方式:默认,自定义捕捉,忽略)
• 信号从产生到递达之间的状态,称为信号未决(Pending)(信号在位图中,还没来得及处理)。
• 进程可以选择阻塞 (Block )某个信号。
• 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
• 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
3.2内核中的表示
• 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
• SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
• SIGQUIT信号未产生过,⼀旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
handle是函数指针数组,数组的下标就是信号编号,还记得在使用信号实际上就是宏,是一个整数
它的第二个参数:
简单的代码理解:
void handle(int sig)
{std::cout<<"这是一个信号:"<<sig<<std::endl;signal(2,SIG_DFL);std::cout<<"恢复处理动作"<<std::endl;
}int main()
{signal(2,handle);while(true){std::cout<<"."<<std::endl;sleep(1);}return 0;
}
这里对信号2进行捕捉之后,后面又用SIG_DFL恢复了默认处理。
3.3sigset_t
从上图来看,每个信号只有一个bit的未决标志, 非0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储, 这个类型可以表示每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞, 而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)。这里的“屏蔽”应该理解为阻塞而不是忽略。
实际上就是一个整数,用这个整数的位图
这是这个意思:
3.4信号集操作函数
#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 sigismember(const sigset_t *set, int signo);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
3.4.1sigprocmask
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。
谁调用sigprocmask就是在更改谁的block表。
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改 进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字 备份到oset里,然后根据set和how参数更改信号屏蔽字。
第一,二个参数:
输入型参数
我们比较喜欢实用SIG_SETMASK
第三个参数:
改完后悔了,我现在想恢复,这个参数就是一个输出型参数。
3.4.2sigpending
获取pending信号集。set是输出型参数
3.5代码实现
我就把pending表里内容全部都打印出来,后来收到2号信号,我要观察到打印出来的pending表对应的第二个位置由0变为1.
#include<iostream>
#include<signal.h>
#include<unistd.h>
void PrintPending(sigset_t& pending)
{for(int i = 31;i>=1;i--){if(sigismember(&pending,i))//如果有当前数字对应的编号就打印1,反之0{std::cout<<"1";}else{std::cout<<"0";}}std::cout<<std::endl;
}
int main()
{sigset_t block;sigset_t old_block;//作为sigprocmask的输出型参数sigemptyset(&block);//清空信号集sigemptyset(&old_block);//清空信号集//1..对二号信号进行屏蔽sigaddset(&block,SIGINT);//把2号信号添加到block里int n = sigprocmask(SIG_SETMASK,&block,&old_block);(void)n;//加一个循环,不断的获取与打印while(true){//2..获取pending信号集sigset_t pending;int m = sigpending(&pending);//sigpending 获取pending表,放在pending里//3..打印pending信号集PrintPending(pending);sleep(1);}return 0;
}
代码效果:
这样只是可以看到2位置的变化过程,还可以继续写,让我们看到pending的恢复过程:
#include<iostream>
#include<signal.h>
#include<unistd.h>
void PrintPending(sigset_t& pending)
{for(int i = 31;i>=1;i--){if(sigismember(&pending,i))//如果有当前数字对应的编号就打印1,反之0{std::cout<<"1";}else{std::cout<<"0";}}std::cout<<std::endl;
}void handle(int sig)
{std::cout<<"递达"<<sig<<"信号"<<std::endl;
}
int main()
{signal(SIGINT,handle);sigset_t block;sigset_t old_block;//作为sigprocmask的输出型参数sigemptyset(&block);//清空信号集sigemptyset(&old_block);//清空信号集//1..对二号信号进行屏蔽sigaddset(&block,SIGINT);//把2号信号添加到block里int n = sigprocmask(SIG_SETMASK,&block,&old_block);(void)n;//加一个循环,不断的获取与打印int cnt=5;while(true){//2..获取pending信号集sigset_t pending;int m = sigpending(&pending);//sigpending 获取pending表,放在pending里//3..打印pending信号集PrintPending(pending);if(cnt==0){//恢复对2号信号的block的情况sigprocmask(SIG_SETMASK,&old_block,nullptr);std::cout<<"解除对2号新号的屏蔽"<<std::endl;}sleep(1);cnt--;}return 0;
}
这里我捕捉了2号信号,再次使用sigprocmask恢复修改之前的sigset_t。5秒后这里就自动的解除了屏蔽,变为了全0.
小问题:pending表的1是在递达之前置为0,还是递达之后?
先修改一下对二号信号的自定义捕捉:
下面打印就可以看出来,在我们自定义捕捉的二号信号里面打印出来的是全0 。说明是先置为0的,在执行handle方法之前,2对应的pending已经被清理了
结论就是:在我们准备递达的时候,要首先清空pending信号集中对应的位图1变为0.
四.捕捉信号
4.1信号捕捉的流程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
• 用户程序注册了 SIGQUIT 信号的处理函数 sighandler 。
• 当前正在执行 main 函数,这时发生中断或异常切换到内核态。
• 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达。
• 内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数, sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
• sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态(函数栈桢)。
• 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。
上面的块说的是上图的执行流程,当然我们的信号也不一定是我们自定义捕捉的,还有默认和忽略,这种情况下就只是可以在内核中,因为大多信号都是直接终止程序的,这里就可以直接终止掉进程。
自己:
4.2操作系统是怎么运行的
4.2.1硬件中断
CPU有很多针脚用来传递信息,之前我们的冯诺依曼体系结构图主要是讨论数据传送的,其实CPU也与外设有联系,CPU可以直接收到外部的中断信息(通过中断控制器),并不是OS轮询实现的。
外部设备就绪后(比如键盘设备按下了回车键),会发起硬件中断到中断控制器,中断控制器去通知CPU有外设发起了中断,CPU知道了之后会再去中断控制器找到引起中断的那个终端号(这里可以理解为CPU与对应设备的针脚的位置)。找到中断号了(此时CPU可能在进行某个进程,此时要把当前进程的上下文保护起来),CPU就会在IDT(理解为函数指针数组,下标对应中断号)中找到对应的方法处理中断。
上面的信号思想就是模拟硬件中断!!!
• 中断向量表就是操作系统的一部分,启动就加载到内存中了
• 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
• 由外部设备触发的,中断系统运行流程,叫做硬件中断
先进行初始化加载到内存中:
然后暂停在这里:
4.2.2时钟中断
没有中断到来时,OS在暂停!!OS是基于中断进行工作的软件
有了上面硬件中断的思想,这里在这里多加一个时钟源的东西,按照一定的时间向CPU发送中断。在IDT中也加了一个中断服务。 所以操作系统在时钟中断的驱动下,进行调度。
所以CPU就有一个概念叫做主频。自己的电脑上的处理器上就有这个主频信息。
一个CPU一个运行队列,每个进程都有时间片(计数器),这个时间片是每个进程运行的最大时间。
4.2.3死循环
操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可.操作系统的本质:就是一个死循环!
这样,操作系统,就可以在硬件时钟的推动下,自动调度了.
4.2.4软中断
• 上述外部硬件中断,需要硬件设备触发。
• 有没有可能,因为软件原因,也触发上面的逻辑?有!
• 为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int 或者syscall),可以让CPU内部触发中断逻辑。
上面所说的除0错误。规定成为一种有CPU内部触发的中断。像是除0错误,野指针,内存重复释放,这些是由软件导致硬件出错,异常发生,导致中断。
CPU内部,自己也可以让软件触发软中断。
• 用户层怎么把系统调用号给操作系统? - 寄存器(比如EAX)
• 操作系统怎么把返回值给用户?- 寄存器或者用户传入的缓冲区地址
• 系统调用的过程,其实就是先int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法
• 系统调用号的本质:数组下标!
这里在中断向量表注册一个方法,里面会做两件事:
源码中,把0x80注册到里面去:
在system_call里,这里就是在找数组下标:
• 可是为什么我们用的系统调用,从来没有见过什么 int 0x80 或者 syscall 呢?都是直接调用上层的函数的啊?(OS不提供系统调用,只提供系统调用号)
• 那是因为Linux的glibC标准库,给我们把几乎所有的系统调用全部封装了。
这个意思就是先把vfork的系统调用号放在eax里,然后使用syscall陷入内核:
然后在syscall里:
4.2.5 缺页中断?内存碎片处理?除零野指针错误?
缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断, 然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来 处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。
• 操作系统就是躺在中断处理例程上的代码块!
• CPU内部的软中断,比如int 0x80或者syscall,我们叫做 陷阱
• CPU内部的软中断,比如除零/野指针等,我们叫做 异常。
4.3理解用户态和内核态
用户和内核都在同一个[0,4]GB的地址空间上,如果用户随便拿一个3-4GB空间的地址不就是可以随便访问内核了吗?OS为了保护自己不相信任何人,只能通过系统调用的方式进行访问。
那么用户或者操作系统怎么知道当前是用户态还是内核态?
在CPU内部的寄存器:
CPL 概述
CPL 是处理器当前执行代码时的特权级别,它反映了当前执行代码的信任程度和所能访问的资源范围。特权级别分为 4 级,从 0 到 3,其中 0 级特权最高,3 级特权最低。在实际应用中,通常只使用 0 级和 3 级,分别对应内核态和用户态。
结论:
• 操作系统无论怎么切换进程,都能找到同一个操作系统!换句话说操作系统系统调用方法的执行, 是在进程的地址空间中执行的!
• 关于特权级别,涉及到段,段描述符,段选择子,DPL,CPL,RPL等概念,而现在芯片为了保证兼容性,已经非常复杂了,进而导致OS也必须得照顾它的复杂性,这块我们不做深究了。
• 用户态就是执行用户[0,3]GB时所处的状态
• 内核态就是执行内核[3,4]GB时所处的状态
• 区分就是按照CPU内的CPL(0代表内核态,3代表用户态)决定,CPL的全称是Current Privilege Level,即当前特权级别。
• 一般执行 int 0x80 或者 syscall 软中断,CPL会在校验之后自动变更
4.4按下crtl+c发生什么?
下面整个熟悉流程:
- 用户按下
Ctrl + C
组合键,键盘硬件产生一个电信号,通过中断线向中断控制器发送中断请求信号,这是硬件层面产生中断的第一步。- 中断控制器接收到键盘的中断请求后,经过优先级判断等处理,向 CPU 发送中断信号。
- CPU 在执行完当前指令后,检测到中断信号,根据中断向量表(IVT)或中断描述符表(IDT)找到对应的键盘中断处理程序入口地址,这里会获取到一个中断号,该中断号用于索引中断向量表或中断描述符表来确定具体的中断处理程序。
- 操作系统的键盘中断处理程序被调用,它会读取键盘控制器中的数据,识别出用户按下的是
Ctrl + C
组合键,然后操作系统内核生成信号 2(SIGINT
)。- 内核根据当前终端的关联信息,确定前台进程(目标进程),并通过修改目标进程控制块(PCB)中的
pending
位图表,将信号 2 对应的位置为 1,表示该信号已发送给进程且处于待处理状态。
4.5sigaction
struct sigaction里有一个信号集sigset_t。
同signal
#include<iostream>
#include<signal.h>
#include<unistd.h>void handle(int signum)
{std::cout<<"hello signal "<<signum<<std::endl;exit(0);
}int main()
{struct sigaction act,oldact;//sigaction的两个参数act.sa_handler = handle;//注册我们自定义捕捉的方法sigaction(SIGINT,&act,&oldact);while (true){std::cout<<"hello world"<<std::endl;sleep(1);}return 0;
}
1.当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字(block表),当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。2.如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数。
验证 第一点:
#include<iostream>
#include<signal.h>
#include<unistd.h>void handle(int signum)
{std::cout<<"hello signal "<<signum<<std::endl;while(true){sigset_t pending;sigpending(&pending);for(int i=31;i>=1;i--){if(sigismember(&pending,i)){std::cout<<"1";}else{std::cout<<"0";}}std::cout<<std::endl;sleep(1);}exit(0);
}int main()
{struct sigaction act,oldact;//sigaction的两个参数act.sa_handler = handle;//注册我们自定义捕捉的方法sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGINT,&act,&oldact);while (true){std::cout<<"hello world"<<std::endl;sleep(1);}return 0;
}
后面再去使用crtl+c的时候pending就会变成1,说明此时的block变成了1.对二号信号进行了阻塞。
第二点,当然也是可以进行对其他信号的屏蔽:
就是在sa_mask里修改位图:
kill -3 +pid:
五.可重入函数
这里的node2会内存泄漏。
这里叫做函数被重入了。
如果一个函数符合以下条件之一则是不可重入的:
• 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
• 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
六.volatile
int flag = 0;
void handle(int signu)
{std::cout<<flag<<"-> 1"<<std::endl;flag=1;
}
int main()
{signal(2,handle);while(!flag);std::cout<<"process exit normal"<<std::endl;return 0;
}
这里不论按多少次最后打印出来的结果都是:
因为在main里并不涉及到对flag的修改,本来的CPU需要做两个动作,把物理内存的值flag放在CPU的寄存器里,然后进行main函数里的while的判断。这里直接优化为一个动作,会直接把flag 的值放在寄存器里,不去频繁的访问物理内存(反正我的main函数并没有对flag进行修改)。
while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。while 检 测的 flag 其实已经因为优化,被放在了CPU寄存器当中。
这里就只需要:
volatile int flag = 0;
保存了内存空间的可见性。
七.SIGCHLD
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
验证发送的17号信号:
void Say(int num)
{std::cout<<"father get a signal:"<<num<<std::endl;
}int main()
{signal(SIGCHLD,Say);pid_t id =fork();if(id==0){std::cout<<"I am child,exit"<<std::endl;sleep(3);exit(3);}waitpid(id,nullptr,0);std::cout<<"I am father,exit"<<std::endl;return 0;
}
上面的代码验证了会发送SIGCHLD信号。
下面用信号进行对子进程的回收,回收6个,剩下4个不回收:
void Say(int num)
{//waitpid(-1,nullptr,0);while (true){pid_t n = waitpid(-1,nullptr,WNOHANG);//这里用非阻塞轮询//大于0,等待成功if(n==0)//等于0说明子进程没有结束{break;}else if(n<0){std::cout<<"waitpid error"<<std::endl;break;}}std::cout<<"father get a signal:"<<num<<std::endl;
}int main()
{signal(SIGCHLD,Say);for(int i=0;i<10;i++)//10个子进程{pid_t id =fork();if(id==0){sleep(3);std::cout<<"I am child,exit"<<std::endl;if(i<=6) exit(3);else pause();}}while (true){std::cout<<"I am father,exit"<<std::endl;sleep(1);}return 0;
}
3秒之后子进程就只剩下了4个:
事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将 SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
也就是说:
int main()
{signal(SIGCHLD,SIG_IGN);for(int i=0;i<4;i++)//10个子进程{pid_t id =fork();if(id==0){sleep(3);std::cout<<"I am child,exit"<<std::endl;exit(3);// if(i<=6) exit(3);// else pause();}}while (true){std::cout<<"I am father,exit"<<std::endl;sleep(1);}return 0;
}
也就是说这里不再需要我们自己调用wait函数了!!
默认就是ign,还要我们自己设置SIG_IGN?
signal(SIGCHLD,SIG_IGN);
默认是Ign,并不是说处理动作就是ign!!是signal(SIGCHLD,SIG_DFL)它的缺省是SIG_IGN。
如果是系统的ign进程会变成僵尸,但是我们自己的会是ign(回收)。