进程,线程,中断简述
进程,线程,中断
当初学习8086汇编的时候了解到cpu是不断地提取cs:ip(IP也就是常说的pc)指向的指令然后执行,先来回顾一下cpu相关的知识。
- cs:ip经过地址加法器得到指令地址
- 通过地址总线确定要访问的数据
- 通过数据总线将指令地址相应的数据送到cpu内部的指令缓冲器
- ip的值加上当前读取到的指令字节长度,即cs:ip指向下一条要执行的指令。
- 执行缓冲器中的指令
cpu就是这样不停的周而复始取指执行(由于图示cpu地址总线只有20位所有,cs:ip相加时(cs*16+ip)为目标指令地址)。我们还可以了解到地址总线的宽度决定了cpu的寻址方位,数据总线的宽度决定了cpu与外部交换一次数据的数据量上限。很多时候为了提高读写数据的效率,会浪费一些内存做数据对齐操作。
简述对齐操作:
上边为数据对齐,下边为没有进行数据对齐,一目了然,数据对齐以后浪费了内存空间但是读取数据的时候可以更快(假如数据总线16位,黄色部分位要读取的数据)。c语言中bool类型明明只需要1bit为什么要用一int来实现这也算是部分原因吧。
接着我们来了解中断
中断分为内中断与外中断。内中断顾名思义就是cpu内部产生的中断,外中断是cpu外部产生的中断。内中断主要产生原因:
- div指令除法溢出
- 单步执行(主要便于程序的调试,例如dos下的debug中的t命令)
- 执行int指令
- 等等
我们假如在看全屏看视频,有事可能会按下ESC退出全屏,为了能让cpu及时的监听到键盘的输入并作出处理,就要使用外中断,但是有时假如我们在玩游戏的时候不管怎么键盘上的按钮都不会有任何反应。外中断分为可屏蔽中断和不可屏蔽中断。
在详细介绍各种中断之前我们先介绍一个特殊的寄存器:标志寄存器
主要作用:
- 存储相关指令的某些指令结果。
- 控制cpu的工作方式。
- 为cpu执行指令提供相关依据。
它内部存储的数据按照不同Bit作为不同的标志位,他存储的信息成为PWS(程序状态字)。
ZF:标志位若为1则表示cpu指令执行后结果为0
如:
mov ax,1
sub ax,1
sub指令执行后ax为0结果为0因此ZF标志位为1
我们接下来关注两个中断相关重要的标志位:
- IF标志位:为0则禁止可屏蔽的中断。
- TF标志位:为1则标识接下来执行单步中断,终端类型码为1。
还有一些标志位用来结果的正负,是否发生进位等等信息。
中断程序:当中断出现说明有特殊情况出现,我们要去解决这些情况就要运行异常处理程序来解决这些特殊情况。
刚开始写汇编程序的时候在程序的最后我们都要调用 int 21h 这句话什么含义。int + 整形值A 产生一个类型码为A的中断。依据这个中断类型码我们可以用来处理不同的中断。中断处理程序任何时候都可能被执行(中断发生的不确定性)因此我们要让中断程序常驻内存,这样才可以快速响应。并且各个进程之间是可以共享中断处理程序的。
但是有了中断类型码我们要如何定位到中断处理程序的起始地址,我们也需要一个类似于页表的机制来帮我们完成中断类型码 -> 目的地址 的转换。这也就是中断向量表的由来,中断向量表中只放置各个中断处理程序的起始地址,高位放置段地址CS,低地位放置IP。当产生一个2号中断的时候8086cpu就会到0:8处开始寻找目标地址(8086cpu的字长为2字节)。
相比于每个进程都要持有自己的页表要使用多级页表来减少内存的使用,中断向量表是可以被共享。
中断处理过程:
- 拿到中断类型码
- 标志寄存器的值入栈(可能在执行中断处理程序的时候改变flag寄存器的值)
- 设置TF=0设置IF=0(进入中断处理程序禁止响应可屏蔽中断)
- cs,ip内容依次入栈(保存程序返回点)
- 根据中断类型码读取中断处理程序入口地址
中断处理程序在执行主要代码前也要做一些保存寄存器状态的行为:
- 保存用到的寄存器
- 处理中断
- 恢复用到的寄存器
- 返回保存的cs:ip地址
这里要提一下特殊的TF标志位,当我们进行单步调试的时候程序可以一条一条执行,原因就在于dos下的debug可以将TF标志位置为1这样每当执行一条指令cpu发现TF为1就执行单步中断的中断处理程序输出各个寄存器的值。
进程
当单核的cpu面对一个程序的时候直接从程序起始地址开始执行然后不断地改变cs:ip的指向就可以了,但是我们要是想同时运行多个程序呢?同时运行多个程序也是常见的,并且当一个程序做IO操作,cpu处于空闲状态就是浪费资源不如执行其他程序。
//// 程序1
mov ax,2 2000:3000
add ax,1 2000:3003
//// 程序2
mov ax,3 1000:3000
add ax,1 1000:3003
为了让上述两个程序成功并发 运行 我们都要做哪些操作?
假如代码调度顺序为 2000:3000 ->1000:3000 ->2000:3003->1000:3003我们至少要保存程序所使用寄存器的当前值ax,还有当前程序PC的指向,然后改变pc指向1000:3000 我们就可以执行程序2,程序2要想其他程序转化的时候我们就要改变pc,并且回复pc指向。
我们可以看到程序的正确运行离不开两个东西 环境 + 代码 环境体现在cpu的状态 ,我们使用进程来描述运行中的程序,为了合理的处理各种值的保存与恢复我们使用了PCB来描述和控制进程。
PCB:应该含有哪些信息?
- 唯一的标识符
- 当前进程的状态
- 进程的优先级(进程调度有关)
- 进程同步与通信相关
- cpu处理器的数据,用于恢复运行
为了更好的组织并调度进程,我们为进程定义各个状态,操作系统维护着各个状态的进程队列,当进程进行切换的时候OS要移动进程PCB的位置,保存CPU寄存器的数据,恢复PCB中的数据到CPU。
当我们切换进程的时候我们要切换地址映射表,为了避免多进程的数据冲突我们采用的是各个进程有自己的页表。但是切换进程的时候我们也要切换页表以及其他进程相关的信息,为了满足我们实现并发的要求并且减少切换指令序列的代价我们就使用线程,进程作为资源分配(内存,权限等)对象,线程作为程序调度执行的对象。
OS给进程分配内存地址块,切换进程时切换映射表,进程处理同步问题就是不共享也就没有同步问题。线程处理同步问题,留给用户自己解决如java中我们要使用各种锁机制来进行同步处理。这也导致了进程间通信与线程间通信的差别。
内核级线程与用户级线程假如我们的线程不需要OS内核来管理,OS的内核对于我们所创建的线程是无感知的那我们就称之为用户级线程。
用户级线程如上图所示,OS内核只可以感受到进程的存在无法感知到用户级线程的存在,这样线程A做IO操作的时候进程1就会被调度为阻塞态进程2运行,但是此时进程1的线程就无法做到并发。只要让OS内核感受到线程1~n的存在这样才可以被调度。因此就有了核心级线程
用户级线程只需要维护线程自己的用户栈,而内核级线程需要维护两套栈:内核栈与用户栈。核心级线程切换的代价也就高于用户级线程。
当执行了A()以后调用read()函数以后进入执行80H号中断进入核心态执行代码,进入内核态的时候会将用户态的栈指针保存在内核栈中同时保存的还有标志寄存器(EFLAGS)cs:ip,中断出口地址
线程切换的5段论:
- 线程1 的用户栈切换到内核栈
- 内核栈修改TCB的信息
- TCB被调度切换
- 通过新的TCB找到内核栈位置
- 通过内核栈返回用户态继续执行程序