本篇学习笔记围绕用户态、内核态;用户空间、内核空间;用户线程、内核线程进行连贯、细致的阐述
kernel mode & user mode
什么是内核态和用户态?
所谓内核态和用户态,本质上是权限上的划分。通常来说,像内存、磁盘这些底层硬件的使用权限是应该受控制的。如果所有程序都能轻易访问并操作它们,那么一旦发生崩溃,那是灾难性的结果。
x86 CPU提供了**protection rings**, 如下所示,**Ring 0**代表着内核态;**Ring 3**代表用户态。
至于Ring 1、2这些非本次讨论重点,它们主要是为一些设备驱动程序而设计的,因为它们不需要全部权限,但有需要部分底层硬件的权限,例如键盘、音响等等
运行在内核态和用户态的程序有什么不同?
运行在**Ring 0**,也就是内核态的程序,它们具有对底层硬件完全的、不受限制的使用权限;它们可以执行所有CPU 指令以及使用任意的内存地址
内核态通常是给操作系统最底层、最受信任的函数使用的。
运行在**Ring 3**,也就是用户态的程序,它们没有直接访问硬件或使用任意内存地址的权限;它们必须通过系统调用来访问硬件资源或内存
用户态陷入内核态的开销到底在哪里?
我理解的从用户态陷入内核态,并不意味着程序所拥有的权限提升了。
用户创建的程序都运行在用户态,而不会运行在内核态
CPU运行在用户态是指当前程序拥有CPU资源,并在权限受限的情况下进行执行;而CPU陷入内核态是指由于中断、异常等信号,打断了当前CPU的调度,CPU需要执行相应的(具有特权)处理函数
case:
某程序请求磁盘上的数据,而发起系统调用时,此时假设数据还没有准备好,那么该程序会进入阻塞状态,CPU将调度其他程序到CPU上执行;当数据准备好后,磁盘会触发中断,CPU被中断后,停止当前程序的调度,唤醒最初阻塞的程序,并把磁盘数据拷贝给它。
在这个过程中,发起系统调用(软中断)时会由用户态陷入内核态,发现数据没准备好,则CPU会存储当前上下文,并从运行队列中调度待执行的程序,随后进行上下文切换。 接着返回用户态,执行新的被调度的程序。 当数据准备好后,CPU由于中断触发,会陷入内核态,进行上下文存储和切换,并唤醒最初阻塞的程序,将数据从内核缓冲区拷贝给程序。 接着返回用户态,继续执行程序
由上述case可知,开销随处可见~感觉最主要的开销在于上下文切换,以及可能存在的快表TLB缓存失效的问题,当然中断处理也会导致额外开销
reference
What is the difference between user and kernel modes in operating systems?
Understanding User and Kernel Mode
kernel space & user space
什么是内核空间和用户空间?
这主要是内存布局中的概念。例如在linux内存布局中,将虚拟内存划分为用户空间和内核空间;其中每个进程都有独立的虚拟内存,但是每个虚拟内存中的内核空间,它们都关联相同的物理内存。
当进程拥有CPU并运行在用户态时,只能访问用户空间的内存;
而当CPU(进程)陷入内核态后,才能访问内核空间的内存(也可以访问用户空间)
此处的表述有些奇怪,再次厘清一下,实际上用户创建的程序都是用户态程序,这是出于保护整个系统的权限设置;而那些内核程序,它们是真正能够访问底层资源,拥有完整、没有限制的特权的程序。 进程运行在用户态的表述并不太准确,我理解是
**CPU(而不是进程)**运行在用户态,因此此时用户程序占有CPU,并正在运行;而程序陷入内核态也不准确,应该CPU陷入内核态,例如中断后,陷入内核态,执行一些中断处理函数,它们是具有特权的程序!
结合用户态、内核态来理解用户空间与内核空间
由上所述:只有内核态程序才能访问内核空间;用户态程序只能访问用户空间。 实际上,并不存在程序从用户态进入内核态的情况。这个表象的本质是CPU从用户态进入了内核态,这是因为持有CPU并正在运行的程序发生了变化!
例如:当前运行的用户态程序(持有CPU并正在运行)没有访问底层硬件的权限,需要通过系统调用触发软中断,CPU从用户态进入内核态,将当前持有CPU并运行的程序替换为中断处理函数(内核态程序)等,中断处理函数执行完毕后,CPU再从内核态返回用户态,将控制权交还给原来的进程。
reference
What is the difference between the kernel space and the user space?
user thread &kernel thread
什么是用户线程?什么是内核线程?
这个问题比较复杂,所站角度不同也有不同的理解。
从CPU运行权限的角度来看:
所有用户创建的线程都是用户线程;而像pdflush线程(脏页回写)、kswap线程(支持swap机制)、kcompact线程(用于内存整理)等,这些线程都是内核线程。
用户创建的线程是用户线程;而内核线程是os自己实现的线程。它们之间没有任何的映射关系
那么在该视角下,每个用户线程都有两个栈:用户栈和内核栈;
- 用户栈是在用户态时使用的
- 内核栈是在内核态时使用的,主要用于存储在内核态执行过程中的临时数据和函数调用的上下文信息。
从os调度的角度来看:
调度发生在用户态的线程都是用户线程;调度发生在内核态的线程都是内核线程;
在这个视角下,用户线程和内核线程就有了所谓的映射关系,由前面所述的内核态、用户态、内核空间、用户空间,我们知道在os的视角下,所有线程的调度都发生在内核态。(因为它只能感知到内核态的线程调度); 那用户线程从何而来?比如像Golang中,它实现了自己的调度器,这个调度器是运行在用户态的,因此我们所说的goroutine,它的调度都有用户态调度器完成,在这个视角下,goroutine都是用户线程
那么在该视角下,用户线程和内核线程是有mapping关系的,也就是我们常说的**1 : 1,n : 1,n : m**
并且相较于上述视角的“每个用户线程都有两个栈:用户栈和内核栈”;此处表述变更为“每个用户线程具有一个用户栈,每个内核线程具有一个内核栈” 。实际上是换汤不换药
- 用户栈是在用户线程时使用的。如果有多个用户线程,那么视为将原本一个用户栈划分为多个用户栈。
- 内核栈是在内核线程使用的,主要用于存储在内核态执行过程中的临时数据和函数调用的上下文信息。
1:1线程模型:
例如先前在linux中,我们通过pthread创建的线程就是1:1的关系,它会在堆空间(用户空间)申请一块内存,用来作为线程私有的栈空间(用户栈),然后会通过clone系统调用,在内核创建一个task_struct结构。
os在调度时,调度的实际上是task_struct结构,也就是说它是一个内核线程;在我的理解下,1:1模型实际上是没有用户线程的,它只是为了方便和n : 1、n : m线程模型进行归纳
它没有实现自己的用户态调度器,也没必要实现!但是在用户栈分配的动作上,可以勉强视为这是一个用户线程
n:1线程模型:
在早期时,Golang的调度模型即GM模型,它所做的事情就是将n个用户线程调度到1个内核线程上来执行;它实现了用户态调度器,内核线程会通过用户态调度器,调度用户线程并执行它!
实际上,它还是和1:1线程模型一样,只是在用户态实现了一个调度器(也就是一段程序),将原本的用户栈划分为多个用户栈,供多个用户线程去独立使用。
n:m线程模型:
Golang优化后的GM模型以及GMP模型,都是n : m的线程模型,它所做的事情是将n个用户线程调度到m个内核线程上来执行;它也实现了用户态调度器,此处不展开GM和GMP的差别等。
n:m线程模型的调度器我并没有思考出大概的运行方式,但从1:1模型到n:1线程模型,可以猜测:
用户态调度器屏蔽了本质上用户线程和内核线程的概念,我们从1:1模型以及CPU运行权限视角下的理解:其实它们在被os调度时,本质上都只有一个task_struct,只不过在1:1模型时,由于用户栈(用户空间下的堆内存)的存在,我们将它视为用户线程,并在逻辑上假设了一种用户线程、内核线程的映射关系
而正因为用户态调度器的存在,我们把1:1模型中只有一个的用户栈,划分为多个用户栈,它们也就对应了多个用户线程。
例如将每一个task_struct的用户栈划分为n/m个用户栈,那么每一个内核线程就对应了n/m个用户线程;而m个内核线程,总得来说就能对应n个用户线程!
当然,它们或许不是完全均分的情况,具体每个task_struct的用户栈要怎么分配,这是用户态调度器所做的事情!
reference
Threads: Why must all user threads be mapped to a kernel thread?
Relationship between a kernel and a user thread
User Thread to Kernel Thread mapping in Linux systems
What exactly is a kernel thread and how does it work with processes?