1.信号的理解
1.1.信号的概念
1.什么是信号?
信号:(闹钟)红绿灯,上课铃声,狼烟,电话铃声,肚子叫,敲门声,脸色不好
2.信号是给谁的?
上面的信号都是给人的 ->那么此时系统的信号就是给进程的
3.信号的作用是干嘛用的?
中断我们人正在做的事情,是一种事件的异步通知机制
1.我们自习一会,等张三回来再讲,我们继续上课,上课 — 同步
2.张三取快递–异步
信号是一种给进程发送的,用来进行事件异步通知的机制!
信号的产生,相对于进程的运行,是异步的!
信号的四个基本结论:
1.信号处理,进程在信号没有产生的时候,早就知道信号该如何处理了
2.信号的处理,不是立即处理,而是可以等一会在处理,合适的时候,进行信号的处理
3.人能识别信号,是提前被”教育”过的,进程也是如此,OS程序员设计的进程,进程早已经内置了对于信号的识别和处理方式!
4.信号源非常多->给进程产生信号的,信号源,也非常多
1.2.查看信号
kill -l:查看信号
每个信号都有编号和宏定义名称,这些宏定义名称可以在signal.h可以找到
62的信号可以大致分成两部分,编号34以上的都是实时信号,这些信号在什么条件下产生的,这些信号的默认行为是什么可以在 man 7 signal 中查看
1.3.信号处理
信号处理可以分成三种:
1.默认处理动作
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGINT, SIG_DFL); // 设置信号默认处理行为
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
// 编译运行日志:
/*
$ g++ sig.cc -o sig
$ ./sig
我是进程:212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C // 输入Ctrl+C,进程退出(SIGINT默认行为)
*/
2.自定义信号处理动作
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGQUIT, handler); // SIGQUIT 信号编号通常为 3
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
// 编译运行日志及注释:
/*
$ g++ sig.cc -o sig
$ ./sig
我是进程:213056
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\\我是:213056,我获得了一个信号:3 // 按下 Ctrl+\\ 触发 SIGQUIT,执行自定义 handler
// 注释掉第13行(signal注册代码)后:
$ ./sig
我是进程:213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\\Quit // 按下 Ctrl+\\ 触发 SIGQUIT,执行默认行为(终止进程并生成 core)
*/
3.忽略处理
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGINT, SIG_IGN); // 设置忽略信号的宏
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
// 编译运行日志:
/*
$ g++ sig.cc -o sig
$ ./sig
我是进程:212681
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C^C^C^C^CI am a process, I am waiting signal! // 输入ctrl+c毫无反应
I am a process, I am waiting signal!
*/
2.信号的产生
2.1.进程的理解
要了解信号的产生,首先要理解进程的种类
先说结论:进程分成两种
1.前台进程:# ./xxx (在启动系统的时候,会默认开启一个进程shell进程,默认是前台进程)
2.后台进程:# ./xxx &
谈谈这两个进程之间的区别:
后台进程无法从标准输入(键盘)中获取信息
前台进程可以从标准输入(键盘)中获取信息
为什么后台进程不可以?
键盘只有一个,输入数据,一定是给一个确定的进程,所以只能给前台进程发送消息,
所以前台进程只能有一个(启动shell的时候,bash进程默认为前台进程),后台进程可以有多个,所以前台进程的本质就是就是从从键盘中获取数据。
但是都是可以向标准输出中打印信息
观看几个现象:
1.当./testing的时候,testing就是前台进程了,就把原有的bash进程给挤下去了,使用 命令行 ls 等等的,也就并没有效果(验证了前台进程只能有一个的特性)
2.当执行以后台进程进程进行执行的时候,我输入命令行是有反应的(bash进程)
此时我们想要杀掉后台进程的和就要重新再开启以Xshell,查询他的PID,并把它给杀掉
3.创建子进程,父进程先退出,子进程ctrl+c 无法退出(僵尸孤儿),就是子进程被自动提到后台进程
补充一组关于后台和前台进程的指令:
第一组:
jobs查看所有的后台任务,可以看到 它的任务号为1
fg 任务号,特定的进程,提到前台,ctrl+c,此时就可以杀死进程
第二组:
ctr+z:进程切换到后台,使用jobs查看后台进程
bg任务号:让后台进行回复运行,让后台进程继续运行
什么叫做进程发送消息?
之前的信号的一个特性:信号的处理,不是立即处理,而是可以等一会在处理,合适的时候,进行信号的处理
所以信号产生之后,并不是立即处理的,所以要求,进程必须把信号记录下来
记录在哪里?
sigs:也是一个位图的结构
比特位的位置,信号编号
比特位的内容,是否收到
所以发送信号,本质是向目标进程写信号修改位图 根据pid,信号的编号
位图属于操作系统内的数据结构对象 所以修改位图本质:修改内核的数据
但是修改内核数据只能是操作系统自己,
所以最后的结论:不管信号怎么产生,发送信号,在底层,必须让给os发送
所以,操作系统必须提供发送信号的系统调用–kill
2.2.信号产生的方式
2.2.1.通过键盘获取
Ctrl+C (SIGINT) :终止一个进程
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGINT, SIG_DFL); // 设置信号默认处理行为
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
// 编译运行日志:
/*
$ g++ sig.cc -o sig
$ ./sig
我是进程:212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C // 输入Ctrl+C,进程退出(SIGINT默认行为)
*/
Ctrl+\\(SIGQUIT)可以发送终⽌信号并⽣成core dump⽂件,
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGQUIT, handler); // SIGQUIT 信号编号通常为 3
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
// 编译运行日志及注释:
/*
$ g++ sig.cc -o sig
$ ./sig
我是进程:213056
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\\我是:213056,我获得了一个信号:3 // 按下 Ctrl+\\ 触发 SIGQUIT,执行自定义 handler
// 注释掉第13行(signal注册代码)后:
$ ./sig
我是进程:213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^\\Quit // 按下 Ctrl+\\ 触发 SIGQUIT,执行默认行为(终止进程并生成 core)
*/
Ctrl+Z(SIGTSTP)可以发送停⽌信号,将当前前台进程挂起到后台等。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGTSTP, handler); // SIGTSTP 信号编号通常为 20(终端暂停信号,如 Ctrl+Z)
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
// 编译运行日志及注释:
/*
$ ./sig
我是进程:213552
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z我是:213552,我获得了一个信号:20 // 按下 Ctrl+Z 触发 SIGTSTP,执行自定义 handler
// 注释掉第13行(signal注册代码)后:
$ ./sig
我是进程:213627
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z
[1]+ Stopped ./sig // 触发 SIGTSTP 默认行为(暂停进程)
whb@bite:~/code/test$ jobs
[1]+ Stopped ./sig // 查看暂停的进程
*/
2.2.2.调用系统命令向进程发信号
sig代码:
以后台进程进行运行:
⾸先在后台执⾏死循环程序,然后⽤kill命令给它发SIGSEGV信号。
2.2.3.系统调用(函数)
2.2.3.1.kill
代码示例:自己处理kill型号
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>
// ./mykill signumber pid
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cout << "./mykill signumber pid" << std::endl;
return 1;
}
int signum = std::stoi(argv[1]);//stoi:将字符串转成整数
pid_t target = std::stoi(argv[2]);
int n = kill(target, signum);
if(n == 0)
{
std::cout << "send " << signum << " to " << target << " success.";
}
return 0;
}
2.2.3.2.raise
raise 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)。
代码示例:自己给自己发送型号,并自定义处理
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
// 整个代码就只有这一处打印
std::cout << "获取了一个信号:" << signumber << std::endl;
}
// mykill -signumber pid
int main()
{
signal(2, handler); // 先对2号信号进行捕捉
// 每隔1S,自己给自己发送2号信号
while(true)
{
sleep(1);
raise(2);
}
}
// 编译运行日志:
/*
$ g++ raise.cc -o raise
$ ./raise
获取了一个信号:2
获取了一个信号:2
获取了一个信号:2
*/
2.2.3.3.abort
abort 函数使当前进程接收到信号⽽异常终⽌。
代码示例:
其实也是自己给自己发信号,但是此时是发一个固定的信号,6,用于终止
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void handler(int signumber)
{
// 整个代码就只有这一处打印
std::cout << "获取了一个信号:" << signumber << std::endl;
}
// mykill -signumber pid
int main()
{
signal(SIGABRT, handler);
while(true)
{
sleep(1);
abort();
}
}
// 编译运行日志:
/*
$ g++ Abort.cc -o Abort
$ ./Abort
获取了一个信号:6 // 实验可以得知, abort给自己发送的是固定6号信号, 虽然捕捉了, 但是还是要退出
Aborted
// 注释掉第15行(signal注册代码)后:
$ ./Abort
Aborted
*/
2.2.4.硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。
再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
1.除0操作
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\\n", sig);
}
// vi
int main()
{
//signal(SIGFPE, handler); // 8) SIGFPE(算术异常信号,如除零错误)
sleep(1);
int a = 10;
a /= 0; // 触发除零错误,产生 SIGFPE 信号
while(1);
return 0;
}
// ——————– 运行日志 ——————–
// 情况1:不注释第11行(注册信号处理函数):
// $ gcc sigfpe.c -o sigfpe
// $ ./sigfpe
// catch a sig : 8 # handler 捕获到 SIGFPE(编号8)
// Floating point exception (core dumped) # 系统仍终止进程(算术异常为致命错误)
// 情况2:注释第11行(未注册处理函数):
// $ gcc sigfpe.c -o sigfpe
// $ ./sigfpe
// Floating point exception (core dumped) # 直接崩溃,无 handler 输出
2.野指针非法访问
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\\n", sig);
}
int main()
{
//signal(SIGSEGV, handler); // SIGSEGV:段错误信号(编号11,如解引用空指针)
sleep(1);
int *p = NULL;
*p = 100; // 解引用空指针,触发 SIGSEGV
while(1);
return 0;
}
// ——————– 运行日志 ——————–
// 情况1:注释第37行(未注册信号处理函数):
// $ gcc sigsegv.c -o sigsegv
// $ ./sigsegv
// Segmentation fault (core dumped) # 直接崩溃,无 handler 输出
// 情况2:不注释第37行(注册 handler):
// $ gcc sigsegv.c -o sigsegv
// $ ./sigsegv
// catch a sig : 11 # handler 捕获到 SIGSEGV(编号11)
// Segmentation fault (core dumped) # 仍崩溃(段错误为致命异常)
2.2.5.软件条件产生信号
此时继续介绍一个函数— 》》alarm闹钟和 SIGALRM 信号。
调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终⽌当前进程。
2.2.5.1.使用alarm函数验证io的速度
1. IO 密集型代码
// IO 多
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int count = 0;
while(true)
{
std::cout << "count : " << count << std::endl;
count++;
}
return 0;
}
… …
count : 107148
count : 107149
Alarm clock // 长时间未退出,系统可能因终端缓冲或超时强制终止(或触发默认闹钟信号)
2. CPU 密集型 + 信号示例
// IO 少
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
std::cout << "count : " << count << std::endl;
exit(0);
}
int main()
{
signal(SIGALRM, handler);
alarm(1); // 1秒后触发 SIGALRM 信号
while (true)
{
count++; // 纯计算,无IO
}
return 0;
}
$ g++ alarm.cc -o alarm
whb@bite:~/code/test$ ./alarm
count : 49233713 # 1秒内纯计算的循环次数(远高于IO密集型,体现IO耗时影响)
3.保存信号
当前阶段:
3.1.概念铺垫
保存信号中的相关的概念:
1.实际执行信号的处理动作称为信号递达(Delivery)
2.信号从产生到递达之间的状态,称为信号未决(Pending)。
3.进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
接下来我们要了解这三张表:保存信号就是依靠这三张表结构
前提:这三张表都是无符号整数(unsigned int ),位图的结构
block表:
比特位的位置:表示的是第几个信号
比特位的内容:是否阻塞!
penging表:
比特位的位置:表示的是第几个信号
比特位的内容:是否收到!
handler表:里面存储的是函数指针
数组下标:信号编号
SIG_DEL:默认
SIG_IGN:忽略
(所以之前我们捕捉信号,并对信号进行自定义处理,其实就是改变handler对应下标里面的操作是 忽略 or 默认 or 自定义)
所以处理一个信号是看三张表的同下标位置,
先看pending表:看信号是否收到,如果收到
再看block表:看信号是否先存放着还是执行,如果不阻塞
最后看handler表:是如何执行该信号
sigser_t:
3.2.信号集操作函数
想要使用信号集操作函数,所以要先了解一下信号集(signal_t)的概念:
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigpet_t称为信号集,
这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
1.sigemptyset
功能:初始化信号集,使其为空(不包含任何信号)。
参数:set 指向要初始化的 sigset_t 变量。
返回:成功返回 0,失败返回 – 1(并设置 errno)。
注意:使用信号集前必须先初始化,否则内容不确定。
2.sigfillset
功能:初始化信号集,使其包含所有已定义的信号(如 SIGINT、SIGTERM 等,具体取决于系统)。
参数:set 指向要初始化的 sigset_t 变量。
返回:成功返回 0,失败返回 – 1。
场景:需要阻塞所有信号时,先 fill 再调整。
3.sigaddset
功能:向信号集中添加一个信号(signo 指定的信号加入集合)。
参数:set 是目标集合,signo 是信号编号(如 SIGUSR1)。
返回:成功 0,失败 – 1。
注意:添加前集合需已初始化(如用 sigemptyset 或 sigfillset)。
4.sigdelset
功能:从信号集中删除一个信号(移除 signo 指定的信号)。
参数:set 是目标集合,signo 是信号编号。
返回:成功 0,失败 – 1。
场景:比如先 fill 所有信号,再 del 掉不想阻塞的信号。
5.sigismember
功能:检查信号是否在集合中。
参数:set 是信号集,signo 是要检查的信号。
返回:
1:信号在集合中;
0:信号不在集合中;
-1:出错(如 signo 无效)。
用途:判断某个信号是否被包含,常用于调试或逻辑判断。
3.2.1.sigprocmask
用于 修改进程的信号屏蔽字(阻塞信号集),决定哪些信号会被 “拦截”
参数:
- how:指定修改屏蔽字的方式(3 种可选,见下表)。
- set:指向要操作的信号集(NULL 表示 “不修改屏蔽字”,仅备份旧值)。
- oset:输出参数,用于保存旧的屏蔽字(NULL 表示 “不保存”)
3.2.2.sigpending
用于 获取当前进程的未决信号集
函数原型:
参数:
- set:输出参数,存储未决信号集(被屏蔽但已到达的信号)。
- 返回值:成功返回 0,失败返回 -1(需检查 errno)。
代码示例:
两个特性:
1.9号信号,不可被捕捉,不可被阻塞
2.当我们准备递达的时候,要首先清空pending信号集中对应的信号位图 1->0
#include <iostream>
#include <signal.h>
using namespace std;
void Pint(sigset_t &pending)
{
for(int i=31;i>=1;i–){
if(sigismember(&pending,)i){
cout<<"1"<<;
}else{
cout<<"0"<<;
}
}
}
int main(){
//1.屏蔽2号信号
sigset_t block, oldset;
//初始化
sigmptyset(&block);
sigmptyset(&oldset);
//设置2号信号
sigaddset(&block,2);
int n=sigprocmask(SIG_SETMASK,&block,&oldset);
//2.获取pending信号
sigset_t pending;
int m=sigpending(&pending);
//3.打印pending信号
Pint(pending);
}
在此还要填充之间的几个坑:
细节一:
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
细节二:corr和term之间的区别(两者都是终止信号)
结论:
core的核心区别就是:会在当前路径下形成一个文件,进程异常退出的时候,进程在内存中的核心数据会在磁盘中拷贝一份,形成一个文件。这个就叫做—–核心转储
核心转储的目的就是支持debug,
开启core dump,直接运行崩溃,gdb,core-file core,直接帮助我们定位到出错行— 事后调试。
为什么从来没有见过这个文件?
在云服务器上core dump这个功能是默认关闭的。
为什么要禁用?
还产生大量的core dump 会造成储存的浪费
ulimit -a 进行查看
4.捕捉信号
现在我直接把信号捕捉的流程结论给整理出来:
根据之前我们的一个结论:会在合适的时候处理信号
1.在主函数中,如果我们遇见了中断 异常,或者是 系统调用,会进行用户切换,从用户态-》》内核态。
2.此时我就处于内核态,会进行信号的检查(do_signal),如何检查 ?查询下面三张表
先看pending表,再看block表,最后看hangler表如何处理:
此时就会有三种情况:
1.忽略(SIG_DFL),此时只需将pending表改成0,在切换成用户态执行主函数后面的代码。
2.默认(SIG_DFL),根据特定信号的约定来执行(大部分默认动作是终止),此时只需将pending表改成0,在切换成用户态执行主函数后面的代码。
3.自定义,会先切换成用户态执行我们自定义的代码,执行完后,在切换成内核态,在内核空间中,再切换成用户态
抽象出来就是下面这个流程表:
4.1.内核态和用户态
4.1.1.操作系统是如何运行的
操作系统是如何知道键盘上(硬件)发送来了数据?
其实cpu的针脚与这个硬件是间接相连的,当硬件给cpu发送完成以后,会跟cpu说我的数据发送好了,你可以进行接受了,这个操作就叫做—-》》硬件中断
所以
cpu不会不断地查看这个硬件看它发送完没,cpu之后看内存中地数据,但是看冯诺依曼体系这些硬件是和cpu有连接的
4.1.1.1.硬件中断
此时我们要将话题分成两个部分,进行讲解:硬件部分和软件部分
硬件部分:
有上面的知识:硬件和cpu其实是间接连接的==》》中断控制器
所以此时的操作流程:
1.外部设备输入完成,通过特定的针脚向中断控制器发送一个高频信号,此时中断控制器,根据是哪个针脚给我发送的高频信号,判断出是哪个硬件给我发送的形成一个中断号。
2.此时的中断控制器,通过特定的针脚向cpu发送一个高频信号,cpu接受到了信号,就会向中断控制器中访问形成的中断号
3.根据中断号,就知道哪一个外部设备准备好了CPU就知道了!
软件部分:
此时硬件部分给我们提供了中断号,但是根据中断号如何处理,这个只能交给软件部分处理
1.操作系统会在内部提供一张中断向量表(就是一个函数指针数组)
2.cpu再根据给定的中断号,找到我们要执行的中断方法
4.1.1.2.时钟中断
当没有中断到来的时候,OS在做什么?
什么都没做,OS是暂停的。
1.此时外部设备有个叫做时钟源的东西,它会以固定的,特定的频率,向CPU发送特定的中断
2.此时cpu就会以一定的频率执行进程调度
3.但是如果将时钟源设计再外部,此时时钟源就会不断地和其他的外部设备抢夺中断号,会降低效率,所以现在的时钟源都是cpu嵌设计的
操作系统是什么?
操作系统,就是基于中断,进行工作的软件!
时间片的概念:
schedule:进程调度
时间片耗尽了,就会进行进程的调度
由外部设备触发的,中断系统运行流程,叫做硬件中断,所以时钟中断也是硬件中断的一种
4.1.1.3.硬件异常
在 进程信号领域,野指针访问 和 除零错误 既不是传统的硬件中断,也不是纯粹的软中断,而是 “硬件检测异常 → 内核转换为软中断(信号)” 的过程。
常见的硬件异常有:除0,野指针,指针重复释放,缺页中断
以除零异常为例:
1.进程在执行自己的代码的时候,执行到除零,cpu上会发生硬件上的溢出。
2.但是此时cpu不知道后面该怎么处理,所以就规定成为一种有CPU内部触发的中断。
3.此时cpu内部就生成一个特有的中断号,再去中断向量表中,去执行异常中断
4.1.1.4.软中断
CPU内部,自己可以 可以让软件触发中断吗?
其实有下面两个(不同版本)指令集可以让CPU通过软件主动中断
1.而我们C/C++代码:本质就是编译成为了指令集+数据
2.所以syscall就会让cpu内部进行中断,并生成一个中断向量表的编号 –80(就相当于之间硬件异常的流程)
3.此时0x80对应的方法假设会做下面几件事
A.获取系统调用号 n
B.调用系统调用的方法
为了更好的理解上面第三步的步骤我们要了解几组概念
1.系统调用表:从此往后,每一个系统调用,都有一个唯一的下标,这个下标,我们叫做,系统调用号
2.当我们进行系统调用的时候,具体是怎么进入操作系统?完成系统调用过程的
以open()方法为例,用户层面会进行下面的操作,给定指定的系统调用号给cpu的寄存器中
所以此时我们也可以得出一个结论:
os操作系统,不会提供任何的接口,只会提供系统调用号,而所谓的open(),fork()方法只是
被glibc给分装而已
4.2.内核态和用户态总结
此时的总结就是对上面这张图的解析:
前提:
1.之前我们讲的虚拟地址空间不仅包括用户区[0~3]还包括内核区[3~4],所以我们就可以通过用户的页表映射访问到用户的磁盘数据,通过内核的页表映射就可以访问到内核的磁盘数据。
结论:
2.操作系统也是软件,一定也在内存当中,系统中内存只有一份,所有的进程共享。这就意味着,无论进程如何的调度,我们的进程总是能够找到操作系统。
问题:
用户和内核,都在同一个[0,4GB]的地址空间上了,如果用户,随便拿一个虚拟地址[3,4GB],用户不就可以随便访问内核中的代码和数据了吗?
OS为了保护自己,不相信任何人,必须采用系统调用的方式进行访问
1.用户态:以用户身份,只能访问自己的[0,3GB]
2.内核态:以内核的身份,运行你通过系统调用的方式,访问OS[3,4GB]
问题
在系统中,用户或者OS自己,怎么知道当前处于内核态,还是用户态?
CPU中有个cs段寄存器,控制两个bit位,
11->3:用户态
00->0:内核态
所以:其实就是更改cs寄存器段的数据完成身份之间的切换
4.3.sigaction信号捕捉
函数原型:
参数讲解:
- signum:信号编号(如 SIGINT、SIGTERM)。
- act:指向 struct sigaction 的指针,用于设置新的信号处理方式。
- oldact:输出参数,保存旧的信号处理配置(若不为 NULL)。
返回值:成功返回 0,失败返回 -1(并设置 errno)。
其实这个捕捉函数和之前那个signal函数十分的相似,下面是代码示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
void handler(int signum)
{
std::cout << "hello signal: " << signum << std::endl;
exit(0);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigaction(SIGINT, &act, &oact); // 对2号信号进行了捕捉
while(true)
{
std::cout << "hello world" << std::endl;
sleep(1);
}
return 0;
}
这是struct sigaction结构体,此时我们的重点聚焦在信号集上
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外-些信号,则用sa mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
通俗来讲就是捕捉到指定的信号集的时候,会把他相对应的block表置为1,进行阻塞,防止多个信号同时来的情况
5.进程信号衍生的几组问题
5.1.可重入函数
接下来我将举例一个链表头插的例子:
头插分成两个步骤:
A.新加入的节点的下一个节点指向head的下一个节点。
B.head节点的下一个节点指向新加入的节点。
1.假设此时node1要头插到head节点后面,当它执行完A,准备执行B的时候,发生了进程的调度。
2.此时node2也要头插到head节点后面,当node2插入完的时候。又发生了进程的调度到node1头插的进程。
3.此时执行B操作,最后就会造成下图4的现象,造成node2数据的丢失
总结:
不可重入函数和可重入函数只是一个特性。
不可重入函数:
大部分的函数,都是不可重入的
调用了malloc或free,因为malloc也是用全局链表来管理堆的。调用了标准I/0库函数。标准I/0库的很多实现都以不可重入的方式使用全局数据结构。
可重入函数:
只有自己的临时变量– 可重入的。
5.2.volatile
依旧是举个例子:观察下面代码,
对于flag 变量
正常的情况下操作系统会进行下面操作
1.不断的从物理内存中取数据,看falg是否改变
2.对falg变量进行判断,是否结束循环
但是此时main函数里不会对flag变量进行改变,所有就会有些优化级别高的编辑器,就会把从物理内存中拿数据这个操作给砍断,提高性能。
此时volatile的作用:
5.3.SIGCHLD信号
⼦进程在终⽌时会给⽗进程发SIGCHLD信号,该信号的默认处理动作是忽略,⽗进程可以⾃ 定义
SIGCHLD信号的处理函数
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0 ) {
printf("wait child success: %d\\n", id);
}
printf("child is quit! %d\\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0){//child
printf("child : %d\\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\\n");
sleep(1);
}
return 0;
}
评论前必须登录!
注册