os 4 线程

发布于
os

线程类型

  • 内核线程

创建 / 撤消由内核内部需求决定,不需和用户进程联系起来

共享内核全局数据,具有自己的内核堆栈

能单独被调度且用标准的内核同步机制,可被单独分配到一个处理器上运行

调度不需要经过态的转换并进行地址空间的重新映射,内核线程间上下文切换比在进程间做上下文切换快得多

  • 轻量级进程 LWP

内核支持的用户线程,在一个单独的进程中提供多线程控制

这些轻量级进程被单独的调度,可以在多个处理器上运行,每一个轻量级进程都被绑定在一个内核线程上

独立调度并且共享地址空间和进程中的其它资源

但每个 LWP 都应该有自己的程序计数器、寄存器集合、核心栈和用户栈

  • 用户线程

如通过线程库实现,提供同步和调度的方法

可在没有内核参与下创建、释放和管理,不消耗内核资源,省去大量系统开销

用户线程的上下文可以在没有内核干预的情况下保存和恢复

每个用户线程都可以有自己的用户堆栈,一块用来保存用户级寄存器上下文以及如信号屏蔽等状态信息的内存区

库通过保存当前线程的堆栈和寄存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下文切换

内核仍然负责进程的切换,因为只有内核具有修改内存管理寄存器的权力

用户线程不是真正的调度实体,内核对它们一无所知,而只是调度用户线程下的进程或者轻量级进程,这些进程再通过线程库函数来调度它们的线程

当一个进程被抢占时,它的所有用户线程都被抢占,当一个用户线程被阻塞时,它会阻塞下面的轻量级进程,如果进程只有一个轻量级进程,则它的所有用户线程都会被阻塞

pthread 实现

  • Linux2.4 前:LinuxThread

  • Linux2.5 后:NPTL


fork && clone

  • 函数: fork,拷贝父进程而创建一个新进程
  • 系统调用:clone,为进程创建提供

fork & pthread_create

fork & pthread_create 都调用的 clone

clone系统调用 ,参数包括如 CLONE_VM, CLONE_FILES, CLONE_SIGHAND 等,指定了克隆时需要拷贝的东西

  • fork

调用 clone 时不设置 CLONE_VM

内核看来就是产生了两个拥有不同内存空间的进程

  • pthread_create

调用 clone 时设置了 CLONE_VM

内核看来就是产生了两个拥有相同内存空间的进程

通过 clone 的参数共享进程资源,创建出的就是 LWP

所以用户态创建一个新线程,内核态就对应生成一个新进程

LinuxThreads

所谓 “库” 实现,用clone系统调用,完全在用户级模拟了线程

与POSIX标准在一致性上面存在大量的问题

  • 信号处理

对一个进程发送一个信号后,只有拥有这个进程号的进程才有反应,而属于这个进程的线程因为拥有不同的进程号而无法做出响应

  • 任务调度

管理线程

管理线程 kill 掉后,某个线程退出后就会出现 Zombie 进程

  • 进程间同步原语(用信号来模拟同步互斥,比如互斥锁)

没有 OS 的支持,线程对 OS 不可见,OS 也不负责对其调度,所有的此类工作由线程库来完成

既然调度不由 OS 进行,SMP机器优势完全用不上。一个进程只能运行在一个CPU上,不管它包含多少线程

2.4内核,用一个内核线程(管理线程)来处理用户态进程中的多个线程的上下文切换(线程切换)

内核中没有线程组的概念,即一个进程的多个线程,必须依靠在pthread库中实现一个额外的线程来管理其他用户线程(即用户程序生成的线程)的建立,退出,资源分配和回收以及线程的切换

当时硬件没有线程寄存器之类来支持多线程

需要在进程的栈中为各线程划分出各自的栈数据所在位置,并且在切换时进行栈数据拷贝

最大问题是内核缺乏对线程间的同步机制的支持,pthread库不得不在底层依靠信号方式来实现同步,线程互斥中的互斥量操作和条件量操作都转换为进程的信号操作

由于内核对线程的无知,必须由管理线程来接收信号后投递给相应的线程,一方面是效率低,另外一方面由于信号产生的不确定性(比如读取一个文件的时候突然出错了),要准确投递所有的信号给正确的线程难以保证

大致过程:新建互斥锁时,在内核里把所有的进程 mask 掉一个特定信号,然后再 kill () 发出一个信号,等某个线程执行锁定时,就用 sigwait () 查看是否有发出的信号,如果没有就等待,有则返回,相当于锁定

解锁时就再 kill () 发出这个信号


NPTL(Native POSIX Thread Library)

创建线程

同样使用 1 * 1 模型,但对应内核的管理结构不再是 LWP。管理进程有进程组,此时提出线程组

在进程管理结构加 TGIP字段

getpid 返回 TGID 字段,线程号返回 PID 字段

当线程PID=TGID ,这个线程就是线程组长,线程组内的所有线程的 TGID 字段都指向线程组长的 PI

NPTL 线程同样用 clone () 创建,flag 参数新增了一个标志位 CLONE_THREAD

内核把 TGID 指向调用者的 PID,原来的 PID 位置填新线程号(也就是以前的进程号)

LinuxThread 因为在内核是一个 LWP 而产生的跟 POSIX 标准不兼容的错误都消除了

同步与互斥

从 LinuxThread 中的线程同步与互斥中可看到使用信号来模拟的缺点

内核增加新互斥同步原语 futex(fast usesapace locking system call)

进程内所有线程共享相同内存空间,所以这个锁可以保存在用户空间。这样对这个锁的操作不需要每次都切换到内核态,大大加快了存取的速度

NPTL 提供的线程同步互斥机制都建立在 futex 上,所以无论在效率上还是咋对程序的外部影响上都比 LinuxThread 的方式有了很大的改进

信号处理

此时因为同一个进程内的线程都属于同一个进程,所以信号处理跟 POSIX 标准统一

发SIGSTP 信号给进程,此进程的所有线程都会停止

因为所有线程内用同样的内存空间,所以对一个 signal 的 handler 都是一样的

但不同的线程有不同的管理结构所以不同的线程可以有不同的 mask。这一段对 LinuxThread 也成立

管理线程

线程创建与结束的管理都由内核负责

  • Linux

以线程为单位来调度

  • POSIX

以进程为单位来调度