用户态和内核态
用户态和内核态概述
简单来说内核态就是操作系统运行线程,用户态就是线程执行用户自己的程序。
用户态:
- 不能直接使用系统资源,也不能改变 CPU 的工作状态,并且只能访问这个用户程序自己的存储空间!
内核态:
-
系统中既有操作系统的程序,也有普通用户程序。为了安全性和稳定性,操作系统的程序不能随便访问,这就是内核态。即需要执行操作系统的程序就必须转换到内核态才能执行。
-
内核态可以使用计算机所有的硬件资源。
为什么要区分用户态和内核态?
1. 安全性
- 保护系统资源:内核态拥有对所有硬件和系统资源的完全控制权。通过区分用户态和内核态,可以防止用户程序直接访问和修改系统关键数据结构和硬件资源。
- 防止恶意行为:如果没有用户态和内核态的区分,恶意程序可能会直接操作硬件或篡改系统内核,从而危害整个系统的安全。
2. 稳定性
- 防止错误传播:用户态程序运行在受限环境中,不能直接访问硬件和其他进程的内存。即使用户程序发生崩溃或错误,也不会直接影响到系统内核和其他程序的运行。
- 隔离故障:通过用户态和内核态的隔离,可以确保一个程序的错误不会影响到整个系统的稳定性。
3. 控制和管理
- 资源管理:操作系统需要对系统资源(如CPU、内存、I/O设备等)进行统一管理和调度。通过内核态,可以实现对资源的有效控制和分配。
- 系统调用接口:通过系统调用接口,用户程序可以请求内核服务,但这些请求是受控的。内核可以检查和验证这些请求,确保其合法性和安全性。
4. 提高效率
- 中断处理:许多硬件中断(如定时器中断、I/O中断)需要立即响应。这些中断处理程序运行在内核态,可以快速地访问和操作硬件。
- 直接硬件访问:内核态允许直接操作硬件设备,而不需要经过额外的抽象层,从而提高了操作的效率。
CPU 指令集权限
指令集是 C P U 实现软件指挥硬件执行的媒介,具体来说每一条汇编语句都对应了一条 C P U 指令,而非常非常多的 C P U 指令 在一起,可以组成一个、甚至多个集合,指令的集合叫 C P U 指令集。
同时 C P U 指令集 有权限分级,大家试想,C P U 指令集 可以直接操作硬件的,要是因为指令操作的不规范`,造成的错误会影响整个计算机系统的。好比你写程序,因为对硬件操作不熟悉,导致操作系统内核、及其他所有正在运行的程序,都可能会因为操作失误而受到不可挽回的错误,最后只能重启计算机才行。
而对于硬件的操作是非常复杂的,参数众多,出问题的几率相当大,必须谨慎的进行操作,对开发人员来说是个艰巨的任务,还会增加负担,同时开发人员在这方面也不被信任,所以操作系统内核直接屏蔽开发人员对硬件操作的可能,都不让你碰到这些 C P U 指令集。
针对上面的需求,硬件设备商直接提供硬件级别的支持,做法就是对 C P U 指令集设置了权限,不同级别权限能使用的 C P U 指令集 是有限的,以 Intel C P U 为例,Inter把 CPU 指令集 操作的权限由高到低划为4级:
- ring 0
- ring 1
- ring 2
- ring 3
其中 ring 0 权限最高,可以使用所有 C P U 指令集,ring 3 权限最低,仅能使用常规 C P U 指令集,不能使用操作硬件资源的 C P U 指令集,比如 I O 读写、网卡访问、申请内存都不行,Linux系统仅采用ring 0 和 ring 3 这2个权限。
高情商
ring 0被叫做内核态,完全在操作系统内核中运行
ring 3被叫做用户态,在应用程序中运行
低情商
执行内核空间的代码,具有ring 0保护级别,有对硬件的所有操作权限,可以执行所有C P U 指令集,访问任意地址的内存,在内核模式下的任何异常都是灾难性的,将会导致整台机器停机。
在用户模式下,具有ring 3保护级别,代码没有对硬件的直接控制权限,也不能直接访问地址的内存,程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存,在这种保护模式下,即时程序发生崩溃也是可以恢复的,在电脑上大部分程序都是在,用户模式下运行的。
用户态与内核态的空间
在内存资源上的使用,操作系统对用户态与内核态也做了限制,每个进程创建都会分配「虚拟空间地址」,以Linux32位操作系统为例,它的寻址空间范围是 4G(2的32次方),而操作系统会把虚拟控制地址划分为两部分,一部分为内核空间,另一部分为用户空间,高位的 1G(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,而低位的 3G(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用。
- 用户态:只能操作 0-3G 范围的低位虚拟空间地址
- 内核态:0-4G 范围的虚拟空间地址都可以操作,尤其是对 3-4G 范围的高位虚拟空间地址必须由内核态去操作
- 补充:3G-4G 部分大家是共享的(指所有进程的内核态逻辑地址是共享同一块内存地址),是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。每个进程的 4G 虚拟空间地址,高位 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用,换句话说就是, 高位 1G 的内核空间是被所有进程共享的!
最后做个小结,我们通过指令集权限区分用户态和内核态,还限制了内存资源的使用,操作系统为用户态与内核态划分了两块内存空间,给它们对应的指令集使用
用户态与内核态的栗子
用户态:应用程序请求读取文件
1 | int main() { |
系统调用:从用户态到内核态
- 当应用程序调用
open()
,read()
和close()
函数时,这些函数实际上是对内核提供的系统调用的封装。 - 例如,当
read()
被调用时,用户程序会发出一个系统调用陷入(trap)指令,触发从用户态到内核态的切换。read
函数会执行一个系统调用指令(通常是syscall
或int 0x80
),这是一种特殊的 CPU 指令,用于从用户态切换到内核态。- 系统调用指令会引发一个陷入(trap),这是一种同步的中断,触发 CPU 切换到内核态,并跳转到内核中的系统调用处理入口。
内核态:操作系统处理请求
-
内核接管控制权,进入内核态。CPU 跳转到内核的系统调用处理程序,根据系统调用号(每个系统调用都有一个唯一的编号,
read
对应的编号通常是 0)找到相应的内核函数(sys_read)。内核中的文件系统代码开始执行,处理文件读取请求。 -
内核验证文件描述符,检查访问权限,然后执行实际的文件读取操作,将数据从磁盘读取到内核缓冲区。
从内核态返回用户态
- 文件读取操作完成后,内核将读取的数据复制到用户提供的缓冲区(
buffer
)。 - 内核返回结果,控制权交还给用户程序【通过
iret
或sysret
指令返回用户态】,此时切换回用户态。
用户态:处理读取的数据
- 用户程序继续执行,处理从文件中读取的数据(如
printf()
打印数据)。
1 | 用户态: 应用程序(用户代码) -> 内核态: 操作系统(系统调用处理) |
用户态与内核态的切换
- 保留用户态现场(上下文、寄存器、用户栈等)
- 复制用户态参数,用户栈切到内核栈,进入内核态
- 额外的检查(因为内核代码对用户不信任)
- 执行内核态代码
- 复制内核态代码执行结果,回到用户态
- 恢复用户态现场(上下文、寄存器、用户栈等)
从上图我们可以看出来通过系统调用将Linux整个体系分为用户态和内核态,为了使应用程序访问到内核的资源,如CPU、内存、I/O,内核必须提供一组通用的访问接口,这些接口就叫系统调用。
库函数就是屏蔽这些复杂的底层实现细节,减轻程序员的负担,从而更加关注上层的逻辑实现,它对系统调用进行封装,提供简单的基本接口给程序员。
Shell顾名思义,就是外壳的意思,就好像把内核包裹起来的外壳,它是一种特殊的应用程序,俗称命令行。Shell也是可编程的,它有标准的Shell语法,符合其语法的文本叫Shell脚本,很多人都会用Shell脚本实现一些常用的功能,可以提高工作效率。
最后来说说,什么情况会导致用户态到内核态切换
系统调用:用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用,系统调用的机制核心使用了操作系统为用户特别开放的一个中断来实现,如Linux 的 int 80h 中断,也可以称为软中断。
异常:当 C P U 在执行用户态的进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常。
中断:当 C P U 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 C P U 发出相应的中断信号,这时 C P U 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。