操作系统 Chap.2 CPU模式 / OS服务
本篇博文我们将更加详细地介绍关于CPU不同模式的实现方式, 以及操作系统的系统服务
1. CPU模式(CPU Mode)
在上篇博文中我们提及过, CPU有两种广为人知的模式:
- 内核模式(Kernel Mode): 也称为管态(Manager Mode), CPU必须处于此模式才能执行内核代码
- 用户模式(User Mode): 也称为目态(Usual Mode), CPU通常在这个模式上执行用户代码
1.1 保护环(Protection Ring)
在现在操作系统中, CPU的不同模式是通过 保护环(Protection Ring) 来实现的, 不同等级的保护环代表着不同的权限等级. 通常 保护环等级越低, 权限越高 .
保护环是由上世纪60年代的操作系统: Multics 6400 所引入的概念, 尽管这个操作系统并不成功, 但其引入的许多概念直至今日仍然再为许多操作系统所采用
举例而言, 一个x86架构的CPU, 通常会有以下几种保护环:
- Ring 0: 内核模式
- Ring 1: 系统服务
- Ring 2: 设备驱动
- Ring 3: 用户模式
1.2 有必要吗?
为什么操作系统必须衍生出这么多的环? 有何好处?
从分层角度而言, 衍生出不同权限等级的保护环至少有三种好处:
- Fault Isolation : 即 故障隔离 , 一个在高层环(如Ring 3)的程序崩溃, 不会影响到底层环(如Ring 0)的程序, 同时, 这个故障通常还能够被底层环中的某些代码处理
- Privileged instructions : 即 特权指令 , 我们在第一章提到过这个概念, 通过不同保护环的划分, 操作系统能够更清晰的明确指令的权限等级, 并将其分配给不同等级的环.
- Privileged Memory Space : 即 特权内存 , 不同的保护环有不同的内存访问权限. 这有利于内存的划分.
我们通过几个例子来进一步理解上面的说法:
- 在用户模式下出现的各种问题(Eg. 除0 / 非法访问 / 空指针引用等)能够被内核捕获并抛出异常, 从而不会使得整个系统崩溃.
- 很多特权指令只能通过内核模式来运行, 用户模式通常会抛出异常(Eg. 在x86架构中, 在Ring 3 中执行特权指令会引发异常: GP (General Protection) exceptions)
- 某些内存仅有内核模式能够读写(Eg: 在上篇博文中提及的, 用于Multi programming的进程列表等)
我们需要指出, 除Kernel Mode和User Mode外, 还有两个概念: Real Mode 和 Protect Mode , 这两个模式与前两者并不相同, Real Mode 模式下, 并没有保护环的概念, 所有指令都可以执行, 所有内存都可以访问, 它通常被用于早期计算机中; 而Protect Mode 模式下, 引入了保护环机制, 是现代操作系统的基础.
1.3 模式切换(Mode Switch)
如果特权指令的限制如此严格(甚至I/O都是特权指令), 那么用户程序如何能够与硬件进行交互?
通常, 用户程序通过 系统调用(System Call) 来进行模式切换, 进而调用特权指令.
举例而言, printf函数的执行过程:
printf libc call(Ring 3) => write system call => Kernel Mode(Ring 0)
Root & UnRoot?
明确指出, Root这个概念与Kernel Mode并不是一个概念!
Root 通常指的是一个具有超级权限的用户, 他可以访问所有文件; 而未获得Root权限的用户只能访问系统中的部分文件.
但不论是否是Root用户, CPU都将处于User Mode, 当需要执行特权指令时, 仍然需要通过上文所述的方式进入内核模式.
1.4 保护环 & 虚拟化(Ring & Virtualization)
在介绍本部分之前, 我们需要先引入一个概念: HyperVisor.
A Hypervisor is a Virtual Machine Monitor (VMM) that runs and manages virtual machines
Hypervisor是一个虚拟机监控设备, 负责运行 / 管理全部虚拟机.
因此理论上, HyperVisor所处的保护环层级必须要比虚拟机的操作系统(VM Kernel)要低, 这样其才能控制虚拟机, 或捕获由于虚拟机所产生的异常.
那放哪呢?
传统的方式是 HyperVisor运行在Ring 0, 将 VM Kernel上移到 Ring 1 . 当虚拟机需要运行特权指令时, 通过陷阱(Trap)机制, 由HyperVisor捕获并执行.
这种方式的代价是运行效率的降低, 因为这种陷阱机制需要二进制转译, 额外增加了模式切换的开销.
为了解决这个问题, 在2005 / 2006年, Intel和AMD分别推出了一个额外的保护环层级: Ring -1, 这使得虚拟化有了新的构建方式: 将HyperVisor单独运行在Ring -1, 将VM Kernel运行在Ring 0.
这样做的好处在于, 虚拟机系统本身仍处于Ring 0, 因此它与VMM的通信无需进行二进制转译, 这大幅度提高了运行效率.
2. 操作系统相关服务(Operating System Services)
Operating systems provide an environment for execution of programs and services to programs and users.
操作系统为程序的正确执行提供相关环境, 并为程序及用户提供相对应的服务.
2.1 用户服务
- 用户界面(User Interface / UI)
- Command Line(CLI)
- Graphics User Interface(GUI)
- Batch
- 程序执行(Program Excution)
- 向内存中装载程序
- 提供中断服务(正常中断 / 异常)
- I/O操作(I/O Operations)
- 操作文件系统(File-System Manipulation)
- 增删改查
- 信息交换(Communications)
- 通过共享内存或消息传递管道
- 错误检测(Error Detection)
2.2 提升运行效率的服务
- 合理的资源分配(Resource Allocation)
- 处理器周期
- 主存
- 文件储存
- 审计 / 统计(Accounting)
- 跟踪当前所有的用户, 即他们在使用多少 / 如何使用当前的计算机资源
- 保护 / 安全事项
- 保证当前所有访问系统的操作均处于控制下
2.3 系统调用(System Calls)
本质上而言, 是操作系统为程序提供服务的接口. 通常使用高级语言(C / C++)书写.
当然, 通常会通过更高层的 应用程序接口(Application Programming Interface / API) 进行间接调用, 直接进行调用的情况较少.
典型的案例如:
- Win32 API(Windows)
- POSIX API(POSIX based systems, UNIX / Linux / Mac OS X)
- Java API(Java Virtual Machine)
2.3.1 系统调用的实现
正常情况下, 系统会给出对每个系统调用给一个单独的数字来标识它. 系统调用接口(System-call Interface) 维护着这样的一个索引表.
当应用程序接口(API)向系统调用接口(System call Interface)发出了对应的系统调用请求后, 系统调用接口在内核中调用预期的系统调用服务, 并返回执行状态(或任何返回值).
这样设计的目的在于简化程序设计的流程, 程序设计者不需要知道系统调用的具体细节, 只需要遵守API的规则并得知内核会返回什么东西即可.
2.3.2 系统调用的信息 / 参数传递(Parameter Passing)
通常而言, 应用进行系统调用时, 除了索引表中的索引外, 还需要传递其余不少参数.
这种参数传递通常有三种方法:
- 直接通过寄存器传递参数(Through registers)
- 将参数存储在块或表中, 并通过寄存器将指定块 / 表的地址传入(Address Passing)
- 将参数入栈, 操作系统将自行将栈中的参数弹出.(Through Stack)
2.3.3 系统调用提供的服务
总体而言, 系统调用提供的服务与上文中操作系统所提供的服务高度重合. (废话, 不高度重合怎么实现操作系统的功能)
可以通过 starce / ltrace 来跟踪系统调用(System Call) / 库调用(Library Call)
本章节主要对CPU的不同模式以及操作系统服务(尤其是系统调用)进行了说明.
这篇博文就到这里~