重学操作系统(十一).md

重学操作系统(十一)

前言

上一个文章讲了内核(Kernel)相关的一些内容,也讲了Linux和Windows系统结构的不同之处。我们可以知道,操作系统分为内核空间和用户空间。比如微内核和宏内核的区别,在于很多功能是否在内核空间内运行。这一章节主要是讲一下内核态(Kernel Mode)和用户态(User Mode)一些知识点。


贯穿全文的一道题:用户态线程和内核态线程有什么区别?


用户态(User Mode)和内核态(Kernel Mode)

内核(Kernel)运行在超级权限模式(Supervisor Mode)下,拥有很高的权限。按照权限管理原则,多数程序应该运行在最小权限下,因此内存被分为两个部分:

  • 内核空间(Kernel Space):这个空间只有内核程序可以访问
  • 用户空间(User Space):这个空间给应用程序使用

用户态和内核态

用户空间中的代码被限制了只能使用一个局部的内存空间,这些程序在用户态执行。内核态的代码可以访问所有内存,这些程序可以被看作在内核态执行。

系统的调用过程

用户态程序需要执行系统调用,就需要切换到内核态执行。

图片来源于: @Tutorialspoint

用户态到内核态

上图可以看出用户态程序进行系统调用时的流程:

  1. 当用户态程序执行系统调用时,因为系统调用中牵扯特权指令,用户态程序权限不足,因此会中断执行,也就是Trap(Trap是一种中断)。

  2. 中断后,当前CPU执行的程序会中断,跳转到中断处理程序。内核程序开始执行,处理系统调用。

  3. 内核处理完成后,主动触发Trap,这样会再次发生中断,并切换回用户态继续执行。

线程模型

进程和线程

一个应用程序启动后,会在内存中创建一个执行副本,这就是进程。Linux进程是宏内核(Monolithic Kernel),因此可以看作一个进程。开机时,磁盘的内核镜像被导入内存作为一个执行副本,成为内核进程。

进程可被分为用户态进程和内核态进程。用户态进程通常是应用程序的副本,内核态进程就是内核本身的进程。如果用户态进程需要申请资源,比如内存,可以通过系统调用向内核申请。

对于现代操作系统,执行程序的最小单位不是进程(Process),而是线程(Thread),因此,线程也可以称作是轻量级进程(Light Weighted Process)。

一个进程可以拥有多个线程。进程创建时,一般会有一个主线程随之创建。

进程可以通过API创建用户态线程,也可以通过系统调用,创建内核态线程。

用户态线程

用户态线程也称作用户级线程(User Level Thread)。操作系统内核并不知到它的存在,它完全在用户空间内创建。它的优势包括:

  1. 管理开销小:创建和销毁不需要系统调用
  2. 切换成本低:用户空间程序可以自己维护,不需要走操作系统调用

它的缺点是:

  1. 与内核协作成本高:比如类似Linux的用户态线程,如果进行I/O通信,就需要进行系统调用,因此用户态线程就需要额外成本运行。
  2. 线程间协作成本高:两个线程需要通信的话,需要I/O,因此需要系统调用,需要额外的成本。
  3. 无法利用多核优势:操作系统调度的仍然是这个线程所属的进程,所以无论每次一个进程有多少用户态线程,都只能并发执行一个线程,因此一个进程的多个线程无法利用多核优势。
  4. 操作系统无法针对线程调度进行优化:当一个进程的一个用户态线程阻塞了(Blocked),操作系统无法及时发现和处理阻塞问题,它不会更换执行其它线程,从而造成资源浪费。

内核态线程

内核态线程也称作内核级线程(Kernel Level Thread)。这个线程执行在内核态,可以通过系统调用创建一个内核级线程。

内核级线程的优势是:

  1. 可以利用多核CPU优势:内核拥有较高权限,因此可以在多个CPU核心上执行内核线程。
  2. 操作系统级优化:内核中的线程操作I/O不需要进行系统调用,一个内核级线程阻塞,可以立即让另一个执行。

它的缺点是:

  1. 创建成本高:创建时需要系统调用,需要切换到内核态。
  2. 扩展性差:由一个内核程序管理,不可能数量太多。
  3. 切换成本高:切换的时候,同样需要内核操作,需要切换内核态。

用户态线程和内核态线程之间的映射关系

用户态线程创建成本低,但是不能利用多核优势。而内核态线程,可以利用多核优势,但是创建成本高,切换速度慢。因此通常我们会在内核中预先创建一些线程,并反复利用这些线程。

多对一(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

[2] https://www.tutorialspoint.com/User-Mode-vs-Kernel-Mode


 上一篇
重学操作系统(十二) 重学操作系统(十二)
重学操作系统(十二)前言之前提到了用户态进行系统调用会有一个中断的操作(Trap)。今天就来看下中断到底是什么。 在此之前,我们可以回想下。我们每天都会和键盘,鼠标这种外设有互动。可是,Java等语言是怎么捕获到键盘的输入的呢?后面会先讲一
下一篇 
重学操作系统(十) 重学操作系统(十)
重学操作系统(十)前言之前看了很多Linux相关的内容,最后有分析日志和自动化运维的一些知识,有些太深入了,所以只是看了下,跟着做了下练习,没有写成文章。 今天继续,来说一下Windows和Linux的区别。 Windows,Linux,M
  目录