1.线程概念
在一个程序里的一个执行路线就叫做线程(thread),更准确定义:线程是一个进程内部的控制序列
进程至少有一个执行路线,线程在进程内部运行,本质是在进程地址空间内运行,在Linux系统中,CPU眼中,看到的PCB都要比传统的进程更加轻量化,透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
例子
社会分配资源的基本实体是家庭,家庭内部有人员,父亲,母亲,孩子,爷爷和奶奶。每一个人都有自己的事情做,而家庭的每一人就是线程,家庭就是一个进程,社会就是操作系统分配资源。
线程的实现
在Linux内核中,线程的实现是通过共享同一个 mm_struct
来实现的。具体步骤如下:
-
创建任务结构(
task_struct
):-
为每个线程创建一个
task_struct
,每个task_struct
包含线程的独立上下文信息,如栈、寄存器状态等。
-
-
共享内存管理结构(
mm_struct
):-
所有线程共享同一个
mm_struct
,这意味着它们共享同一个虚拟内存空间,包括代码段、数据段、堆和栈。
-
-
连接
task_struct
和mm_struct
:-
每个
task_struct
通过mm
指针指向共享的mm_struct
,从而实现线程之间的内存共享。
-
线程进行资源划分:本质是划分地址空间,获得一定合法虚拟地址,更深则是划分页表
线程进行资源共享:本质是对地址空间共享,更深是对页表条目的共享(物理地址空间)
分页式存储管理
虚拟地址和页表的由来
如果没有虚拟内存和分页机制,每一个用户程序在物理内存上所对应的空间必须是连续的。
因为每一个程序的代码,数据长度都是不一样的,按照这样的映射方式,物理内存将会被分割成各种离散的,大小不同的块。运行一段时间后,有些程序会退出,那么它们占据的物理内存空间可以被回收,由于程序的运行和退出是随机的,释放出来的内存块大小和位置也各不相同,这就导致了物理内存被分割成许多离散的、大小不一的碎片,导致物理内存很多都是以碎片形式存在。
需要的是系统提供给用户的空间必须是连续的,但是物理内存最好不要连续。此时虚拟内存和分页便出现了。
把物理内存按照一个固定长度的页框进行分割,叫做物理页,每个页框包含一个物理页(page),一个页的大小等于页框的大小,大多数32位体现结构支持4KB的页,而64位的体系结构一般会支持8KB的页。
页框:一个存储区域
页:是一个数据块,可以存放在任何页框或磁盘中
有了这种机制,CPU便也不是直接访问物理内存空间,而是通过虚拟地址空间来间接访问物理内存地址。所谓的虚拟地址空间,是操作系统为每一个正在执行的进程分配一个逻辑地址,在32位机上,范围0~4G-1。
操作系统通过将虚拟地址和物理内存地址之间建立映射关系,也就是页表,这张表上记录了每一对页和页框的映射关系,能让CPU间接的访问物理内存地址。
把虚拟内存下的逻辑地址空间分为若干页,将物理内存空间分为若干页框,通过页表就能把连续的虚拟内存,映射到若干个不连续的物理内存页。就可以解决物理内存造成的碎片问题。
本质相同但用途不同
本质相同:页和页框在本质上都是固定大小的内存块,它们的大小相同,都是内存管理的基本单位。
用途不同:
页:属于虚拟地址空间,是程序逻辑上的内存划分,方便程序访问和管理。
页框:属于物理地址空间,是实际物理内存的划分,用于存放逻辑地址空间中的页。
关于碎片
内存碎片化的类型
外部碎片 :这是你描述的情况,即物理内存被分割成许多离散的、大小不同的空闲块,这些空闲块之间存在间隔,无法被合并成一个连续的大块内存。例如,一个程序需要分配 1MB 的连续内存,但内存中存在许多小于 1MB 的空闲块,这些空闲块加起来可能总大小超过了 1MB,但由于它们不是连续的,所以无法满足程序的分配请求,这些分散的空闲块就构成了外部碎片。
内部碎片 :这是由于内存分配单位和程序实际需求之间的差异导致的。例如,在固定分区分配方式中,操作系统将物理内存划分成若干个固定大小的分区,每个分区分配给一个程序。如果程序的实际大小小于分区的大小,那么分区中剩余的部分就会产生内部碎片。比如,一个分区大小为 2MB,而程序只需要 1.5MB 的内存,那么剩下的 0.5MB 就是内部碎片。
物理内存不连续好处
. 物理内存最好不连续的原因
内存分配的灵活性 :物理内存不连续可以提高内存分配的灵活性。计算机系统中运行的程序数量和大小是动态变化的,如果要求物理内存必须连续,那么在分配内存时会受到很大的限制。例如,当一个大程序请求一块很大的连续物理内存时,如果物理内存已经被其他程序分割成许多小块,那么就很难找到足够大的连续空闲块来满足请求。而物理内存不连续时,操作系统可以通过将多个不连续的物理内存块分配给一个程序,从而更好地利用有限的物理内存资源。
减少内存碎片化的影响 :物理内存不连续可以有效减少外部碎片化的影响。如前面所述,内存碎片化会导致物理内存被分割成许多离散的、大小不同的空闲块,这些空闲块很难被有效地利用。而通过将不连续的物理内存块分配给程序,可以避免因为寻找连续空闲块而产生的碎片化问题。操作系统可以采用一些内存分配算法,如分页管理或分段管理,将物理内存划分为多个小块,然后根据程序的需求动态地分配这些小块,从而提高内存的利用率。
支持虚拟内存技术 :现代计算机系统普遍采用虚拟内存技术,虚拟内存技术的核心是将逻辑地址空间和物理地址空间分离。物理内存不连续是虚拟内存技术的基础之一。虚拟内存技术通过在逻辑地址空间和物理地址空间之间建立映射关系,使得程序可以访问比实际物理内存更大的地址空间。操作系统可以根据需要将部分程序和数据从外存调入物理内存,或者将物理内存中暂时不用的部分程序和数据换出到外存。这种映射关系使得物理内存的分配和管理更加灵活,而不需要物理内存是连续的。例如,在分页管理方式下,逻辑地址空间被划分为多个固定大小的页面,物理地址空间被划分为多个固定大小的页框,操作系统通过页表将逻辑页面映射到物理页框,逻辑页面可以映射到不连续的物理页框,从而实现物理内存的不连续分配。
物理内存的管理
假设有一个可用的物理内存有4GB空间。按照一个页框的大小进行4kb进行划分,4gb空间就是4GB/4KB=1048576个页框。有这么多的物理页,操作系统就就要管理起来,操作系统需要知道那些页正在被使用,哪些页空闲等。
内核用struct page结构表示系统中的每个物理页,出于节省内存考虑,struct page中使用的大量的联合体union。
参数解释:
flags:用来存放页的状态,这些状态是不是脏的,是不是被锁定在内存中等。flags每一位单独表示一种状态,所以它至少可以表示出32种不同的状态。
_mapcount:表示页表中有多少项指向该页,也就是这一页被引用了多少次。当计数值变化为-1的时候,就说明当前内核并没有引用这一项,于是在新的分配中就使用它。
virtual:是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址。有些内存(所谓的高端内存)并不永久的映射到内核地址空间上,
在内存管理的数据结构中,每个页结构(Page Structure)通常有一个
virtual
域,用于存储该页的虚拟地址。对于低内存,virtual
域会存储一个有效的虚拟地址,内核可以直接通过这个地址访问该页。对于高端内存,virtual
域为NULL
。
struct page于物理页相关,而并非于虚拟页相关。而系统中的每一个物理页都要分配一个这样的结构体。
struct page_mem[1048576],每一个page都有一个下标,就可以知道每一个page起始地址,具体的物理地址=起始物理地址+页内(4KB)偏移,就可以不用在page里保存page起始地址了。
假设struct page占40个字节内存,假定系统的物理页为4KB大小,系统有4GB物理内存,那么系统中共有1048576个,所以描述这么多页面的page结构体消耗的内存只不过40MB,相对于4GB内存而言,是很小一部分。因此,管理系统中这么多物理页,代价是不大的。(windows系统页框大小为4KB)
详细计算过程
2.页表
页表中的每一个表项,指向一个物理页的开始地址。在32位系统中,虚拟内存的最大空间是4GB,
这是每一个用户程序都拥有的虚拟内存空间。要把4GB虚拟内存全部使用,那么页表中就需要能够表示所有的4GB空间,那么就需要4GB/4KB=1048576个表项。
虚拟内存看上去被虚线分割成一个个单元,其实并不是真的分割,虚拟内存仍然是连续的。这个虚线的单元仅仅表示它于页表中每一个表项的映射关系,可用映射到相同大小的一个物理内存页上。
页表中的物理地址,与物理内存之间,是随机映射的关系,那里可用就指向那里。虽然最终使用的物理内存是离散的,但是与虚拟内存对应的线性地址是连续的。处理器在访问数据,获取指令时,使用的是线性地址,只要它是连续的就行了,最终都能通过页表找到实际的物理地址。
在32位系统中,地址长度是4个字节,那么页表中的每一个表项就是占用四个字节,所以页表占据的总空间就是:1048576*4=4MB,也就是说映射表自己本身,就要占用4MB/4KB=1024个物理页。
解决大容量页表的最好方法是:把页表看出普通文件,对它进行离散分离,即对页表再分页,由此形成多次页表的思想。
可以把单一页表拆分成1024个体积更小的映射表,这样,1024*1024,仍然可用覆盖4GB的物理内存。

页目录结构

1. 在32位处理器中,采⽤4KB的⻚⼤⼩,则虚拟地址中低12位为⻚偏移,剩下⾼20位给⻚表,分成两级,每个级别占10个bit(10+10)。2. CR3 寄存器 读取⻚⽬录起始地址,再根据⼀级⻚号查⻚⽬录表,找到下⼀级⻚表在物理内存中存放位置。3. 根据⼆级⻚号查表,找到最终想要访问的内存块号。4. 结合⻚内偏移量得到物理地址。

4KB对齐的含义:如果一个地址是4KB对齐的,那么它的最后12位必须全部为0。这是因为4KB = 212字节,所以4KB对齐的地址在二进制表示中,最低12位必须为0。
页框的起始地址:假设物理页框的大小为4KB,那么每个页框的起始地址必须是4KB的整数倍。例如,0x00000000、0x00001000、0x00002000等都是4KB对齐的地址。
为什么只需要记录物理页地址的高20位?
在一个32位的系统中,地址总共有32位。如果物理页框的大小为4KB,那么:
低12位:用于表示页框内的偏移量(Offset)。因为每个页框有4KB = 212字节,所以低12位可以表示页框内的任意位置。
高20位:用于表示页框的编号(Frame Number)。因为低12位已经用于偏移量,所以高20位用于表示物理页框的起始地址。
因此,对于4KB对齐的页框,只需要记录高20位即可唯一标识一个物理页框。
补充:
MMU要先进行两次页表查询确定物理地址,在确认权限问题后,MMU再将这个物理地址发送到总线,内存收到之后开始读取对应地址的数据并返回,那么当页表变成N级时,就变成了N次检索+1次读写,页表级数越多查询的步骤越多,对于CPU来说等待时间越长,效率越低。
单级页表对连续内存要求高,于是引进了多级页表,但是多级页表也有问题所在,虽然减少了连续存储的要求且减少存储空间的,但是降低了查询效率。于是增添了TLB来提高查询效率。
TLB的作用
为了缓解多级页表带来的性能问题,MMU使用TLB来缓存最近使用的页表项。TLB是一个高速缓存,存储了虚拟地址到物理地址的映射关系。通过TLB,可以快速完成地址转换,减少对页表的查询次数。
TLB命中率:如果TLB命中率较高,那么地址转换的延迟可以显著降低。
TLB未命中:如果TLB未命中,MMU需要访问页表,这会导致较大的延迟
缺页异常
CPU给MMU的虚拟地址,在TLB和页表都没有找到对应的物理页,就出现了缺页异常Page Fault,
它是由一个硬件中断触发的可用由软件逻辑纠正的错误。
假设目标内存页在物理内存中没有对应的物理页或者存在但无对应权限,CPU就无法获取数据,CPU就会报告一个缺页错误。
由于CPU没有数据就无法进行计算,CPU摆工了用户进程也就出现了缺页中断,进程会从用户态切换到内核态,并将缺页中断交给Page Fault Handler处理。
线程的优点
创建一个新线程的代价比要创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
1.最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的,内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容换出。
2.还有一个隐藏损耗是上下文的切换会扰乱处理器的缓存机制。一旦切换上下文,处理器中的所有已经缓存的内存地址就没用了。还有一个区别是改变虚拟内存空间的时候,处理页表的TLB(块表)会被全部刷新,导致内存的访问在一段时间内相当的低效。但是在线程的切换不会有,还有硬件cache。
Cache是计算机系统中用于提高数据访问速度的重要组件。它通过存储频繁访问的数据和指令的副本,减少CPU访问主内存的次数,从而显著提高系统的性能。
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其它的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作
多处理器系统
多处理器系统(Multiprocessor System)是指一个计算机系统中包含多个CPU(或核心)。这些CPU可以同时执行多个任务,从而提高系统的整体性能。多处理器系统可以是多核CPU(一个芯片上有多个核心)或多个CPU芯片的组合。
多线程并行执行:
在多处理器系统中,操作系统可以将多个线程分配到不同的CPU核心上,从而实现并行执行。
例如,一个进程包含多个线程,这些线程可以同时在不同的CPU核心上运行,充分利用多处理器的并行能力。
这种并行执行可以显著提高程序的执行效率,尤其是在处理多任务或计算密集型任务时。
计算密集型应用
计算密集型应用(Compute-Intensive Application)是指那些主要消耗CPU资源进行计算的应用。这些应用通常涉及大量的数值计算、数据处理或复杂的算法,对CPU的计算能力要求较高。
I/O密集型应用
I/O密集型应用(I/O-Intensive Application)是指那些主要消耗I/O资源进行数据输入和输出的应用。这些应用通常涉及大量的文件读写、网络通信等操作,对I/O性能要求较高。
线程缺点
1.创建太多线程会造成性能损失
2.健壮性降低(时间分配)
3.缺乏访问控制
线程异常
单个线程出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出现异常,就类似进程出异常,进而触发信号进制,终止进程,进程终止,该进程内的所有线程也就随机退出。
Linux进程与线程
进程和线程
进程是资源分配的基本单位
线程是调递的基本单位
线程共享进程数据,但也有一部分自己的数据:
线程ID,一组寄存器,栈,errno,信号屏蔽字,调度优先级
进程的多个线程共享
同一地址空间,Text Segement,Data Segment都是共享的,如果定义一个函数,在各线程都能调用,定义一个全局变量,在各线程都可以访问到,还有一下资源是共享:
文件描述符表,每种信号的处理方式(SIG_ IGN、SIG_ DFL或者⾃定义的信号处理函数),当前工作目录,用户id和组id。
进程和线程关系图