用户态与内核态 -- 帮你解惑,直达本质

CPU 指令集权限

1. 了解下 CPU 指令集

指令集是 CPU 实现软件指挥硬件执行工作的媒介,具体来说每一条汇编语句都对应了一条 CPU 指令,CPU 指令不止一条的,而是由非常非常多的 CPU 指令集合在一起,组成了一个、甚至多个的集合,每个指令的集合叫:CPU 指令集。随着时间的推移,CPU 指令越来越多,从最早的几十条,到现在的几千条

Inter、AMD CPU 都是复杂指令集 CPU,支持的 CPU 指令多达几千个,每一个指令都指代一种数学运算,比如加减乘除4种数学计算在 CPU 中就是 4条 CPU 指令,以此类推,大家脑补下

CPU 指令集发展到现在有很多的,典型的 Intel CPU 支持:EM64T, MMX, SSE, SSE2 ,SSE3,SSE4A, SSE4.1, SSE4.2, AVX, AVX2, AVX-512, VMX 等指令集

这些指令集中,每个 CPU 指令都有唯一的、不重复的指令编号,CPU 硬件中的 控制单元 可以解析、识别程序想要执行的 CPU 指令的编号,然后指挥相关硬件执行相关操作,所以 汇编语言 被视为是最底层的编程语言

一般认为:CPU 硬件直接支持的数学计算公式执行效率最高。就是因为 CPU 指令集中包含这个数学公式,数学计算只需要一条指令就可以执行完毕。反之就需要通过多条 CPU 指令以组合的方式完成数学计算,需要执行多条 CPU 指令,在空间和时间复杂度上都会复杂 N 倍。所以请大家理解 CPU 执行效率高低的来源,当然指令集只是制约 CPU 执行效率高低的其中一个因素,但是该因素绝对是重量级的、起决定性的因素

2. CPU 指令权限分级

CPU 指令也是有权限分级的 --> 大家试想,CPU 指令是可以直接操作硬件的,要是因为指令操作的不规范,造成的错误是会影响整个 计算机系统 的。好比你写 一个程序,但是因为你对 硬件操作 不熟悉,出现问题,那么影响范围是多大?是整个计算机系统,操作系统内核、及其其他所有正在运行的程序,都会因为你操作失误而受到不可挽回的错误,那么你只有重启整个计算机才行

而对于 硬件的操作 是非常复杂的,参数众多,出问题的几率相当大,必须及其谨慎的进行操作,这对于个人开发者来说是个艰巨的任务,同时个人开发者在这方面也是不被信任的。所以 操作系统内核 直接屏蔽了个人开发者对于硬件操作的可能,我都不让你能碰到这些 CPU 指令,你还能搞个大新闻出来嘛~

这方面 系统内核 对 硬件操作 进行了封装处理,对外提供标准函数库,操作更简单、更安全。比如 我们要打开一个文件,C标准函数库中对应的是fopen(),其内部封装的是内核中的系统函数open()

因为这个需求,硬件设备商直接提供了硬件级别的支持,做法就是对 CPU 指令设置了 权限,不同级别的权限可以使用的 CPU 指令是有限制的。以 Inter CPU 为例,Inter 把 CPU 指令操作的权限划为4级:

  • ring 0
  • ring 1
  • ring 2
  • ring 3

其中 ring 0 权限最高,可以使用所有 CPU 指令,ring 3 权限最低,仅能使用常规 CPU 指令,这个级别的权限不能使用访问硬件资源的指令,比如 IO 读写、网卡访问、申请内存都不行,都没有权限

Linux 系统内核采用了:ring 0 和 ring 3 这2个权限

  • ring 0 被叫做 内核态,完全在 操作系统内核 中运行,由专门的 内核线程 在 CPU 中执行其任务
  • ring 3 被叫做 用户态,在 应用程序 中运行,由 用户线程 在 CPU 中执行其任务

内核线程、用户线程 下面会说,Linux 系统中所有对硬件资源的操作都必须在 内核态 状态下执行,比如 IO 的读写,网络的操作

用户态、内核态

1. 用户态、内核态是什么

大家在学习 操作系统原理、多线程 时肯定听说过这个:系统中断会造成 CPU 从用户态 --> 内核态 的切换 ,虽然也会有一些解释,但是我想应该有很多人其实不明白 用户态、内核态到底是个什么东东

用户态、内核态是什么呢?其实不是多复杂。用户态、内核态的概念就是指 CPU 指令权限的区别,你要在应用程序中读写 IO,那么就必然会用到 ring 0 级别的 CPU 指令,而应用程序的 CPU 指令权限只有 ring 3,那么就必须到拥有 ring0 权限的系统内核 中去执行这行代码,必然会造成 CPU 从 用户态到内核态 的切换。其表现形式是:代码会从应用程序所在的 用户线程 切换到 内核中的 内核线程 去执行,恩,就是这么回事

关于 用户线程、内核线程 下面会解释,等你看到后面 用户线程、内核线程 的解释,你就彻底明白了,现在还迷糊的,请继续往后看

2. 用户态、内核态在资源上的区别

CPU 指令权限的高低,一样会反映在对资源的操作上,区别就是高 CPU 指令权限可以操作更多、更核心的资源,典型的就是内存和硬件资源

虽然从概念上说:用户态、内核态 只是 CPU 指令权限的区别,但是在 程序、系统内核设计上,必然会有相对应的运行机制来支持的,这体现在2个显著的区别上:

  1. 用户态的代码必须由 用户线程 去执行、内核态的代码必须由 内核线程 去执行
  2. 用户态、内核态 或者说 用户线程、内核线程 可以使用的资源是不同的,尤体现在内存资源上。Linux 内核对每一个进程都会分配 4G 虚拟内存空间地址
  • 用户态: --> 只能操作 0-3G 的内存地址
  • 内核态: --> 0-4G 的内存地址都可以操作,尤其是对 3-4G 的高位地址必须由内核态去操作,因为所有进程的 3-4G 的高位地址使用的都是同一块、专门留给 系统内核 使用的 1G 物理内存

3.所有对 硬件资源、系统内核数据 的访问都必须由内核态去执行

补充一下:

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

3. 系统调用

这里我不详细说 系统调用了

上面说了:硬件资源、系统内核数据 的访问都必须由内核态去执行,这就涉及到一个从用户态到内核态的切换的问题,具体的需要在详细了解 用户线程、内核线程 之后才能理解的清楚

从 用户态到内核态 的切换有3种方式:系统调用、中断、异常,其中系统调用也是使用 中断 实现的,这里主要说下系统调用,下面的操作都需要系统调用到 内核态 中去执行:

  • 进程操作、获取进程信息
  • 文件操作
  • 硬件设备操作
  • 系统内核信息查询、硬件信息查询
  • 通信,进程通信、malloc 申请内存,pipe 开辟管道都是

man syscalls 指令可以看到所有的 Linux 系统内核调用方法

用户线程和内核线程

1. 什么是进程

面试都会问什么是进程,什么是线程吧,一般我们对进程的回答是:系统调度资源的最小或基本单位,怎么理解呢,我想很多人其实不是特别清楚

这点其实一点都不复杂,系统内核中 进程 就是:一段记录专有资源和状态的 task_struct 结构体,就是一个数据结构或者理解为一个存储资源信息的对象,task_struct 结构体

其存储的信息就是这些:

对于 结构体 不理解请看 C 语言语法,很简单,就是 java 中的对象,只不过 结构体 内部只有属性没有方法

这个 task_struct 结构体有个专门的名字:PCB --> PROCESS control block,也叫进程控制块,PCB 数据保存在 进程4G内存虚拟地址中的内核态中,也就是 3-4G 内存这一段内,显然用户态时是无法访问的,想要访问就必须从用户态切换到内核态,而且这么核心的数据直接暴露给用户程序也是很危险的

进程在内核中就是这么个东西,就是一个叫 PCB 的 task_struct 结构体,系统内核中有一个 PCB TABLE 用来存储、调度进程

2. 什么是线程

线程在内核中同 进程 一样,也是一个 task_struct 结构体,线程在 new 出来之后就是把 进程的 PCB 复制一份给自己,然后加上 PCB 没有的 PC程序计数器,SP堆栈,State状态,寄存器 这些信息。线程的 task_struct 结构体 叫做:TCB --> thread control block。线程本质上就是多了一个任务列表,就是 栈帧 这个东西

大家想 线程为什么能共享堆内存资源呢,怎么做到的呢,真的很简单,把 进程资源信息 PCB 复制一份给 自己的 TCB 就行了,所以这里大家体会下

3. 换个角度理解进程、线程

  • 进程 --> 是向系统内核申请资源的最小、基本单位
  • 线程 --> 是竞争 CPU 资源的最小、基本单位

进程 申请的是内存资源,线程 竞争的是 CPU 资源,这个角度理解我觉得就容易多了

4. 内核线程

线程分:内核线程、用户线程,内核线程的 TCB 和 进程 的 PCB 保存在一个位置,都是在 3-4G 的内核地址内存中。系统内核中调度的是 内核线程,不是用户线程

内核线程一样有自己的栈帧,所有ring0的代码都淂切换到 内核线程 来执行

5. 用户线程

有了内核线程,为什么还得设计用户线程呢,不用想就是为了和 ring0,ring3 指令权限对应,为的就是权限责任分明。系统内核操作、硬件操作、安全权限高的代码必须淂在内核中执行,用户不能碰,用户自己的低权限代码在用户自己的线程中执行就好了,这样设计代码分层,扩展性能好,相互不影响

Linux 中默认采用 1:1 线程模型,就是有一个 用户线程,就得在内核中启动一个对应的 内核线程,然后把这个 用户线程 绑定到 内核线程 上。当然还有其他的对应方式,这就得依靠第三方实现方案了。C 标准函数库 PThread 线程函数库就是 1:1 线程模型,这点大家想想 Dart 中的线程为啥就能实现资源、内存隔离,肯定就是自己做出修改的

需要说一下 java JVM 采用 Linux 默认函数库,也就是 PThread,1:1 线程模型。java new 一个 Thread 时,是创建了 1个用户线程和内核线程的,然后把用户线程绑定到内线线程中,ring3 的代码在 用户线程中执行,ring0 的代码切换到 内核线程 中去执行,然后使用 内核线程 接受 系统内核的调度,内核线程抢到 CPU 时间片后,用户线程就会激活执行代码

用户线程的 TCB 保存在 进程 0-3G 虚拟地址空间的堆内存中,对于 系统内核 来说是不可见的,所以线程的调度是由 内核线程 来承担、处理

6. 用户线程和内核线程的切换

现在知道了吧,用户线程和内核线程的区别就是 TCB 位置的不同,执行的代码权限级别不同,然后内核线程会接受操作系统内核调度单元的调度指令

那么对于 用户线程和内核线程的切换 怎么理解呢?大家就当做2个线程切换就好了,用户线程和内核线程都有自己的栈帧,寄存器值、CPU 加载的缓存 等属于自己线程的数据,用户线程到内核线程的切换一样会涉及这些 线程上下文 的切换,性能损失在这里

一般来说像 IO 操作这些都是淂在 内核线程 中去执行,所以 IO 代码就会涉及到 用户态到内核态的切换,其中性能损失就是线程上下文的切换,重点是 IO 操作会造成频繁的造成 用户态到内核态再到用户态 的切换。优化的重点是减少切换,所以 mmap 很重要。mmap 实现了不用借助 系统中断,不用到 内核态 执行系统函数,直接往硬盘中读写数据,少了用户态到内核态的切换,所以性能好,效率高

好了就这么多,这么理解就行了,不用想的太复杂,就是这么回事。用户态=用户线程,内核态=内核线程,2者的区别就是对内存操作的范围有区别,对指令的操作权限有区别

7. JVM 线程

看完上面,一定会有人问 JVM 创建出来的线程详细是怎么回事

系统内核创建、调度的一定是内核线程,但是在 用户程序 中使用的一定也必须是用户线程,这点从安全和内核隔离来说是必须的。通用操作系统设计中探讨了 内核线程和用户线程 的捆绑关系,有3种方案:

  • 1:N
  • N:N
  • 1:1

Linux 内核默认使用了 1:1 模型,但是并不是说不可以更改的。Linux 内核为对外接口统一设计了 Posix 规范,接口是固定的,具体怎么实现看更上层的设计了,语言级别的 协程 就是在这个基础上设计的

JVM 方面的 用户线程 设计,我找到了比较通俗易懂的解释:

Java 用户线程 与 Linux 内核线程 的映射关系从 Linux 内核2.6 开始使用 NPTL (Native POSIX Thread Library)支持,但这时线程本质上还轻量级进程。JVM 为用户程序提供了 创建、同步、调度和管理线程函数来控制用户线程,不需要用户态/核心态切换

注意上面的说辞,JVM 提供了 不依赖、不涉及、不触发 内核线程调度机制的,在 用户态 层面上对 用户线程 进行调度的机制,这样就和 JVM 线程调度机制接上了,知识点之间的跨界衔接这块是真的难,太过细节很多人都忽略了,以至于大家学完很难从整个系统层面来理解

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值