Linux线程(一):从进程到线程,揭开并发执行的面纱

张开发
2026/4/13 14:49:31 15 分钟阅读

分享文章

Linux线程(一):从进程到线程,揭开并发执行的面纱
一、Linux线程概念1-1 简要概括进程 内核数据结构 代码和数据 进程会被CPU调度 执行流线程 是进程内部的一个执行分支 执行流内核和资源【并没有否定上面的概念只是从不同的角度去理解】进程承担分配系统资源的基本实体线程CPU调度的基本单位1-2进程可以访问多少资源本质取决于进程能通过地址空间窗口看到多少资源也就是进程所拥有的虚拟地址数量决定的因为虚拟地址是资源的代表进程拥有的虚拟地址越多能通过地址空间访问窗口的资源就越多进程的主要资源你的/库的/操作系统的代码和数据所以不同的进程大家的资源不同本质是因为大家的窗口不同创建进程就要分配一大批的资源1-3 创建一个进程共享窗口只需要让新创建的进程指向同一个地址空间将资源再分配给不同的task_struct 用进程模拟出来线程了划分地址空间划分地址空间本质上在划分虚拟地址范围划分虚拟地址本质上划分页表层级含义1. 资源分配给不同task_struct创建多个执行流2. 划分地址空间决定它们共享还是隔离3. 划分虚拟地址范围决定能看到哪些内存区域4. 划分页表决定虚拟地址映射到哪里1-4 结论本质是单线程进程的特殊情况之前学习的单进程指的是一个进程内部仅包含一个线程整个进程的所有函数代码都以这一个线程作为执行入口依次调用执行进程独占自身的代码和地址空间属于进程的特例形态。与当前进程概念逻辑自洽且兼容之前学习的单进程概念并非错误它与当前对进程的定义承担分配系统资源的基本实体包含内核数据结构、代码和数据且可容纳多个线程不存在冲突二者逻辑自洽、互相兼容单进程只是进程多线程这一普世情况中的特殊场景是对进程概念的补充。1-5 window对于线程的设计Windows对于线程的实现是在内核中为线程专门设计对应的线程控制块TCB同时保留进程的进程控制块PCB形成PCB与TCB并存的体系。具体而言线程作为进程内部的执行分支会被内核先描述再组织管理每一个线程都对应一个独立的TCBPCB内部通过结构体指针关联包含所有线程TCB的链表以此构建进程与线程的关联关系。这种实现方式下线程同样需要具备创建、终止、调度、切换等能力因此需要为线程单独设计调度算法与切换逻辑导致内核维护的数据结构更多系统整体复杂度更高内核维护对象的成本也相应提升。1-6 线程再次理解计算机概念生活场景核心定位操作系统社会资源分配框架进程家庭资源分配基本单位线程家庭成员执行任务的最小单位家庭分工示例父母赚钱、学生学习、老人养老 → 各线程执行不同任务。共同目标“过好日子” → 进程的整体目标。资源特性独占资源作业本、存折 → 线程私有数据。共享资源冰箱、电视 → 进程共享内存。二、 分页式存储管理2-1 虚拟地址和页表的由来如果没有虚拟内存和分页机制的情况下每一个用户程序在物理内存上所对应的空间必须是连续的如下图 :因为每一个程序的代码、数据长度都是不一样的按照这样的映射方式物理内存将会被分割成各种离散的、大小不同的块。经过一段运行时间之后有些程序会退出那么它们占据的物理内存空间可以被回收导致这些物理内存都是以很多碎片的形式存在。怎么办呢?我们希望操作系统提供给用户的空间必须是连续的但是物理内存最好不要连续。此时虚拟内存和分页便出现了如下图所示:把物理内存按照一个固定的长度的页框进行分割有时叫做物理页。每个页框包含一个物理页(page)。一个页的大小等于页框的大小。大多数32位体系结构支持4KB的页而64位体系结构一般会支持8KB的页。区分一页和一个页框是很重要的:页框是一个存储区域页是一个数据块可以存放再任何页框或磁盘中有了这种机制CPU便并非是直接访问物理内存地址而是通过虚拟地址空间来间接的访问物理内存地址。所谓的虚拟地址空间是操作系统为每一个正在执行的进程分配的一个逻辑地址在32位机上其范围从0~4G-1。操作系统通过将虚拟地址空间和物理内存地址之间建立映射关系也就是页表这张表上记录了每一对页和页框的映射关系能让CPU间接的访问物理内存地址。总结一下其思想是将虚拟内存下的逻辑地址空间分为若干页将物理内存空间分为若干页框通过页表便能把连续的虚拟内存映射到若干个不连续的物理内存页。这样就解决了使用连续的物理内存造成的碎片问题。2-2 物理内存管理4KB的数据块 - 4KB的内存块1. 磁盘与内存的I/O 均是以4KB为单位2. 物理内存和磁盘均以4KB为基本管理单位称为页框Page Frame或页帧。物理内存有这么多4KB哪些4KB被占用了一共有多少4KB被用了多少4KB哪些4KB当前内部存储的数据是存数据的哪些是空的哪些物理内存上面它对应的物理的内容不能被换出哪些是纯内存的不能刷新到我们的磁盘的操作系统是否需要管理内存中的一个一个的页框如何管理先描述再组织/* include/linux/mm_types.h */ 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 { /* slab, slob and slub */ union { struct list_head slab_list; /* uses lru */ struct { /* Partial pages */ struct page* next; #ifdef CONFIG_64BIT int pages; /* Nr of pages left */ int pobjects; /* Approximate count */ #else short int pages; short int pobjects; #endif }; }; struct kmem_cache* slab_cache; /* not slob */ /* Double-word boundary */ void* freelist; /* first free object */ 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 */ }; #if defined(WANT_PAGE_VIRTUAL) /* 内核虚拟地址如果没有映射则为NULL即高端内存 */ void* virtual; #endif /* WANT_PAGE_VIRTUAL */ };这里怎么没有虚拟和物理地址 ?要注意的是struct page与物理页相关而并非与虚拟页相关。而系统中的每个物理页都要分配一个这样的结构体让我们来算算对所有这些页都这么做到底要消耗掉多少内存。算struct page占40个字节的内存吧假定系统的物理页为4KB大小系统有4GB物理内存。那么系统中共有页面1048576个(1兆个)所以描述这么多页面的page结构体消耗的内存只不过40MB相对系统4GB内存而言仅是很小的一部分罢了。因此要管理系统中这么多物理页面这个代价并不算太大。要知道的是页的大小对于内存利用和系统开销来说非常重要页太大页页必然会剩余较大不能利用的空间(页内碎片)。页太小虽然可以减小页内碎片的大小但是页太多会使得页表太长而占用内存同时系统频繁地进行页转化加重系统开销。因此页的大小应该适中通常为512B-8KBwindows系统的页框大小为4KB。内存划分单位物理内存和磁盘均以4KB为基本管理单位称为页框Page Frame或页帧。操作系统将内存划分为约100万个4KB块4GB内存为例并通过数组结构管理这些页框。内核数据结构struct page结构体描述每个4KB页框的状态信息包含以下关键字段标志位占用、锁定、读写权限等。LRU最近最少使用链表指针用于内存回收。引用计数map_count、映射关系等。全局数组mem_map存储所有struct page实例通过下标直接计算物理地址下标 × 4KB。内存申请与分配操作系统以4KB为单位分配内存即使程序仅申请少量字节。分配流程查找空闲页框 → 修改struct page标志位→ 建立进程与内存的关联。申请内存的载体是进程或者线程未来通过为进程和线程关联内存的方式实现勾连具体体现在进程或线程在执行各类操作如打开文件、申请地址空间等时都需要使用内存系统会围绕进程和线程的内存需求构建对应的内存管理关联让进程、线程与分配的内存建立起对应关系从而完成载体与内存的勾连。2-3 页表页表不能是单张页表核心原因是单张页表的存储空间需求过大完全不具备可行性。以32位系统为例若采用单张页表实现虚拟地址与物理地址的全映射假设单条页表条目大小为8字节要覆盖4GB的虚拟地址空间仅页表本身的大小就需要达到32GB。而操作系统还需要额外存储用户数据、源代码、中断向量表、PCB等各类关键数据结构根本无法在内存中容纳如此庞大的单张页表因此单张页表的设计完全不现实。页表中的每一个表项指向一个物理页的开始地址 。 在32位系统中 虚拟内存的最大空间是4GB 这是每一个用户程序都拥有的虚拟内存空间 。 既然需要让4GB的虚拟内存全部可用那么页表中就需要能够表示这所有4GB空间那么一共需要4GB/4KB 1048576个表项2-4 页目录结构到目前为止每一个页框都被一个页表中的一个表项来指向了那么这1024个页表也需要被管理起来。管理页表的表称之为页目录表形成二级页表。如下图所示:所有页表的物理地址被页目录表项指向页目录的物理地址被CR3寄存器指向 这个寄存器中 保存了当前正在执行任务的页目录地址所以操作系统在加载用户程序的时候不仅仅需要为程序内容来分配物理内存还需要为用来保存程序的页目录和页表分配物理内存2-5 两级页表的地址转换下面以一个逻辑地址为例。将逻辑地址(转换为物0000000000,0000000001,11111111111转化为物理地址的过程:1.在32位处理器中采用4KB的页大小则虚拟也址中低12位为页偏移剩下高20位给页表分成两级每个级别占10个bit(1010)。2.CR3寄存器读取页目录起始地址再根据一级页号查页目录表。找到下一级页表在物理内存中存放的位置3.根据二级页号查表找到最终想要访问的内存块号。4.结合页内偏移量得到物理地址。高位聚集效应高20位相同的地址属于同一页保证代码/数据在物理内存中的局部性局部性原理保证访问到的都在一个范围5.注 一个物理页的地址一定是4KB对齐的最后的12位全部为0所以起始只需要物理页地址的高20位即可6. 以上其实就是MMU的工作流程 。MMUMemory Manage Unit是一种硬件电路 其速度很快 主要工作是进行内存管理地址转化只是它承接的业务之一到这里其实还有个问题MMU要先进行两次页表查询确定物理地址在确认了权限等问题后MMU再将这个物理地址发送到总线内存收到之后开始读取对应地址的数据并返回。那么当页表变为N级时就变成了N次检索1次读写。可见页表级数越多查查询的步骤越多对于CPU来说等待时间越长效率越低。让我们现在总结一下:单级页表对连续内存要求高于是引入了多级页表但是多级页表也是一把双刃剑在减少连续存储要求且减少存储空间的同时降低了查询效率。有没有提升效率的办法呢?计算机科学中的所有问题都可以通过添加一个中间层来解决。MMU引入了新武器江湖人称快表的TLB(其实就是缓存当CPU给MMU传新虚拟地址之后MMU先去问TLB那边有没有如果有就直接拿到物理地址发到TLB总线给内存齐活。但TLB容量比较小难免发生Cache Miss这时候MMU还有保底的老武器页表再页表中找到之后 MMU出了把地址发送到总线传给内存还把这条映射关系给到TLB让它记录一下刷新缓存。三、 线程的优点优点说明创建快创建线程只需分配栈和TCB线程控制块不需要复制进程资源如页表、文件描述符表。切换快线程切换时虚拟地址空间不变因此页表不变TLB无需刷新缓存依然有效。进程切换则需要刷新TLB开销大。资源占用少一个进程内的多个线程共享代码段、数据段、文件描述符等相比多进程大大节省内存。充分利用多核可将计算任务分解到多个线程在多处理器上并行执行。I/O重叠一个线程等待I/O时其他线程可继续计算提高吞吐量。四、线程的缺点缺点说明性能损失多个线程竞争同一把锁、频繁同步会引入额外开销。健壮性降低一个线程访问野指针导致段错误整个进程崩溃进程内所有线程都完蛋。缺乏访问控制线程可以随意修改进程中的全局数据容易引发bug。编程难度高多线程程序需要考虑竞态条件、死锁、数据同步等问题调试困难。五、线程异常单个线程如果出现除零 野指针问题导致线程崩溃 进程也会随着奔溃线程是进程的执行分支线程出现异常就类似进程出现异常进而触发信号机制终止进程进程终止该进程内的所有线程也随之退出六、 线程用途合理的使用多线程能提高CPU密集型程序的执行效率合理的使用多线程能提高IO密集型程序的用户体验如生活中我们一边写代码一边开发工具就i是多线程运行的一种表现

更多文章