一、更新认知
之前的认知
- 进程:一个执行起来的程序。进程 = 内核数据结构 + 代码和数据
- 线程:执行流,执行粒度比进程要更细。是进程内部的一个执行分值
更新认识:
- a. 进程是承担分配系统资源的基本实体
- b. 线程是OS调度的基本单位
二、Linux下线程的概念和实现
线程是进程内部的一个执行分支,说明一个进程就可能有多个线程,这样OS就必须将线程管理起来,Linux系统管理线程会通过先描述,再组织的方式,先将线程描述成一个结构 struct threadctlblock
,再对该结构进行组织,达到对系统内线程的管理组织。
线程结构也是 task_struct
?
实际上,在 Linux系统中,没有真正的 struct threadctlblock
的 TCB 结构,Linux下的线程是用进程模拟实现的!
线程也使用进程的 task_struct
来描述,这样就无需为线程单独创建一套线程的管理机制,毕竟线程和进程过于相像,用进程模拟线程,大大减少了操作系统内核设计的难度,复用了历史代码,增加代码可维护性
减少了许多设计工作,如:线程创建、线程队列链表组织…、线程切换、线程….
Linux 这一点设计是非常厉害巧妙的,因为其他一些操作系统如 windows
是真的有线程的,内核代码和管理复杂不少!不像 Linux 的伪线程
进程内多线程共用进程资源
一个进程 task_struct
内部还有多个用于描述线程的 task_struct
。
进程 task_struct
和线程 task_struct
同属于一个PCB结构,他们共享同一个进程地址空间,共享进程内的资源。
比如划分代码区,各个线程负责执行代码的不同部分:
三、进程vs线程 概念统一
进一步理解
重新理解进程和线程
进程:task_struct
+ 地址空间 + 页表 + 代码和数据
线程:可以暂时简单理解为一个task_struct
(后续会进行更详细的解释)。
进程由PCB描述,每个进程中可能存在多个这样的控制块,它们共享同一个地址空间,分配给不同的任务使用,这便是我们所说的线程。
所以,线程实际上是一个PCB加上代码和数据(还有一些相关数据结构将在后面讨论)。
单线程 vs 多线程
在之前的学习中,每个进程就是单独的一个执行流,只有一个执行分支。这实际上是内部只有一个线程的单线程结构!
当前我们学习了多线程的概念,可以理解到只有一个执行流的进程,是多进程的特殊情况,即单线程结构
这就将进程和线程的概念统一起来了
进程与资源管理
进程作为系统资源分配的基本实体,当我们创建一个进程时,需要为其准备地址空间、PCB、页表等,这些都需要占用内存并申请系统资源。因此,进程承担了分配系统资源的责任。相反,线程无需单独申请系统资源,因为它们依赖于进程已经准备好的资源。线程更像是一个个执行流,负责拿着进程申请的资源去执行各自的程序段。
因此,线程只需划分并使用需要资源(执行程序段所需的资源),而进程才是需要申请系统资源
CPU视角下的进程与线程
从CPU的角度来看,Linux将执行流统一称为轻量级进程(LWP
: Lightweight Process
)。一个进程由多个轻量级进程组成。然而,在Linux中并没有真正意义上的线程存在;所谓的线程实际上是通过LWP模拟实现的。
轻量级进程是在 UNIX 系统中的一种进程模型,用于支持多线程编程。LWP 提供了一种比传统进程更轻便的方式来创建和管理并发执行的任务。
在传统的 UNIX 系统中,进程是独立运行的基本单位,每个进程都有自己的地址空间和其他资源。而 LWP 则共享一个进程的地址空间和其他资源,因此它们之间的切换开销较小,更适合于需要大量并发任务的应用场景。
LWP 与线程的关系密切,实际上,LWP 就是线程在操作系统中的实际表示形式之一。在某些操作系统中,如 Solaris,LWP 是线程调度的基础,而在其他操作系统中,如 Linux,LWP 可能被隐藏起来,由操作系统内部使用。
总之,LWP 是一种轻量级的进程模型,旨在提供高效的并发执行能力,适用于需要大量并发任务的应用场景。
通俗理解进程和线程
这个世界是一个巨大的操作系统和资源库,我们每个家庭都是一个个进程,作为承担世界分配资源的基本实体(每个家庭都有一定的租房地、生活空间….等资源),而家庭内的每个人都是一个个线程,每个人都有自己需要做的事:如爷爷奶奶早起公园散步锻炼、爸爸妈妈工作赚钱、你和兄弟姐妹上学学习….
每个人做事的最终目的都是为了家庭,为了提高家庭幸福而努力,即进程内的每个线程做着不同的任务,最终目的都是为了完成进程的整个大任务
认识一下线程:代码创建
我们写一些代码来创建一个线程来见识一下(仅为了了解,暂时还没开始讲代码)
创建线程的函数:
线程相关函数并不是系统调用,而是有一个单独的线程库,编译含有线程创建等函数的程序需要主动连接线程库,线程库是一种动态库
编译时加上链接线程库:
g++ -o testpthread testpthread.cc -lpthread
代码如下
#include <iostream>
#include <pthread.h>
#include <unistd.h>
void *run(void *args)
{
while (true)
{
std::cout << "I am new thread" << '\n';
sleep(1);
}
}
int main()
{
// 创建新线程
pthread_t tid;
pthread_create(&tid, nullptr, run, (void *)"thread-1");
// 主线程
while (true)
{
std::cout << "I am main thread" << '\n';
sleep(1);
}
return 0;
}
运行结果如下:运行一个程序,有两个执行流打印语句!
代码:打印进程 pid
这些执行流(线程)都是同属于一个进程的,打印出来的进程pid都是一样的!
#include <iostream>
#include <pthread.h>
#include <unistd.h>
void *run(void *args)
{
while (true)
{
std::cout << "I am new thread" << '\n';
std::cout << "pid : " << getpid() << '\n';
sleep(1);
}
}
int main()
{
std::cout << "pid : " << getpid() << '\n';
// 创建新线程
pthread_t tid;
pthread_create(&tid, nullptr, run, (void *)"thread-1");
// 主线程
while (true)
{
std::cout << "I am main thread" << '\n';
std::cout << "pid : " << getpid() << '\n';
sleep(1);
}
return 0;
}
上述内容讲解了 LWP
轻量级进程的概念,下面我们见识一下系统层面运行起来的轻量级进程!
ps axj # 这是查询进程的
ps -aL # 这是查询轻量级进程的
其中:pid
一样,LWP
不同
LWP
和 pid
一样 的是主线程
CPU区分线程(LWP),即区分执行流的唯一性是以 LWP 号为基准的
CPU真实调度执行流是用 LWP 号
而我们以前学习的说 CPU 用 pid 作为调度,也没错
之前学到进程都是单执行流单线程的,pid = lwp
因此实际上,进程 task_struct
内部有 pid 和 LWP 两种字段,就是为了兼容 Linux 模拟实现线程的情况
四、分页式存储管理
我们通过这篇博客认识和理解一下分页式存储管理系统:【Linux系统】分页式存储管理-CSDN博客
五、进程划分资源给线程
如何理解?如何做到?
逐步引入
首先被划分的资源肯定有代码区,通过划分代码给不同的线程执行?
其次这些划分有没有必要刻意的划分?
根据之前学过的线程创建:pthread_create(..., ..., run, ...)
线程创建出来就会去执行这个 run
函数,这意味着无论创建出来多少个线程都是会通过这些 run
的函数指针的指引找到并执行对应的函数
真相浮出
这些 run
函数都是有自己在虚拟地址空间中的虚拟地址的,我们没必要刻意的划分虚拟地址空间的代码给不同线程执行,因此当线程创建出来时,就已经根据指令为不同线程提供了对应的函数地址,线程只需根据该地址就能找到自己负责的那部分!
每个函数都是一个代码块部分,只要拥有该函数的虚拟地址,就等同于拥有该函数所有代码指令,即该代码块部分的虚拟地址。执行程序时,每个线程都查询不同部分的页表执行代码,不就完成了逻辑上对代码的划分吗!!
总的来说,CPU调度一个 LWP ,只需给他一个函数的入口地址,CPU就能讲该执行流调度运行起来
六、拓展:如何理解写时拷贝?
可以看这篇博客:【Linux系统】如何理解写时拷贝?一次拷贝4KB不浪费?-CSDN博客