欢迎光临
我们一直在努力

【Linux】malloc 1GB 内存为什么没立刻占满?从缺页、COW 到 Cache/TLB,看懂线程为什么更轻

封面

🔥个人主页:爱和冰阔乐 📚专栏传送门:《数据结构与算法》 、C++ 🐶学习方向:C++方向学习爱好者 ⭐人生格言:得知坦然 ,失之淡然

在这里插入图片描述


🏠博主简介 在这里插入图片描述

文章目录

  • 前言
  • 一、缺页异常:虚拟地址合法,不代表物理页已经存在
    • 1.1 什么是缺页异常
    • 1.2 三种缺页类型
    • 1.3 查页表"失败"到底什么意思
    • 1.4 延迟分配(惰性分配)
  • 二、写时拷贝(COW):fork 为什么不用立刻复制全部内存
    • 2.1 fork 之后发生了什么
    • 2.2 只读是页表的标记,不是硬件锁死
      • 踩坑点
  • 三、局部性原理:为什么缓存和延迟加载有效
    • 3.1 为什么低 12 位变化不跨页面
      • 空间局部性
    • 3.2 完整链路串起来
    • 3.3 易混淆辨析
  • 四、new/malloc、延迟分配与越界访问
    • 4.1 申请内存到底在干什么
    • 4.2 越界了不一定会报错
    • 4.3 如何区分缺页和越界
  • 五、线程为什么比进程更轻
    • 5.1 创建代价小
    • 5.2 切换开销小
      • 表面原因:不用切换地址空间
      • 真正原因:Cache 和 TLB 失效
      • 进程切换 vs 线程切换
      • 线程更轻量化的主要原因
    • 5.3 占用资源少
    • 5.4 充分利用多处理器
    • 5.5 计算密集型应用
    • 5.6 IO 密集型应用
      • "IO 等待重叠"是什么意思
      • 为什么多线程有效
  • 六、多线程并不是只有优点
    • 6.1 性能损失
    • 6.2 健壮性降低
    • 6.3 缺乏访问控制
    • 6.4 编程难度提高
  • 七、线程异常与适用场景
    • 7.1 线程异常
    • 7.2 线程用途
  • 八、进程 VS 线程:哪些共享,哪些必须独占
    • 8.1 基本定位
    • 8.2 线程的"私有"数据
      • 上下文数据为什么重要
      • 栈为什么独立
      • errno 为什么不能共享
    • 8.3 线程共享的进程资源
    • 8.4 关于进程线程的问题
  • 总结

前言

上一篇把地址空间和页表建立起来,这一篇继续看“地址暂时没有物理页”以后会发生什么。

缺页、COW、malloc/new 的延迟分配看起来属于内存管理;Cache、TLB、线程切换看起来又属于线程性能。实际上它们是一条线:线程之所以更轻,正是因为它不用像进程那样频繁更换整套地址空间和页表环境。


一、缺页异常:虚拟地址合法,不代表物理页已经存在

1.1 什么是缺页异常

设想 CPU 给 MMU 的虚拟地址,在 TLB 和页表都没有找到对应的物理页,该怎么办?这就是缺页异常 Page Fault——一个由硬件中断触发的、可以由软件逻辑纠正的错误。

假如目标内存页在物理内存中没有对应的物理页或者存在但无对应权限,CPU 就无法获取数据,就会报告缺页错误。

CPU 没有数据就无法计算,CPU "罢工"了。用户进程出现缺页中断,进程从用户态切换到内核态,将缺页中断交给内核的 Page Fault Handler 处理。

在这里插入图片描述

过程:

  • CPU 访问虚拟地址,MMU 进行虚拟和物理地址转换时没有找到对应的物理页
  • CPU 内部自动触发缺页异常
  • CPU 执行缺页中断程序
  • CPU 执行中断向量表里的内核中断处理器,做内存申请和重新构建页表
  • 1.2 三种缺页类型

    Hard Page Fault(硬缺页 / 主要缺页):物理内存中没有对应的物理页,需要 CPU 打开磁盘设备读取到物理内存中,再让 MMU 建立虚拟地址和物理地址的映射。要 IO 磁盘,速度很慢。

    Soft Page Fault(软缺页 / 次要缺页):物理内存中存在对应物理页,只不过可能是其他进程调入的,发出缺页异常的进程不知道而已。MMU 只需要建立映射即可,无需从磁盘读取写入内存。一般出现在多进程共享内存区域。比如要访问动态库,其库代码可能已经被其他进程提前载入了。又比如共享内存:多个进程共享同一块物理内存,后访问的进程触发软缺页,建立映射。

    Invalid Page Fault(无效缺页错误):比如进程访问的内存地址越界访问,又如对空指针解引用,内核就会报 segment fault 错误,中断进程直接挂掉。

    类型物理内存有数据吗需要磁盘 IO 吗开销
    硬缺页 (Major) 没有 需要 很慢
    软缺页 (Minor) 不需要 很小
    无效缺页 进程挂掉

    1.3 查页表"失败"到底什么意思

    给虚拟地址查页表,发现页目录所对应的页表不存在——可是判定这个虚拟地址是合法地址(在进程的地址空间是合法的),但页目录项存在,页表不存在,说明磁盘上的代码数据还没加载到内存。

    MMU 虚拟地址转换失败,触发缺页中断。OS 进入中断处理:

  • 确认虚拟地址合法
  • 内存管理模块申请空闲物理页框,从描述物理内存的 page 数组找到空闲页框
  • 如果页表本身还没存在,先分配物理内存创建页表
  • 把磁盘的数据读入刚分配的物理页框
  • 把物理页框号填入页表中,标志位置为 1
  • 中断返回,重新执行指令
  • 合法 ≠ 已经在物理内存 合法只是"你有权用这块虚拟地址",不代表物理内存已经分配、页表已经填好。

    1.4 延迟分配(惰性分配)

    操作系统使用延迟分配:进程申请虚拟地址,只是在进程的 vma(虚拟内存区域)记录这块地址归你用,暂时不建页表、不分配物理页框。等到真正访问时才现场分配。

    • 标志位 = 0 有两种可能:

    • 页表不存在(页目录 present=0)
    • 页表存在,但该页面没有物理页框(页表 present=0)
    • 都会触发缺页中断。

    • 合法虚拟地址 ≠ PDE/PTE 有效。惰性分配只登记 VMA,不建页表不分页框。

    • 缺页中断里面才做:分配页框、读磁盘、现场构建 / 补全页表项。

    • 写时拷贝只是缺页中断其中的一个分支,不是所有缺页都拷贝。

    在这里插入图片描述 在这里插入图片描述


    二、写时拷贝(COW):fork 为什么不用立刻复制全部内存

    2.1 fork 之后发生了什么

    fork 之后,父子进程共享同一个物理页框。全局变量处在这个 4KB 页框中,操作系统把双方页表项的 R/W 权限设置只读。

    权限管理是以整个页框为单位的,不能单独针对页内某个变量设置权限。页表项不光存物理页框号,还附带一堆标志位(读写权限、存在位等)。

    全局变量只是 4 字节,但它放在一个 4KB 的物理页框里。页表的权限位作用于整个页,不能给页里面某一个 4 字节变量单独设置只读。只要这个页表项标记只读,页框里面全部内容都只读。哪怕你只修改页内一个 int 全局变量,只要写这个页内任意字节,都会触发保护。

    fork 做的核心操作:

  • 给子进程创建一套全新独立的页表
  • 子进程的每一个虚拟页,全部填写和父进程一模一样的物理页框号(父子映射到同一个物理页框)
  • 把父子双方对应的这组页表项的 R/W 权限全部改成只读
  • 重点:不是给变量加只读,是整个物理页框被标记只读。

    2.2 只读是页表的标记,不是硬件锁死

    物理页框硬件本身没有只读属性。只读是 MMU 硬件读取页表项里面 R/W 标志做检查。CPU 尝试写这个虚拟地址,MMU 看到页表项 R/W = 只读,直接抛出缺页异常。

    场景 1:只是读全局变量

    父子只是读取全局变量,没有写操作。MMU 检查权限:读是允许的,不会触发异常。父子继续共用同一个物理页框,不复制内存。这就是 COW 的好处——fork 瞬间完成,不消耗大量物理内存。

    场景 2:任意一方尝试写全局变量

    比如子进程执行 g_val = 100;

  • CPU 执行写指令,MMU 查页表项:R/W 是只读。触发 #PF 缺页异常
  • 进入内核缺页处理。内核判断:这是 COW 写时复制场景,不是真的权限错误
  • 内核分配一块全新的物理页框,把原来共享页框里面完整 4KB 全部拷贝到新页框
  • ⚠️ 哪怕你仅仅修改 4 字节全局变量,也要复制整个 4KB 页框,不是只拷贝那一个变量!

  • 修改执行写操作那个进程的页表项:虚拟页映射到新复制的物理页框,R/W 改成可写
  • 另一个进程仍然保留原来的物理页框,保持只读
  • 返回用户态,重新执行写指令,修改新页框里面的全局变量
  • 踩坑点

    说法正误
    物理页框硬件变成只读 ❌ 是两边进程的页表项标记只读
    只拷贝被修改的全局变量 ❌ 拷贝完整 4KB 页框
    读会触发复制 ❌ 只有写才触发复制
    fork 的时候有物理内存拷贝 ❌ 写的时候才分配新物理页,属于软缺页,不需要磁盘 IO

    三、局部性原理:为什么缓存和延迟加载有效

    3.1 为什么低 12 位变化不跨页面

    为什么是低的 12 位?可执行程序在磁盘上编址从全 0 到全 F。在 ELF 中认为每一个区域都是起始地址 + 偏移量。基于平坦模式,所有数据段编址时起始偏移量都为 0。

    可执行程序加载到内存时,前面若干位(前 20 位)相同的地址一定在一起,因此低 12 位连续的地址一定属于同一个 4KB。所以加载内存时,4KB 内部的数据在 ELF 编码中一定是聚集在一块的——这就是局部性原理。

    当我们访问某一行代码时,较大概率会访问这一行周围的代码,因为程序大部分情况都是顺序执行的。

    局部性原理分为时间局部性与空间局部性,这里主要体现空间局部性。

    空间局部性

    如果一个存储位置被访问,那么它附近相邻的地址,很大概率很快也会被访问。

    程序运行特征:

    • CPU 执行代码,指令顺序执行,访问的虚拟地址是连续递增的
    • 遍历数组,也是连续访问相邻内存地址

    既然程序大概率访问连续地址,操作系统就利用分页配合 ELF 布局:

    • 把连续虚拟地址打包进同一个 4KB 页面
    • ELF 磁盘文件上把这一块内容聚集存放
    • 一次缺页,读入完整 4KB 页面

    读进来之后,后续访问页面内其他地址,就不再需要访问磁盘,直接命中内存。

    3.2 完整链路串起来

  • 程序代码逻辑:顺序执行,访问的虚拟地址连续(空间局部性)
  • 编译器生成 ELF:把连续虚拟地址对应的代码、数据在磁盘文件聚集存放,按页面大小对齐
  • 运行时第一次访问这片虚拟地址:发生缺页中断
  • OS 发起磁盘 IO,一次性把磁盘上聚集的 4KB 内容读入一个物理页框,填充 PTE
  • 后续访问该页面内其余上千个字节,都在内存,不再需要磁盘读取
  • 一句话总结:因为程序有空间局部性,相邻虚拟地址大概率会被访问;所以 ELF 把相邻虚拟地址对应的内容在磁盘聚集;分页以 4KB 为粒度加载,一次 IO 预加载一大块,减少磁盘访问次数,提升效率。

    3.3 易混淆辨析

  • ❌ 不是:ELF 文件里面每 4KB 严格切割分开 ✅ 是:逻辑临近虚拟地址对应的内容,在磁盘上聚集,段按页边界对齐

  • ❌ 局部性不是分页带来的 ✅ 局部性是程序本身的行为特征;分页机制、ELF 布局是利用局部性来优化性能

  • 低 12 位变化不会跨页面;高 20 位变化,才切换新页面,才可能触发新的缺页


  • 四、new/malloc、延迟分配与越界访问

    4.1 申请内存到底在干什么

    当我们 new/malloc 时并没有在物理内存上开辟空间。其底层系统调用是 brk(更改数据段的大小)或 mmap(基于文件的)。new 和 malloc 时只需要修改虚拟地址空间上堆空间的范围,改的是虚拟地址,没有申请物理地址。

    申请完堆空间后不一定立马使用它,OS 就不会立马去申请物理空间。当真正想要使用虚拟地址进行访问时,系统触发缺页中断,再做内存的二次申请。在虚拟地址上开辟空间的本质是对物理地址的延迟申请。

    当用户申请内存但没有使用,这个内存可以给有需要的用户使用,变相提高了内存使用效率。

    申请内存 = 申请地址空间:即将堆空间的 start 和 end 指针移动即可。

    4.2 越界了不一定会报错

    在程序里定义了野指针和数组越界,一旦出现错误一定会报错吗?不一定:

    int i = 0;
    int a[10];
    for (int i = 0; i <= 10; i++)
    {
    a[i] = 0;
    }

    编译器把变量 i 放在数组的高地址后方;数组下标变大地址往高地址增长,a[10] 的地址正好就是 i 的地址;访问 array[10] 刚好命中 i 的内存,把 i 强制写为 0,循环条件永远成立——死循环。

    访问的代码空间,指针指向的地址不一定是越界的地址而是合法的地址。你的越界,OS 都不知道,否则就会将程序终止。

    4.3 如何区分缺页和越界

    1. 页号合法性检查

    不是去查页表,是先拿触发异常的虚拟地址,对比进程自己的虚拟地址区间(vm_area_struct),看这个虚拟地址是不是属于这个进程允许使用的范围。

    合法虚拟地址 = 落在某一个 vm_area_struct 的 [vm_start, vm_end) 区间内。不在任何一个区间,就是非法地址。

    如果页号合法但页不在内存中,则为缺页中断;如果页号非法,则为越界访问。

    2. 内存映射检查

    检查触发事件的虚拟地址是否在当前进程的内存映射范围内。在映射范围内但页不在内存中 → 缺页中断;不在映射范围内 → 越界访问。


    五、线程为什么比进程更轻

    5.1 创建代价小

    创建一个新线程的代价要比创建一个新进程小得多。

    创建进程需要分配 PCB、地址空间、页表等一整套结构,而线程复用进程的地址空间,只需要创建新的 task_struct,不需要重新构建地址空间和页表。

    5.2 切换开销小

    与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。

    表面原因:不用切换地址空间

    最主要的区别是线程切换时虚拟内存空间依然是相同的,但进程切换是不同的。线程共享地址空间,线程的 PCB 指向同一个 mm_struct,因此页表也不需要换,映射关系也没变。所以线程切换时不用对 CR3 寄存器的内容进行保存。

    但仅凭"有无保存 CR3",其实看不出进程和线程谁更轻量化。

    真正原因:Cache 和 TLB 失效

    另外一个隐藏的损耗是上下文切换会扰乱处理器的缓存机制。在 CPU 内部有两个缓冲:

    Cache 缓存

    CPU 运算速度远远快于主存。如果没有 Cache,CPU 每读一个变量都要访问慢腾腾的主存,大部分时间在等待内存,性能极低。Cache 把近期很可能要用的数据放到 CPU 内部高速存储。

    Cache 不是按单个字节存放,是以 Cache 行(块) 为最小单位和内存交换数据,典型大小 64 字节。一次从内存搬一整块进来,这就是"把周围数据一起放入 Cache"的来源。

    • 命中:CPU 直接从 Cache 拿数据,不用访问主存,速度极快
    • 不命中:会去主存读取数据;依据空间局部性,不只是读目标那几个字节,会把相邻一整块(Cache 行)全部加载进 Cache
    • Cache 满了:执行替换算法(LRU 最常用),淘汰掉某一个旧 Cache 行

    Cache 存的是内存真正的数据 / 指令,不是地址映射。比如数组元素、变量、代码指令。

    Cache 解决:取数据慢的问题(拿到物理地址之后读取内存数据)

    cat /proc/cpuinfo

    在这里插入图片描述

    TLB 快表

    进程在虚拟和物理转换时还有 TLB,会将虚拟到物理之间的映射也缓存起来。

    TLB 解决:地址翻译慢的问题(翻译虚拟→物理地址)

    进程切换 vs 线程切换

    一旦切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废。当你改变虚拟内存空间的时候,处理的页表缓冲 TLB 会被全部刷新,导致内存访问在一段时间内相当低效。进程切换时会导致 TLB 和 Cache 失效,下次运行需要重新缓存。

    而在线程的切换中,不会出现这个问题:

    线程切换不用刷新 TLB,但 Cache 内容会被污染、被置换。线程 A 在用一部分数据,切到线程 B,线程 B 访问另外一批数据,会把 Cache 里面旧的数据挤掉。Cache 不会特意清空,只是内容被新访问覆盖。

    切换类型TLBCache
    进程切换 全部刷新,失效 数据失效,需重新缓存
    线程切换 不用刷新 内容被新数据覆盖(不主动清空)

    Cache 中存真实数据,不管进程还是线程切换,硬件不会主动清空 Cache,只是数据会被新访问挤掉。

    线程更轻量化的主要原因

    线程切换不切换页表和虚拟地址空间,不刷新 TLB,Cache 污染更小——这就是线程更轻量化的主要原因。

    5.3 占用资源少

    线程占用的资源要比进程少。线程共享进程的地址空间,不需要像进程那样独占一整套资源。

    5.4 充分利用多处理器

    线程是调度的基本单位,当有多个 CPU 时,可以创建多线程进行利用。多核 CPU 上,多个线程可以真正并行执行。

    5.5 计算密集型应用

    为了能在多处理器系统上运行,将计算分解到多个线程中实现。所有的代码都会变成进程,在进行某种计算时会有不同类型的计算,比如加密、解密、压缩等。使用 CPU 资源的都是计算密集型。

    举例:要进行 4G 数据的排序,假设有 4 个 CPU,那么就可以将 4G 分给四个线程,每个线程 1G 进行排序,最后再进行归并即可。

    那是不是线程越多越好?不是。

    在计算密集型应用中,如果只有一个 CPU 但创建了很多线程,线程切换会消耗本应给计算的算力。只有一个线程反而可以更快地计算。

    计算密集型应用中,最快的方法是 CPU 有多少个就创建多少个线程。

    5.6 IO 密集型应用

    为了提高性能,将 IO 操作重叠。线程可以同时等待不同的 IO 操作——CPU 不要卡在那里干等一个 IO 完成,让多个 IO 等待时间互相重叠,硬件 IO 设备并行干活,CPU 切换处理别的任务,提升整体吞吐。

    "IO 等待重叠"是什么意思

    每个线程执行网络 recv 的时候,线程会阻塞,让出 CPU:

    • 线程 1:发起请求拿 A 块 → 陷入等待网络 IO(不占 CPU)
    • CPU 切走,调度线程 2:发起请求拿 B 块 → 陷入等待网络 IO(不占 CPU)
    • CPU 切走,调度线程 3:发起请求拿 C 块 → 陷入等待网络 IO(不占 CPU)

    三个线程同时都在等待网络,三个网络 IO 在硬件层面并行跑,等待时间互相重叠。

    不是 CPU 同时在干活,是多个 IO 硬件等待并发进行。如果只用单线程,只能 A 下载完再 B 再 C,IO 等待是串行,总耗时 = T_A + T_B + T_C。多线程 IO 重叠后,总耗时 ≈ max(T_A, T_B, T_C),速度大幅提升。

    为什么多线程有效

    CPU 同一时刻只有一个线程在跑,那多线程和单线程有啥区别?

    关键点:下载绝大部分时间,线程根本不占用 CPU。线程在阻塞等待 IO,根本不在 CPU 上运行。

    把两件事严格拆开:

  • CPU 执行(运行线程代码):短暂,只有发请求、收到数据拷贝这一小段需要 CPU
  • 网络 IO 传输(下载数据):硬件网卡自己干活,完全不需要 CPU 参与,这是漫长的等待阶段
  • IO 密集型应用是不是线程越多越好?可以适当多创建线程。 IO 的时候大部分在等待,可以提高上传或下载的效率。多个 IO 请求并行在硬件上执行,等待时间重叠,充分利用 IO 设备带宽。 只有发送请求那一小段需要 CPU,网络传输主体阶段 CPU 基本不参与,网卡硬件干活。但数据到达之后,还是要 CPU 接手处理。


    六、多线程并不是只有优点

    6.1 性能损失

    一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失。

    这里的性能损失指的是增加了额外的同步和调度开销,而可用资源不变。

    6.2 健壮性降低

    编写多线程需要更全面更深入的考虑。在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性很大。线程之间是缺乏保护的。

    6.3 缺乏访问控制

    进程是访问控制的基本粒度。在一个线程中调用某些 OS 函数会对整个进程造成影响。

    6.4 编程难度提高

    编写与调试一个多线程程序比单线程程序困难得多。


    七、线程异常与适用场景

    7.1 线程异常

    • 单个线程如果出现除零、野指针问题导致线程崩溃,进程也会随着崩溃
    • 线程是进程的执行分支,线程出异常就类似进程出异常,进程触发信号机制终止进程,进程终止,该进程内的所有线程也就随即退出

    线程异常的验证:让新线程进行除 0 操作——任何一个线程崩溃都会导致整个进程崩溃。

    在这里插入图片描述

    7.2 线程用途

    • 合理地使用多线程,能提高 CPU 密集型程序的执行效率
    • 合理地使用多线程,能提高 IO 密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

    八、进程 VS 线程:哪些共享,哪些必须独占

    8.1 基本定位

    • 进程是资源分配的基本单位
    • 线程是调度的基本单位
    • 进程间具有独立性
    • 线程共享地址空间,也就共享进程资源

    进程和线程的关系如下图:

    在这里插入图片描述

    进程与线程资源模型

    8.2 线程的"私有"数据

    线程共享进程数据,但也拥有自己的一部分"私有"数据:

    私有数据说明
    线程 ID 每个线程有唯一标识
    一组寄存器 线程的上下文数据
    局部变量的地址只存在当前线程自己的栈、寄存器里
    信号屏蔽字 哪些信号当前要被屏蔽、暂时不递送
    errno 线程 1 系统调用出错修改自己的 errno,不会影响线程 2 的 errno
    调度优先级 独立的调度优先级

    上下文数据为什么重要

    线程的上下文数据可以证明线程是可以被独立调度的。

    栈为什么独立

    函数调用时会形成栈帧,其内部会形成临时变量,需要栈对临时数据进行保存。线程也需要函数调用,因此栈也要独立——即表明线程是一个动态的概念。

    errno 为什么不能共享

    如果 errno 是进程全局共享,多线程程序会出现严重 bug:一个线程出错把 errno 改了,另一个线程读到错乱错误码。所以 errno 必须是线程私有的。

    ⚠️ 用户进程并不是只有一块栈区。 每新建一个 pthread 线程,操作系统会在这个进程的虚拟地址空间里,再开辟一块全新独立的虚拟内存区域(vm_area_struct),作为这个新线程的栈。

    整个进程的虚拟地址空间是一块巨大的地址池子,不是代码段、数据段、堆、主线程栈这四块就占满全部。剩下大量空闲虚拟地址可以分配给各个子线程做栈。

    8.3 线程共享的进程资源

    同一地址空间,因此 Text Segment、Data Segment 都是共享的。如果定义一个函数,在各线程中都可以调用;如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:

    • 文件描述符表
    • 每种信号的处理方式(SIG_IGN、SIG_DFL 或自定义的信号处理函数)
    • 当前工作目录
    • 用户 id 和组 id

    8.4 关于进程线程的问题

    如何看待之前学习的单进程?——具有一个线程执行流的进程。


    总结

    缺页异常解决的是“地址合法,但映射暂时不完整”;COW 解决的是“先共享,真正写时再复制”;局部性让 Cache、TLB 和按需加载真正有意义。

    再回到线程,就能看出线程轻量化并不是一句“创建快、切换快”就结束了。地址空间不换、页表不换、TLB 不需要像进程切换那样重新建立工作集,才是这些现象背后的联系。

    资源分享: 【Linux】线程到底是什么?从轻量级进程、虚拟地址到页表与 MMU,一次理清线程底层模型 【Linux】信号到底什么时候被处理?sigaction、中断、用户态内核态与 SIGCHLD 【Linux】信号产生后去了哪里?从 Pending、Block 到 Core Dump,讲清信号的保存

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux】malloc 1GB 内存为什么没立刻占满?从缺页、COW 到 Cache/TLB,看懂线程为什么更轻
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址