本文介绍了地址空间和二级页表、Linux下的线程、线程的优缺点以及线程与进程的关系等概念。
地址空间是进程能看到的资源窗口:一个进程可以看到代码区、堆栈区、共享区、内核区等,大部分的资源是在地址空间上看到的。
页表决定进程真正有用资源的情况:进程认为自己独占系统的4GB资源,但实际上进程拥有多少物理资源是由页表决定的。
合理的对地址空间和页表进行资源划分,我们就可以对进程所拥有的资源进行分类:通过地址空间的区域划分,划分为栈区、堆区……,通过页表映射到不同的物理内存中。
在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是一种硬件映射,虚拟地址转为物理地址实际上是软硬件结合的。
修改常量字符串为什么会发送错误?
如果要修改一个常量字符串,虚拟地址需要经过页表映射查找到对应的物理内存,但是在查表的过程中会发现该地址的权限是只读,对一个只读地址进行修改会导致在MMU内部触发硬件错误,OS识别到这个错误会该对应进程发送信号终止对应进程。
线程对应的模型:进程的创建实际上伴随着进程控制块(PCB)、进程地址空间(mm_struct)以及页表的创建(虚拟地址和物理地址是通过页表建立映射的):
进程 = 内核数据结构 + 代码和数据。
每个进程都有字节独立的进程地址空间和独立的页表,这意味着每个进程在运行时会具有独立性,
如果我们在创建进程时只创建进程的PCB,并要求创建出来的PCB不再独立创建资源,而是与父进程共享资源。那么创建的结果就是下面这样的:
因为我们可以通过虚拟地址空间 + 页表的方式对进程的资源进行划分,单个进程的执行力度会比之前的进程更细。
上图中每个线程都是当前进程的一个执行流,线程在进程的内部运行,在进程的地址空间运行,拥有该进程的一部分资源。
创建进程时,申请的PCB、虚拟内存空间、页表以及加载到物理内存中的代码和数据:花费CPU资源创建进程并初始化;花费内存资源保存进程的内核数据结构、代码和数据;花费CPU的IO资源从外设IO到内存。所以承担分配系统资源的基本实体是进程。
总结一下,我们创建进程时,OS申请一堆的内核数据结构占用资源,进程的代码和数据加载到内存中也要占用资源,以及其他部分占用的资源。因此,进程是承担系统资源分配的基本实体。
我们之前讨论的进程都是只有一个PCB,也就是说该进程内部只有一个执行流,即单执行流,这与我们上面讲的并不冲突,如果是像上面这样的一个进程内部由多个执行流,那它就是多执行流进程。
不能,也不需要,CPU不关心当前调度的是进程还是线程。CPU以task_struct为单位进行调度,今天我们喂给CPU的task_struct是小于等于过去所说的task_struct的,它比之前的更轻量化。因此,在Linux中可以把进程和线程做一个统一,CPU看到的task_struct称为轻量级期间进程。
在Linux中,什么是线程?——线程是CPU的基本调度单位。
Linux下的线程是用进程模拟的。
如果OS真正要专门设计“线程”概念,OS就要管理线程了(先描述,再组织)。
Windows下确实是为线程专门设计了数据结果表示线程对象TCB,但是线程的创建就是为被执行,执行需要被调度、存在ID/状态、优先级、上下文、栈……等内容,这些线程调度需要的东西与进程有很多地方是重叠的。因此,Linux下没有为“线程”专门设计对应的数据结构,而是直接复用了进程的PCB,用PCB来表示Linux下的“线程”。
一个线程如果出现了异常会影响其他线程(健壮性、鲁棒性较差)
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号段错误信号,这就终止了当前的进程执行流,当前进程就退了,而线程所拥有的资源是进程给的,进程没了,线程也就得退出了。
当线程如果出现除零、野指针问题,会导致当前线程崩溃,进程也会随之崩溃。线程是进程的执行分支,线程出现异常,就等同于进程出现异常,进而触发信号机制,终止进程。进程终止了,进程内运行的所有线程也就终止了。
进程是承担分配系统资源的基本实体,线程是系统调度的基本单位。
进程内的线程共享进程的数据,但是也拥有自己独立的一部分数据。
线程ID、一组寄存器:存储线程的上下文信息、栈:线程的临时数据、errno、信号屏蔽字、调度优先级。
以上就是今天要讲的内容,本文介绍了本文介绍了地址空间和二级页表、Linux下的线程、线程的优缺点以及线程与进程的关系等概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!