暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

进程管理与调度--现代操作系统线程模型概述

二进制人生 2020-05-07
234

微信公众号:二进制人生
专注于嵌入式linux开发。问题或建议,请发邮件至hjhvictory@163.com。
更新日期:2020/05/07,内容整理自网络,转载请注明出处。

参考:
[1] 操作系统概念第七版中文版
[2] 知乎大牛们的讨论

线程模型线程库实现

线程模型

首先我们需要了解线程(threads)是个什么概念。在传统UNIX中,进程(process,就是Intel所谓的task)是调度的最小单位,复杂的大型软件往往需要有多个进程,fork+exev是很常用的技巧。但是随着需求的扩大,特别是网络服务的复杂性增长,fork的开销就成为一个瓶颈问题。为此产生了vfork和copy-on-write技术,都是为了减小fork的开销。

pthreads的引入也是为了解决fork开销问题,同时能够支持SMP(对称多处理器)。在SMP机器上,一个进程内的多个线程可以分布在各个处理器上并行运行。

由于历史原因,2.5.x以前的linux对pthreads没有提供内核级的支持,所以在linux上的pthreads实现只能采用n:1的方式,也称为库实现。下面先说一下pthreads的3种实现方式。

pthreads的实现有3种方式:
第一,多对一,也就是库实现。模型图如下:


将许多用户级线程映射到一个内核线程。线程管理是由线程库在用户空间进行的,因而效率比较高。这种实现线程对OS来说是不可见的,OS不知道线程的存在与否,也不负责对它们进行调度。所有的此类工作由线程库来完成。但是如果1个线程执行了阻塞系统调用,那么整个进程会阻塞。而且,因为任一时刻只有一个线程能访问内核,多个线程不能并行运行在多处理器上,那么SMP机器的优势完全用不上。



第二种,1:1模式。

1:1模式中,每个线程对应存在一个内核线程。也就是说,OS知道每个线程的生老病死,对它们进行调度。
1:1模式适合CPU密集型的机器。我们知道,进程(线程)在运行中会由于等待某种资源而阻塞,可能是I/O资源,也可能是CPU。该模型在一个线程执行阻塞系统调用时,能允许另一个线程继续执行,所以它提供了比多对一模型更好的井发功能;它也允许多个线程能并行地运行在多处理器系统上。因此1:1的优点就是,能够充分发挥SMP的优势。

这种模式也有它的缺点。由于OS为每个线程建立一个内核线程,导致内核级的内存空间(IA32机器的4G虚存空间的最高1G)的大开销。第二个缺点在于,在传统意义上,在mutex互斥锁和条件变量上的操作要求进入内核态,这是因为OS负责调度,它要为线程的状态转换负全责。后面我们会看到,Linux的NPTL库避免了这个缺点,它的互斥锁与条件变量的操作在用户态完成。

下面是1:1模式的示意图:


第三种,M:N模式。
这种模式试图兼有上面2种模式的优点。它要求一个分层的模型。比方说,某个进程内有4个线程,其中每2个线程对应一个内核线程。这样,OS知道2个实体的存在,负责它们的调度。而在一个实体内有2个线程,这2个线程间的调度就是OS不加干涉、也不知道的了。

简单的说,M:N模型的OS级调度上,跟1:1模型相似;线程间调度上,跟n:1模型相似。

优点是,既可以在进程内利用SMP的优势,又可以节省系统空间内存的消耗,而且环境切换大多在用户态完成。
缺点是显然的:复杂。

下面是M:N模型的示意图:

事实上还存在一个二级模型。
多对多模型的一种变种仍然多路复用多个用户级线程到同样数量或更少数量的内核线程,但也允许绑定某个用户线程到一个内核线程。

  • 1,    Linux上的posix线程库最初实现是n:1模型,就是没有OS支持的库实现。这也是狭义上的“LinuxPthreads”,它支持2.0及以后的Linux。Linux Kernel  Mailing List FAQ上一位Hacker曾大大推崇这种方式,拿来与Solaries的1:1模型相比较并证明这种模型是优秀的。至少拿现在的情况来说他的观点是不正确的。这观点曾让偶糊涂了很久。

  • 2,   IBM公司实现的NGPT。它采用M:N模式,好象与第一种的LinuxPthreads合作工作。

  • 3,   NPTL。1:1模型。今后Linux平台的POSIX线程库事实上的标准实现。
    在2002年8、9月份,一直不肯松劲的Linus Torvalds先生终于被说服了,Ingo Molnar把一些重要特性加入到2.5开发版官方内核中。这些特性大体包括:新的clone系统调用,TLS系统调用,posix线程间信号,exit_group(exit的一个变体),等等。有了OS的支持,Ingo Molnar先生同Ulrich Drepper(glibc的LinuxThreads库的维护者,NPTL的设计者与维护者,现工作于RedHat公司)和其他一些Hackers开始NPTL的完善工作。题外话一句:IBM公司给了他们很大的赞助。

在Linux上,可以用getconf GNU_LIBPTHREAD_VERSION来查看你的posix线程库到底是那一个实现,什么版本。Drepper先生做过测试,据他的测试数据,IA32机器上启动撤销10万个线程,以前的库实现需要15分钟,而NPTL只需要2s。
ubuntu16.04:

root@AI-Machine:~# getconf GNU_LIBPTHREAD_VERSION
NPTL 2.23
root@AI-Machine:~

线程库实现

到这里本该结束了,但是大家可能还不知道linux下的线程是怎么实现的,所以有了这一节。

线程库(thread library)为程序员提供创建和管理线程的API。
实现线程库的主要方法有两种:

第一种方法:在用户空间中提供一个没有内核支持的库。调用库内的一个函数只是导致了用户空间内的一个本地函数的调用,而不是系统调用。
第二种方法:实现由操作系统直接支持的内核级的一个库。调用库中的一个API函数通常会导致对内核的系统调用(在linux里,这个api其实就是clone)。

Linux里,用户代码通过pthread库创建线程的过程虽然看似是用户在创建“用户级线程”,实际上是pthread_create暗中调用了clone系统调用,由操作系统帮忙创建内核级线程的过程,因而不能称作用户级线程。

一般的用户级线程指的是创建、调度和销毁都不经过操作系统,他们的创建与调度由库来实现,操作系统是无法感知多个用户级线程的。

但是在Linux中使用pthread_create创建的“用户线程”准确讲应该叫轻量级进程。每使用pthread_create创建一次轻量级进程,OS都会相应地为应用程序生成一个可供内核调度的实体,暂且称作内核线程吧。这就是前面有人讲的Linux线程管理中的NPTL 1:1模型,即1个轻量级线程对应一个内核级线程。

关于进程、用户级线程(实际上linux没有这个东西)、轻量级进程(对于linux,实际上就是内核线程)、内核线程这若干个概念,后面会有专门文章介绍。

愿你有所收获…

图:二进制人生公众号



文章转载自二进制人生,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论