「操作系统」什么是用户态和内核态?为什么要区分
参考&鸣谢
从根上理解用户态与内核态 程序员阿星
并发编程(二十六)内核态和用户态 Lovely小猫
操作系统之内核态与用户态 fimm
文章目录
- 「操作系统」什么是用户态和内核态?为什么要区分
- 一、用户态和内核态概述
- 二、C P U 指令集权限
- 三、用户态与内核态的空间
- 四、用户态与内核态的切换
一、用户态和内核态概述
简单来说内核态就是操作系统运行线程,用户态就是线程执行用户自己的程序。
用户态:
- 不能直接使用系统资源,也不能改变 CPU 的工作状态,并且只能访问这个用户程序自己的存储空间!
内核态:
- 系统中既有操作系统的程序,也有普通用户程序。为了安全性和稳定性,操作系统的程序不能随便访问,这就是内核态。即需要执行操作系统的程序就必须转换到内核态才能执行
- 内核态可以使用计算机所有的硬件资源
为什么要区分用户态和内核态?
- 在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。如果所有的程序都能使用这些指令,那么你的系统一天死机N回就不足为奇了。
- 所以,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 指令集
设置了权限,不同级别权限能使用的 C P U 指令集
是有限的,以 Intel C P U 为例,Inter把 C P U 指令集
操作的权限由高到低划为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
的内核空间是被所有进程共享的!
最后做个小结,我们通过指令集权限区分用户态和内核态,还限制了内存资源的使用,操作系统为用户态与内核态划分了两块内存空间,给它们对应的指令集使用
四、用户态与内核态的切换
相信大家都听过这样的话「用户态和内核态切换的开销大」,但是它的开销大在那里呢?简单点来说有下面几点
- 保留用户态现场(上下文、寄存器、用户栈等)
- 复制用户态参数,用户栈切到内核栈,进入内核态
- 额外的检查(因为内核代码对用户不信任)
- 执行内核态代码
- 复制内核态代码执行结果,回到用户态
- 恢复用户态现场(上下文、寄存器、用户栈等)
实际上操作系统会比上述的更复杂,这里只是个大概,我们可以发现一次切换经历了**「用户态 -> 内核态 -> 用户态」**。
用户态要主动切换到内核态,那必须要有入口才行,实际上内核态是提供了统一的入口,下面是Linux整体架构图
从上图我们可以看出来通过系统调用将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 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。