暑假跑出去玩了好几个省份,尤爱云南。
如云如水,水流云在。
彩云之南,人在路上。
其余时间就是在巩固SpringCloud、深入探索已学技术的底层和偶尔给老师的项目修修bug中度过了。
嗯,挺完满的一个暑期,开心~
突然想起还有掘金这个平台,我把这个平台上写的文章定位为我花了好长时间或者被困扰了很长时间才终于弄懂了的知识的输出,下面就是正文啦。
虚拟内存
在进程的运行过程中离不开对内存的分配和使用,那我们申请内存得到的真的是物理内存吗?
实际上并不是的。如果操作的都是物理内存,那对程序员的要求就高上不少了,我们需要处处小心,大脑时刻保持清醒,规划好自己写的代码会被加载到哪个位置,一点不能错乱。
所以,操作系统很细心地为了做好了这种隔离,为每个进程分配独立的一套[虚拟地址],至于虚拟地址最终如何落到物理内存,对进程来说是透明的,操作系统会把这一切安排。
Linux操作系统中,又把内存分为内核空间和用户空间两部分。虽然每个进程都有独立的虚拟内存,但是每个虚拟内存中的内核空间,其实都是直接映射到物理内存的内核空间的,至于虚拟内存中的用户空间,你无需担忧操作系统会帮你做好映射工作。
再来说说为什么要分内核空间和用户空间。
内核空间中存放的都是操作系统最重要的代码,用户空间存放的是普通的程序代码。要避免用户程序能够肆意地在内核空间干“坏事”,所以用户程序要进行一些必须去读写内核空间的“危险”操作时,它不能直接干,只能委托给操作系统这个我们信得过的管家去做。
用户态和内核态
相信你肯定听说过操作系统的内核态和用户态,用户进程都在用户态这个特权级,而有时程序想要做一些读写内核空间的“危险”操作时,就需要请求操作系统在内核态特权级下执行。
在汇编层次来看,执行内核空间的代码,无非就是jmp、call和中断。如果是短跳转,那不涉及段的变化,也就无需特权级检查这回事,但如果是长跳转,就要跳到另一个段,需要特权级检查。
特权级的检查要复杂的多,这里就举个例子简单阐述下原理。cs寄存器中存放了CPL,也就是当前cpu所处的特权级,每个段有自己的段特权级dpl,在大多数情况下,必须要求cpl等于dpl才能跳转成功。
以上是代码段的跳转所做的特权级检查,访问数据段时也会有对应的检查。
总的来说,cpu处于用户态时只可以访问用户空间,处于内核态时可以访问用户空间和内核空间。
进程和线程
“进程是分配资源的基本单位,线程是执行的基本单位。”
在技术圈和互联网上,对进程和线程的讨论大多聚焦于两者的不同之处,但事实上两者的相同点要大于不同点,在Linux中,都是用task_struct来表示,区别仅仅是task_struct中的虚拟空间、文件系统、打开文件列表是独立的还是共享的,最主要的区分点就是在于看它是否有独立的地址空间。
可以理解为,进程是一个载体它开辟了虚拟空间,而进程内的线程们就共享了虚拟空间便于共事。
用户线程与内核线程
好了,前面铺垫了那么多,终于到本文的主角了——用户线程和内核线程。
在Linux中, 我们接触最多的莫过于用户线程,因为他们太活跃了,也太耀眼了以至于我们感受不到内核线程的存在,但是内核线程却在背后默默地付出着,如内存回收,脏页回写,处理大量的软中断等,如果没有内核线程那么linux世界是那么的可怕!
其实,内核线程并没有什么特别神秘的地方,它和普通的用户任务一样参与系统调度,也可以被迁移到任何cpu上运行,但内核线程只能在内核态运行,使用的是所有进程共享的内核空间。由于内核线程运行于内核态,所有它的权限很高,可以直接做到操作页表,维护cache, 读写系统寄存器等操作。
用户线程运行在用户态下,只能够访问用户空间,权限较低,有权限高的操作必须委托给操作系统完成。
协程
我们知道线程是进程中的执行体,拥有一个执行入口,以及分配到的用户栈和内核栈,而线程获得cpu时间片后才可以执行,cpu的栈基,栈指针、指令指针等寄存器都要切换到对应线程。
那如果线程又给自己创建几个执行体,给它们各自划定执行入口,申请一些内存给它们用作栈,那线程就可以按需调度这几个执行体了,为了管理这些执行体,线程也需要记录它们的执行入口,标识符,栈位置,这样就可以从上次中断的地方继续执行,这些由线程创建的执行体就是所谓的协程。
因为用户程序不能操作内核空间,所以只能给协程分配用户栈,而操作系统毫不知情仍把它当做一个线程处理,所以又被称为“用户态”线程
在互联网高速发展的当下,对高并发的要求不断提高,瞬间抵达的海量请求,让多线程之间切换的成本对我们来说越来越不可接受,而协程这种灵活、轻量的用户态调度模型,便得到了广泛关注,而让协程大放异彩的是它与IO多路复用的搭配,助力协程成为高并发解决方案。
协程的本质是线程自己在用户栈又划出了一套“栈”,在一个线程又模拟了多个执行体。
常见的称谓辨析
用户线程和内核线程是一对概念。
用户线程:cpu处于用户态,只能访问用户空间,有“危险”操作必须委托给操作系统的线程。
内核线程:cpu处于内核态,能访问所有空间,由于都是操作系统这个我们可以信息的管家生成的线程,可以执行“危险”操作。
用户态线程是对协程的一个别称,没有内核态线程这一说法。
其实我不太赞成把协程称为用户态线程这一说法,协程就是协程,不用把它的名字和线程混一起。很容易让人把它与用户线程迷糊,笔者就被困扰了起码半年之久,写下本文的动力也是最近弄懂了这两者的区别。
全文总结
好了,终于可以做个总结了。
为了保护计算机的重要资源,以及各个进程间互不干扰,操作系统这个管家给每个进程划分了一套独立的虚拟空间分为用户空间和内核空间具体映射由操作系统完成,而涉及计算机重要资源的代码都被放在内核空间,普通代码放在用户空间。
cpu和内存提供了这样一套机制:cpu有内核态和用户态之分,内存的段有自己的特权级要求,cpu的级别必须高于要跳转的段才能跳转成功,内核态才能访问所有空间,用户态只能访问用户空间
用户线程:cpu处于用户态,只能访问用户空间,有“危险”操作必须委托给操作系统的线程。
内核线程:cpu处于内核态,能访问所有空间,由于都是操作系统这个我们可以信息的管家生成的线程,可以执行“危险”操作。
协程:线程自己在用户栈又划出了一套“栈”,在一个线程又模拟了多个执行体。又被称为用户态线程
引用
深入理解Linux内核之内核线程(上) - 知乎 (zhihu.com)
【协程第二话】协程和IO多路复用更配哦~_哔哩哔哩_bilibili
Linux源码趣读/闪客著. ——北京:电子工业出版社,2023.9
深入理解Linux进程与内存/张彦飞著. ——北京:电子工业出版社2024.7