1. Linux线程概念
1.1 什么是线程
- 一个程序内部的控制序列(一个执行路线)
- 线程在进程地址空间内运行
- Linux 线程可以用进程来模拟
- 对资源的划分,本质是对地址空间虚拟地址范围的划分,虚拟地址就是资源的代表
- 代码区划分就是让线程未来执行ELF程序不同的函数即可
- 以前的进程:内部只有一个线程的进程
- Linux的线程,就是轻量级进程,或者用轻量级进程模拟实现的
- 轻量级进程(LWP)=单个PCB(进程控制块,Linux里是task_struct)
- 进程 = 1个或多个LWP(PCB/task_struct)+共享的虚拟内存空间+页表+文件描述符等全局资源
1.2 分页式存储管理
- 我们希望操作系统提供给用户的空间是连续的,但是物理内存最好不要连续
- 把物理内存按照一个固定的长度的页框进行分割,也叫做物理页
- 页框是一个存储区域,页是一个数据块,可以存放在任何页框或磁盘中
- 将虚拟内存下的逻辑地址分为若干页,将物理内存空间分为若干页框,通过页表便能把连续的虚拟内存,映射到若干个不连续的物理内存页
1.2.1 物理内存管理
- 具体物理地址 = 真实物理地址 + 页内(4KB)偏移
- 申请物理内存是在做什么
- 查数组,改page(内核用 struct page 的结构表示系统中的每个物理页)
- 建立内核数据结构的对应关系
1.2.2 页表
- 页表中的每一项,指向一个物理页的开始地址
- 虚拟内存的最大空间是 4GB,这是每一个用户程序都拥有的虚拟内存空间
- 映射表本身,就占有 4MB/4KB=1024个 物理页
1.2.3 页目录结构
- 管理页表的表称为页目录表,形成二级页表
- 细节1:申请内存-》查找数组-》找到没有被使用的page-》page index -》物理页框地址
- 细节2:写时拷贝,缺页中断,内存申请等,背后都可能要重新建立新的页表和建立映射关系的操作
- 细节3:进程时一张页目录+n张页表构建的映射体系,虚拟地址时索引,物理地址页框是目标,虚拟地址(低12)+页框地址=物理地址
- 细节4:为什么是低12位?数字?低?
- 页框大小是4KB(2^12B),高20位标记是页号(同一个页框页号不变),那么虚拟地址是连在一起的,低12位用作物理地址偏移量
- 页目录的物理地址被CR3寄存器指向
- CR3存放的是 页表的物理起始地址
- CPU给出虚拟地址后,MMU根据CR3找到页目录起始位置,再将虚拟地址翻译为物理地址
- 单级页表对连续内存要求高,于是引入了多级页表,但是多级页表在减少连续存储要求且减少存储空间的同时降低了查询效率
1.2.4 缺页异常
- 页表中虚拟地址先被分配,物理地址的分配被推迟到真正需要访问的时候,就触发了这种机制
- 如果页号合法但页面不在内存中,则为缺页中断;如果页号非法,则为越界访问
- 如果地址在映射范围内但页面不在内存中,则为缺页中断;如果地址不在映射范围内,则为越界访问
1.3 线程优缺点
1.3.1 优点
- 创建新线程的代价比创建一个新进程小
- 线程的切换虚拟内存空间依然是相同的,但是进程切换不同
- 上下文的切换回扰乱处理器的缓存机制
- 进程切换回导致TLB(快表)和Cache(高速缓存),下次运行,需要重新存储
- 线程占用的资源比较少
- 可以同时等待不同的I/O操作
1.3.2 缺点
- 增加了额外的同步和调度开销,而可用的资源不变(创建太多线程和进程)
- 线程之间是缺乏保护的
- 在一个线程中调用某些OS函数会对整个进程造成影响
1.4 线程异常
- 单个线程容易出现除0,野指针导致线程崩溃,进程也会崩溃
- 线程是进程的执行分支,线程出异常回终止进程
2. Linux进程VS线程
- 进程具有独立性
- 线程共享地址空间,也就是共享进程资源
2.1 进程和线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程私有的数据
- 线程ID
- 一组寄存器(线程的上下文数据)
- 独立的栈结构(线程是一个动态的概念)
- errno
- 信号屏蔽字
- 调度优先级
2.2 进程的多个线程共享
- 各线程共享以下进程资源和环境
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户id和组id
3. Linux线程控制
3.1 POSIX线程库
- 头文件 <pthread.h>,链接时要加 "-lpthread" 选项
- linux不存在真正意义上的线程,而是用轻量级进程模拟的
3.2 创建线程
cpp
复制代码
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:传给线程启动函数的参数
- 返回值:
3.2.1 获取当前线程ID
cpp
复制代码
pthread_t pthread_self(void);
- pthread库给每个线程定义的进程内唯⼀标识,是pthread库维持的
- 此"ID"的作用域是进程级而非系统级
- ps -aL 查看所有线程信息
- 除了主线程之外的其他线程的栈都在共享区
3.3 线程终止
cpp
复制代码
void pthread_exit(void *value_ptr);
- 功能:线程终止,自己退出(在谁的要执行的函数中就杀谁)
- 参数列表
- value_ptr:线程退出的返回值,不需要写NULL
cpp
复制代码
int pthread_cancel(pthread_t thread);
- 功能:取消一个执行中的线程,其他人强制杀死
- 参数列表
- 返回值:
3.4 线程等待
- 为什么需要
- 已经退出的线程,空间没有被释放,仍然在进程的地址空间内
- 创建新的线程不会复用退出线程的地址空间
cpp
复制代码
int pthread_join(pthread_t thread,void **value_ptr);
- 功能:
- 参数列表:
- thread:线程ID
- value_ptr:指向一个指针,它指向线程的返回值
- 返回值:
3.5 分离线程
- 默认情况下,线程是joinable的,线程退出后要进行 pthread_join 操作,否则无法释放资源,造成内存泄漏
cpp
复制代码
int pthread_detach(pthread_t thread);
- 功能:线程自己分离 pthread_detach(pthread_self()); ,自己释放资源
- 参数列表: