线程基本概念
目录
一、线程的基本概念
二、进程和线程的对比
三、重谈地址空间
(1)页表的作用
(2)物理内存管理
四、虚拟到物理地址的转化
页表的原理
五、线程
(1)POSIX 线程库
(2)Linux 中多线程的实现——内核角度
(3)线程接口
线程创建
LWD VS pthread_t
线程等待
线程退出
线程分离
(4)线程切换
(5)Linux 的线程vs 进程
1、进程和线程的区别
2、特点
线程崩溃
六、c++实现线程库
一、线程的基本概念
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
- 一切进程至少都有一个执行线程。
- 线程在进程内部运行,本质是在进程地址空间内运行。
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
二、进程和线程的对比
进程:一个运行起来的执行流,一个加载到内存中的程序, 进程 = 内核数据 + 自己的代码和数据
一个进程的创立必须要建立进程控制块、进程地址空间 以及页表的建立

线程:进程内部的一个执行流,轻量化
在Liunx角度如果我们只创建进程控制块 task_stuct 就是在建立线程

观点:进程是系统分配资源的基本单位(从内核角度),线程是CPU调度的基本单位
三、重谈地址空间
(1)页表的作用
如果在没有虚拟内存和分页制的情况下,每⼀个用户程序在物理内存上所对应的空间必须是连续的

因为每⼀个程序的代码、数据长度都是不⼀样的,按照这样的映射方式式,物理内存将会被分割成各种离散的、大小不同的块。经过⼀段运行时间之后,有些程序会退出,那么它们占据的物理内存空间可以被回收,导致这些物理内存都是以很多碎片的形式存在
 我们希望操作系统提供给用户的空间必须是连续的,但是物理内存最好不要连续。此时虚拟内存和分⻚便出现了,如下图所示
 
物理内存实际上被分为一个个以4KB为基本单位的页框
把物理内存按照⼀个固定的长度的页框进行分割,有时叫做物理页。每个页框包含⼀个物理页(page)。⼀个页的大小等于页框的大小。大多数32位体系结构支持4KB的页,而64位体系结构⼀般会支持8KB的页。区分一页和⼀个页框是很重要的
- 页框是⼀个存储区域;
- 页是⼀个数据块,可以存放在任何页框或磁盘中。
有了这种机制,CPU 便并非是直接访问物理内存地址,而是通过虚拟地址空间来间接的访问物理内存地址。所谓的虚拟地址空间,是操作系统为每⼀个正在执行的进程分配的⼀个逻辑地址,在32位机上,其范围从0 ~ 4G-1。
操作系统通过将虚拟地址空间和物理内存地址之间建立映射关系,也就是页表,这张表上记录了每⼀对页和页框的映射关系,能让CPU间接的访问物理内存地址。
(2)物理内存管理
内存在逻辑上被分为无数个页框,我们靠先描述在组织来管理物理内存
描述

组织:
- 对整个物理内存管理就转化成为了对数组的增删查改
- 所以,我们访问内存的之后可以直接访问数组下标, 物理块起始地址 = 数组下标*4kb
- 我们申请一个内存块4kb,本质是只要你申请到struct page, 得到struct page的下标,那么物理内存的所有地址
- 申请内存,本质就是申请一个struct page结构
OS系统如何得知,所有物理内存块的地址
OS只要得到page数组的起始地址即可
四、虚拟到物理地址的转化
页表的原理

- 从虚拟地址,转换到物理地址,默认是没有直接转化到字节的,查页表,只帮我们找到你要访问那一个页框(物理页的起始地址)
- 通过虚拟地址前十位来找到页目录的哪一个位置,知道哪一个页表后,通过中间的十位来找到页表的哪一个物理页框的起始地址,通过虚拟地址的低12位,做页内偏移 (真正的物理地址 = 页框起始地址+ 页内偏移)
细节
- cr3保存当前进程页表的基地址,物理地址
- 虚拟地址的划分,这个过程编译器不关心
- 高二十位相同的地址,一定是连续存放在一个页框的
- 如果知道任意一个虚拟地址,& 1111 1111 1111 1111 1111 0000 0000 0000得到处在的页框
- 页框号/4kb = 数组下标
- 进程PO首次加载磁盘块的时候,OS做什么 内存管理 ,申请内存 --> 申请page --> index --> 页框的物理地址 --> 填充页表
- 如何重新理解写时拷贝,缺页中断 OS内申请和管理物理内存,都以4kB为单位。
- 我们使用new ,malloc 申请为什么是随意申请的 new ,malloc 底层一定要使用系统调用,而调用系统调用是有成本的,所以c c++自己在语言层,会有自己的内存管理机制。
五、线程
(1)POSIX 线程库
- 应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方帮我们提供的。
- 原生指的是大部分Linux系统都会默认带上该线程库。
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
- 要使用这些函数库,要通过引入头文件<pthreaad.h>。
- 链接这些线程函数库时,要使用编译器命令的“-lpthread”选项
错误显示
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分POSIX函数会这样做),而是将错误代码通过返回值返回。
- pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小。
(2)Linux 中多线程的实现——内核角度

- Linux系统中,内核中,线程的实现,是用进程模拟的,复用了进程代码和结构,window中实现了专门控制线程的TCB
- Linux中,一个线程,在进程内部运行,线程在进程的虚拟地址空间中运行
- 让不同的线程未来执行,不同的入口函数
- Linux中不分线程和进程统一叫做轻量级进程
(3)线程接口
线程创建
clone

作用:系统调用, pthread_creat的底层调用, 创建轻量级进程(创建一个task_struct)
*fn : 函数指针,调用那个新函数
pthread_create

返回值:

*thread_t : 输出型参数 (并不是lwp,只是一个线程id具有独立性)

ps -aL (-L : 打印线程信息)

LWD VS pthread_t
pthread_t :
- 这个“ID”是 pthread 库给每个线程定义的进程内唯⼀标识,是 pthread 库维持的。
- 由于每个进程有自己的内存空间,故此“ID”的作用域是进程级而非系统级(内核不认识)。 其实 pthread 库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程创建
- 系统全局唯⼀的“ID”来唯⼀标识这个线程。
LWD
- LWP 得到的是真正的线程ID。之前使⽤ pthread_self 得到的这个数实际上是⼀个地址,在虚拟地址空间上的⼀个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。
- 在 ps -aL 得到的线程ID,有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
 
*attr: 线程属性(我们一般不设置,有内核设置)

为什么线程函数的返回值是void*
线程退出,不需要考虑异常
线程等待
- 如果线程执行完了自己的入口函数,表明该线程退出
- 如果main 结束,表示主线程结束,同时也表示,当前进程结束

- 所以线程创建必须让新线程等待
pthread_join

作用:阻塞等待 自动解决新线程僵尸问题
**retval :
- 如果thread新线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数
返回值: 等待成功返回0 ,失败返回错误码


如何拿到返回值的
线程的概念是由pthread的库所提供的,线程存在很多,所以库中会对线程进行先描述在组织
类似:


而pthread_t其实是一个地址,指向对应描述结构体的虚拟地址,而线程的返回值其实是写进了结构体中,pthread_join 从指定结构体中获取返回值

线程退出
exit

任何一个线程调用exit 都会导致整个进程退出
pthread_exit(返回值)


pthread_cancel

作用:让一个线程结束另外一个线程

pthread_self
得到自己的线程id可以自己结束自己
线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
如果不关心线程的返回值,join 是一种负担,这个时候,我们可以告诉线程,当线程退出时,自动释放线程资源
pthread_detach

注意: 一个线程不能即joinable的又是detach的
(4)线程切换
与进程之间切换,线程之间的切换需要操作系统做的工作要少很多
- 线程间切换,不需要切换CR3寄存器(指向页表)
- TLB (缓存虚拟到物理地址)线程间切换不要更新
- CPU中有一个硬件cache硬件:缓存的是内存数据,线程间切换cache不用更新
(5)Linux 的线程vs 进程
1、进程和线程的区别
- 进程:承担分配系统资源的基本实体
- 线程:操作系统调度的基本单位
2、特点
- 进程具有独立性
- 线程共享地址空间,也就共享进程资源,但也拥有自己的一部分私有的数据:线程ID,一组寄存器,线程的上下文数据 , 栈, errno,信号屏蔽字,调度优先级
观察下面的代码

有上图我们会发现名字基本都是thread-10那是因为,我们进入一个线程的时候由于时间片到了可能切换被切换执行另外一个线程,又因为线程是共享数据的可能出现数据被修改了,才回到原来的线程,从而造成上述的情况
正确书写

线程崩溃
线程中只要有一个线程崩溃了,整个进程就会崩溃

六、c++实现线程库

线程管理不是以进程为单位的,是全部系统中的lwp最后都有pthread库管理
线程封装(以面向对象)


