当前位置: 首页 > article >正文

【Linux】线程概念详析

我们已经了解了Linux操作系统进程部分相关知识:

博主有关Linux进程相关介绍的文章:

  • 💥[Linux] 系统进程相关概念、系统调用、Linux进程详析、进程查看、fork()初识

  • 💥[Linux] 进程状态相关概念、Linux实际进程状态、进程优先级

  • 💥[Linux] 什么是进程地址空间?父子进程的代码时如何继承的?程序是怎么加载成进程的?为什么要有进程地址空间?

  • 💥[Linux] 详析进程控制:fork子进程运行规则?怎么回收子进程?什么是进程替换?进程替换怎么操作?

通过阅读这几篇文章, 至少可以让我们对Linux系统中的进程 有一个最基本又相对全面的认识.

但是今天这篇文章, 可能又会对之前介绍过的进程多少有一些推翻.

本篇文章的主要内容是:Linux操作系统中, 有关多线程的相关介绍.


文章目录

  • Linux线程概念
    • 什么是线程
    • Linux进程-再理解
    • Linux线程的创建、查看
      • pthread_create 和 pthread_join
    • 线程相关概念总结
    • 线程的优点
    • 线程的缺点
    • 线程异常
  • Linux进程 VS 线程

Linux线程概念

线程可以说是实际区别于进程的一个概念, 但也可以说是实际没有区别于进程的一个概念.

而实际区别与否, 其实 与平台有关

什么是线程

有关线程的概念, 大概可以通过三个要点介绍:

  1. 线程是在进程内部运行的执行流
  2. 线程相比进程, 粒度更细, 调用成本更低
  3. 线程是CPU调度的基本单位

不过这三个要点只能让你大概的对线程有一个最最最基本的认识:线程比进程要小. 但具体怎么小 是不知道的.

不过可以举个例子来简单的介绍一下, Linux下的线程:

在之前有关进程的介绍中, Linux系统中的进程 = PCB + 被加载到内存中的程序数据, 不过 PCB和内存中的程序数据 并不是直接相映射的.

之间还要通过 进程地址空间和相应的页表, 不过CPU实际只是是通过访问PCB 来实现对进程的调度的:

PCB(task_struct)中描述着进程地址空间, 进程地址空间与物理内存 通过两张页表来相互映射.

这是Linux系统中, 单个进程实际在操作系统中的存在形式.

系统创建进程会创建这所有的格式和数据.

不过我们也介绍过, 如果通过fork创建子进程, 在未作数据修改时 子进程与父进程是共享进程的数据和代码的. (子进程也存在自己的进程地址空间和页表, 只不过指向同一块数据和代码)

而且, 我们可以通过对fork()返回值的判断, 让父子进程执行不同的代码块.

这, 其实说明了一个 细节不同的执行流, 可以做到执行不同的资源, 即 可以做到对特定资源的划分

那么, 如果下次创建进程, 操作系统并不创建有关进程的所有结构, 而是只创建 PCB. 将新的PCB 指向已经存在的进程.

不同PCB指向同一个进程地址空间 - CPU与PCB之间的虚线表示, 此PCB也被CPU调度, 但当前可能没有被调度

然后, 以子进程划分程序资源类似的手段, 将进程的代码划分为不同的区域, 并将不同的PCB设置为实际分别负责执行不同的区域的代码:

PCB与代码区之间连接的红色虚线表示, PCB实际负责执行的代码区域

最终, 不同的PCB可以访问进程地址空间内代码区的不同区域, 并通过相应的页表来访问到实际的物理内存.

实际上这样就在进程内部创建了多个PCB执行流, 而每个PCB执行流都只能访问一小部分的代码一小部分的页表.

那么, 在Linux操作系统中, 我们就 可以将这样的PCB执行流称作 "线程".

这里只是介绍了一下Linux操作系统中, 线程的 粗粒度 的实现原理


介绍了Linux平台下 线程的粗粒度的实现原理, 我们应该可以理解一个内容:线程 : 进程 = N : 1

操作系统 对 比线程数量要少的进程 都会用 PCB 将进程的所有属性 描述、组织、管理起来, 那么对线程, 毫无疑问也是需要用一个结构体描述、组织、管理起来的. 在大多数的操作系统中, 描述线程的结构体叫做 TCB

如果一个操作系统, 为了描述管理进程和线程, 在内核分别实现了不同的 PCBTCB. 那么 PCBTCB 之间一定存在非常复杂的耦合关系. 因为 PCB 描述一个进程, 而 TCB 描述进程内部的线程. 这两部分一定存在相当一部分的重叠属性, 还有一定的包含关系.

那么, 在以后维护一个进程与其内线程的关系时, 一定是一个非常复杂的维护过程.


其实文章介绍线程概念到现在, 一举到具体的例子, 就在强调 在Linux操作系统下.

什么原因呢? 其实 不同操作系统实现线程的方式可能是不同的.

我们在上面提到, 操作系统会存在一个描述线程属性的结构体, 以维护线程.

但是, 实际上 并不是所有的操作系统都会对线程另外描述一个结构体, 使TCB与PCB之间的关系变得非常复杂.

Linux操作系统 就没有另外实现一个描述线程的结构体, 而是 用task_struct(进程PCB)模拟了线体. 即Linux操作系统中, 描述进程和描述线程的结构体实际上是同一个结构体: task_struct

而我们常用的 Windows操作系统, 则是真正将进程与线程区分开, 分别实现了PCB和TCB 以分别用来维护线程和进程. 这样的被称为 真·线程 操作系统

为什么不同的操作系统会对进程和线程之间的关系, 设计出这样的差别呢?

其实是开发者对 进程和线程在执行流层面的理解不同.

以 Windows 来说, Win为了维护线程真正实现了一个不同于PCB的TCB. 也就是说, Win的开发者认为进程和线程在执行流层面是不同的东西. 进程有自己的执行流, 线程在进程内部也有自己的执行流

而 Linux 则认为 进程和线程在概念上不做区分, 都是执行流. PCB要不要被CPU调度?TCB要不要被CPU调度?PCB调度要不要优先级?TCB要不要?要不要通过PCB找到代码和数据?要不要通过TCB找到代码和数据?进程切换要不要保护进程的上下文数据?线程切换要不要保护上下文数据?……

在Linux看来, 种种迹象表明 PCB和TCB的功能 不从更细节来细分的话, 其实是大致相同的. 无非就是PCB和TCB中描述的代码量和数据量的不同, 所以 进程和线程都只看成一个执行流.

所以 Linux线程, 其实就使用task_struct(进程PCB)模拟实现的.

只不过, 线程的TCB(实际上还是PCB)只能访问执行 整个进程中的一小块的代码和数据

这样做有什么好处?

用进程PCB模拟实现线程, 对线程 可以复用操作系统中已经针对进程实现的各种调度算法, 因为进程和线程的描述结构是相同的.

也不用维护进程和线程之间的关系.

也就是说, Linux操作系统中 线程TCB底层就可以看作进程PCB

Linux复用PCB实现TCB, 那么从CPU的角度看待线程, 其实与进程没有区别. CPU调度线程实际上看到的还是PCB(task_strcut)


Linux进程-再理解

上面已经介绍了, Linux中 线程使用进程PCB来模拟实现的, 那么现在又该如何理解进程呢?

在没有介绍线程之前, 我们可以说 CPU看到的所有task_struct都是一个进程

而现在, CPU看到的所有task_struct都是一个执行流

之前我们说, 进程 = PCB + 内存中对应的代码和数据.

而现在, 我们知道进程内部可以存在许多task_srtuct, 那么又可以怎样理解进程呢?

现在, 不能只认为 PCB + 代码和数据 就是一个进程. 而是 需要理解, 上图中的所有结构加起来才能叫一个进程.

我们可以说, 进程是 承担操作系统资源分配的基本实体. 即 进程是 向系统申请资源的基本单位

在没有介绍线程时, 我们可以说 PCB可以表示一个进程, 因为之前进程只有一个执行流, 即只有一个task_struct.

现在 我们可以称 只有一个执行流的进程 为 但单执行流进程, 称 内部存在多个执行流的进程 为 多执行流进程

那么现在, 以CPU的视角来再次看待 task_struct, 我们 现在理解的CPU看到的task_struct 比 没有介绍线程时CPU看到的task_struct 体量要小

因为 Linux中, 现在我们理解的CPU看到的 task_struct 可能是 线程, 可以看作是 轻量化的进程

进程是向系统申请资源的基本单位, CPU调度进程是通过 PCB(task_struct) 调度的, 所以 现在我们说 线程, 是CPU调度的基本单位

那么此时, 我们应该就可以理解 有关线程的概念三个要点介绍:

  1. 线程是在进程内部运行的执行流

    线程只访问执行进程的一部分数据和代码

  2. 线程相比进程, 粒度更细, 调用成本更低

    进程切换调度, 需要切换PCB、进程地址空间、页表等

    而线程切换调度, 只需要切换TCB(实际还是PCB)就可以

  3. 线程是CPU调度的基本单位

Linux线程的创建、查看

介绍线程介绍了这么多, 那么 Linux中如何创建并查看线程呢?

下面我们来直接简单演示一下, 不做太多的介绍. 只创建和查看线程.

pthread_create 和 pthread_join

Linux操作系统为我们提供了创建线程的系统调用:

int pthread_create(pthread_t *thread, const pthread_attr *attr, void *(*start_routine)(void *), void *arg);

这个接口看起来, 非常的复杂

不过, 实际上也没有太复杂. pthread_t 就是一个无符号长整型:

|inline

第一个参数就是此类型的指针, 第一个参数是一个输出型参数, 用于获取线程id

第二个参数, 是线程属性结构体的指针, 暂时不过多介绍 现在我们传入 nullptr

第三个参数, 返回值为空指针 参数为空指针的 函数指针, 用于 传入此线程需要执行的函数.

第四个参数, 一个空指针, 此空指针其实就是 第三个参数(函数指针)所指向的函数的参数

处理创建线程之外, 线程与子进程一样, 还需要等待:

int pthread_join(pthread_t thread, void **retval);

此函数的参数很简单:

  1. 第一个参数传入 需要等待的线程的id
  2. 第二个参数接收退出结果, 暂时不关心. 我们只是看一看现象

简单的了解之后, 我们就可以使用此接口 创建线程:

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void* threadFun1(void* args) {
    string str = (char*)args;
    while (true) {
        cout << str << ": " << getpid() << " " << endl;
        sleep(1);
    }
}

void* threadFun2(void* args) {
    string str = (char*)args;
    while (true) {
        cout << str << ": " << getpid() << " " << endl;
        sleep(1);
    }
}

int main() {
    pthread_t tid1, tid2;

    pthread_create(&tid1, nullptr, threadFun1, (void*)"thread_1");
    pthread_create(&tid2, nullptr, threadFun2, (void*)"thread_2");
    sleep(1);

    while (true) {
        cout << " 主线程运行: " << getpid() << " " << endl;
        sleep(1);
    }

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);

    return 0;
}

不过, 我们编译时会发现有错误:

是连接错误, 为什么呢?

其实 man 手册中已经说了, 使用pthread_create() 接口, 编译连接时需要链接 pthread 库

因为, pthread 是第三方库, 所以我们需要手动链接:

此时, 编译链接成功.

我们运行程序:

thread_show

可以看到线程在分别运行, 所输出的进程pid都是相同的.

输出结果可能会混乱, 可能与线程的优先级与CPU核心、线程数有关

输出结果混乱, 也说明了线程可以并行运行

我们进程运行时查看系统的进程表:

系统中只有一个有关threadTest的进程

可以看到, 有关threadTest 的进程只有一个.

即, 只有一个进程但是进程内存在多个线程.

那么如何查看线程呢?

我们可以在命令行使用 ps -aL 命令 来查看线程(a: all, -L: 轻量级进程):

可以看到, 线程列表中 存在三个相同命令名的线程. 且这三个线程同时属于一个PID 23412. 还拥有各自的 LWP 轻量级进程编号.

并且此 有一个线程的LWP与PID相同, 表示此线程是主线程

有兴趣的话, 可以在创建两个线程之后, 再创建一个子进程.

创建之后, 观察子进程有没有什么地方与之前创建的子进程时的情况不同

线程相关概念总结

上面 从 介绍线程 到 Linux中的线程 再到 Linux线程查看, 已经分析了很多.

但是似乎还是不能对什么是线程、线程的特点做出一个总结, 那么究竟什么是线程呢?

  1. 线程是程序内部的一个执行线路, 更准确一点的定义是:线程是进程内部的控制序列
  2. 一切进程, 至少都有一个线程
  3. 线程在进程内部运行, 本质是在进程地址空间内运行
  4. Linux操作系统中, CPU看到的PCB都比传统意义的PCB要轻量化. 因为Linux中的PCB可能表示的只是一个线程
  5. 透过进程地址空间是可以看到进程的, 将进程资源合理的分配给每一个进程内部的执行流, 就形成了线程执行流

虽然我们说 Linux操作系统中的线程使用进程PCB模拟实现的, 不过其实在设计进程PCB时已经考虑到了线程.

也就是说, 其实PCB(task_struct)内部其实是有用来表示线程的东西的:

task_struct内部, 线程专用的结构体

thread_struct{}结构体内部存储的大部分都是寄存器相关信息. 与维护不同线程的上下文数据有关系

Linux内核源码中, 有关于task_struct内部的成员其实我们已经可以看懂一部分了. 可以尝试去分辨一下成员都代表什么

线程的优点

Linux操作系统中其实可以创建多进程来分配代码并执行, 就比如我们创建子进程并让其执行指定部分的代码.

那么为什么还要有线程呢?其实是因为, 多线程相比进程有一定的优势:

  1. 创建一个新线程的成本比创建一个新进程的成本小的多

创建一个新进程, 操作系统需要分别创建PCB、进程地址空间、页表, 如果对数据做了修改还需要写时拷贝等

而创建一个新线程, 则只需要创建一个PCB就可以了, 进程地址空间、页表、数据等都直接使用原进程的就可以

  1. 与进程之间的切换相比, 线程之间的切换需要操作系统做的工作也会少很多

如果CPU需要切换进程运行, 那么不仅需要切换PCB还需要切换页表等诸多的数据

而切换线程的话, 就只需要切换PCB就可以了

  1. 线程占用资源比进程要少很多

    还是那个原因, 多线程是公用一个进程地址空间和同一页表运行的, 而每个进程都拥有自己的进程地址空间和页表

  2. 对于计算密集型应用, 为了能在多处理器系统上运行, 会将计算分解到多线程去实现

    比如文件加密应用, 可以用多线程将加密工作拆分, 加密完成之后再将文件合并, 就可以完成加密

  3. 对于I/O密集型应用, 为了提高性能, 将I/O操作重叠. 线程可以同时等待不同的I/O操作

    比如一个程序运行时, 需要等待操作系统和网卡之间的I/O操作, 又要等待操作系统和磁盘之间的I/O操作.

    如果单线程的话, 这两个I/O操作只能一个一个等, 不过, 如果是多线程的话就可以同时等待不用排队.

不过, 线程并不是越多越好, 与平台有关, 更准确一点就是与 CPU有关

一般 线程数最好小于等于CPU支持的多线程数.

一般来说, 有多少CPU就可以支持多少线程同时工作.

不过现在CPU都可以模拟多线程, 一个CPU也可能模拟出多线程.

线程的缺点

虽然线程有许多的优点, 但是线程也是存在很大的缺点的:

  1. 可能造成性能损失

    比如一个密集计算型线程正在运行, 且很少或不会被其他外部事件阻塞. 那么这类线程往往是无法与其他线程共用一个CPU的.

    如果密集计算型线程的数量比CPU支持的多线程数量还要多, 这些线程就可能不停的被CPU调度:不停的换出、换入. 因为这些线程都是要运行一下的, 不会只照着一部分线程一直运行, 而是会这一部分执行执行、那一部分执行执行. 这就会因为不停调度而造成性能损失.

    最好线程不要太多.

  2. 健壮性低

    如果是进程, 由于进程地址空间的存在 进程是非常健壮的, 一个进程再怎么运行如果不是刻意为之一般也无法影响另一个进程.

    一个多线程程序内, 可能会因为 时间分配的细微偏差、共享了某些不该共享的数据, 而对其他线程或整个程序造成很大的不良影响.

  3. 缺乏访问控制

    操作系统中, 进程是访问控制的基本粒度, 因为进程具有独立性. 多线程访问可能会同时访问同一个数据, 而且很有可能出大问题

  4. 编程难度高

上面就是多线程的缺点, 不过这些缺点除了第一条, 其他的其实都是对编写者素质的高要求, 什么缺乏访问控制会影响其他线程或整个进程. 其实就是BUG率要高一些. 这就对程序员的素质有较高的要求了.

线程异常

有关线程异常, 其实可以从一个方面理解:

一个多线程进程中, 虽然一般每个线程访问执行的代码和数据不同, 但这些代码和数据都是属于整个进程的, 只有一份.

如果线程出现了异常, 那就说明什么?就说明是进程某处代码出现了异常.

也就是所, 线程出现异常是会影响整个进程 的. 线程出现异常其实就是进程出现了异常.

线程出现异常, 操作系统就会像线程发送信号, 然后会将整个进程终止. 整个进程终止, 进程中的其他所有线程也会退出.

Linux进程 VS 线程

文章已经介绍过了Linux下线程的概念, 那么结合之前介绍的Linux进程.

我们来对比一下, 进程和线程有什么是相同的, 什么是不同的.

  • 进程是系统资源分配的基本单位

  • 线程是调度的基本单位

  • 多线程共享进程数据, 不过不同线程也有自己的一部分数据:

    • 线程ID

      就像每个进程都有自己的ID一样, 每个线程也都有自己的ID

    • 一组寄存器

      每个线程都有一组寄存器, 用来维护线程的上下文数据

    • 线程栈

      进程在运行时, 都会有自己的栈结构, 来给函数的压栈、临时变量等数据提供空间

      其实每个线程也都会维护自己的栈区, 因为线程也可能会不停的函数调用等操作. 所以是需要维护自己的栈区的.

    • errno

    • 信号屏蔽字

      上面介绍信号异常时提到, 线程异常 就是 进程异常. 线程异常操作系统会向线程发送信号.

      不过线程是与进程共享信号处理方法的, 所以一般情况下线程异常 也就是进程异常

      不过, 虽然线程与进程共享信号处理方法, 但是线程是有自己的信号屏蔽字的.

      也就是说, 操作系统向线程和进程发送同一信号, 可能进程会递达, 而线程却会阻塞.

    • 调度优先级

  • 线程和进程会共享这些资源:

    • 代码和数据

      进程中定义的函数, 每个线程都可以调用. 进程中定义的全局变量, 每个线程也都可以访问

    • 文件描述符表

      虽然 文件描述符表并不是进程地址空间内的数据 而是内核数据(在PCB中维护)

      但是 进程的文件描述符表 也是与线程共享的, 线程PCB会指向主线程PCB的文件描述符表

    • 信号的处理方法

    • 进程当前运行路径

    • 用户ID和组ID


文章到这里, 其实Linux线程概念的部分 就已经介绍的差不多了.

感谢阅读~


http://www.kler.cn/a/9806.html

相关文章:

  • 大语言模型在序列推荐中的应用
  • Mysql数据类型面试题15连问
  • 准确--FastDFS快速单节点部署
  • EDUCODER头哥 基于MVC模式的用户登录
  • Redis安装(Windows环境)
  • 【AI写作宝-注册安全分析报告-无验证方式导致安全隐患】
  • 博客平台用户模块设计原则:构建简洁、高效的用户体验
  • 100种思维模型之非共识思维模型-48
  • 板块模型构建、k点选定及Miller指数对表面分类
  • 代码随想录算法训练营第五十二天 | 300.最长递增子序列、674. 最长连续递增序列、718. 最长重复子数组
  • 液压传动与控制实验教学培训系统平台
  • Bootloader的作用
  • [ 应急响应基础篇 ] Windows系统隐藏账户详解(Windows留后门账号)
  • 从存算分离说起:金融行业数据库分布式改造之路
  • 【Linux】工具(5)——gdb
  • 浏览器:好用的浏览器插件,亲测好用
  • 分布式场景下,Apache YARN、Google Kubernetes 如何解决资源管理问题?
  • Quaternion插值方法
  • FFMPEG VCL Pack Crack显示位置支持或光标
  • 是面试官放水,还是公司实在是太缺人?这都没挂,字节原来这么容易进...
  • 【并发编程】异步编程CompletableFuture实战
  • MySQL开发05-MySQL开发规范
  • 分布式文件系统FastDFS
  • android framework实战车机手机系统开发环境相关问题补充
  • 为什么提升客户服务是长期成功的关键
  • 高精度尘埃粒子计数器工厂空气质量监测必备