[Linux] 多线程概念相关分析: 什么是线程、再次理解进程、线程的创建与查看、线程异常、线程与进程的对比...
博主有关Linux进程相关介绍的文章: 💥[Linux] 系统进程相关文章
Linux
系统中的进程 有一个最基本又相对全面的认识Linux
操作系统中, 有关多线程的相关介绍Linux线程概念
什么是线程
- 线程是在进程内部运行的执行流
- 线程相比进程, 粒度更细, 调用成本更低
- 线程是CPU调度的基本单位
Linux
下的线程:Linux
系统中的进程=PCB
+被加载到内存中的程序数据, 不过 PCB
和内存中的程序数据 并不是直接相映射的PCB
来实现对进程的调度的:PCB(task_struct)
中描述着进程地址空间, 进程地址空间与物理内存 通过两张页表来相互映射Linux
系统中单个进程, 实际在操作系统中的存在形式, 系统创建进程会创建这所有的格式和数据fork
创建子进程, 在未作数据修改时, 子进程与父进程是共享进程的数据和代码的(子进程也存在自己的进程地址空间和页表, 只不过指向同一块数据和代码)fork()
返回值的判断, 让父子进程执行不同的代码块CPU
调度进程是通过调度PCB
实现的PCB
, 并将新的PCB
指向已经存在的进程:PCB
设置为分别负责执行不同的区域的代码:PCB
可以访问进程地址空间内代码区的不同区域, 并通过相应的页表来访问到实际的物理内存PCB
执行流, 而每个PCB
执行流都只能访问一小部分的代码一小部分的页表PCB
执行流称作”线程”这里只是介绍了一下
Linux
操作系统中, 线程的 粗粒度 的实现原理
Linux
平台下 线程的粗粒度的实现原理, 应该可以理解一个内容: 线程 : 进程 = N : 1
PCB
将进程的所有属性: 描述、组织、管理起来TCB
PCB
和TCB
PCB
和TCB
之间一定存在非常复杂的耦合关系PCB
描述一个进程, 而TCB
描述进程内部的线程, 这两部分一定存在相当一部分的重叠属性, 还有一定的包含关系Linux
操作系统下TCB
与PCB
之间的关系变得非常复杂Linux
操作系统就没有另外实现一个描述线程的结构体, 而是用task_struct
(进程PCB)模拟了线程Linux
操作系统中, 描述进程和描述线程的结构体实际上是同一个结构体: task_struct
Windows
操作系统, 则是真正将进程与线程区分开, 分别实现了PCB
和TCB
以分别用来维护线程和进程, 这样的被称为 真·线程 操作系统
Windows
来说, Win
为了维护线程真正实现了一个不同于PCB
的TCB
.Win
的开发者可能认为进程和线程在执行流层面是不同的东西Linux
则可能认为进程和线程在概念上不做区分, 都是执行流PCB
和TCB
都要被CPU调度; PCB
和TCB
的调度都需要优先级; 进程切换和线程切换也都需要保护上下文数据……Linux
看来, 种种迹象表明PCB
和TCB
的功能 不从更细节来细分的话, 其实是大致相同的, 无非就是PCB和TCB中描述的代码量和数据量的不同, 所以 进程和线程都只看成一个执行流Linux
线程, 其实就使用task_struct
(进程PCB)模拟实现的TCB
(实际上还是PCB)只能访问整个进程中的一小块的代码和数据这样做有什么好处?
用进程
PCB
模拟实现线程, 对线程 可以复用操作系统中已经针对进程实现的各种调度算法, 因为进程和线程的描述结构是相同的也不用维护进程和线程之间的关系
也就是说,
Linux
操作系统中线程TCB
底层就可以看作进程PCB
Linux
复用PCB
实现TCB
, 那么从CPU的角度看待线程, 其实与进程没有区别PCB(task_strcut)
Linux进程-再理解
Linux
中线程使用进程PCB
来模拟实现的, 那么现在又该如何理解进程呢?task_struct
都是一个进程task_struct
都只是一个执行流task_srtuct
, 那么又可以怎样理解进程呢?PCB
+代码和数据就是一个进程, 而是需要理解, 上图中的所有结构加起来才能叫一个进程PCB
可以表示一个进程, 因为之前进程只有一个执行流, 即只有一个task_struct
task_struct
, 现在理解的, CPU看到的task_struct
比没有介绍线程时CPU看到的task_struct
体量要小Linux
中CPU看到的task_struct
可能是线程, 可以看作是轻量化的进程PCB(task_struct)
调度的, 所以 线程, 是CPU调度的基本单位-
线程是在进程内部运行的执行流
线程只访问执行进程的一部分数据和代码
-
线程相比进程, 粒度更细, 调用成本更低
进程切换调度, 需要切换
PCB
、进程地址空间、页表等而线程切换调度, 只需要切换
TCB
(实际还是PCB)就可以 -
线程是CPU调度的基本单位
Linux线程的创建、查看
pthread_create
和 pthread_join
int pthread_create(pthread_t *thread,
const pthread_attr *attr,
void *(*start_routine)(void *),
void *arg);
pthread_t
就是一个无符号长整型:-
第1个参数, 就是
pthread_t
类型的指针第一个参数是一个输出型参数, 用于获取线程
id
-
第2个参数, 是线程属性结构体的指针
暂时不过多介绍 现在传入
nullptr
-
第3个参数, 返回值为空指针参数为空指针的 函数指针, 用于传入此线程需要执行的函数
-
第4个参数, 一个空指针
此空指针其实就是第3个参数(函数指针)所指向的函数的参数
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
是第三方库, 所以需要手动链接:pid
都是相同的.输出结果可能会混乱, 可能与线程的优先级与CPU核心、线程数有关
输出结果混乱, 也说明了线程可以并行运行
threadTest
的进程只有一个.ps -aL
命令 来查看线程(a: all, -L: 轻量级进程):PID:23412
, 还拥有各自的LWP
轻量级进程编号LWP
与PID
相同, 表示此线程是主线程有兴趣的话, 可以在创建两个线程之后, 再创建一个子进程
创建之后, 观察子进程有没有什么地方与之前创建的子进程时的情况不同
线程相关概念总结
Linux
中的线程, 再到Linux
线程查看, 已经分析了很多-
线程是程序内部的一个执行线路, 更准确一点的定义是: 线程是进程内部的控制序列
-
一切进程, 至少都有一个线程
-
线程在进程内部运行, 本质是在进程地址空间内运行
-
在
Linux
操作系统中, CPU看到的PCB
都比传统意义的PCB
要轻量化因为,
Linux
中的PCB
可能表示的只是一个线程 -
透过进程地址空间是可以看到进程的
将进程资源合理的分配给每一个进程内部的执行流, 就形成了线程执行流
Linux
操作系统中的线程使用进程PCB
模拟实现的, 不过其实在设计进程PCB
时已经考虑到了线程PCB(task_struct)
内部其实是有用来表示线程的东西的:thread_struct{}
结构体内部存储的大部分都是寄存器相关信息, 与维护不同线程的上下文数据有关系线程的优点
Linux
操作系统中其实可以创建多进程来分配代码并执行, 就比如创建子进程并让其执行指定部分的代码-
创建一个新线程的成本小, 比创建一个新进程的成本小的多
创建一个新进程, 操作系统需要分别创建
PCB
、进程地址空间、页表, 如果对数据做了修改还需要写时拷贝等而创建一个新线程, 则只需要创建一个
PCB
就可以了, 进程地址空间、页表、数据等都直接使用原进程的就可以 -
线程之间切换的成本也比较小, 与进程之间的切换相比, 线程之间的切换操作系统需要做的工作会少很多
如果CPU需要切换进程运行, 那么不仅需要切换PCB还需要切换页表等诸多的数据
而切换线程的话, 就只需要切换
PCB
就可以了 -
线程占用资源比进程要少很多
还是那个原因, 多线程是公用一个进程地址空间和同一页表运行的, 而每个进程都拥有自己的进程地址空间和页表
-
对于计算密集型应用, 为了能在多处理器系统上运行, 会将计算分解到多线程去实现
比如文件加密应用, 可以用多线程将加密工作拆分, 加密完成之后再将文件合并, 就可以完成加密
-
对于I/O密集型应用, 为了提高性能可以将I/O操作重叠, 线程可以同时等待不同的I/O操作
比如一个程序运行时, 需要等待操作系统和网卡之间的I/O操作, 又要等待操作系统和磁盘之间的I/O操作
如果单线程的话, 这两个I/O操作只能一个一个等, 不过, 如果是多线程的话就可以同时等待不用排队.
线程的缺点
- 可能造成性能损失
-
健壮性低
如果是进程, 由于进程地址空间的存在 进程是非常健壮的, 一个进程再怎么运行如果不是刻意为之一般也无法影响另一个进程
一个多线程程序内, 可能会因为 时间分配的细微偏差、共享了某些不该共享的数据, 而对其他线程或整个程序造成很大的不良影响
-
缺乏访问控制
- 编程难度高
线程异常
Linux
进程 VS 线程
Linux
下线程的概念, 那么结合之前介绍的Linux
进程-
进程是系统资源分配的基本单位
-
线程是调度的基本单位
-
多线程共享进程数据, 不过不同线程也有自己的一部分数据:
-
线程
ID
就像每个进程都有自己的ID一样, 每个线程也都有自己的ID
-
一组寄存器
每个线程都有一组寄存器, 用来维护线程的上下文数据
-
线程栈
进程在运行时, 都会有自己的栈结构, 来给函数的压栈、临时变量等数据提供空间
其实每个线程也都会维护自己的栈区, 因为线程也可能会不停的函数调用等操作. 所以是需要维护自己的栈区的.
-
errno
-
信号屏蔽字
上面介绍信号异常时提到, 线程异常 就是 进程异常. 线程异常操作系统会向线程发送信号.
不过线程是与进程共享信号处理方法的, 所以一般情况下线程异常 也就是进程异常
不过, 虽然线程与进程共享信号处理方法, 但是线程是有自己的信号屏蔽字的.
也就是说, 操作系统向线程和进程发送同一信号, 可能进程会递达, 而线程却会阻塞.
调度优先级
-
-
线程和进程会共享这些资源:
-
代码和数据
进程中定义的函数, 每个线程都可以调用. 进程中定义的全局变量, 每个线程也都可以访问
-
文件描述符表
虽然, 文件描述符表并不是进程地址空间内的数据, 而是内核数据(在
PCB
中维护)但是, 进程的文件描述符表也是与线程共享的, 线程
PCB
会指向主线程PCB
的文件描述符表 -
信号的处理方法
-
进程当前运行路径
-
用户ID和组ID
-
Linux
线程概念的部分 就已经介绍的差不多了作者: 哈米d1ch 发表日期:2023 年 4 月 11 日