Linux----中断
这里写目录标题
1. 信号的处理时机
信号的处理,不是立即处理,而是可以等一会处理,合适的时候再进行处理
那合适的时候到底是什么时候呢?
进程从内核态返回内核态的时候,因为这个时候OS要做信号检查,信号检查就是检查pending表
如果信号是默认呢?忽略呢?
如果信号是忽略,OS查handler表,发现是忽略,此时把pending表的比特位1->0,不做任何处理
如果信号是默认,大多默认是杀掉进程,所以OS直接杀掉进程
执行自定义方法(用户写的),OS也要在做身份切换->必须以用户身份执行
为什么?
因为如果用户写的自定义方法有非法操作,比如是原来以用户身份不让你执行操作,现在你以内核身份执行就会出现Bug
我的进程凭什么进入内核啊?
while true
{
}
这种代码也会进入内核吗?
是的,它也会!它会被调度,当一个进程的时间片到了的时候,操作系统会把它从CPU上剥离下来,放入等待队列中,然后适当的时候再唤醒调度,所以只要是代码就会进入内核

4->5:主要为了检查是否有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了
2. 硬中断

OS / 进程怎么知道键盘上面有数据了 -> 硬件中断
当外设就绪时,外设会对中断控制器发起中断,然后中断控制器就可以获取中断号,不光CPU有寄存器,外设也有,所以中断控制器也有,里面存的就是中断号,然后中断控制器通知CPU,告知CPU有外设就绪了,比如键盘上面有数据了,此时CPU再从中断控制器中读取中断号,就知道是哪个设备就绪了,这个过程叫硬件中断,这是硬件话题
这里的通知其实是给cpu的针脚发送高低电频
但是这里只是让CPU知道哪个外设就绪了,但不知道怎么执行数据,比如要不要把数据加载到内存,要不要重新调整空间,要不要进行空间申请,释放,合并,加载到内存的什么位置,这些只有软件最清楚,所以我们需要软件部分,也就是操作系统
中断向量表是OS系统的一部分,它是操作系统内置的,本质就是函数指针数组,下标就是中断号,操作系统加载到内存时,CPU可以通过中断号索引到函数指针内部的方法,然后把方法的入口喂给CPU,CPU就可以执行特定的执行方法了,比如读取键盘数据,这些中断向量表是早就写好了的,拿到中断号就可以执行
当硬件中断时,CPU会保护现场,也就是会保护进程的代码和数据。等处理完中断后,再恢复现场继续执行原来进程的代码和数据,所以OS 再也不关注外部设备是否准备好,而是外设准备好会硬件中断,从而让OS执行固定的方法
在没有中断到来的时候,OS在干什么?
OS系统什么都没做,OS系统是暂停的
for(;;)
pause();//操作系统被暂停什么都不做,除非收到中断或信号才会执行

但是我们的OS系统是要一直进行工作的,所以cpu内部集成了一个时钟中断,以特定频率向操作系统发送中断让操作系统执行进程调度,很高频,所以操作系统就在硬件的推动下,自动调度了,所以OS就是基于硬件中断工作的软件

这是内核代码,由时钟中断引起的,执行0x20编号下的方法_do_timer,判断进程的时间片是否>0,大于0直接返回,继续进程调度,如果时间片到了,switch_to(next),进程切换调度其它进程
2. 软中断->异常
上面都是由硬件产生的中断,当然也有软件产生的硬件中断
当进程执行自己代码时,如果有除0错误,EFLAGS会识别到除0异常,从而向OS发送中断,执行异常方法,就是给目标进程发送信号,野指针和指针重复释放都是这个原理,缺页中断,当MMU查看页表映射关系,发现只有虚拟地址(合法)没有物理地址时,会触发缺页中断,让操作系统在内存申请一块空间,拿到这段空间的起始物理地址填写页表,完成映射,这些软件出异常而引起硬件的中断叫异常
3. 软中断->陷阱
CPU内部可以通过软件主动中断
x86:int x80
x64:syscall
这些都是指令集,c/c++代码本质就是编译成了指令集+数据
上面的表属于操作系统
当我们进行系统调用时,具体是怎么进入操作系统,完成调用过程的,毕竟cpu只有一个?
系统调用表,从此以后每个系统调用都有一个唯一下标,这个下标就是系统调用号
用户层面怎么理解系统调用?
pid_t fork(void);
这个系统调用是操作系统提供的吗?
不是,OS只提供系统调用号,这个系统调用是glibc封装的,这个系统调用在内部只做了图下的两件事,把系统调用号给寄存器,然后进入内核

整个过程是系统调用,把系统调用号交给eax,然后syscall xxx,或int 0x80产生软中断,进入内核,查找中断编号,一般是0x80,然后执行对应系统调用号的函数指针方法
内核代码
4. 内核态和用户态

系统调用的过程也是在进程地址空间上进行的,所有的函数调用都是地址空间上的跳转
用户的页表存在多份,因为每个进程都要有一个用户页表来执行自己的代码和数据,但是OS只有一个,所以系统页表只有一份,所有进程共享
这就意味着,无论进程怎么调度,我们总能找到OS
1. 用户和内核都在同一个[0,4GB]的地址空间上了,如果用户随便拿一个[3,4GB]的虚拟地址,那用户不就可以随便访问访问内核中的代码和数据了吗?
2. OS系统不相信任何人,必须采用系统调用的方式进行访问,此时这两个结论冲突
所有内核态和用户态的概念就出来了
用户态:以用户的身份,只能访问[0,3GB]
内核态:以内核的身份,运行你通过系统调用的方式,访问OS[3,4GB]
在系统中,用户或者OS自己怎么知道当前属于内核态还是用户态?
CPU里存在cs寄存器,里面会存储代码地址和数据,这个数据是二进制整数,0:内核态,3:用户态,所以当我们代码区系统调用时,它会更改CPU的权限级别,0->3,再把cs的代码地址改成内核区你要执行的那个地址,然后陷入内核区,执行你的系统调用,然后软中断查看你页表对应映射的方法!

当前正在处理的信号在执行自定义捕捉方法(递达)之前就会把pending的比特位1->0,并且会把block0->1,所以如果同一个信号如果再来,会把pending的比特位再0->1,但是被阻塞着,无法递达
5. 可重入函数

假如node1刚执行完p->next=head,然后变成图一的样子,然后突然收到信号,转而去执行sighandler,就会再次进入insert,然后执行2和3,最后信号处理完再回到4,此时就会导致node2没人管理,内存泄漏
insert方法被main执行流和sighandler执行流重复进入了,函数被重入了,如果一个函数被重入不会出现问题称这个函数为可重入函数,如果一个函数被重入后出现了问题称这个函数为不可重入函数
一般函数只在自己内部有临时变量这种函数一般是可从入的,如果有全局变量这种函数就是不可重入的,这个例子里的链表是全局的,所以不可重入函数,大部分函数都是不可重入的,可重入或不可重入是特点,不是说不可重入就是错误
6. volatile
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}

这里的flag,在自定义捕捉里面被修改了,但是这个自定义捕捉不是直接的函数调用关系,而是处理信号的,所以编译器优化级别比较高的情况下,会把该变量优化到寄存器中,所以当我们对flag进行修改时,虽然值被修改了,但是CPU只会对原来存入寄存器的变量进行逻辑运算,所以这个程序会死循环
一般不优化的情况下,CPU会从内存读取数据然后进行逻辑运算,但是优化后就把该变量内置在CPU了,而不从内存读取,直接拿CPU内置的变量做判断,所以修改该变量也没用,这样优化省掉了CPU读取内存的时间,但是会出现问题,所以我们 volatile int flag=0,防止编译器过度优化,也就是必须让CPU从内存读取数据
7. SIGCHLD信号
子进程在终止时会给父进程发送SIGCHLD,17号信号,但是这个信号的默认动作是忽略
如果我们不想让父进程等待子进程的话,就将SIGCHLD的处理动作改成SIG_IGN,此时系统自动回收子进程,就不会有僵尸问题了
signal(SIGCHLD,SIG_IGN);//父进程
#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;
}
所以我们可以在父进程收到信号后,再等待子进程,如果没有收到信号,父进程就做自己的事情,这是个好的代码设计思路
更多推荐




所有评论(0)