🔥个人主页:Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
目录
一.Linux线程
进程=内核数据结构+代码和数据(执行流)
线程:是进程内部的一个执行分支(执行流)(线程在进程空间运行)
1.1 什么是线程
在前面的学习中,我们学过,Linux每创建一个进程,就要创建一个全新的PCB(task_struct mm_struct 页表等等),如果创建多个,极其浪费资源,效率还低
为了解决上述问题,于是Linux就创建了多个进程,但让每个进程共享虚拟内存,这样就模拟出了线程

1.1.2进程与线程的本质
进程:承担分配系统资源的基本实体
线程:CPU调度的基本单位
而Linux中
1.在Linux操作系统角度:认为线程是执行流,线程被视为共享资源(地址空间、文件描述符等)的执行流,是进程的一种轻量化形态。
2.在CPU角度:认为线程是轻量级进程,线程与进程一样是独立的调度单元,但因共享资源,创建 / 切换开销远小于传统进程,因此被称为 “轻量级进程(LWP)”。
1.1.3 结论
1.Linux的"线程"是可以用进程模拟的(前面已说)
2.对资源的划分,本质是对地址空间 虚拟地址访问的划分(虚拟地址,就是资源的代表)(每个进程被分配独立的虚拟地址区间,以此隔离不同进程的资源(内存、数据等)
3.函数就是虚拟地址(逻辑地址)空间的集合!就是让线程未来执行ELF不同的函数即可
每个函数的内部每行代码都有地址,函数入口地址其实就是第一行代码的地址
4. Linux线程,就是轻量级进程,或者使用轻量级进程模拟的
1.1.4 问题
1.所有操作系统都是这样设计线程的吗?
其实不是,在Windows中,专门设计了一个线程模型,但开销相比Linux的大
但Linux的更加健壮,因为是复用进程(不用改变代码,因为调度结构和调度算法一样)的相关代码,而这些代码已经经过了验证
1.1.5 CPU看待线程
认为线程是轻量级进程,而进程则是<=执行流的
1.1.6 进程与线程的占有关系
进程强调独占,部分共享(eg:通信时)
线程强调共享,部分独占(因为线程是共享资源的,那我们前面废了老大的劲才实现了进程通信的前提,此处直接拥有了)
1.1.6 引入例子,加强理解
社会分配资源的基本实体:家庭
家庭内部的基本单元:人员
社会上分配资源(学校,医院,超市等等),都是看家庭数进行分配的(每个家庭的资源大部分都是私有的,但医院,公园这些都是所有家庭共享的),而家庭内部的成员是组成基本单位,其中爷爷养老,父母赚钱,孩子上学就是独有,干自己的事,但房子,洗衣机等等,都是共享的
1.2分⻚式存储管理
1.2.1 为什么要有虚拟地址和页表
如果没有,那么每⼀个⽤⼾程序在物理内存上所对应的空间必须是连续的,但因为每⼀个程序的代码、数据⻓度都是不⼀样的,按照这样的映射⽅式,物理内存将会被分割成各种离散的、⼤⼩不同的块。经过⼀段运⾏时间之后,有些程序会退出,那么它们占据的物理内存空间可以被回收,导致这些物理内存都是以很多碎⽚的形式存在。
为了实现提供的逻辑上连续,但物理空间上不连续,防止浪费空间,就引入了虚拟地址和页表
1.2 物理内存管理
我们前面提到过,硬盘读取加载数据,为了效率,都是以4kb为一个单位进行读取的,对应的,在物理内存当中,也应该有4kb为单位进行I/O交流,其中我们将物理空间的该部分内存叫做页框,硬盘的该部分空间叫做页帧
而运行内存有4GB 4GB/4KB=1048576,如此巨大的数,对其进行管理,那么就是先描述,再组织
struct page { /* 原⼦标志,有些情况下会异步更新 */ unsigned long flags; union { struct { /* 换出⻚列表,例如由zone->lru_lock保护的active_list */ struct list_head lru; /* 如果最低为为0,则指向inode * address_space,或为NULL * 如果⻚映射为匿名内存,最低为置位 * ⽽且该指针指向anon_vma对象 */ struct address_space* mapping; /* 在映射内的偏移量 */ pgoff_t index; /* * 由映射私有,不透明数据 * 如果设置了PagePrivate,通常⽤于buffer_heads * 如果设置了PageSwapCache,则⽤于swp_entry_t * 如果设置了PG_buddy,则⽤于表⽰伙伴系统中的阶 */ unsigned long private; };但仔细看上面的代码,发现并没有虚拟/物理地址,其实就是通过组织找到地址的
struct page通过数组进行组织,那么找物理地址,就是数组的起始地址+下标*4kb+(页内4kb)偏移量就可完成
1.3 页目录结构
虽然上面使用了通过数组组织描述内存的结构体可以找到地址,但如果只是一个简单的页表,即一个物理地址,对应一个虚拟地址,那么一行就是4+4 = 8kb
1048576*4 = 4MB 的⼤⼩。也就是说映射表⾃⼰本⾝,就要占⽤ 4MB / 4KB = 1024 个物理⻚,不仅浪费空间,且此处的页表是需要连续的1024个物理页,与前面为了非连续背道而驰
union {
void* s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse : 16; /* ⽤于SLUB分配器:对象的数⽬ */
unsigned objects : 15;
unsigned frozen : 1;
};
}
union {
/* 内存管理⼦系统中映射的⻚表项计数,⽤于表⽰⻚是否已经映射,还⽤于限制逆向映射
搜索*/
atomic_t _mapcount;
unsigned int page_type;
unsigned int active; /* SLAB */
int units; /* SLOB */
};
..
1.3.1 内部结构(与indoe都使用了分层管理思想)
我们知道在32位下,一个虚拟地址是一个字节,有32个比特位,那如果我们对这32个比特位进行划分呢(使用联合体)
0000 0000 0000 0000 0000 0000 0000 0000
划分:将低12位划分为一组,数据范围:[0,4095]
中间10位划分问一组 , 数组范围:[0,1023]
最高10位划分为一组 , 数组范围:[0,1023]
含义:最高10位,代表下级页表的地址
中间10位,存储页的地址
最低12位,表示偏移量
查找过程:
先找到虚拟地址(合法,后面再讲)对应的页框,再根据虚拟地址的低12位,作为偏移量,访问具体字节
1.3.1.2 细察细讲
1.申请内存->查找数组->找到未被使用的page->page index->物理内存地址
2.写时拷贝,缺页中断,内存申请等可能都要重新建立新的页表和映射关系(申请4kb,而不比这更小,是用空间换时间)
3.一张页目录+n张页表构建的映射体系,虚拟地址是索引,物理地址是目标
物理地址等于=页框地址+虚拟地址(低12位)
4. 为什么是低12位作为偏移量?
12位:大小0~4095,刚好是一个页框的大小
低:ELF是可执行文件格式,它的程序段(如代码段、数据段)在加载到内存时,会被按页(4KB)对齐分配(10位),那么每10位都属于同一个4kb,就还剩下低12位
而每张表也不会全部使用来记录数据地址,会拿出12个比特位(位图),用来记录当前的状态(被锁,即将释放等等)
1.3.2 思考
但从总数上看是这样,但是⼀个应⽤程序是不可能完全使⽤全部的 4GB 空间的,也许只要⼏⼗个⻚表就 可以了。例如:⼀个⽤⼾程序的代码段、数据段、栈段,⼀共就需要 10 MB 的空间,那么使⽤ 3 个 ⻚表就⾜够了。
1.3.3TLB
单级页表只有一层,访问效率快,引入了多级页表后,在减少连续存储要求且减少存储空间的同时降低了查询效率
解决:
MMU 引入了新武器,江湖⼈称快表的 TLB (其实,就是缓存,Translation Lookaside Buffer,学名转译后备缓冲器)但CPU给MMU传了虚拟地址后,MMU先去TLB查看是否有对应的物理地址,没有再通过页表查询,并将该条线也映射到TLB,让它记录,刷新一些缓存
虚拟地址由虚拟地址空间提供,由CR3寄存器储存
1.3.4 为何说线程切换代价更低
线程切换是指同进程内(跨进程本质就是进程切换了)
相比之下,线程切换,不用保存CR3寄存器,不用切换页表, 之前已经缓存的物理地址不用更新(TLB)

1.4缺页异常
前面的知识,我们知道计算机有一个MMU硬件,将虚拟地址转换为物理地址,而缺页异常转换后未找到对应的物理地址
三种类型:Hard Page Fault 也被称为 Major Page Fault ,翻译为硬缺⻚错误/主要缺⻚错误,这时物理内存中没有对应的物理⻚,需要CPU打开磁盘设备读取到物理内存中,再让MMU建⽴虚拟地址和物理地址的映射。Soft Page Fault 也被称为 Minor Page Fault ,翻译为软缺⻚错误/次要缺⻚错误,这时物理内存中是存在对应物理⻚的,只不过可能是其他进程调⼊的,发出缺⻚异常的进程不知道 ⽽已,此时MMU只需要建⽴映射即可,⽆需从磁盘读取写⼊内存,⼀般出现在多进程共享内存区域。Invalid Page Fault 翻译为⽆效缺⻚错误,⽐如进程访问的内存地址越界访问,⼜⽐如对空指针解引⽤内核就会报 segment fault 错误中断进程直接挂掉。
1.5 线程的优点及缺点
优点:创建⼀个新线程的代价要⽐创建⼀个新进程⼩得多
• 与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下⽂切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。◦ 另外⼀个隐藏的损耗是上下⽂的切换会扰乱处理器的缓存机制。简单的说,⼀旦去切换上下⽂,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚拟内存空间的时候,处理的⻚表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在⼀段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。• 线程占⽤的资源要⽐进程少• 能充分利⽤多处理器的可并⾏数量• 在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务• 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现(线程数不推荐大于CPU个数,调度线程的算力远不如拿去计算有性价比)•I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作(可线程数大于CPU个数)
缺点:性能损失◦ ⼀个很少被外部事件阻塞的计算密集型线程往往⽆法与其它线程共享同⼀个处理器。如果计算密集型线程的数量⽐可⽤的处理器多,那么可能会有较⼤的性能损失,这⾥的性能损失指的是增加了额外的同步和调度开销,⽽可⽤的资源不变。• 健壮性降低编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序⾥,因时间分配上的细微偏差或者因共享了不该共享的变量⽽造成不良影响的可能性是很⼤的,换句话说线程之间是缺乏保护的。• 缺乏访问控制进程是访问控制的基本粒度,在⼀个线程中调⽤某些OS函数会对整个进程造成影响。(线程出现报错,终止进程)• 编程难度提⾼◦编写与调试⼀个多线程程序⽐单线程程序困难得多
1-5 线程异常
1-6 线程⽤途
总结:
1. 明白在Linux中,进程与线程的关系
2. 明白页表的设计原理
3.合理利用线程,提高效率

302

被折叠的 条评论
为什么被折叠?



