在日常使用中, 其实能很明显感觉进程, 线程和协程是完全不一样的概念. 但一旦深入到底层, 这三者又有些难以区分, 本篇从本质上看看三者的区别

关于线程说明

NPTL[1]是 Linux 2.6 引入的新的线程库实现,符合 POSIX 在线程方面的标准,用来替代旧的 LinuxThreads 线程库。不管是NPTL还是LinuxThreads,用户创建的每个线程都对应着一个内核态的线程。若无特殊说明,这里的线程特指操作系统内核线程

进程和线程

之所以将进程和线程放一块是因为在 Linux 里面,无论是进程,还是线程,到了内核里面,统一都叫任务
(Task),由一个统一的结构task_struct进行管理[2]

image-20200830195551210

通常所说的pcb在linux中就是task_struct

任务ID

那么如何区分在不同进程的线程呢?

task_struct中有如下字段定义任务ID

pid_t pid;
pid_t tgid;
struct task_struct *group_leader;

其中pid 是 process id,tgid是 thread group ID
若一个进程只有主线程, pid,tgid,group_leader 均为自己
若一个进程创建了其他线程,线程有自己的 pid,tgid 是进程主线程的 pid,group_leader 也指向主线程

有了tgid,在任务切换的时候就知道是 线程上下文切换 还是 进程上下文切换了

创建过程

进程是通过fork系统调用创建,而线程是pthread_create,最终为什么都是task了呢?

  • 进程创建完全通过系统调用实现

    fork系统调用的过程主要有两个重要的事件

    1. 调用sys_fork->do_fork,将task_struct结构复制一份并且初始化

      image-20200830212651155

    2. 试图唤醒新创建的子进程

  • 线程创建是由内核态和用户态合作完成的
    pthread_create 不是一个系统调用,而是 Glibc 库(nptl/pthread_create.c中)的一个函数
    在用户态也有一个用于维护线程的结构(pthread)

    大致的步骤如下:

    1. 用户态创建线程栈,存放在堆中(通过mmap分配)
    2. 系统调用clone->do_fork复制 task_struct

      这里复制的时候由于clone_flags的影响,要么只是引用计数增加,要么直接指向原来的结构

    3. 用户态执行通用的 start_thread -> 调用用户指定函数

      和创建进程不同,clone在子线程返回时,我们还需要修改栈指针和指令指针,栈顶指针应该指向新线程的栈,指令指针应该指向线程将要执行的那个函数

上下文切换

  1. 前后两个task属于不同进程。资源不共享,判断为进程上下文切换,需要切换进程空间(虚拟内存),也需要切换寄存器和 CPU上下文。
  2. 第二种,前后两个线程属于同一个进程。资源不共享,判断为进程上下文切换,只用切换寄存器和 CPU上下文。

协程

协程被称为‘轻量级的线程’,但我觉得是有点误导,因为协程和内核线程毫无关系

协程应该被叫做‘用户态的线程’比较好理解,也就是说,协程是一个能挂起并且一段时间后恢复执行的东西,并且不需要操作系统调度的参与,完全由各个语言自己实现

根据具体实现大致可分为

  1. 有栈协程(Stackful Coroutine):每一个协程都会有自己的调用栈,类似于线程的调用栈,这种情况下的协程实现其实很大程度上接近线程,主要不同体现在调度上。如 goroutine
  2. 无栈协程(Stackless Coroutine):协程没有自己的调用栈。如 JavaScript 、 C# 和 Python

无栈协议栈是由CPS(continuation-passing-style)来保存上下文,这里不做过多介绍

转载申请

知识共享许可协议

本文知识共享署名 4.0 国际许可协议进行许可,转载时请注明原文链接


  1. Wikipedia: Native POSIX Thread Library https://en.wikipedia.org/wiki/Native_POSIX_Thread_Library ↩︎

  2. 趣谈Linux操作系统: https://time.geekbang.org/column/intro/164 ↩︎