文章目录
- 前言
- 一、地址空间和页表
- 1.二级页表
- 2.例子
- 二、线程
- 1.概念
- 重新理解前面讲的进程:在内核的视角,进程是承担分配系统资源的基本实体。
- 站在CPU角度,能否去识别当前调度的task_struct是进程还是线程?
- Linux下并不存在真正的线程
- 总结
- 2.线程的优点
- 3.线程的缺点
- 健壮性降低的例子
- 4.线程的异常
- 5.线程的用途
- 三、Linux下的进程与线程
- 进程的多个线程共享的资源
- 线程独立的数据
- 进程与线程的关系
- 总结
前言
本文介绍了地址空间和二级页表、Linux下的线程、线程的优缺点以及线程与进程的关系等概念。
一、地址空间和页表
地址空间是进程能看到的资源窗口:一个进程可以看到代码区、堆栈区、共享区、内核区等,大部分的资源是在地址空间上看到的。
页表决定进程真正有用资源的情况:进程认为自己独占系统的4GB资源,但实际上进程拥有多少物理资源是由页表决定的。
合理的对地址空间和页表进行资源划分,我们就可以对进程所拥有的资源进行分类:通过地址空间的区域划分,划分为栈区、堆区……,通过页表映射到不同的物理内存中。
1.二级页表
在32位平台下,一共有2^32
个地址,这也意味着有2^32
个地址需要被映射。
地址空间有2^32
个地址,每个地址单位都是1字节,页表也要有2^32
个条目(每个地址都要经过页表映射,它们都是页表的条目),包括是否命中,包括RWX权限,包括U/K权限。一个条目假设有6个字节的数据,那么光保存页表的空间就需要24GB(4GB大约40亿字节)。
每个表项中处理要有虚拟地址和它映射的物理地址外,时间还需要一些权限相关的信息,用户级页表和内核级页表实际就是通过权限进行区分。
虚拟地址:32位下是32位。
物理地址:被划分为一块块的数据框。
OS要对物理内存进行管理:先描述(结构体:struct Page{//内存的属性——4KB}
),再组织(数组:struct Page mem[]
)。
在OS中把物理内存一块块的数据框称为页框,磁盘上编译形成可执行程序的时候被划分为一个个4KB的区域称为页帧。当内存和磁盘进行数据交换时,也是以4KB大小为单位进行加载和保存的。
因此,将数据加载到内存时,在文件系统级别需要按照4KB为基本单位将数据从外设搬到内存。最后,OS系统想要管理内存,除了结构匹配还要有管理算法,Linux常见的管理算法称为伙伴系统。
虚拟地址转化为物理地址:虚拟地址形成后(以10,10,12的二进制构成),页表不止一张。第一级页表页目录:前十个在页目录中查找,2^10
个指向页表的内容。页表:页表的条目项为2^10
个,条目写的是指定页框的起始物理地址,页表项指向物理内存中某一页,剩下的12位虚拟地址刚好与页框的大小是等价的(4KB = 2^12
B),因此,从物理地址的起始处 + 虚拟地址的低12位(2^12偏移量)作为页内偏移,就可以直接在某个页内找到某个地址。
其中的页目录项是一级页表,页表项是二级页表。映射过程由MMU这个硬件完成(该硬件集成在CPU内),页表是一种软件映射,MMU是一种硬件映射,虚拟地址转为物理地址实际上是软硬件结合的。
2.例子
修改常量字符串为什么会发送错误?
如果要修改一个常量字符串,虚拟地址需要经过页表映射查找到对应的物理内存,但是在查表的过程中会发现该地址的权限是只读,对一个只读地址进行修改会导致在MMU内部触发硬件错误,OS识别到这个错误会该对应进程发送信号终止对应进程。
二、线程
1.概念
- 在一个程序里的一个执行路线就叫做线程(可以参考进程)。更准确的定义是:线程是一个进程内部的控制序列。
- 一切进程都至少有一个执行线程。
- 线程在进程内部运行本质是在进程的地址空间内运行。
- Linux中,在CPU眼中看到的PCB都比传统的进程更加轻量化。
- 透过进程的虚拟地址空间可以看到进程的大部分资源,将进程的资源合理分配给每个执行流,就形成了线程执行流。
- 不同平台的多线程底层实现策略都是不同的,本文我们了解的是Linux下的多线程策略。
线程对应的模型:进程的创建实际上伴随着进程控制块(PCB)、进程地址空间(mm_struct)以及页表的创建(虚拟地址和物理地址是通过页表建立映射的):
进程 = 内核数据结构 + 代码和数据。
每个进程都有字节独立的进程地址空间和独立的页表,这意味着每个进程在运行时会具有独立性,
如果我们在创建进程时只创建进程的PCB,并要求创建出来的PCB不再独立创建资源,而是与父进程共享资源。那么创建的结果就是下面这样的:
因为我们可以通过虚拟地址空间 + 页表的方式对进程的资源进行划分,单个进程的执行力度会比之前的进程更细。
上图中每个线程都是当前进程的一个执行流,线程在进程的内部运行,在进程的地址空间运行,拥有该进程的一部分资源。
重新理解前面讲的进程:在内核的视角,进程是承担分配系统资源的基本实体。
创建进程时,申请的PCB、虚拟内存空间、页表以及加载到物理内存中的代码和数据:花费CPU资源创建进程并初始化;花费内存资源保存进程的内核数据结构、代码和数据;花费CPU的IO资源从外设IO到内存。所以承担分配系统资源的基本实体是进程。
总结一下,我们创建进程时,OS申请一堆的内核数据结构占用资源,进程的代码和数据加载到内存中也要占用资源,以及其他部分占用的资源。因此,进程是承担系统资源分配的基本实体。
我们之前讨论的进程都是只有一个PCB,也就是说该进程内部只有一个执行流,即单执行流,这与我们上面讲的并不冲突,如果是像上面这样的一个进程内部由多个执行流,那它就是多执行流进程。
站在CPU角度,能否去识别当前调度的task_struct是进程还是线程?
不能,也不需要,CPU不关心当前调度的是进程还是线程。CPU以task_struct为单位进行调度,今天我们喂给CPU的task_struct是小于等于过去所说的task_struct的,它比之前的更轻量化。因此,在Linux中可以把进程和线程做一个统一,CPU看到的task_struct称为轻量级期间进程。
在Linux中,什么是线程?——线程是CPU的基本调度单位。
Linux下并不存在真正的线程
Linux下的线程是用进程模拟的。
如果OS真正要专门设计“线程”概念,OS就要管理线程了(先描述,再组织)。
Windows下确实是为线程专门设计了数据结果表示线程对象TCB,但是线程的创建就是为被执行,执行需要被调度、存在ID/状态、优先级、上下文、栈……等内容,这些线程调度需要的东西与进程有很多地方是重叠的。因此,Linux下没有为“线程”专门设计对应的数据结构,而是直接复用了进程的PCB,用PCB来表示Linux下的“线程”。
总结
- Linux内核中严格来说是没有真正意义的线程的,Linux用进程PCB来模拟线程,它有一套完全属于自己的线程方案。
- 站在CPU角度,每一个PCB都可以称为轻量级进程。
- Linux下,线程是CPU调度的基本单位,进程是承担分配系统资源的基本单位。
- 进程用来整体申请资源,线程是伸手向进程要资源。(所以线程在执行时申请的资源,实际上是进程向系统申请的资源)
- 进程模拟线程的好处:用PCB模拟线程,则为PCB编写的结构和算法都可以进行复用,不用单独再为线程创建结构和调度算法,降低了系统的维护成本,同时复用进程的那套,可靠高效。
2.线程的优点
- 创建一个线程要花费的代价比创建一个进程的代价要小得多,与进程切换相比,线程之间的切换需要操作系统做的工作要少很多。
进程切换:要切换页表、虚拟地址空间、PCB、上下文;
线程切换:切换PCB和上下文。 - 线程占用的资源要比进程占用的资源少很多。
- 线程能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他计算任务。
- 计算密集型应用(CPU、加密、解密、算法等),为了能在多处理器系统上运行,可以讲计算分解到多个线程中实现。
- I/O密集型应用(外设、磁盘、显示器、网络),为了提高性能,讲I/O操作重叠,使线程可以同时等待不同的I/O操作。
3.线程的缺点
- 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。
如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用资源是不变的。 - 健壮性降低:编写多线程需要更全面深入的考虑。
在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的。换而言之,线程之间是缺乏保护的。 - 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高:编写与调试一个多线程程序比单线程程序困难的多。
健壮性降低的例子
一个线程如果出现了异常会影响其他线程(健壮性、鲁棒性较差)
1 #include<iostream>
2 #include<string>
3 #include<unistd.h>
4 #include<pthread.h>
5 using namespace std;
6 void* start_routine(void* args)
7 {
8 string name = static_cast<const char*>(args);//安全的进行强制类型转换
9 while(1)
10 {
11 cout<<"new thread create success, name:"<<name<<endl;
12 sleep(1);
13 int* p = nullptr;
14 *p = 0;
15 }
16 }
17 int main()
18 {
19 pthread_t id;
20 pthread_create(&id, nullptr, start_routine, (void*)"thread new");
21 while(1)
22 {
23 cout<<"new thread create success, name: main thread"<<endl;
24 sleep(1);
25 }
26 return 0;
27 }
运行:
线程出现异常会影响其他线程,这是因为信号是由OS发送给整个进程的,当前线程出现异常,那么OS识别到当前硬件报错、地址转化出现失败、没有权限的空间进行写入、MMU+页表执行异常等问题,OS会立即识别是哪个线程/进程出错,而所有的线程的PID是相同的,因此OS会直接给所有该PID的线程的PCB写入11号段错误信号,这就终止了当前的进程执行流,当前进程就退了,而线程所拥有的资源是进程给的,进程没了,线程也就得退出了。
4.线程的异常
当线程如果出现除零、野指针问题,会导致当前线程崩溃,进程也会随之崩溃。线程是进程的执行分支,线程出现异常,就等同于进程出现异常,进而触发信号机制,终止进程。进程终止了,进程内运行的所有线程也就终止了。
5.线程的用途
- 合理使用多线程,可用提高CPU密集型程序的执行效率;
- 合理使用多线程,可用提高IO密集型程序的用户体验(例如,我们一边写代码,一边下载开发工具,就是多线程运行的一种表现)
三、Linux下的进程与线程
进程是承担分配系统资源的基本实体,线程是系统调度的基本单位。
进程的多个线程共享的资源
- 因为这些线程在同一个地址空间,所以代码段(Text Segment)、数据段(Data Segment)都是共享的。
- 如果是函数,那么在各个线程内都是可用调用的;如果是变量,那么在各个线程中都可以访问到。
- 线程还贡献一下进程资源和环境:
文件描述符表、每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)、当前的工作目录、用户id和组id。
线程独立的数据
进程内的线程共享进程的数据,但是也拥有自己独立的一部分数据。
线程ID、一组寄存器:存储线程的上下文信息、栈:线程的临时数据、errno、信号屏蔽字、调度优先级。
进程与线程的关系
我们之前接触到的只有一个线程执行流的进程,就是单线程进程。
总结
以上就是今天要讲的内容,本文介绍了本文介绍了地址空间和二级页表、Linux下的线程、线程的优缺点以及线程与进程的关系等概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!