操作系统运行机制

一、内核态与用户态概念

1、概念

内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。

用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。

2、用户态和内核态的概念区别

究竟什么是用户态,什么是内核态,这两个基本概念以前一直理解得不是很清楚,根本原因个人觉得是在于因为大部分时候我们在写程序时关注的重点和着眼的角度放在了实现的功能和代码的逻辑性上,先看一个例子:

2.1 例子

void testfork(){
if(0 = = fork()){
printf(“create new process success!\n”);
}
printf(“testfork ok\n”);
}
<span style="font-size:14px;">void testfork(){  
if(0 = = fork()){  
printf(“create new process success!\n”);  
}  
printf(“testfork ok\n”);  
}</span>  

这段代码很简单,从功能的角度来看,就是实际执行了一个fork(),生成一个新的进程,从逻辑的角度看,就是判断了如果fork()返回的是0则打印相关语句,然后函数最后再打印一句表示执行完整个testfork()函数。代码的执行逻辑和功能上看就是如此简单,一共四行代码,从上到下一句一句执行而已,完全看不出来哪里有体现出用户态和进程态的概念。

如果说前面两种是静态观察的角度看的话,我们还可以从动态的角度来看这段代码,即它被转换成CPU执行的指令后加载执行的过程,这时这段程序就是一个动态执行的指令序列。而究竟加载了哪些代码,如何加载就是和操作系统密切相关了。

2.2 特权级

熟悉Unix/Linux系统的人都知道,fork的工作实际上是以系统调用的方式完成相应功能的,具体的工作是由sys_fork负责实施。其实无论是不是Unix或者Linux,对于任何操作系统来说,创建一个新的进程都是属于核心功能,因为它要做很多底层细致地工作,消耗系统的物理资源,比如分配物理内存,从父进程拷贝相关信息,拷贝设置页目录页表等等,这些显然不能随便让哪个程序就能去做,于是就自然引出特权级别的概念,显然,最关键性的权力必须由高特权级的程序来执行,这样才可以做到集中管理,减少有限资源的访问和使用冲突。

特权级显然是非常有效的管理和控制程序执行的手段,因此在硬件上对特权级做了很多支持,就Intel x86架构的CPU来说一共有0~3四个特权级,0级最高,3级最低,硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查,相关的概念有CPL、DPL和RPL,这里不再过多阐述。硬件已经提供了一套特权级使用的相关机制,软件自然就是好好利用的问题,这属于操作系统要做的事情,对于Unix/Linux来说,只使用了0级特权级和3级特权级。也就是说在Unix/Linux系统中,一条工作在0级特权级的指令具有了CPU能提供的最高权力,而一条工作在3级特权级的指令具有CPU提供的最低或者说最基本权力。

2.3 用户态和内核态

现在我们从特权级的调度来理解用户态和内核态就比较好理解了,当程序运行在3级特权级上时,就可以称之为运行在用户态,因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;反之,当程序运行在0级特权级上时,就可以称之为运行在内核态。

虽然用户态下和内核态下工作的程序有很多差别,但最重要的差别就在于特权级的不同,即权力的不同。运行在用户态下的程序不能直接访问操作系统内核数据结构和程序,比如上面例子中的testfork()就不能直接调用sys_fork(),因为前者是工作在用户态,属于用户态程序,而sys_fork()是工作在内核态,属于内核态程序。

当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态,比如testfork()最初运行在用户态进程下,当它调用fork()最终触发sys_fork()的执行时,就切换到了内核态。

3、用户态与内核态的切换

3.1 概念

所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等. 而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先操作系统请求以程序的名义来执行这些操作.

这时需要一个这样的机制: 用户态程序切换到内核态, 但是不能控制在内核态中执行的指令

这种机制叫系统调用, 在CPU中的实现称之为陷阱指令(Trap Instruction)

他们的工作流程如下:

  • 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈(stack frame), 以此表明需要操作系统提供的服务.
  • 用户态程序执行陷阱指令
  • CPU切换到内核态, 并跳到位于内存指定位置的指令, 这些指令是操作系统的一部分, 他们具有内存保护, 不可被用户态程序访问
  • 这些指令称之为陷阱(trap)或者系统调用处理器(system call handler). 他们会读取程序放入内存的数据参数, 并执行程序请求的服务
  • 系统调用完成后, 操作系统会重置CPU为用户态并返回系统调用的结果
    当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。

此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。

内核态与用户态是操作系统的两种运行级别,跟intel cpu没有必然的联系, intel cpu提供Ring0-Ring3三种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态,Ring0作为 内核态,没有使用Ring1和Ring2。Ring3状态不能访问Ring0的地址空间,包括代码和数据。

Linux进程的4GB地址空间,3G-4G部 分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。

用户运行一个程序,该程序所创建的进程开始是运 行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过write,send等系统调用,这些系统调用会调用内核中的代码来完成操作,这时,必 须切换到Ring0,然后进入3GB-4GB中的内核地址空间去执行这些代码完成操作,完成后,切换回Ring3,回到用户态。

这样,用户态的程序就不能 随意操作内核地址空间,具有一定的安全保护作用。

至于说保护模式,是说通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程的地址空间中的数据。

3.2 用户态和内核态的转换

3.2.1 用户态切换到内核态的3种方式

a. 系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。

b. 异常

当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

c. 外围设备的中断

当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。

3.2.2 具体的切换操作

从触发方式上看,可以认为存在前述3种不同的类型,但是从最终实际完成由用户态到内核态的切换操作上来说,涉及的关键步骤是完全一致的,没有任何区别,都相当于执行了一个中断响应的过程,因为系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本上也是一致的,关于它们的具体区别这里不再赘述。关于中断处理机制的细节和步骤这里也不做过多分析,涉及到由用户态切换到内核态的步骤主要包括:

[1] 从当前进程的描述符中提取其内核栈的ss0及esp0信息。

[2] 使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个

过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一

条指令。

[3] 将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始

执行中断处理程序,这时就转到了内核态的程序执行了。

二、中断与异常

1、中断和异常

中断何和异常是指明系统、处理器或当前执行程序(或任务)的某处出现一个事件,该事件需要处理器进行处理。通常,这种事情会导致执行控制器被强迫从当前运行程序转移到被称为终端处理程序或异常处理程序的特殊软件函数或任务中。处理器响应中断或异常所采取的行动称为中断/异常服务(处理)。

通常中断发生在程序执行的随机时刻,以响应硬件发出的信号。系统硬件使用中断来处理外部事件,例如要求为外部设备提供服务。当然,软件也能通过执行 INT n 指令产生中断。

异常发生在处理器执行一条指令时,检测到一个出错条件时发生,例如被0除出错条件。处理器可以检测到各种出错条件,包括违反保护机制。页错误以及机器内部错误。对应用程序来说,80x86的中断和异常处理机制可以透明地处理发生的异常和中断事件。当收到一个中断或检测到一个异常时,处理器会自动把当前正在正在执行的程序或任务挂起,并开始运行中断或异常处理程序。当处理程序执行完毕,处理器就会恢复并继续执行被中断的程序或任务。被中断程序的恢复过程并不会失去程序执行的连贯性,除非从异常中恢复是不可能的或者中断异常导致当前运行程序被终止。

2、异常和中断向量

为了有助于处理异常和中断,每个需要被处理器进行特殊处理的处理器定义的异常和中断条件都被赋予了一个标识号,称为向量。处理器把赋予异常或中断的向量用作中断描述符表IDT中的一个索引号,来定位一个异常或中断的处理程序入口点位置。

允许的向量号范围是0到255.其中0到31保留用作80x86处理器定义的异常和中断,不过目前该范围内的向量号并非每个都已定义了功能,未定义功能的向量号将留在以后使用。

范围在32到255的向量号用于用户定义的中断。这些中断通常用于外部I/O设备,使得这些设备可以通过外部硬件中断机制向处理器发送中断。

3、中断源和异常源

3.1、中断源

处理器从两种地方接收终端:

外部(硬件产生)的中断。
软件产生的中断。

外部中断通过处理器芯片上两个引脚(INTR 和 NMI)接收。当引脚接收到外部发生的中断信号时,处理器就会从系统总线上读取外部中断控制器提供的中断向量号。当引脚 NMI 接收到信号时,就产生一个非屏蔽中断。它使用固定的中断向量号2。 任何通过处理器 INTR 引脚接收的外部中断都被称为可屏蔽硬件中断,包括中断向量号0到255。标志寄存器 EFLAGS 的 IF 标志可用来屏蔽所有这些硬件中断。

通过在指令操作数中提供中断向量号, INT n 指令可用于从软件中产生中断。例如,指令 INT 0x80 会执行Linux的系统系统中断调用中断0x80。向量0到255中的任何一个都可以用作INT指令的中断号。如果使用了处理器预先定义的 NMI向量,那么处理器对它的响应将与普通方式产生的该NMI中断不同,如果NMI的向量号2用于该INT指令,就会调用NMI的中断处理程序,但此时并不会激活处理器的NMI处理硬件(???)。

注意,EFLAGS中的IF标志位不能屏蔽使用INT执行从软件产生的中断。

3.2、异常源

处理器接收的异常也有两个来源:

处理器检测到的程序错误异常。
软件产生的异常。

在应用程序或操作系统执行期间,如果处理器检测到程序错误,就会产生一个或多个异常。80x86处理器为其检测到的每一个异常定义了一个向量。异常可以被细分为故障、陷阱和中止。

指令INTO、INT 3 和 BOUND 指令可以用来从软件中产生异常。这些指令可对指令流中指定点执行的特殊异常条件进行检查。例如,INT 3 指令会产生一个断点异常。

INT n 指令可用于在软件中模拟指定的异常,但有一个限制。如果INT指令中的操作数n是80x86异常的向量号之一,那么处理器将为该向量号产生一个中断,该中断就会去执行与该向量有关的异常处理程序。但是,因为实际上是一个中断,因此处理器并不会把一个错误号压入栈,即使硬件产生的该向量相关的中断通常会产生一个错误码。对于那些会产生错误码的异常,异常的处理程序会试图从栈上弹出错误码。因此,如果使用INT指令来模拟产生一个异常,处理程序则会把EIP(正好处于缺少的错误码位置处)弹出栈 ,从而会造成返回位置错误。

4、异常的分类

根据异常被报告的方式以及导致异常的指令是否能够被重新执行,异常可被细分为故障(Fault)陷阱(Trap)和中止(Abort)

Fault 是一种通常可以被纠正的异常,并且一旦被纠正程序就可以继续运行。当出现一个Fault,处理器会把机器的状态恢复到产生Fault的指令之前的状态。因此异常处理程序的返回地址会指向产生Fault的指令,而不是其后面一条指令。因此在返回后产生Fault的指令将被重新执行。

Trap 是一个引起陷阱的指令被执行后立刻会报告的异常。Trap也能够让程序或任务连贯地执行。Trap处理程序的返回地址指向引起陷阱指令的随后一条指令,因此在返回后会执行下一条指令。

Abort 是一种不会总是报告导致异常的指令的精确位置的异常,并且不允许导致异常的程序重新继续执行。Abort用于报告严重错误,例如硬件错误以及系统表中存在不一致性或非法值。

5、程序或任务的重新执行

为了让程序或任务在一个异常或中断处理完之后能重新恢复执行,除了中止之外的所有异常都能报告精确的指令位置,并且所有中断保证是在指令边界上发生。

对于故障类异常,处理器产生异常时保存的返回指针指向出错指令。因为,当程序或任务在故障处理程序返回后重新开始执行时,原出错指令会被重新执行。重新执行引发出错的指令通常用于处理访问指令操作数受阻止的情况。Fault最常见的一个例子是页面故障(Page-Fault)异常。当程序引用不在内存中页面上的一个操作数时就会出现这种异常。当页故障异常发生时,异常处理程序可以把该页面加载到内存中并通过重新执行出错指令来恢复程序运行。为了确保重新执行对于当前执行程序具有透明性,处理器会保存必要的寄存器和堆栈指针信息,以使得自己能够返回到执行出错指令之前的状态。

对于陷阱Trap类异常,处理器产生异常时保存的返回指针指向引起陷阱操作的后一条指令。如果在一条执行控制转移的指令执行期间检测到一个Trap,则返回指令指针会反映出控制的转移情况。例如,如果在执行JMP指令时检测到一个Trap异常,那么返回指令指针会指向JMP指令的目标位置,而非指向JMP指令随后的一条指令。

中止Abort类异常不支持可靠地重新执行程序或任务。中止异常的处理程序通常用来收集异常发生时有关处理器状态的诊断信息,并且尽可能恰当地关闭程序和系统。

中断会严格地支持被中断程序的重新执行而不会失去任何连贯性。中断所保存的返回指令指针指向处理器获取中断时将要执行的下一条指令的边界处。如果刚执行的指令有一个重复前缀,则中断会在当前重复结束并且寄存器已为下一次重复操作设置好时发生。

6、开启和禁止中断

标志寄存器EFLAGS的中断允许标志IT能够禁止为处理器INTR引脚上收到的可屏蔽硬件中断提供服务。当IF=0时,处理器禁止发送INTR引脚的中断;当IF=1时,则发送到INTR引脚的中断信号会被处理器进行处理。

IF标志并不影响发送到NMI引脚的非屏蔽中断,也不影响处理器产生的异常。如同EFLAGS中的其它标志一样,处理器在相应硬件复位操作时会清楚IF标志(IF=0)。

IF标志可以使用指令STI和CLI来设置或清除。只有当程序的CPL<=IOPL 时才可执行这两条指令,否则将引发一般保护性异常。IF标志也会受以下操作影响:

PUSHF 指令会把EFLAGS内容存到栈中,并且可以在那里被修改。而POPF指令可用于把已经被修改过的标志内容放入EFLAGS寄存器中。
任务切换、POPF 和 IRET 指令会加载EFLAGS寄存器。因此,它们可用来修改IF标志。

当通过中断门处理一个中断时,IF标志会被自动清除(复位),从而会禁止可屏蔽硬件中断。但如果是通过陷阱门来处理一个中断,则IF标志不会被复位。

7、异常和中断的优先级

如果在一条指令边界有多个异常或中断等待处理时,处理器会按规定的次序躲他们进行处理。处理器会首先处理最高优先级类的异常或中断。低优先的异常会被丢弃,而低优先级的中断则会保持等待。当中断处理程序返回到产生异常和/或中断的程序或任务时,被丢弃的异常会重新发生。

异常和中断的优先级

优先级说明
1(最高) 硬件复位: RESET
2任务切换陷阱: TSS中设置了T标志
3外部硬件介入
4前一指令陷阱: 断点、调试陷阱异常
5外部中断: NMI中断、可屏蔽硬件中断
6代码断点错误
7取下一条指令错误: 违反代码段限长、代码页错误
8下一条指令译码错误: 指令长度>15字节、无效操作码、协处理器不存在
9(最低) 执行指令错误: 溢出、边界检查、无效TSS、段不存在、堆栈错误、一般保护、数据页、对其检查、浮点异常。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值