linux笔记归纳10:进程信号
进程信号
目录
一、信号的理解
1.1.信号的概念
信号:事件的异步通知机制
异步通知
- 同步:我们自习一会,等张三同学回来再讲课
- 异步:我们继续上课,张三去取快递
生活中的信号:闹钟,上课铃声
- 闹钟:睡觉 -> 醒来
- 上课铃声:休息 -> 听课
信号的特点:会中断当前正在做的事情
信号:一种给进程发送的,用来进行事件异步通知的机制
信号发送给进程,信号的产生相对于进程的运行是异步的
1.2.信号的结论
进程在信号没有产生的时候,早就知道该如何处理信号
信号的处理不是立即处理,而是在合适的时候进行处理
进程是提前被设计过的,内置了信号的识别与处理方式
给进程产生信号的信号源非常多
1.3.信号的种类
Linux有62种信号
- 34-64为实时信号:需要立即处理
- 1-31为普通信号:可以不被立即处理

man 7 signal

1.4.信号的处理
默认处理、自定义处理、忽略处理
1.5.signal函数
作用:自定义进程对信号的捕捉方式
参数1:信号编号
参数2:自定义捕捉方式

实验:自定义捕捉SIGINT信号
#include <iostream>
#include <unistd.h>
#include <signal.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++ << std::endl;
sleep(1);
}
return 0;
}
实验现象

1.7.目标进程
前台进程(#./xxx.exe):可以向标准输出打印内容,可以从标准输入获取内容
后台进程(#./xxx.exe &):可以向标准输出打印内容,无法从标准输入获取内容
前台进程本质:从键盘获取数据的进程
前台进程必须只有一个,后台进程可以有多个
命令行shell进程:默认为前台进程,执行其他前台程序时,自动变为后台进程

父进程先退出,子进程变为孤儿进程,是后台进程,ctrl + c无法杀掉
1.8.jobs命令
作用:查看当前所有的后台任务

1.9.fg命令
fg 任务号:将指定后台进程提到前台

1.10.ctrl + z命令
ctrl + z:暂停进程,将前台进程自动提到后台

1.11.bg命令
bg 任务号:将指定后台进程恢复运行

1.12.信号的记录
信号产生后并不是立即处理的,进程必须记录信号在合适的时候进行处理
信号会被记录在进程的PCB的一个无符号整型变量中,看作一个位图结构
- 比特位的位置代表信号的编号
- 比特位的内容代表信号是否收到
向目标进程发送信号的本质:获取目标进程pid与信号编号,修改位图
PCB属于OS内的数据结构对象,修改位图的本质是修改内核的数据
所以不管信号怎么产生,发送信号在底层必须让给操作系统来发送
操作系统必须给用户提供发送信号的系统调用函数
1.13.信号与通信
信号:用户与进程之间交互
通信IPC:进程与进程之间交互
二、信号的产生

2.1.键盘产生的信号
ctrl + c:给目标进程发送2号信号,进程处理该信号的动作是让自己终止
ctrl + \:给目标进程发送3号信号,进程处理该信号的动作是让自己终止并生成core dump文件
ctrl + z:给目标进程发送20号信号,进程处理该信号的动作是让自己暂停,并挂到后台

2.2.系统调用产生的信号
大部分信号可以被自定义捕捉,但是9号信号无法被自定义捕捉
2.2.1.kill函数
作用:指定一个进程,发送指定的信号
参数1:目标进程pid
参数2:信号编号

实验:使用kill函数向指定进程发送指定信号
- mykill.cc
#include <iostream>
#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]);
//获取目标进程pid
pid_t target = std::stoi(argv[2]);
int n = kill(target, signum);
if(n == 0)
{
std::cout << "send " << signum << " to " << target << " success" << std::endl;
}
return 0;
}
- testSig.cc
#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);
}
return 0;
}
- Makefile
.PHONY:all
all:testsig mykill
testsig:testSig.cc
g++ -o $@ $^ -std=c++11
mykill:mykill.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f testsig mykill
实验现象


2.2.2.raise函数
作用:进程给自己发送任意信号
参数:信号编号

实验:给自己进程发送信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
}
int main()
{
for(int i = 1; i < 32; i++)
{
signal(i, handlerSig);
}
for(int i = 1;i < 32; i++)
{
sleep(1);
raise(i);
}
int cnt = 0;
while(true)
{
std::cout << "hello,world, " << cnt++ << " ,pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
实验现象

2.2.3.abort函数
作用:终止进程

实验:使用abort函数终止进程
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
}
int main()
{
for(int i = 1; i < 32; i++)
{
signal(i, handlerSig);
}
int cnt = 0;
while(true)
{
std::cout << "hello,world, " << cnt++ << " ,pid: " << getpid() << std::endl;
abort();
sleep(1);
}
return 0;
}
实验现象

2.3.系统命令产生的信号
2.3.1.kill命令
kill命令
2.4.硬件异常产生的信号
2.4.1.除0错的信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
exit(13);
}
int main()
{
for(int i = 1; i < 32; i++)
{
signal(i, handlerSig);
}
int cnt = 0;
while(true)
{
std::cout << "hello,world, " << cnt++ << " ,pid: " << getpid() << std::endl;
int a = 10;
a /= 0;//除0错误
sleep(1);
}
return 0;
}
实验现象
SIGFPE(信号编号8):浮点数溢出错误


2.4.2.野指针错的信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
exit(13);
}
int main()
{
for(int i = 1; i < 32; i++)
{
signal(i, handlerSig);
}
int cnt = 0;
while(true)
{
std::cout << "hello,world, " << cnt++ << " ,pid: " << getpid() << std::endl;
sleep(1);
int *p = nullptr;//野指针错
*p = 100;
}
return 0;
}
实验现象
SIGSEGV(信号编号11):段错误


操作系统如何知道进程出错
浮点数溢出报错:
CPU的标志寄存器EFLAGS中有一个比特位表示CPU当前运算时是否出现溢出
CPU属于硬件,操作系统是软硬件资源的管理者,可以识别CPU中寄存器情况
CPU寄存器保存的是当前进程的上下文与PCB,发生溢出后操作系统发送信号
野指针报错:
CPU的CR3寄存器保存着当前进程对应页表的起始地址
CPU内部需要寻址的地址是虚拟地址,CPU将虚拟地址和CR3的页表地址交给MMU硬件单元
进行虚拟地址与物理地址转换,如果MMU转换失败,就会发送硬件报错,操作系统发送信号

2.4.3.core dump核心转储
Core Dump:核心转储
进程异常退出前,进程在内存中的核心数据,从内存拷贝到磁盘,在当前路径形成一个文件
在云服务器上,该功能是被禁止的,为了防止服务器出错时,多次打开关闭,出现大量的core文件
#include <iostream>
int main()
{
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
int a = 10;
a /= 0;
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
return 0;
}




信号为0[1 ~31]:进程正常终止,没有接收到信号
core dump标志:是否发送核心转储
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
int a = 10;
a /= 0;
printf("hello\n");
exit(1);
}
int status = 0;
waitpid(id, &status, 0);
printf("signal: %d, exit code: %d, core dump: %d\n",
(status & 0x7f), (status >> 8) & 0xff, (status >> 7) & 0x01);
return 0;
}

如果不设置core dump,则不会发生核心转储

Term:进程直接退出
#include <iostream>
#include <signal.h>
int main()
{
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
raise(2);
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
return 0;
}

2.4.4.gbd调试core文件
开启core dump,直接运行崩溃,使用gdb的core-file命令,直接定位到出错行
#include <iostream>
int main()
{
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
int a = 10;
a /= 0;
printf("hello\n");
printf("hello\n");
printf("hello\n");
printf("hello\n");
return 0;
}
//Makefile
testsig:testSig.cc
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f testsig

2.5.软件条件产生的信号
2.5.1.管道通信的软件异常
进程 === 管道 === 进程
读端关闭,软件条件不满足,写端再写时,会收到SIGPIPE信号
2.5.1.alarm函数
作用:为当前进程设置闹钟,闹钟到时间后,会给当前进程发送SIGALRM信号,终止进程
参数1:设置闹钟的时间长度,0-取消闹钟
返回值:
如果设定时间跑完:返回0
如果设定时间还没结束,又开启一个闹钟:返回上一个闹钟的剩余时间

实验:有IO操作,设置1s闹钟,1s后查看接收到的信号编号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << std::endl;
exit(13);
}
int main()
{
for(int i = 1; i < 32; i++)
{
signal(i, handlerSig);
}
//设定1s闹钟,1s以后,当前进程会收到一个信号
alarm(1);
int cnt = 0;
while(true)
{
//本质是IO xshell->./xxx->云服务器->网络->终端显示
std::cout << "count: " << cnt++ << std::endl;
}
}
实验现象

实验:无IO操作,设置1s闹钟,1s后查看接收到的信号编号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int cnt = 0;
void handlerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << " cnt: " << cnt << std::endl;
exit(13);
}
int main()
{
signal(SIGALRM, handlerSig);
//设定1s闹钟,1s以后,当前进程会收到一个信号
alarm(1);
while(true)
{
cnt++;
}
}
实验现象

有IO操作的运行效率比无IO操作的运行效率低很多
2.5.2.pause函数
作用:等待一个信号

实验:使用闹钟,每秒调度任务
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
typedef struct task_struct
{
pid_t id;//进程pid
int count = 10;//时间片 本质就是一个计数器
void(*code)();
}t;
std::vector<task_struct> task_list;
int timestamp = 0;
//////////////func//////////////////
void Sched()
{
std::cout << "我是进程调度" << std::endl;
t->count--;
if(t.count <= 0)
{
//切换其他进程
}
}
void MemManger()
{
std::cout << "我是周期性的内存管理,正在检查有没有内存问题" << std::endl;
}
void Fflush()
{
std::cout << "我是刷新程序,我在定期刷新内存数据到磁盘" << std::endl;
}
//////////////func//////////////////
using func_t = std::function<void()>;
std::vector<func_t> funcs;
//每隔一秒 ,完成一些任务
void handlerSig(int sig)
{
timestamp++;
std::cout << "#########################################" << std::endl;
for(auto f : funcs)
{
f();
}
std::cout << "#########################################" << std::endl;
int n = alarm(1);
}
int main()
{
funcs.push_back(Sched);
funcs.push_back(MemManger);
funcs.push_back(Fflush);
signal(SIGALRM,handlerSig);
alarm(1);
while(true)//这就是操作系统
{
pause();
}
return 0;
}
实验现象

2.5.3.系统闹钟的理解
系统闹钟的本质:操作系统自身具有的定时功能,并能让用户设置这种定时功能
操作系统内同时存在很多闹钟,需要对多个闹钟管理(先描述,再组织)
struct timer_list
{
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_t_base_s *base;
};
小根堆结构,堆顶是最短超时的闹钟,将堆顶的超时时间与系统时间戳进行比较
当堆顶节点出堆,堆结构重新调整为小根堆,让操作系统执行函数指针指向方法
向目标进程发送SIGALRM(定时器超时)
三、信号的保存

3.1.信号的其他概念
信号递达(delivery):处理信号的动作(自定义、默认、忽略)
信号未决(Pending):信号从产生到递达之间(信号在位图中,但未处理)
信号阻塞(屏蔽):进程可以阻塞信号,被阻塞的信号处于信号未决,解除阻塞后才能执行递达
老师在课堂布置作业,就是发信号的过程
同学将作业记录下来,就是进行记录信号
但同学未在课堂上写作业,此时作业处于信号未决
同学放学回家开始写作业,此时作业才被执行递达
同学不喜欢这个上课老师,但为了学分,只好正常上课,并且记录作业
但就是不写作业,作业被未决了,但不会被递达,该过程叫做信号阻塞
期末要考试时,同学将处于未决的作业全部写完,该过程叫做解除阻塞
注:信号被阻塞就不会递达,信号被递达后才能忽略
3.2.信号的内核示意图
进程PCB与普通信号有三张位图
pending(信号未决表):保存未决的信号的位图
- 比特位的位置:信号编号
- 比特位的内容:是否收到
block(信号阻塞表):保存阻塞的信号的位图
- 比特位的位置:信号编号
- 比特位的内容:是否阻塞
pending & ~blocks:能被递达的信号
handler:保存SIG_DFL(0默认信号)SIG_IGN(1忽略信号)sighandler(自定义捕获)
- 数组下标:信号编号
- 数组元素:信号处理函数

// 内核结构 2.6.18
struct task_struct
{
...
/* signal handlers */
struct sighand_struct *sighand;
sigset_t blocked
struct sigpending pending;
...
}
struct sighand_struct
{
atomic_t count;
struct k_sigaction action[_NSIG]; // #define _NSIG 64
spinlock_t siglock;
};
struct __new_sigaction
{
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void); /* Not used by Linux/SPARC */
__new_sigset_t sa_mask;
};
struct k_sigaction
{
struct __new_sigaction sa;
void __user *ka_restorer;
};
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
struct sigpending
{
struct list_head list;
sigset_t signal;
};
Linux的常规信号在递达之前产生多次时,只会被记录一次
POSIX.1允许系统递送该信号一次或者多次
3.3.sigset_t类型
sigset_t:信号集类型
#ifndef ____sigset_t_defined
#define ____sigset_t_defined
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
#endif
阻塞信号集:有效和无效表示该信号是否阻塞
未决信号集:有效和无效表示该信号是否处于未决
阻塞信号集也叫做信号屏蔽字(Signal Mask),类似权限的位掩码umask
位图结构
index:39 / 32 = 1......bitmap[1]
pos:39 % 32 = 7......bitmap[1]的第七位
struct bis
{
int bitmap[10];//32 * 10 = 320
}
3.4.信号集操作函数
#include <signal.h>
//全置为0
int sigemptyset(sigset_t *set);
//全置为1
int sigfillset(sigset_t *set);
//将比特位置1
int sigaddset(sigset_t *set, int signo);
//将比特位置0
int sigdelset(sigset_t *set, int signo);
//判断比特位是否为1
int sigismember(const sigset_t *set, int signo);
3.4.1.sigprocmask函数
作用:修改block表
参数1:如何操作
SIG_BLOCK:添加屏蔽信号,mask = mask | set
SIG_UNBLOCK:解除屏蔽信号,mask = mask & ~set
SIG_SETBLOCK:拷贝屏蔽信号,mask = set
参数2:设置修改值set
参数3:输出型参数,输出过去的set值

3.4.2.sigpending函数
作用:获取当前进程的pending信号集
参数:输出型参数,输出pending信号集

默认情况下,进程不对任何信号屏蔽
实验一:屏蔽2号信号,循环获取pending信号集并且打印,全为0
向进程发送2号信号,打印pending时2号信号所在比特位由0变1
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>
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;
}
int main()
{
// 1. 创建阻塞位图
sigset_t block, oblock;
// 2. 清空阻塞位图
sigemptyset(&block);
sigemptyset(&oblock);
// 3. 阻塞2号信号
sigaddset(&block, SIGINT); // 设置阻塞位图
int n = sigprocmask(SIG_SETMASK, &block, &oblock); // 写入进程
(void)n;
// 重复获取和打印
while(true)
{
// 4. 创建未决位图
sigset_t pending;
// 5. 写入进程
int m = sigpending(&pending);
// 6. 打印未决位图
PrintPending(pending);
sleep(1);
}
return 0;
}
实验现象


实验二:验证9号信号不可被捕捉,不可被阻塞
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>
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;
}
int main()
{
// 1. 创建阻塞位图
sigset_t block, oblock;
// 2. 清空阻塞位图
sigemptyset(&block);
sigemptyset(&oblock);
// 3. 阻塞所有信号
for(int i = 1; i < 32; i++)
{
sigaddset(&block, i);
}
int n = sigprocmask(SIG_SETMASK, &block, &oblock); // 写入进程
(void)n;
// 重复获取和打印
while(true)
{
// 4. 创建未决位图
sigset_t pending;
// 5. 写入进程
int m = sigpending(&pending);
// 6. 打印未决位图
PrintPending(pending);
sleep(1);
}
return 0;
}
实验现象


实验三:验证2号信号递达后,pending由1变为0
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>
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 << "递达 " << sig << " 信号!" << std::endl;
}
int main()
{
signal(SIGINT, handler);
// 1. 创建阻塞位图
sigset_t block, oblock;
// 2. 清空阻塞位图
sigemptyset(&block);
sigemptyset(&oblock);
// 3. 阻塞2号信号
sigaddset(&block, SIGINT); // 设置阻塞位图
int n = sigprocmask(SIG_SETMASK, &block, &oblock); // 写入进程
(void)n;
// 重复获取和打印
int cnt = 0;
while(true)
{
// 4. 创建未决位图
sigset_t pending;
// 5. 写入进程
int m = sigpending(&pending);
// 6. 打印未决位图
PrintPending(pending);
if(cnt == 10)
{
std::cout << "解除对2号信号的屏蔽" << std::endl;
sigprocmask(SIG_SETMASK, &oblock, nullptr);
}
sleep(1);
cnt++;
}
return 0;
}
实验现象


实验四:验证是递达之前pending设置为0,还是递达之后pending设置为0
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>
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 << "递达 " << 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. 创建阻塞位图
sigset_t block, oblock;
// 2. 清空阻塞位图
sigemptyset(&block);
sigemptyset(&oblock);
// 3. 阻塞2号信号
sigaddset(&block, SIGINT); // 设置阻塞位图
int n = sigprocmask(SIG_SETMASK, &block, &oblock); // 写入进程
(void)n;
// 重复获取和打印
int cnt = 0;
while(true)
{
// 4. 创建未决位图
sigset_t pending;
// 5. 写入进程
int m = sigpending(&pending);
// 6. 打印未决位图
PrintPending(pending);
if(cnt == 10)
{
std::cout << "解除对2号信号的屏蔽" << std::endl;
sigprocmask(SIG_SETMASK, &oblock, nullptr);
}
sleep(1);
cnt++;
}
return 0;
}
实验现象
在准备递达的时候,首先要清空pending信号集中对于的位图


四、信号捕捉

4.1.信号捕捉的流程
信号的处理是自定义捕获:
流程1:执行到主函数的某一条指令时,会因为中断、异常或者系统调用进入操作系统内核
流程2:内核处理完异常后,在准备回到用户模式前,会先处理当前进程中可以递达的信号
流程3:内核调用do signal函数,查询进程PCB中的三张信号表
流程4:如果此时信号对应的block为0,pending为1,handler为自定义捕获,则会跳转到用户层,
执行用户自定义的方法sighandler
流程5:在信号处理函数返回时,执行系统调用函数sigretum再次进入内核
流程6:在内核中返回用户模式,从上次被中断的地方继续往下执行程序

信号的处理是忽略处理:
操作系统将进程PCB中的pending对应比特位从1改为0,然后返回用户层
信号的处理是默认处理:
如果默认处理是终止,进程直接被杀,如果默认处理是暂停,就被挂载到,暂停相关的数据结构
必须以用户身份执行自定义方法,如果在内核中执行,出现非法操作时会出问题

进程凭什么进入内核
while(ture){}也会进入内核,因为进程会被调度到CPU,当时间片到了后
操作系统会把它从CPU强制剥离出来,进入内核态
4.2.用户态和内核态
4.2.1.硬件中断
操作系统怎么知道键盘上面有数据
外部设备在硬件上会与CPU的针脚进行间接的连接
步骤1:当外部设备就绪时,会向CPU的针脚发起硬件中断
步骤2:通过主板上的中断控制器接收中断信息通知到CPU
步骤3:CPU得知中断信息,获取中断号
注:在外部设备中也有寄存器
在磁盘设备中,有控制寄存器,地址寄存器,数据寄存器
当CPU给磁盘发送读写信息时,磁盘通过对应寄存器接收
操作系统会维护一张中断向量表,是操作系统的一部分,数组的下标是中断号
步骤4:CPU保护现场,保存当前进程上下文在寄存器中的值
步骤5:CPU根据中断号,执行中断处理函数
步骤6:执行完毕后恢复现场,继续之前工作

中断向量表
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);
outb_p(inb_p(0x21)&0xfb,0x21);
outb(inb_p(0xA1)&0xdf,0xA1);
set_trap_gate(39,¶llel_interrupt);
}
void rs_init(void)
{
set_intr_gate(0x24,rs1_interrupt);
set_intr_gate(0x23,rs2_interrupt);
init(tty_table[1].read_q.data);
init(tty_table[2].read_q.data);
outb(inb_p(0x21)&0xE7,0x21);
}
信号的本质是用软件来模拟硬件中断
- 发中断 -- 发信号
- 保存中断号 -- 记录信号
- 中断号 -- 信号编号
- 处理中断 -- 信号处理
- 外部设备 -- 信号源
- 中断号 -- pending表
- 中断向量表 -- handler表
- 中断屏蔽 -- block表
4.2.2.时钟中断
集成在CPU内部的时钟源,以固定的频率向CPU发送中断
操作系统就可以在硬件时钟中断的驱动下,进行进程调度
计算机上电,从网络中获取当前时间,将时间转化时间戳
设置为历史总频率total,时钟源每触发一次中断total加一
就可以让计算机在离线的情况,也能够知道时间是几点了
CPU主频:中断触发的频率

操作系统:基于中断来进行工作的软件


4.3.3.死循环
当中断未发生时,操作系统什么都不干,是暂停的
在硬件中断和时钟中断的推动下,自动地进行调度

4.3.4.软中断(异常)
异常:软中断的一种,软件导致硬件先出错,再由硬件触发的中断
常见异常:除0异常,野指针,指针重复释放,缺页异常
- 除0异常:
在处理当前进程的代码1ns时间片内,如果进程在执行时出现除0错误
CPU内部的状态寄存器EFLAGS会发生溢出,将该错误规定成一种由CPU内部触发的中断
CPU会给自己生成一个中断号,操作系统进行中断服务,处理异常,给目标进程发送信号
- 缺页异常:
要访问的物理内存空间在虚拟地址空间中已经申请,但是页表并没有进行映射
MMU会发生错误,产生缺页中断,操作系统进行中断服务,重新申请空间映射

4.3.5.软中断(陷阱)
陷阱:从用户态陷入内核态
为了让CPU通过软件主动中断,在CPU的指令集中引入了两个新的汇编指令
通过这两个指令,在CPU内部自动触发中断(C/C++代码本质就是编译成为了指令集+数据)
- x86:int 0x80
- x86_64:syscall

进行系统调用的时候,具体是怎么进入操作系统完成的
所有的系统调用都被写入一个系统调用函数指针表里面
每一个系统调用都有一个唯一的下标,叫做系统调用号


注:OS不会提供任何系统调用接口,只提供系统调用号,而open、fork这些函数都是glibc封装的
用户层(glibc)
- x86

- x86_64

#define SYS_ify(syscall_name) __NR_##syscall_name:将系统调用名称转为系统调用号
示例:SYS_ify(open):__NR_open

//linux-2.6.18\linux-2.6.18\include\asm-x86_64\unistd.h 12
/* at least 8 syscall per cacheline */
#define __NR_read 0
__SYSCALL(__NR_read, sys_read)
#define __NR_write 1
__SYSCALL(__NR_write, sys_write)
#define __NR_open 2
__SYSCALL(__NR_open, sys_open)
#define __NR_close 3
__SYSCALL(__NR_close, sys_close)
#define __NR_stat 4
__SYSCALL(__NR_stat, sys_newstat)
#define __NR_fstat 5
__SYSCALL(__NR_fstat, sys_newfstat)
#define __NR_lstat 6
__SYSCALL(__NR_lstat, sys_newlstat)
#define __NR_poll 7
__SYSCALL(__NR_poll, sys_poll)
#define __NR_lseek 8
__SYSCALL(__NR_lseek, sys_lseek)
#define __NR_mmap 9
__SYSCALL(__NR_mmap, sys_mmap)
#define __NR_mprotect 10
__SYSCALL(__NR_mprotect, sys_mprotect)
#define __NR_munmap 11
__SYSCALL(__NR_munmap, sys_munmap)
#define __NR_brk 12
__SYSCALL(__NR_brk, sys_brk)
#define __NR_rt_sigaction 13
__SYSCALL(__NR_rt_sigaction, sys_rt_sigaction)
#define __NR_rt_sigprocmask 14
__SYSCALL(__NR_rt_sigprocmask, sys_rt_sigprocmask)
#define __NR_rt_sigreturn 15
__SYSCALL(__NR_rt_sigreturn, stub_rt_sigreturn)
//...
内核层

4.3.6.理解用户态与内核态
系统调用的也是在进程地址空间上进行
所有的函数调用都是地址空间之间跳转
操作系统也是软件,一定也在内存当中
用户页表存在多份,内核页表只有一份

无论进程如何调度,总能够找到操作系统
用户和内核都在同一个[0,4]GB地址空间
如果用户随便用一个[3,4]GB的虚拟地址,就可以随便访问内核的代码与数据
但操作系统为了保护自己,不相信任何人,必须采用系统调用的方式进行访问
用户态:以用户身份,只能访问自己的[0,3]GB
内核态:以内核身份,通过系统调用访问[3,4]GB
如何区分当前处于内核态与用户态(段描述符表、RPL、DPL)
CPU中的CS寄存器(代码段寄存器)的低两位比特位
(Current Privilege Level,当前特权级)
- 如果为0:内核态
- 如果为3:用户态
如果处于用户态时,访问了内核中的代码与数据,就会触发硬件中断,终止进程
而int 0x80或者syscall指令,就可以让CS由用户态切换为内核态,执行系统调用
4.3.signaction函数
作用:检测并且改变信号处理动作
参数1:信号编号
参数2:信号处理结构体(设置自定义捕捉方法)
参数3:输出型参数,输出上次的信号处理动作

sa_handler:自定义捕捉方法
sa_mask:信号屏蔽字

正在处理某个信号时,为了防止某个信号再次被递达,操作系统会自动修改当前进程的block表
进行阻塞,所以在处理自定义捕捉方法之前,除了把pending表由1变0,还要把block表由0变1
如果在调用信号处理函数时,除了当前信号自动屏蔽,还希望屏蔽其他信号,需要用mask说明
实验:对2号信号进行捕捉,2、3、4都屏蔽
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
void handler(int signum)
{
std::cout << "hello signal: " << signum << std::endl;
while(true)
{
//不断获取pending表
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, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
act.sa_flags = 0;
sigaction(SIGINT, &act, &oact);
while(true)
{
std::cout << "hello world: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
实验现象


五、可重入函数
main执行流与handler执行流是串行的,是在同一个进程里执行的
所以会导致insert方法被两个以上的执行流重复进入,即函数重入
如果在函数重入时,进程出现异常,称为不可重入函数
如果在函数重入时,进程没有异常,称为可重入函数
注:大多数函数,都是不可重入函数
比如:
调用了malloc或free,因为malloc也是用全局链表来管理堆的
调用了标准I/O库函数,因为很多实现都是使用全局数据结构

六、volatile
C语言中的关键字
作用:保证内存空间的可见性
main执行流中并不会对flag进行修改,而是不断地进行循环检查
编译器优化级别较高时会把flag直接优化成register到寄存器当中

CPU的计算方式
- 算术运算:加减乘除取模
- 逻辑运算:或与异或取反真假
编译器的优化级别,默认优化级别为O0

实验一:没有加volatile关键字
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
int flag = 0;
void handler(int signum)
{
std::cout << "更改全局变量, " << flag << "-> 1" << std::endl;
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
std::cout << "process quit normal" << std::endl;
return 0;
}
实验现象

实验二:使用volatile关键字修饰
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
volatile int flag = 0;
void handler(int signum)
{
std::cout << "更改全局变量, " << flag << "-> 1" << std::endl;
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
std::cout << "process quit normal" << std::endl;
return 0;
}
实验现象

七、SIGCHLD信号
子进程退出,父进程等待,回收子进程
子进程在退出前会给父进程发送SIGCHLD信号


实验:子进程退出时给父进程发生信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>
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;
}
实验现象

实验:使用信号捕捉的方式回收子进程
如果使用阻塞调用,父进程会报持访问子进程的状态
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>
void WaitAll(int num)
{
while (true)
{
pid_t n = waitpid(-1, nullptr, 0);
if (n == 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, WaitAll);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
sleep(3);
if(i <= 6)
{
std::cout << "I am child, exit" << std::endl;
exit(3);
}
else
{
pause();
}
}
}
while (true)
{
std::cout << "I am father, exit" << std::endl;
sleep(1);
}
return 0;
}
实验现象:父进程发生阻塞


所以需要用非阻塞等待的方式回收子进程
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>
void WaitAll(int num)
{
while (true)
{
// waitpid使用非阻塞轮询
// 如果使用阻塞调用,waitpid会阻塞
pid_t n = waitpid(-1, nullptr, WNOHANG);
if (n == 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, WaitAll);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
if(i < 6)
{
std::cout << "I am child, exit" << std::endl;
exit(3);
}
else
{
pause();
}
}
}
while (true)
{
std::cout << "I am father, exit" << std::endl;
sleep(1);
}
return 0;
}
实验现象

将SIGCHLD的处理动作设置为SIG_IGN就可以自动回收子进程
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>
int main()
{
signal(SIGCHLD, SIG_IGN);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
sleep(3);
std::cout << "I am child, exit" << std::endl;
exit(3);
}
}
while (true)
{
std::cout << "I am father, exit" << std::endl;
sleep(1);
}
return 0;
}
实验现象

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)