🔥个人主页:Quitecoder
🔥专栏:linux笔记仓
01.背景知识
OS进行内存管理,不是以字节为单位的,而是以内存块为单位的,默认大小为4kb,我们也学过,系统和磁盘文件进行IO的基本单位是4kb–8个扇区
可执行文件要加载到内存中,我们前面知道,文件系统中文件是以块为单位进行存储的,加载到内存中,也以块的方式加载访问,所以才有了IO的基本单位是4kb
内存块是操作系统进行内存管理的基本单位,称为页(page),操作系统为文件分配内存空间,以页为单位
操作系统现在对内存的管理及转为对一个个页的管理,先描述,再组织
struct page
{
int flag;//是否被占用,是否是脏页,是否被锁定
int mode;
....
}
struct page memory[1048576];
每一个页帧用数组管理
页表我们学过,是虚拟地址到物理地址的映射,详细讲解
虚拟地址就是二进制构建的32个比特位的数据
虚拟地址被拆分为3部分,10位10位12位,页表也不是只有一张,页表开始以前十个bit位,表示的范围是2的十次方
• 页目录索引(Page Directory Index):高 10 位,用于定位 页目录表(Page Directory) 中的条目。
• 页表索引(Page Table Index):中间 10 位,用于定位 页表(Page Table) 中的条目。
• 页内偏移(Page Offset):低 12 位,用于定位页内的具体字节。
页表中放的是指向页框的起始地址
虚拟地址最低十二位,范围【0,4095】,页内偏移,刚好就是一个页的大小,用于定位页内具体字节的部分,例如一个整数,我就连续读取四个字节
还有一部分page不通过页表映射,通过struct file,缓冲区
每行代码都有地址,函数是连续的代码地址构成代码块,一个函数对应一批连续的虚拟地址
虚拟地址到物理地址的转换过程
以下是通过二级页表将虚拟地址转换为物理地址的过程:
虚拟地址分解
假设虚拟地址为 0x00401234
,其二进制表示为:
0000 0000 0100 0000 0001 0010 0011 0100
分解为:
• 页目录索引:0000 0000 01
(高 10 位,值为 1
)
• 页表索引:00 0000 0001
(中间 10 位,值为 1
)
• 页内偏移:0010 0011 0100
(低 12 位,值为 0x234
)
- 从 CR3 寄存器获取页目录表的基地址。
- 使用页目录索引(
1
)找到页目录表中的条目,获取页表的基地址。 - 使用页表索引(
1
)找到页表中的条目,获取物理页帧的基地址。 - 将物理页帧基地址与页内偏移(
0x234
)相加,得到物理地址。
页表项(PTE)的结构
每个页表项(PTE)的大小为 4 字节(32 位),包含以下字段:
• 物理页帧地址(Physical Page Frame Address):20 位(实际使用 20 位,支持 4GB 物理内存)。
• 标志位(Flags):12 位,包括:
• 有效位(Present Bit):指示页是否在内存中。
• 可写位(Writeable Bit):指示页是否可写。
• 用户位(User Bit):指示用户程序是否可以访问该页。
• 脏位(Dirty Bit):指示页是否被修改过。
• 访问位(Accessed Bit):指示页是否被访问过。
页表的大小
• 页目录表:1024 个条目,每个条目 4 字节,总大小为 4KB。
• 页表:1024 个条目,每个条目 4 字节,总大小为 4KB。
• 总大小:页目录表和所有页表的总大小取决于进程的虚拟地址空间使用情况。在最坏情况下,需要 1024 个页表,总大小为:
4 KB + 1024 × 4 KB = 4 MB + 4 KB ≈ 4 MB 4\text{KB} + 1024 \times 4\text{KB} = 4\text{MB} + 4\text{KB} \approx 4\text{MB} 4KB+1024×4KB=4MB+4KB≈4MB
02.线程概念
线程:在进程内部运行,是CPU调度的基本单位
以前我们知道,每次创建一个进程,都要创建一个地址空间和页表
现在不想给“进程”重新创建地址空间加载数据,直接让你新的pcb和父进程指向同一个地址空间,正文部分拆成多份由不同task_struct执行,这一部分task_struct就叫做Linux中的线程
上面一个整体为一个进程,进程=内核数据结构+进程代码和数据
线程实现基于 轻量级进程(LWP)
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
cpu对task_struct是进程还是线程不做区分,cpu看到的执行流为进程,Linux中的执行流:轻量级进程
Linux是用进程模拟的线程
OS要单独设计线程,先描述再组织,现在给线程专门设计结构TCB,可以复用PCB,用PCB统一表示执行流,这样的话,我们就不需要为线程单独设计数据结构和调度算法了
简单使用线程
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
参数分别为:
- 线程标识符tid
- 用于设置线程的属性,传入NULL使用默认属性
- 指定线程启动后执行的函数
- 传递给 start_routine 函数的参数
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
//新线程
void *threadStart(void *args)
{
while(true)
{
sleep(1);
cout<<"new thread run..."<<endl;
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,threadStart,(void*)"thread-new");
//主线程
while(true)
{
sleep(1);
cout<<"main thread run.."<<endl;
}
return 0;
}
注意这里的makefile里编译那一步必须链接thread库
testthread:testthread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f testthread
这里系统只有一个进程,我们还可以让线程输出它的pid
LWP:light weight process 轻量级进程,lwp就是轻量级进程的id
我们发现,有一个lwp与pid相同,为主线程。OS调度的时候,用的是LWP
进程创建成本非常高,创建线程,只需要创建pcb,然后把进程的资源全部给线程即可
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多,页表,地址空间都不需要切换,线程的调度成本低
删除一个线程成本也低
但线程也有劣势,一个线程出错可能会影响整个进程。这是因为线程是进程内的执行单元,它们共享进程的资源(如内存地址空间、文件描述符等)。如果某个线程出现错误(如非法内存访问、未捕获的异常等),可能会导致整个进程崩溃或进入不可预期的状态
线程调度成本更低
进程上下文切换:
- 进程拥有独立的虚拟地址空间,切换时需要切换页表,切换页表需要刷新 CPU 的 TLB(Translation Lookaside Buffer),这是一个耗时的操作
- 进程上下文需要保存和恢复CPU寄存器状态
- 还需要处理其他资源(文件fd,信号处理函数等)
线程共享进程的虚拟地址空间和资源,切换时不需要切换页表,也不需要分配和释放fd,内存等的资源,硬件只需要关注线程的私有数据,线程上下文切换也需要保存和恢复CPU寄存器状态
CPU 缓存的影响:线程共享进程的内存地址空间,因此线程切换时 CPU 缓存(Cache)的命中率较高。缓存中的数据可以继续被新线程使用,减少了内存访问的延迟
进程拥有独立的内存地址空间,因此进程切换时 CPU 缓存的命中率较低
线程私有的部分:一组寄存器:硬件上下文数据–线程可以动态运行
栈:线程在运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区
某一个线程将来也会被页表映射到物理内存,以4kb为单位的代码块