重学操作系统(十一)
前言
上一个文章讲了内核(Kernel)相关的一些内容,也讲了Linux和Windows系统结构的不同之处。我们可以知道,操作系统分为内核空间和用户空间。比如微内核和宏内核的区别,在于很多功能是否在内核空间内运行。这一章节主要是讲一下内核态(Kernel Mode)和用户态(User Mode)一些知识点。
贯穿全文的一道题:用户态线程和内核态线程有什么区别?
用户态(User Mode)和内核态(Kernel Mode)
内核(Kernel)运行在超级权限模式(Supervisor Mode)下,拥有很高的权限。按照权限管理原则,多数程序应该运行在最小权限下,因此内存被分为两个部分:
- 内核空间(Kernel Space):这个空间只有内核程序可以访问
- 用户空间(User Space):这个空间给应用程序使用
用户态和内核态
用户空间中的代码被限制了只能使用一个局部的内存空间,这些程序在用户态执行。内核态的代码可以访问所有内存,这些程序可以被看作在内核态执行。
系统的调用过程
用户态程序需要执行系统调用,就需要切换到内核态执行。
图片来源于: @Tutorialspoint
用户态到内核态
上图可以看出用户态程序进行系统调用时的流程:
当用户态程序执行系统调用时,因为系统调用中牵扯特权指令,用户态程序权限不足,因此会中断执行,也就是Trap(Trap是一种中断)。
中断后,当前CPU执行的程序会中断,跳转到中断处理程序。内核程序开始执行,处理系统调用。
内核处理完成后,主动触发Trap,这样会再次发生中断,并切换回用户态继续执行。
线程模型
进程和线程
一个应用程序启动后,会在内存中创建一个执行副本,这就是进程。Linux进程是宏内核(Monolithic Kernel),因此可以看作一个进程。开机时,磁盘的内核镜像被导入内存作为一个执行副本,成为内核进程。
进程可被分为用户态进程和内核态进程。用户态进程通常是应用程序的副本,内核态进程就是内核本身的进程。如果用户态进程需要申请资源,比如内存,可以通过系统调用向内核申请。
对于现代操作系统,执行程序的最小单位不是进程(Process),而是线程(Thread),因此,线程也可以称作是轻量级进程(Light Weighted Process)。
一个进程可以拥有多个线程。进程创建时,一般会有一个主线程随之创建。
进程可以通过API创建用户态线程,也可以通过系统调用,创建内核态线程。
用户态线程
用户态线程也称作用户级线程(User Level Thread)。操作系统内核并不知到它的存在,它完全在用户空间内创建。它的优势包括:
- 管理开销小:创建和销毁不需要系统调用
- 切换成本低:用户空间程序可以自己维护,不需要走操作系统调用
它的缺点是:
- 与内核协作成本高:比如类似Linux的用户态线程,如果进行I/O通信,就需要进行系统调用,因此用户态线程就需要额外成本运行。
- 线程间协作成本高:两个线程需要通信的话,需要I/O,因此需要系统调用,需要额外的成本。
- 无法利用多核优势:操作系统调度的仍然是这个线程所属的进程,所以无论每次一个进程有多少用户态线程,都只能并发执行一个线程,因此一个进程的多个线程无法利用多核优势。
- 操作系统无法针对线程调度进行优化:当一个进程的一个用户态线程阻塞了(Blocked),操作系统无法及时发现和处理阻塞问题,它不会更换执行其它线程,从而造成资源浪费。
内核态线程
内核态线程也称作内核级线程(Kernel Level Thread)。这个线程执行在内核态,可以通过系统调用创建一个内核级线程。
内核级线程的优势是:
- 可以利用多核CPU优势:内核拥有较高权限,因此可以在多个CPU核心上执行内核线程。
- 操作系统级优化:内核中的线程操作I/O不需要进行系统调用,一个内核级线程阻塞,可以立即让另一个执行。
它的缺点是:
- 创建成本高:创建时需要系统调用,需要切换到内核态。
- 扩展性差:由一个内核程序管理,不可能数量太多。
- 切换成本高:切换的时候,同样需要内核操作,需要切换内核态。
用户态线程和内核态线程之间的映射关系
用户态线程创建成本低,但是不能利用多核优势。而内核态线程,可以利用多核优势,但是创建成本高,切换速度慢。因此通常我们会在内核中预先创建一些线程,并反复利用这些线程。
多对一(Many to One)
用户态进程中的多个线程复用一个内核态线程。这样极大减少了创建内核态线程的成本,但是对于一个用户态进程,它的线程不可并发。因此这个模式用的很少。
Q:用户态线程怎么用内核态线程执行程序?
A:程序是存储在内存中的指令,用户态线程可以准备好程序让内核态线程执行。其它映射方式也是利用这种方法。
图片来源于: @拉钩教育
用户线程-内核线程:多对一
一对一(One to One)
该模型为每个用户态线程分配一个单独的内核线程,在这种情况下,每个用户态都需要通过系统调用创建并绑定一个内核线程进行执行。这种模型允许所有线程并发执行,能够充分利用多核优势,Windows NT就是这种模式。但是因为线程多,对内核调度的压力会明显增加。
图片来源于: @拉钩教育
用户线程-内核线程:一对一
多对多(Many To Many)
这种模式是n个用户态线程分配m个内核态线程,m通常可以小于n。一种可行的策略是将m设置为核数。这种模式减少了内核线程,同时保证了多核并发。Linux目前采用这种模式。
图片来源于: @拉钩教育
用户线程-内核线程:多对多
两层设计(Two Level)
这种模式混合了一对一和多对多的特点,多数用户态线程和内核线程是n对m的关系,少量用户态线程和内核线程是1对1的关系。
图片来源于: @拉钩教育
用户线程-内核线程:两层设计
总结
通过这个章节,深刻理解到了用户态,内核态,用户线程,内核线程,以及它们的对应关系。之前接触最多的应该是Java多线程。感觉其实对于Java的多线程,设计思想也逃脱不出去这几种映射的设计思想。这应该就是所谓的,开发语言在底层,其实是殊途同归的,因此很多公司,接受转语言的应聘者。从此更加体现出底层知识的重要性。
Q: JVM 的线程是用户态线程还是内核态线程?
A: JVM自己本身有一个线程模型。在JDK1.1时候,JVM自己管理用户级线程。这样做,操作系统只调度内核级线程。用户级线程相当于基于操作系统分配到进程主线程的时间片,再次拆分,因此无法利用多核优势。
后来Java改用了线程映射模型,因此需要操作系统支持。Windows上是1对1的模型,Linux是n对m。映射关系操作系统自动完成。Linux的PThreadAPI用来创建用户级线程,KThreadAPI用来创建内核级线程。
参考
[1] https://kaiwu.lagou.com/course/courseInfo.htm?courseId=478#/detail/pc?id=4621