Linux:线程基础(虚拟地址,分页)
目录
线程概念
线程优点
线程缺点
分页式存储管理
虚拟地址和分页
多级页表思想
页目录结构
缺页异常
线程控制
线程创建
线程终止
线程等待
线程分离
线程概念
在一个程序里的一个执行路线就叫做线程
线程是“一个进程内部的控制序列”
一切进程都至少有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理的分配给每个执行流,就形成了线程执行流
在Linux下线程也叫轻量级进程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
线程优点
- 创建一个新线程比创建一个新进程的代价要小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要小得多(1.线程切换虚拟地址空间仍然是相同的,进程的切换是不同的。2.上下文的切换会扰乱处理器的缓存机制)
- 线程占用的资源要比进程少得多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可进行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程缺点
- 性能损失(这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变)
- 健壮性降低(线程之间是缺乏保护的)
- 缺乏访问控制(在一个线程中调用某些OS函数会对整个进程造成影响)
- 编程难度提高(编写与调试一个多线程程序比单线程程序困难得多)
分页式存储管理
虚拟地址和分页
若是没有虚拟地址和分页机制的情况下,每一个用户程序在物理内存上对应的空间必须是连续的
这样的话,经过一段时间的运行,有的程序就会退出,它所占据的物理内存空间会被回收,这样就导致了这些物理内存很多都是以碎片的形式存在
我们希望操作系统提供给用户的空间必须是连续的,但是物理内存最好不要连续,所以就需要虚拟地址和分页了
把物理内存按照一个固定长度的页框进行分割,有时叫做物理页
一个页的大小等于页框的大小
32位体系结构支持4kb的页,64位体系结构支持8kb的页
- 页框是一个存储区域
- 页是一个数据块,可以存放在任何页框或磁盘中
其思想是将虚拟地址下的逻辑地址空间分成若干页,将物理内存空间分成若干页框,通过页表就能把连续的虚拟内存映射到若干个不连续的物理内存页。这样就解决了使用连续物理内存造成碎片的问题
多级页表思想
在32位系统中,虚拟内存的最大空间是4GB,这是每一个用户程序都拥有的虚拟地址空间。要让这4GB全都可用都能表示出来,那么就需要4GB / 4KB = 1048576个表项
在32位系统中,地址的长度是4字节的,那么页表的每一个表项就需要占用4个字节,所以页表占据的总空间大小就是1048576*4 = 4MB的大小
也就是说光是映射页表本身就需要4MB / 4KB = 1024个物理页。使用页表不就是为了解决物理内存可以不用连续存放的问题吗,但如今却需要连续存放了,由此就产生了多级页表的思想
把页表看成普通文件,对它进行离散分配,即对页表再分页
可以把单一页表拆分成1024个体积更小的映射表,这样一来1024(表的表项个数) * 1024(表的个数) = 4GB仍然能覆盖4GB的物理内存空间
一个应用程序是不可能完全使用全部的4GB空间的,这样就可以让我们只需要部分的页表就能够让一个进程正常运行了
页目录结构
每一个页框都被一个页表中的表项来指向了,那么这个1024个页表也需要被管理起来。
管理页表的表称为页目录表,形成二级页表
页目录的物理地址被CR3寄存器指向,这个寄存器会保存当前正在执行任务的页目录的地址
缺页异常
若是有了虚拟地址,在页表中没有找到对应的物理地址怎么办?这就是缺页异常Page Fault
它是一个由硬件中断触发的可以由软件逻辑纠正的错误
这样CPU就无法获取数据,用户进程也就出现了缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的Page Fault Handler处理
缺页中断会交给Page Fault Handler处理,其根据缺页中断的不同类型会进行不同的处理:
- Hard Page Fault 也被称为 Major Page Fault,翻译为硬缺页错误/主要缺页错误,这时物理内存中没有对应的物理页,需要 CPU 打开磁盘设备读取到物理内存中,再让 MMU 建立虚拟地址和物理地址的映射。
- Soft Page Fault 也被称为 Minor Page Fault,翻译为软缺页错误/次要缺页错误,这时物理内存中是存在对应物理页的,只不过可能是其他进程调入的,发出缺页异常的进程不知道而已,此时 MMU 只需要建立映射即可,无需从磁盘读取写入内存,一般出现在多进程共享内存区域。
- Invalid Page Fault 翻译为无效缺页错误,比如进程访问的内存地址越界访问,又比如对空指针解引用,内核就会报 segment fault 错误中断进程直接挂掉。
线程控制
要使用线程相关的函数,需要我们引入线程库的头文件
#include <pthread.h>
链接这些线程函数库时要使用编译器命令的“-lpthread”选项
线程创建
创建一个新线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值
成功:返回0
失败:返回错误码
我们也可以获取当前线程的ID
pthread_t pthread_self(void);
线程终止
终止调用它的线程
void pthread_exit(void *value_ptr);
value_ptr线程退出时的返回值
取消⼀个执⾏中的线程
int pthread_cancel(pthread_t thread);
thread:目标线程的线程 ID,指定要取消的线程
返回值
成功:返回0
失败:返回错误码
线程等待
用于等待一个线程终止,并获取它的退出状态
int pthread_join(pthread_t thread, void** retval);
thread:要等待的线程的线程 ID
retval:用于存储被等待线程的退出状态。如果线程是通过pthread_exit退出的,retval会指向线程返回的值。如果不需要获取返回值,可以将retval设置为NULL。
返回值
成功:返回0
失败:返回错误码
线程分离
用于将线程设置为分离状态。分离状态的线程在终止时会自动释放其占用的资源,而不需要其他线程通过pthread_join来回收资源
int pthread_detach(pthread_t thread);
thread:要分离的线程的线程 ID
返回值
成功:返回0
失败:返回错误码
完