线程的概念
目录
线程的概念
创建线程快速验证
物理内存管理
再谈页表
今天我们学习线程的概念
线程的概念
进程是一个指向起来的程序,进程=内核数据结构+代码和数据,线程称为指向流,执行粒度比进程要更细,是进程内部的一个执行分支,进程是承担分配系统资源的基本实体,线程是OS调度的基本单位,这是他们的区别,Linux下的线程是用进程模拟实现的,线程在进程的地址空间内运行,所以进程和线程是统一的。
一个进程的内部可以有很多个执行分支,我们之前学的进程内部只有一个执行分支,即内部只有一个线程。
既然线程是仿照这进程进行设计的,所以线程肯定也有自己单独的task_struct,然后这个内核数据结构指针的指向线程自己的执行地址空间肯定在一个进程的task_struct的代码区,所以进程的task_struct肯定大于等于线程的task_struct,当等于时,进程就变成线程了,所以我们以前学的单个进程也可以叫做线程。
线程是Linux的一个执行流,又是仿照的进程,所以线程又被统一的称为轻量级进程(LWP),Linux中没有真正意义上的线程,Linux的线程概念是用LWP进行模拟实现的。
轻量级进程(Lightweight Process, LWP)本身并不是一个数字,而是一个操作系统中的概念,指的是进程中的一个执行单元或线程。不过,在某些上下文中,LWP 可能会与一个唯一的标识符(ID)关联,这个标识符通常是一个数字,用于在系统中区分不同的 LWP。虽然这两者在表示上一样。
可以看到上面这个进程里面有很多个分支。Windows上的线程就是单独分开的,Windows 和 Linux 在线程设计上的差异主要源于它们不同的历史背景、设计哲学和系统架构目标。
创建线程快速验证
我们使用pthread_create函数创建线程。pthread_create
是 POSIX 线程(pthread)库中的一个函数,用于创建一个新的线程。它是多线程编程中非常重要的一个函数,允许程序在同一个进程中并发执行多个任务。可以循环的创建一堆的线程,注意是在同一个进程中创建的多个或者一个线程,所以根据上面的理论,这些创建的线程的pid一定是一样的,LWP的id一定是不一样的用来标记不同线程。
返回值不用管,第一个参数就是一个输出型参数,类型如上,第二个参数不用管直接传nullptr,第三个参数是创建的线程必须执行的函数(参数是void*,返回值也是void*),传的是函数指针,第4个参数是执行函数的参数,可以不使用但是不能不传。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
void* run(void* args)
{
while (true)
{
cout << "new thread" << endl;
sleep(1);
}
return nullptr;
}
int main()
{
cout << "我是一个进程" << getpid() << endl;
pthread_t tid;
pthread_create(&tid, nullptr, run, (void*)"thread-1");
//主线程
while (true)
{
cout << "main thread" << getpid() << endl;
sleep(1);
}
return 0;
}
主线程(Main Thread)是多线程程序中的一个特殊线程,它是程序启动时由操作系统自动创建的线程,负责执行 main
函数中的代码。主线程在多线程程序中扮演着核心角色,通常用于初始化资源、创建其他线程以及协调程序的整体执行流程。
我们可以吧线程当成进程来看,pthread_create创建一个线程,那相当于创建一个子线程,然后主线程创建子线程,所以main函数就是主线程是提前创建好的。
一般情况下要运行多线程程序需要自己编译的时候带-l选项自己链接线程的动态库以防找不到。
但是
-
在某些 Linux 发行版或编译器版本中,GCC 或 Clang 可能会默认链接 pthread 库,即使你没有显式指定
-lpthread
。比较新的版本都这样我的也不例外。
按结果来看,确实所有线程的pid都是一样的,
ps -aL
是一个用于查看当前系统中所有线程(包括轻量级进程,LWP)的 Linux 命令。它会列出所有进程及其关联的线程信息。我们可以使用这个选项查看我们刚刚创建的线程的id是否不一样。
可以看出不一样,真实调度的线程得看LWP的值
物理内存管理
操作系统需要管理内存,按照先描述再组织的原则,当有EIF格式的可执行程序运行时inode里面的数据和data block里面的数据都以数据块的形式存入内存,Linux中,内存管理的基本单位是4KB,所以每次存入的数据块都是4KB的,不够4KB的也存4KB,所以物理内存就是多个4KB的数据块的叠加,每个数据块又叫一个页,把物理内存按照⼀个固定的⻓度的⻚框进⾏分割,有时叫做物理⻚。每个⻚框包含⼀个物理⻚ (page)。⼀个⻚的⼤⼩等于⻚框的⼤⼩。⼤多数 32位 体系结构⽀持 4KB 的⻚,⽽ 64位 体系结 构⼀般会⽀持 8KB 的⻚。假设⼀个可⽤的物理内存有4GB 的空间。按照⼀个⻚框的⼤⼩4KB 进⾏划分, 4GB 的空间就是 4GB/4KB = 1048576 个⻚框。有这么多的物理⻚,操作系统肯定是要将其管理起来的,操作系统 需要知道哪些⻚正在被使⽤,哪些⻚空闲等等。
内核⽤ struct page 结构表⽰系统中的每个物理⻚,出于节省内存的考虑, struct page 中使 ⽤了⼤量的联合体union。
在这个结构里面有一下参数比较重要:
1. flags :⽤来存放⻚的状态。这些状态包括⻚是不是脏的,是不是被锁定在内存中等。flag的 每⼀位单独表⽰⼀种状态,所以它⾄少可以同时表⽰出32种不同的状态。这些标志定义在 中。其中⼀些⽐特位⾮常重要,如PG_locked⽤于指定⻚是否锁定, PG_uptodate⽤于表⽰⻚的数据已经从块设备读取并且没有出现错误。当flag等于1时就是表示page管理的数据块(页)未使用,当正在使用时flag = 1 << 1。
2. _mapcount :表⽰在⻚表中有多少项指向该⻚,也就是这⼀⻚被引⽤了多少次。当计数值变 为-1时,就说明当前内核并没有引⽤这⼀⻚,于是在新的分配中就可以使⽤它。
3. virtual :是⻚的虚拟地址。通常情况下,它就是⻚在虚拟内存中的地址。有些内存(即所谓 的⾼端内存)并不永久地映射到内核地址空间上。在这种情况下,这个域的值为NULL,需要的 时候,必须动态地映射这些⻚。
然后肯定有很多的page,所以物理内存里面有可以专门存放page的数组。
这样每个page就有了下标,并且每个struct page的大小是固定的,所以只需要得到mem_map的首地址然后根据下标进行运算就可以得到固定一个page的地址,这可以实现下标到物理地址的快速互相转换。并且每个page都可以映射到自己管理的数据块,数据块和page都是按照下标排好一一对应的,所以当要访问一个数据块时,只需要找到对应的page就可以映射对应的数据块,接着在查找flag标记得到对应数据块的使用状态。page管理数据块的属性inode,引用计数等等。
再谈页表
⻚表中的每⼀个表项,指向⼀个物理⻚的开始地址。在 32 位系统中,虚拟内存的最⼤空间是 4GB , 这是每⼀个⽤⼾程序都拥有的虚拟内存空间。既然需要让 4GB 的虚拟内存全部可⽤,那么⻚表中就需 要能够表⽰这所有的 4GB 空间,那么就⼀共需要 4GB/4KB = 1048576 个表项。如下图所⽰:
那如果只有一个页表作为虚拟地址向物理地址映射的中转站,那岂不是要开得很大,所以操作系统不可能只有一个页表,而是把⻚表看成普通的⽂件,对它进⾏离散分配,即对⻚表再分⻚, 由此形成多级⻚表的思想。
首先由一个装着1024个分项的页目录引出下一级的页表,这个页目录里面分出的1024个等大的空间称为页目录表项,每个页目录表项装着的又是一个1024个分项的页表,我称为(2级)页表,其实就是页表这些2级页表里面装着的是对应的物理内存的页框的起始地址(数据块的首地址)。
这样就相当于由原来的一个页表完成这么多地址的映射,转而通过那么多分页表的映射。那到底是这么由虚拟地址映射到物理地址的。
假设今天来了一个32位的虚拟地址如下,不是整个地址都映射过去的不然分级页表就没有意义了。
先拿着32位地址的前10bite位(绿色部分)来查页目录,再拿中间10个bite位(红色部分)来查页目录中的页表,就是拿着前10位去页目录查找索引对应的页表就找到了这个地址映射的页表,然后拿着中间10位去查那个可以帮忙映射的页表然后引索出对应映射的物理内存的页框的起始地址,由于bite位非0即1,所以每次查找用10个bite位那查找结果就会有1024钟情况,所以页目录和页表才会有1024个空间,所以我们利用地址的·1前20位就可以找到对应页框的起始地址,最后的后12位,是页框的某个字节的地址是一个偏移量,所以前面找到了页框的地址再结合最后12位的偏移量就可以找到某个字节的地址,就是找到起始地址后做页内偏移从而找到对应的物理地址。虚拟地址和物理地址其实没什么关系,在同一个4KB中所有的虚拟地址和物理地址都是按顺序排布的,在同一个4KB中任何一个虚拟地址或者物理地址都是可以互相转化的。那我们这么知道这个映射的页框存不存在或者状态如何呢?,我们页目录中的页表已经拿到了页框的起始地址做物理地址的映射的时候会判断的,再者我们不是已经拿到页框的起始地址的吗,页框的起始地址和对应管理它的page数组的对应下标是可以互相转化的,转化完成后得到管理其的page接着查找struct page的flag状态标识符就可以知道了呀!!!
一般一个进程虚拟地址向物理地址的映射一般不需要这么多的页表同时被创建,具体分配多少个页表取决于要映射的虚拟地址的个数。
一个虚拟地址的映射工作肯定还需要借助CPU的,当一个可执行程序要执行时,会去page数组申请一个数据块,然后将自己的数据代码什么的都导入其中,然后构建页表映射,什么什么的,直到所有的页表映射完成将自己main函数的虚拟地址放进CPU的esp寄存器,那进程调度这个可执行时怎么拿到页表结果的呢,进程里面是没有指针指向的,这个可执行程序的页表指向装在CPU内部的CR3寄存器里面,CPU里面的MMU(内存管理单元)会介个CR3指向的页表将虚拟地址转化成物理地址然后传出来,实现虚拟地址进,物理地址出。