文章目录
一、线程基本概念
1.1 进程地址空间与页表
注意这里的页表部分:
在上一章【linux】进程信号——信号的保存和处理中我们讲了页表有用户级页表和内核级页表。如图其实页表还有其他很多属性。
举个例子:当我们对常量区的数据进行修改时,为什么会报错呢?
OS会先通过页表找到物理地址,然后查RWX权限,发现只有R权限,所以地址转换单元MMU会硬件报错,转化成信号,终止进程(段错误)。
而经过U/K权限的时候,如果是U就直接访问,是K就会去CPU查看当前的运行级别,是内核级别才能访问带K的映射关系。
那么该如何看待进程地址空间和页表呢?
1️⃣ 进程地址空间(虚拟内存)是进程能够看到的资源窗口。因为能看到的资源都是通过进程地址空间让我们看到的。
2️⃣ 页表是决定进程真正拥有资源的情况。
3️⃣ 合理的对进程地址空间+页表进行资源划分,就可以对进程的所有资源进行分类。
进程地址空间一共有2^32
个地址。那么按道理页表也应该有2^32个条目,我们就当一个条目大小为1byte,也需要4GB的大小,更何况每个条目还得存很多数据。所以页表不可能会这么使用。
那么真实的页表到底是什么样子呢?
1.2 页表结构
首先说明几个点:
1️⃣ 进程地址空间的一个地址我们称为虚拟地址,有32个比特位。
2️⃣ 物理内存实际上也划分成了一个一个的数据页。OS为了管理每个数据页,每个数据页都有一个描述的结构体(非常小),存储内存的属性。每个页框大小为4KB
3️⃣ 磁盘上的可执行程序在被编译的时候也被划分成一个一个的4KB大小的数据块,我们把这种4KB的区域称为页帧。所以从磁盘加载到内存是以4KB为单位加载的。
虚拟地址的32个比特位并不是以一个整体转化的,而是分成10、10、12三块二进制构成。
而页表也不止一张,分为页目录、和页表。
先拿着虚拟地址的高十位去查页目录,比如如果是0000000001就是第二个位置,映射到指定的页表,再通过中间10个比特位确定物理内存中页框的起始地址,而一个页框的大小是4KB,有2^12字节,刚好对应虚拟地址的低12位比特位,就可以作为页内偏移量找到对应的位置。
这样我们在使用的时候有可能只使用了几个页表,那么其他的页表就不会加载到内存,只有需要的时候才会创建。由此解决了内存不足的问题。
1.3 线程的理解
首先要知道线程是进程内的一个执行流。
我们知道创建一个进程就会连着创建PCB,虚拟内存、页表。现在我们可以创建一个“进程”(PCB)直接指向虚拟内存,就像下边的绿色的task_struct
。
例如代码区有一大段代码,我们现在就可以划分成几个小段代码,分给每个“进程”。这样就实现了资源的分配。
我们可以通过虚拟地址空间和页表对进程进行资源划分,而单个“进程”的执行力度一定要比之前一个进程要细。
1.3.1 如何描述线程
既然有多个线程,那么OS就会采取先描述后组织的方式进行管理。那么怎么描述呢?是创建一个新的结构体来描述吗?
我们知道PCB是用来描述进程的,那么描述线程的结构体我们叫做TCB(线程控制块)。
在windows中,就是新创建了一个结构体来描述线程。
而单纯的从线程调度角度,进程和线程有很多地方是重叠的。
所以在linux中,没有创建针对线程的数据结构,而是直接复用PCB,用PCB来表示线程。
而CPU在进行调度的时候不关注到底是进程还是线程,只看task_struct。
总结一下:线程在进程内部(进程的地址空间内)执行,拥有该进程的一部分资源。
1.4 再谈进程
什么叫做进程呢?
我们把红色框框圈起来的整体叫做进程:
PCB+进程地址空间+页表+加载到物理内存的代码和数据。
从内核角度:进程是承担分配系统资源的基本实体
在linux中:线程是CPU调度的基本单位
而在之前的文章讲过的进程【linux】进程概念详述它讲的是只有一个PCB的进程(只有一个执行流)。
今天所讲述的是一个进程内有多个执行流的情况。
从CPU角度:以前调度的就是一个进程,今天就是调度进程中的一个分支。
所以现在CPU统一把task_struct看作成轻量级进程。
我们知道linux没有正真意义的线程,这相比拥有真正线程的系统有什么优缺点呢?
优点:简单,维护成本大大降低,即可靠又高效。
缺点:linux无法直接提供线程的基本调用接口,只能提供创建轻量级进程的接口。
1.5 代码理解
1.5.1 原生库提供线程pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.// 链接的时候必须加上-lpthread
RETURN VALUE
On success, pthread_create() returns 0;
on error, it returns an error number, and the contents of *thread are undefined.
参数说明:
thread
:线程id
attr
:线程属性,直接设为null
start_routine
:函数指针
arg
:这个参数会传递进start_routine
的void*
参数中。
这里在链接的时候要注意link到系统给的原生线程库-lpthread
。
说明一下这个原生线程库:
因为用户只关注线程,但是OS不提供线程的接口,只提供创建轻量级进程的接口。所以在用户和OS之间加了一个用户级线程库。 向上提供各种线程接口,向下把对线程的各种操作转化为对轻量级进程的各种操作。
这个库在任何linux操作系统都默认存在。
// Makefile
mythread:mythread.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f mythread
// mythread.cc