操作系统知识点归纳


存储结构


为什么计算机要给储存结构分级?

答:

  • 不同级别的储存结构具有不同的访问速度,CPU 访问 CPU Cache 的延时只需要几纳秒,访问物理内存的延时则是 100 纳秒,速度相差 100 倍,访问磁盘延时就更高了,已经到毫秒级别了,但是访问速度越快的存储器,造价成本会高很多,容量也比较小
  • 通过分级储存结构,可以将数据按照不同的访问速度、容量和成本要求进行管理,CPU Cache 用于存储近期频繁访问的数据,内存用于存储当前执行的程序和数据,而硬盘则用于存储大量数据和长期存储

补充:

计算机的存储结构的层级如下图,从上到下,存储的容量越来越大,但是 CPU 访问的速度越来越慢

计算机的存储结构的层级

下面这张表格是不同层级的存储器之间的成本对比图:

不同层级的存储器之间的成本对比

详细链接:2.2 磁盘比内存慢几万倍? | 小林coding


CPU cache L1 L2 L3 读取数据的时间量级差距有多大?

答:

差距不大,都是纳秒级别,CPU 访问 CPU Cache L1 的延时大概是 1-2 纳秒,访问 CPU Cache L2 的延时大概是 5-10 纳秒,访问 CPU Cache L3 的延时大概是 10-30 纳秒

补充:

CPU 访问 L1 Cache 只需要 2~4 个时钟周期,访问 L2 Cache 大约 10~28 个时钟周期,访问 L3 Cache 大约 28~68 个时钟周期,而访问内存速度大概在 200~388 个时钟周期之间。如下表格:

cache L1 L2 L3的CPU访问所需时间对比

详细链接:2.3 如何写出让 CPU 跑得更快的代码? | 小林coding


CPU 缓存对程序性能的影响都体现在哪些方面?

答:

程序具有局部性原理,如果一个数据被访问,那么附近的数据很可能也会被访问到,CPU Cache 就是基于这个局部性原理,在访问数据的时候,如果访问的数据不在 CPU Cache 中,不会只从内存读这一个数据,而是会从内存连续加载数据到 CPU Cache,这样相邻的数据都会被缓存在 CPU Cache,那么程序在访问相邻数据的时候,相邻数据就可以直接在缓存中命中,无需访问内存,可以提高访问的效率

补充:

CPU 访问内存的速度,比访问 CPU Cache 的速度慢了 100 多倍,所以如果 CPU 所要操作的数据在 CPU Cache 中的话,这样将会带来很大的性能提升。访问的数据在 CPU Cache 中的话,意味着缓存命中,缓存命中率越高的话,代码的性能就会越好,CPU 也就跑的越快,对于遍历二维数组的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 二维数组
array[N][N]= 0;

// 形式一
for (int i = 0; i < N; i += 1) {
for (int j = 0; j < N; j += 1) {
array[i][j]= 0;
}
}

// 形式二
for (int i = 0; i < N; i += 1) {
for(int j = 0; j < N; j += 1) {
array[j][i]= 0;
}
}
  • 形式一 的速度会比 形式二 的速度快很多,因为 形式一 可以更好利用 CPU Cache,访问数据的缓存命中率会比较高
  • 当 CPU 访问内存数据时,如果数据不在 CPU Cache 中,则会一次性会连续加载 64 字节大小的数据到CPU Cache,那么当访问 array[0][0] 时,由于该元素不足 64 字节,于是就会往后顺序读取 array[0][8] ~ array[0][15] 到 CPU Cache 中。顺序访问的 array[i][j] 因为利用了这一特点,所以就会比跳跃式访问的 array[j][i] 要快

详细链接:2.3 如何写出让 CPU 跑得更快的代码? | 小林coding


内核态


什么是内核?

答:

  • 内核 是 操作系统 的核心组件,负责 管理系统资源、提供硬件抽象层、调度任务 和 实现进程间通信 等功能
  • 内核是系统中第一个加载的程序,并拥有最高的特权级别,控制整个系统的运行和资源分配。它与用户空间程序进行交互,提供系统调用接口,实现了操作系统的基本功能

补充:

计算机是由各种外部硬件设备组成的,比如:内存、CPU、硬盘等,如果每个应用都要和这些硬件设备对接通信协议,那这样太累了,所以由「内核」作为中间人,让内核作为应用连接硬件设备的桥梁,应用程序只需关心与内核交互,不用关心硬件的细节

内核的位置

  • 现代操作系统,内核一般会提供 4 个基本能力:
    1. 管理进程、线程,决定哪个进程、线程使用 CPU,也就是进程调度的能力
    2. 管理内存,决定内存的分配和回收,也就是内存管理的能力
    3. 管理硬件设备,为进程与硬件设备之间提供通信能力,也就是硬件通信能力
    4. 提供系统调用,如果应用程序要运行更高权限运行的服务,那么就需要有系统调用,它是用户程序与操作系统之间的接口

详细链接:[14 用户态和内核态:用户态线程和内核态线程有什么区别?](https://learn.lianglianglee.com/专栏/重学操作系统-完/14 用户态和内核态:用户态线程和内核态线程有什么区别?.md)


内核态和用户态区别?为什么要区分内核态和用户态?

答:

  • CPU 将指令分为了 特权指令 和 非特权指令,然后操作系统根据 CPU 的特权分级机制,把进程的运行空间分为 内核空间 和 用户空间
  • 内核态 具有 最高权限,可以执行 特权指令,直接访问所有系统和硬件资源,而 用户态 不能执行 特权指令,只能执行 非特权指令,所以只能访问受限的资源,比如:不可以直接访问内存、硬盘、网卡等硬件设备,如果要访问这些硬件资源,需要通过 系统调用 的方式,让进程陷入到内核态才能访问这些特权资源
  • 之所以要区分 内核态 和 用户态,目的就是 为了保证系统的稳定性和安全性,因为通过限制应用程序对特权指令的访问,可以防止恶意程序直接操作危险的指令导致系统崩溃的问题

补充:

  • 在 CPU 的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃,比如:清空内存,修改时钟等。如果所有的程序代码都能够直接使用这些指令,那么很有可能我们的系统一天将会死 n 次
  • 所以,CPU 将指令分为 特权指令 和 非特权指令 ,对于较为危险的指令,只允许操作系统本身及其相关模块进行调用,普通的、用户自行编写的应用程序只能使用那些不会造成危险的指令
  • 基于安全的考虑,CPU 提供了特权分级机制,把区域分成了四个 Ring,越往里权限越高,越往外权限越低

特权分级机制

  • 操作系统根据 CPU 的特权分级机制,把进程的运行空间分为「内核空间」和「用户空间」,分别对应着上图中, CPU 特权等级的 Ring 0 和 Ring 3

    Windows虚拟内存与物理内存示意图

    • 内核空间(Ring 0)具有最高权限,可以直接访问所有硬件资源,比如:硬盘、网卡、内存等
    • 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,过「系统调用」陷入到内核中才能访问这些特权资源
    • 为什么要区分 内核态 和 用户态 呢? 这是为了实现操作系统的核心功能和保证系统的稳定性、安全性以及资源的合理分配。通过区分 内核态 和 用户态,操作系统可以:
      1. 提供一种安全机制,限制用户程序对系统资源的直接访问,防止恶意程序对系统造成破坏
      2. 实现操作系统的核心功能,如管理进程、文件系统、内存管理等,这些功能需要在内核态下执行
      3. 实现多任务并发执行,通过在内核态和用户态之间进行切换,操作系统可以在不同的应用程序之间共享处理器资源,实现任务的并发执行

详细链接:Linux系统中,为什么需要区分内核空间与用户空间? - 知乎


什么时候会由用户态陷入内核态?

答:

主要有三种情况,会导致应用程序 从 用户态 陷入到 内核态:

  1. 应用程序执行 系统调用 的时候,会触发一个软件中断(比如:int 指令),会进入 内核态 执行相应的 内核代码,完成后再返回 用户态
  2. 当 应用程序发生异常情况,如:访问非法内存、除零错误 等,CPU 会触发一个异常,将控制权转移到内核态的 异常处理程序 中
  3. 当 系统接收到外部设备的中断信号,如:硬件设备的输入 / 输出请求、定时器中断 等,处理器会中断当前的执行,进入 内核态 执行相应的 中断处理程序

补充:

  • 有三种方式程序会从 用户态 陷入 内核态:
    1. 系统调用:这是 用户态进程 主动 要求切换到 内核态 的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作
    2. 异常:当 CPU 在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如:访问非法内存、除零错误等
    3. 外围设备的中断:当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如:硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作 等
  • 这 3 种方式是系统在运行时由 用户态 转到 内核态 的最主要方式,其中 系统调用 可以认为是 用户进程「主动」发起的,异常 和 外围设备中断 则 是「被动」的

详细链接:系统调用 | Liuye Notebook


系统调用的过程?

答:

  • 当 应用程序 发生 系统调用 的时候,会发生 用户态 和 内核态 的切换,过程是这样的:
    • 执行 系统调用 的时候,先将 系统调用名称 转换为 系统调用号,接着将 系统调用号 和 请求的参数 放到 寄存器 中,然后执行 软件中断命令,CPU 会从 用户态 切换到 内核态
    • CPU 跳转到 中断处理程序,将当前 用户态 的现场信息保存到 内核栈 中,接着根据 系统调用号 从 系统调用表 中找到对应的 系统调用函数,并将 寄存器 中保存的参数取出来,作为函数参数,然后在 内核 中执行 系统调用函数
    • 执行完 系统调用 后,执行 中断返回指令,内核栈 会弹出之前保存的用户态的现场信息,将原来用户态保存的现场恢复回来,这时候 CPU 恢复到 用户态,用户态 进程恢复执行

补充:

当程序需要访问硬件资源的时候,比如:内存、硬盘等,就需要通过「系统调用」陷入到内核中,才能访问这些特权资源。系统调用可以理解为内核实现的函数,比如:应用程序要通过网卡接收数据,会调用 Socket 的read 函数

操作系统接口

程序在执行系统调用的过程中会从 用户态 切换到 内核态,再从 内核态 切换到 用户态,过程如下:

执行系统调用的过程

  • 系统调用的过程:

    1. 从 用户态 到 内核态:当应用程序使用系统调用时,先将 系统调用名称 转换为 系统调用号,接着将「系统调用号」和「请求参数」放到寄存器里,然后执行软件中断指令(int $0x80 指令),产生一个中断,CPU 陷入到 内核态
    2. 执行 内核态 逻辑:CPU 跳转到 中断处理程序,先将当前 用户态 的 寄存器(用户态的代码段、数据段、保存参数的寄存器)压入到内核栈中,接着将 系统调用号 从 寄存器 里面取出来,最后根据 系统调用号,在「系统调用表」中找到相应的系统调用函数进行调用,并将寄存器中保存的参数取出来,作为函数参数
    3. 从 内核态 到 用户态:执行完系统调用后,执行中断返回指令(iret 指令),内核栈会弹出之前保存的寄存器,将原来用户态保存的现场恢复回来,包含代码段、指令指针寄存器等。这时候 CPU 恢复到 用户态,用户态 进程恢复执行
  • 一次系统调用过程中的,会发生两次「CPU 上下文切换」(所谓的 CPU 上下文就是 CPU 寄存器 和 程序计数器)

    1. 第一次 CPU 上下文切换是从 用户态 切换到 内核态:CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务
    2. 第二次 CPU 上下文切换是从 内核态 切换到 用户态:CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程

详细链接:为什么系统调用会消耗较多资源 - 面向信仰编程、在 x86 中实现用户态与系统调用 - Menci’s Blog


用户态和内核态是如何切换的?

答:

  • Linux 系统中每个进程都有两个栈,分别是 用户栈 和 内核栈,当应用程序运行在用户态的时候,就会使用用户栈,当应用程序运行在内核态的时候,就会使用内核栈
  • 内核态与用户态的相互切换,其中最重要的一个步骤就是 用户栈 和 内核栈 的切换
  1. 用户栈到内核栈:

    • 执行中断指令(int $0x80 指令),中断发生时,CPU 去一个特定的结构(比如:TSS)中,获取该进程的内核栈的地址信息,也就是内核栈的段选择子和栈顶指针(这两个东西是描述内核栈在内存的哪个地址空间),并分别送入 ss 寄存器和 rsp 寄存器,这时候 CPU 就指向了该进程的内核栈的栈顶位置了,这就完成了用户态到内核态的一次栈的切换

    • 然后,IP 寄存器(指令指针寄存器)跳入 中断服务程序 开始执行,中断服务程序 会把 用户态 的所有 寄存器 压入到 内核栈中。如下图,CPU 自动地将用户态栈的段选择子 ss3,和栈顶指针 rsp3 都放到内核态栈里了。这里的数字 3 代表了 CPU 特权级,内核态是 0,用户态是 3

      用户态栈 vs 内核态栈

  2. 内核栈 到 用户栈

    • 当中断结束时,中断服务程序 会从 内核栈 里将 CPU 寄存器的值全部恢复,最后再执行 “iret” 指令
    • 将 ss3 / rsp3 都弹出栈,并且将这个值分别送到 ss 和 rsp 寄存器中,这时候 CPU 就指向了该进程的用户栈的栈顶位置了,这样就完成了从内核栈到用户栈的一次切换
    • 内核栈的 ss0 和 rsp0 也会被保存到前面所说的 CPU 的一个特定的结构(比如:TSS)中,以供下次切换时使用

详细链接:Stack 顿悟三部曲(1):从CPU的视角说起 | 兔子先生


为什么用户态和内核态的相互切换过程开销比较大?

答:

  1. 上下文切换:在 用户态 和 内核态 之间切换时,需要保存当前进程或线程的上下文信息,包括:程序计数器、寄存器状态、栈指针 等。这些 上下文信息 保存在内存中或者内核数据结构中,在切换完成后需要再次加载回来。这个过程会消耗一定的时间和资源
  2. 权限切换:用户态和内核态具有不同的权限级别,内核态拥有更高的特权级别,可以执行一些用户态不可访问的指令和操作。因此,从用户态切换到内核态需要进行特权级别的变更,这个过程也会增加开销
  3. 地址空间切换:用户态 和 内核态 通常有不同的地址空间,为了保护操作系统内核不受用户程序的直接影响,当切换到内核态时需要进行地址空间的切换和映射,这也会增加开销
  4. TLB 刷新:TLB 是 CPU 中的一种缓存,用于存储虚拟地址到物理地址的映射关系。当发生用户态和内核态的切换时,可能导致 TLB 中的缓存无效,需要进行 TLB 刷新,这会引起一定的性能损失

补充:

  • 当程序中有系统调用语句,程序执行到系统调用时,首先使用类似 int 80 的 软中断 指令,保存现场,去系统调用,在内核态执行,然后恢复现场,每个进程都会有两个栈,一个 内核态栈 和一个 用户态栈。当 int 中断执行时就会由 用户态栈 转向 内核态栈,系统调用 时需要进行栈的切换,而且内核代码对用户不信任,需要进行额外的检查,而且系统调用的返回过程有很多额外工作,比如:检查是否需要调度等
  • 系统调用 一般都需要保存用户程序得上下文(context),在进入内核的时候需要保存用户态的寄存器,在内核态返回用户态的时候会恢复这些寄存器的内容。这是一个开销的地方。 如果需要在不同用户程序间切换的话,那么还要更新 cr3 寄存器(CR3 含有存放页目录表页面的物理地址),这样会更换每个程序的虚拟内存到物理内存映射表的地址,也是一个比较高负担的操作

中断


什么是中断?为什么要有中断?

答:

中断机制的好处是 化 主动 为 被动,避免 CPU 轮询等待某条件成立。如果没有中断机制,那么 “某个条件成立” 就需要 CPU 轮询判断,这样就会增加系统的开销。而使用中断机制,就可以在条件成立之后,向 CPU 发送中断事件,强制中断 CPU 执行程序,转而去执行中断处理程序

补充:

  • 在计算机中,中断 是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的 中断处理程序 来 响应请求
  • 简单来说,中断会让 CPU 停止正在执行的程序,转而让 CPU 去执行中断处理函数程序,执行完再返回原程序。例如:我们敲击键盘的时候,键盘的控制器就会向 CPU 发起一个中断请求。CPU 在接到请求以后,就会停下正在做的工作,把当前的寄存器状态全部保存好,然后去调用中断服务程序。为什么需要中断?我举个生活中取外卖的例子,对比一下有中断和没有中断的场景:
    • 如果没有中断机制(外卖员不会打电话给我),我需要一直一直傻傻地盯着看外卖配送员的进度,没办法去干其他事情,直到外卖到了,我才能做其他事情
    • 如果有中断机制(外卖员会打电话给我),我就不需要盯着外卖配送员的进度,我可以做其他事情,等外卖到了,外卖员会打电话给我(中断发生),我才会停下手中地事情,去拿外卖
    • 这里的打电话,其实就是对应计算机里的中断,没接到电话的时候,我可以做其他的事情,只有接到了电话,也就是发生中断,我才会停下当前的事情,去进行另一个事情,也就是拿外卖
  • 从这个例子,我们可以知道,中断是一种异步的事件处理机制,可以提高系统的并发处理能力

什么是硬件中断和软件中断?

答:

  • 硬件中断:由硬件 自动 触发的中断就属于硬件中断,比如:中断 和 异常 都属于硬件中断,中断是由外部设备触发的,异常是由 CPU 触发的
  • 软件中断:由软件程序 主动 触发的中断属于软件中断,比如:INT 指令,Linux 内核提供的系统调用,就是用了 INT 0x80 这种指令

补充:

先来说说中断的分类,以 Intel CPU 作为例子,Intel CPU 提供了三种「中断(动词)」程序执行的机制,分别是:

中断的分类

  • 中断(interrupt):由硬件设备触发,比如:点击一下鼠标、敲一下键盘,这时候外部设备会给 CPU 发送一个中断号,属于异步事件
  • 异常(exception):CPU 在执行指令时检测到的反常条件,比如:除法异常、错误指令异常,缺页异常等,然后 CPU 自己给自己一个中断号,无需外界给,属于同步事件
  • INT 指令:INT 指令后面跟一个数字,就相当于直接用指令的形式,告诉 CPU 一个中断号。比如:INT 0x80,就是告诉 CPU 中断号是 0x80。Linux 内核提供的系统调用,就是用了 INT 0x80 这种指令

根据触发的方式不同,将这三个机制分成两类:

根据触发方式分类

  • 中断 和 异常 属于「硬件中断」,因为他们都是 硬件 自动触发的,中断是由外部设备触发的,异常是由CPU 触发的
  • INT 指令属于「软件中断」,因为他是由软件程序主动触发的

Linux 启动


说一下 Linux 启动的流程?

答:

下图展示了 Linux 启动的流程具体步骤:

Linux启动流程

  • 第一步:当我们打开电源时,BIOS(基本输入/输出系统,Basic Input/Output System)或 UEFI(统一可扩展固件接口,Unified Extensible Firmware Interface)固件会从非易失性内存中加载,并执行 POST(开机自检,Power On Self Test)
  • 第二步:BIOS / UEFI 检测连接到系统的设备,包括 CPU、内存 和 存储设备
  • 第三步:选择一个启动设备来启动操作系统。可以是 硬盘、网络服务器 或 CD ROM
  • 第四步:BIOS / UEFI 运行引导加载器(GRUB),它提供了一个选择操作系统或内核功能的菜单
  • 第五步:内核准备就绪后,我们现在切换到用户空间。内核启动 systemd 作为第一个用户空间进程,负责管理进程和服务、探测所有剩余硬件、挂载文件系统并运行桌面环境
  • 第六步:系统启动时,systemd 默认激活 default.target 单元。同时还会执行其他分析单元
  • 第七步:系统运行一组启动脚本并配置环境
  • 第八步:用户将看到一个登录窗口。系统现已准备就绪

详细链接:简述 Linux 的启动过程_哔哩哔哩_bilibili


进程与线程


进程与线程有什么区别?

答:

  • 定义区别:进程就是运行起来的可执行程序,当我们运行一个可执行程序的时候,就会创建一个或多个进程,创建进程的时候需要分配空间,比如:栈区、文件映射区、堆区、静态区、常量区、代码段,因此也会说进程是资源分配的最小单位;线程则是程序执行的基本单位,每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束
  • 共享资源区别:每个进程有自己的独立地址空间,不与其他进程分享;一个进程里可以有多个线程,彼此
    共享同一个地址空间。堆内存、文件、套接字等资源都归进程管理,同一个进程里的多个线程可以共享使用。每个进程占用的内存和其他资源,会在进程退出或被杀死时返回给操作系统
  • 上下文切换区别:进程和线程上下文切换的时候,都会发生内核态和用户态的切换,进程间的切换的时候,除了需要切换 CPU 上下文,还需要切换页表,切换了页表会影响 TLB 的命中率,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换只需要切换 CPU 上下文,不会改变虚拟地址空间,不会影响 TLB 命中率
  • 安全性区别:并发应用开发可以用多进程或多线程的方式。多线程由于可以共享资源,效率较高;反之多进程不共享地址空间和资源,开发较为麻烦,在需要共享数据时效率也较低。但多进程安全性较好,在某一个进程出问题时,其他进程一般不受影响;而在多线程的情况下,一个线程执行了非法操作会导致整个进程退出

为什么需要线程?

答:

  • 并发能力:线程可以实现程序的并发执行,即在同一时间内可以同时执行多个任务,提高了系统的响应速度和资源利用率。通过多线程编程,可以将耗时的操作与其他任务并行执行,提升程序的性能
  • 切换开销:线程的切换开销比进程小,线程切换只需要切换 CPU 上下文,不会改变虚拟地址空间,不需要重新加载页表,不会影响 TLB 命中率
  • 通信方便:同一个进程内,多个线程是可以共享进程内存资源的,线程间的通信比进程间的通信更简单和高效

补充:

线程是轻量级的进程,可以从 并行能力、切换开销、通信方便 来说明线程的优势

详细链接:[17 进程和线程:进程的开销比线程大在了哪里?](https://learn.lianglianglee.com/专栏/重学操作系统-完/17 进程和线程:进程的开销比线程大在了哪里?.md)


多线程是不是越多越好,太多会有什么问题?

答:

  • 多线程不一定越多越好,过多的线程可能会导致一些问题
    1. 切换开销:线程的创建和切换会消耗系统资源,包括 内存 和 CPU。如果创建太多线程,会占用大量的系统资源,导致系统负载过高,某个线程崩溃后,可能会导致进程崩溃
    2. 死锁的问题:过多的线程可能会导致竞争条件和死锁。竞争条件指的是多个线程同时访问和修改共享资源,如果没有合适的同步机制,可能会导致数据不一致或错误的结果。而死锁则是指多个线程相互等待对方释放资源,导致程序无法继续执行

补充:

线程数量越多,切换的开销就越高,发生死锁的概率就越大


什么时候用单线程,什么时候用多线程呢?

答:

  • 一般在单个逻辑比较简单,而且速度相对来非常快的情况下,我们可以使用 单线程。例如:Redis 处理命令的实现是用了单线程,从内存中快速读取值,避免了加锁的开销
  • 而在逻辑相对来说很复杂的场景,等待时间相对较长又或者是需要大量计算的场景,建议使用多线程来提高系统的整体性能。例如:NIO 时期的文件读写操作、图像处理以及大数据分析等

补充:

  • 单线程适用于以下情况:
    1. 简单的程序:当程序逻辑简单且任务量较少时,使用单线程可以简化代码结构,减少线程切换的开销
    2. 需要顺序执行的任务:例如,需要按照特定的顺序依次执行任务的场景,单线程可以确保任务按照预期的顺序执行
    3. 对资源访问有限制:某些资源(如:共享数据、文件)可能有访问限制,单线程可以避免并发访问造成的竞争和冲突
  • 多线程适用于以下情况:
    1. 并发处理任务:当需要同时处理多个任务,且任务之间没有强依赖关系时,使用多线程可以提高程序的并发性和整体性能
    2. 大规模计算:对于需要大量计算的任务,使用多线程可以将计算任务分配给多个线程,加快计算速度
    3. 高响应性要求:多线程可以提高系统的响应性,例如在图形界面应用中,使用多线程可以保持界面的流畅性,同时处理用户输入和后台任务

线程共享了进程的哪些资源?

答:

  • 线程共享了进程的 虚拟内存空间、文件描述符、信号处理器
    • 虚拟内存空间:意味着线程可以访问进程的代码区、堆区、数据区的资源等
    • 文件描述符:意味着线程可以通过文件描述符访问进程打开的文件、套接字等资源
    • 信号处理器:意味着线程可以接收和处理进程收到的信号

补充:

单线程与多线程的示意图

  • 同一进程中的线程共享资源
    1. 虚拟内存空间:共享进程的代码区、堆区、数据区的资源等
    2. 文件描述符:共享进程打开的文件、套接字等资源
    3. 信号处理器:共享进程的信号处理器
  • 同一进程中的线程独占资源
    • 程序计数器和寄存器的值
    • 栈:每个线程中的函数调用过程是独立的,因此需要有独立的栈

详细链接:线程间到底共享了哪些进程资源?-腾讯云开发者社区-腾讯云


为什么创建进程比创建线程慢?

答:

  • Linux 中创建一个进程自然会创建一个线程,也就是主线程。创建进程需要为进程划分出一块完整的内存空间,包含 代码区、堆区、栈区、数据区 等内存资源
  • 创建线程则简单得多,只需要确定 PC 指针和 寄存器 的值,并且给线程分配一个栈用于执行程序,同一个进程的多个线程间可以复用进程的虚拟内存空间,减少了创建虚拟内存的开销
  • 因此,创建进程比创建线程慢,而且进程的内存开销更大

补充:

从创建虚拟内存的成本来考虑

详细链接:[17 进程和线程:进程的开销比线程大在了哪里?](https://learn.lianglianglee.com/专栏/重学操作系统-完/17 进程和线程:进程的开销比线程大在了哪里?.md)


为什么进程的切换比线程开销大?

答:

进程间的切换的时候,除了需要切换 CPU 上下文,还需要 切换页表,切换了页表会影响 TLB 的命中率,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换只需要切换 CPU 上下文不会改变虚拟地址空间,不会影响 TLB 命中率

补充:

  • 进程切换分两步:
    1. 切换「页表」以使用新的地址空间。一旦去切换上下文,CPU 中所有已经缓存的内存地址就失效了
    2. 切换内核栈和 CPU 上下文
  • 线程和进程的最大区别就在于地址空间,对于线程切换,第 1 步是不需要做的,第 2 步是进程和线程切换都要做的。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换
  • 进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用 Cache 来缓存常用的地址映射,这样可以加速页表查找,这个 Cache 就是TLB(translationLookaside Buffer,TLB 本质上就是一个 Cache,是用来加速页表查找的)

进程与线程的切换

  • 由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后 TLB 就失效了,Cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致 TLB 失效,因为线程无需切换地址空间,因此我们通常说线程切换要比较进程切换快,原因就在这里

详细链接:深入理解Linux内核进程上下文切换-腾讯云开发者社区-腾讯云


线程的上下文切换是怎么个过程?

答:

线程发生上下文切换的时候,正在运行的线程会将寄存器的状态保存到内核中的 TCB(线程控制块)里,然后恢复另一个线程的上下文。和进程的区别是,线程只需要切换 CPU 的上下文,不会改变地址空间

补充:

当线程切换的时候,当前线程被时钟中断打断,将发生中断时的现场保存到进程内核栈(如:sp、lr 等),然后会切换到下一个线程,当再次回切换回来的时候,返回用户空间的时候会恢复之前的现场,线程就可以继续执行(执行之前被中断打断的下一条指令,继续使用自己用户态sp),这对于用户程序来说是透明的

如下为硬件上下文切换示例图:

硬件上下文切换

详细链接:深入理解Linux内核进程上下文切换-腾讯云开发者社区-腾讯云


僵尸进程、守护进程与孤儿进程的区别?

答:

  • 僵尸进程 是指子进程已经终止,但其父进程尚未调用 wait() 或 waitpid() 函数来获取子进程的终止状态,导致子进程的进程描述符仍然保留在系统进程表中,成为僵死进程,僵死进程不占用系统资源,但会占用一个进程ID
  • 孤儿进程 是指父进程先于子进程退出或异常终止,导致子进程成为孤儿进程。孤儿进程会被 init 进程(进程ID为1)接管,init 进程会成为孤儿进程的新的父进程
  • 守护进程 是在后台运行的一种特殊进程,不与任何终端关联,关闭终端并不会影响守护进程的生命周期,守护进程的生命周期通常是伴随系统的启动和关闭,会一直在后台运行

补充:

  • 僵尸进程(记忆点:儿子死了,爸爸没有收尸,儿子就成僵尸了):僵尸进程是指终止但还未被回收的进程。如果子进程退出,而父进程并没有调用 wait() 或 waitpid() 来回收,那么就会产生僵尸进程。僵尸进程是一个已经死亡的进程,但是其进程描述符仍然保存在系统的进程表中
    • 危害:占用进程号,系统所能使用的进程号是有限的,可能导致不能产生新的进程;占用一定的内存
    • 如何避免产生僵尸进程:
      • 父进程调用 wait 或者 waitpid 等待子进程结束
      • 子进程结束时,内核会发生 SIGCHLD 信号给父进程。父进程可以注册一个信号处理函数,在该函数中调用 waitpid,等待所有结束的子进程;也可以用 signal(SIGCLD,SIG_IGN)忽略 SIGCHLD 信号,那么子进程结束后,内核会进行回收
      • 杀死父进程,僵尸进程就会变成孤儿进程,由 Init 进程接管并处理
  • 孤儿进程(记忆点:爸爸没了,就是孤儿):如果某个进程的父进程先结束了,那么它的子进程会成为孤儿进程每个进程结束的时候,系统都会扫描是否存在子进程,如果有则用 Init 进程(pid = 1)接管,并由 Init 进程调用 wait 等待其结束,完成状态收集工作。孤儿进程不会对系统造成危害
  • 守护进程:守护进程(英语:daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux 系统的大多数服务器就是通过守护进程实现的。守护进程的名称通常以 d 结尾,比如:sshd、xinetd、crond 等,常见的守护进程包括系统日志进程 syslogd、web服务器 htpd、邮件服务器 sendmail 和数据库服务器 mysqld等

详细链接:Linux 之守护进程、僵死进程与孤儿进程 | LiuYongbin


怎么杀死僵尸进程?

答:

僵尸进程是已经死了的,不能直接使用 kill 命令杀掉僵尸进程,只能通过 ps 命令找到僵尸进程的父进程的 pid 号,然后通过 kill 命令杀掉父进程的方式来达到僵尸进程的效果,因为当父进程被杀掉后,操作系统会将僵尸进程的父进程会变为 1 号 init 进程,接着 init 进程会自动接管僵尸进程的回收工作

补充:

可以使用 ps aux | grep z 命令来找到僵尸进程。在状态位带有 2 关键字的进程代表其为僵尸进程。我们可使用 top 命令查看系统中是否有僵尸进程存在

top命令

  • 正常情况下我们可以用 SIGKILL 信号来杀死进程,但是僵尸进程已经死了,你不能杀死已经死掉的东西

  • 所以只能通过杀死僵尸进程的父进程来达到杀死僵尸进程的效果,因为当进程终止时,操作系统会将所有子进程的父进程设置为 init 进程(进程ID为 1),init 进程会自动接管僵尸进程的回收工作

  • 因此,要杀掉一个僵尸进程,需找到其父进程号(PPID)并给它发送 SIGKILL(9) 信号:kill -9 ppid

  • 可使用以下命令去找到僵尸进程的 PPID

    1
    ps -p PID -o ppid

详细链接:技术|什么是僵尸进程,如何找到并杀掉僵尸进程?


一个进程fork出一个子进程,那么他们占用的内存是之前的2倍吗?

答:

  • 不是的

    • fork 的时候,创建的子进程是复父进程的虚拟内存,并不是物理内存,这时候父子的虚拟内存指向的是同个物理内存空间,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读

      写时复制

    • 不过,当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在「写保护中断处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制」,发生了这个过程,内存占用才会增多

补充:

考察写时复制的概念,在发生写操作的时候,操作系统才会去复制物理内存,否则父子进程的物理内存还是共享状态,并不会增加内存占用

详细链接:linux-kernel - Linux 写时复制机制原理 - 个人文章 - SegmentFault 思否


协程


什么是协程?

答:

协程是一个用户态的线程,用户在堆上模拟出协程的栈空间。当需要进行协程上下文切换的时候,主线程只需要交换栈空间和恢复协程的一些相关的寄存器的状态,就可以实现上下文切换。相比于线程上下文切换,没有了从 用户态 转换到 内核态 的切换成本

补充:

要强调协程是用户态的线程,切换的时候是不需要发生用户态和内核态的切换,相当于是一个轻量级线程

详细链接:从头到尾理解有栈协程实现原理 - 知乎


协程和线程有什么区别?

答:

  • 协程 可以理解为 用户态线程,跟 线程 的区别主要有三个方面:
    1. 线程调度由 内核负责,协程调度由 用户负责,所以协程的切换是比线程快的,因为协程的切换不用经过操作系统用户态与内核态的切换,并且协程的切换只需要保留极少的状态和寄存器变量值,比如:SP、BP、PC 寄存器,而线程的切换会保留额外的寄存器变量值,比如:浮点寄存器
    2. 线程的栈比协程的栈大很多,线程的栈大小一般是在创建时指定,默认是大小是 8MB,如果创建成千上万个线程的话,对于系统会有很大的负担,而协程栈通常是 KB 级别的,比如:2KB,所以即使创建上千上万个协程,对于系统来说也不是很大的负担。另外,线程的栈在运行时不能够更改,而协程栈在 Go 运行时的帮助下可以动态检测栈的大小,进行扩容和收缩
    3. 协程的调度是协作式调度,当一个协程处理完自己的任务后,可以主动将执行权限让给其他协程,而线程是抢占式调度,线程的切换的主动权不再线程自己身上。

补充:

特性 进程 线程 协程
定义 资源(包括内存、打开的文件等)分配的单位 CPU 调度的基本单位 用户态轻量级线程,线程内部调度的基本单位
切换情况 进程切换 CPU 环境(栈、寄存器、页表和文件句柄等)的保存以及新调度进程环境的设置 保存和设置程序计数器、寄存器和栈的内容 先将寄存器的上下文和栈保存,等切换回来时进行恢复
切换过程 用户态 → 内核态 → 用户态 用户态 → 内核态 → 用户态 用户态
调用栈 内核栈 内核栈 用户栈
并发性 不同进程之间实现并发,各自占用 CPU 时间片 一个进程内部多个线程并发执行 同一时间只能有一个协程,而其它协程处于休眠状态,适合对任务分时处理
系统开销 切换虚拟地址空间,切换内核栈和硬件上下文,CPU 高速缓存失效,页表切换,开销很大 切换时只需保存和设置少量寄存器内容,有一定的开销 直接操作/恢复基本没有内核切换的开销,可以加锁的访问全局变量,开销最小
通信 进程之间的通信需要借助操作系统 线程之间可以直接写入进程数据段(如全局变量)来进行通信 共享内存,消息队列
占用内存 依据所调用的资源大小 固定不变,由编译器决定 初始化一般较小,可以自动扩展
  • 从 调度方式、切换开销、栈大小 这三个方面来展开说明区别:
    1. 协程的调度是协作式调度(主动放弃执行,并不会被其他协程打断),当一个协程处理完自己的任务后,可以主动将执行权限让给其他协程。这意味着协程能更好地在规定时间内完成自己的工作,而不会被任意抢占。当一个协作式运行的时间过长,Go 语言调度器才会强制抢占其执行。线程是抢占式调度,当发生中断或者 I/O 的时候,CPU 就会自动切换别的线程,也就是线程的切换的主动权不再线程自己身上
    2. 协程的切换要快于线程,因为协程的切换不用经过操作系统用户态与内核态的切换,并且 Go 语言中协程的切换只需要保留极少的状态和寄存器变量值(SP / BP / PC),而线程的切换会保留额外的寄存器变量值(例如:浮点寄存器)。在 Go 语言中,上下文切换速度的一个可参考量化指标是,线程大约为 1~2 微秒,协程大约是 0.2 微秒,比线程快数倍
    3. 线程的栈比协程的栈大很多,线程的栈大小一般是在创建时指定,为了避免出现栈溢出(Stack Overflow)的错误,默认的栈大小会相对较大(例如:8M),这意味着创建 1000 个线程就需要消耗 2G 的虚拟内存,这大大限制了线程创建的数量(虽然今天 64 位的虚拟内存地址空间已经让这种限制变得不太严重)。而 Go 语言中的协程栈默认为 2 KB,在实践中,我们经常会看成千上万的协程存在。另外,线程的栈在运行时栈不能够更改,而协程栈在 Go 运行时的帮助下可以动态检测栈的大小,进行扩容和收缩。由于协程栈很小,在实践中,协程常被看作是一种轻量级的资源

详细链接:进程线程协程的本质区别 | Anonymous’ Blog


协程切换的本质是什么?

答:

协程的切换本质就是切换 CPU 寄存器,把当前协程的 CPU 寄存器状态保存起来,比如:IP、SP寄存器,然后将需要切换进来的协程的 CPU 寄存器状态加载到 CPU 寄存器上就完成了切换的过程,整个切换过程发生在用户态,不会陷入内核态

补充:

  • 协程实际上是将函数实现成可以任意中断的函数,比如:函数 1 执行到一半,可以直接切换执行别的函数,给人的感觉就是多个函数在并发,所以协程的切换类似于函数调用栈切换

  • 已知函数运行在调用栈上,如果将一个函数作为协程,保存上下文即是保存从这个函数栈帧存储的值,以及此时寄存器存储的值;恢复上下文即是将这些值分别重新写入对应的栈帧和寄存器;而切换上下文无非是保存当前正在运行的函数的上下文,恢复下一个将要运行的函数的上下文。下图是协程切换前的状态:

    协程切换前的状态

  • 从协程 1 切换到协程 2 后的状态如下图所示:

    从协程1切换到协程2后的状态

详细链接:有栈协程与无栈协程 - 知乎


为什么协程切换的开销比线程切换小?

答:

  • 用户态切换:协程的切换是在用户态进行的,不需要操作系统的介入。相比之下,线程的切换需要操作系统进行调度和上下文切换,需要从用户态切换到内核态,这涉及到 CPU 寄存器的保存和恢复等操作,开销较大
  • 协作式调度:协程的调度是协作式的,由协程自身主动让出执行权,而不是被操作系统强制切换。这种调度方式避免了不必要的上下文切换,减少了切换开销

补充:

从 用户态切换、协作式调度 方式去对比

详细链接:进程线程协程的本质区别 | Anonymous’ Blog


进程间通信


进程间有哪些通信方式?

答:

  • 进程间通信方式主要有 管道、消息队列、共享内存、信号、信号量 和 socket 通信
    • 管道通信的数据是 无格式的字节流,并且通信方向是 单向 的,只能在一个方向上流动,管道分为 匿名管道 和 有名管道,匿名管道 是没有文件实体,只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。而有名管道有文件实体,可以用于任何进程间的通信,并且数据可以持久化保存,即使创建它的进程退出,其他进程仍然可以使用该管道
    • 消息队列在内核中是通过链表来组织消息的,克服了管道通信的数据是无格式的问题
    • 管道和消息队列在读写数据的时候,都需要经过用户态与内核态之间的拷贝过程,共享内存就解决了这个问题,多个进程可以将共享内存映射到各自的虚拟地址空间,实现共享数据的读写,不会涉及用户态和内核态之间的数据拷贝,所以共享内存方式是进程间通信方式里最高效的,不过共享内存带来新的问题,多进程竞争同个共享资源会造成数据的错乱,因为需要同步机制来保证多进程下的读写数据的安全
    • 信号量可以实现进程间的同步和互斥访问共享资源,信号量其实是一个计数器,表示的是资源个数,当一个进程想要访问资源时,它会尝试递减信号量(称为P操作)。如果信号量的值大于零,这个操作会成功,否则进程就会阻塞,直到信号量变为正数。当进程完成对资源的访问后,它会递增信号量(称为V操作),允许其他阻塞的进程访问资源
    • 信号是一种异步的通知机制,用于通知进程某个事件已经发生。当一个信号发送给一个进程时,操作系统会中断进程的正常流程来处理信号,只适用于简单的通知和事件处理
    • 前面提到的这些通信方式都只能在本上进行进程间通信,如果要实现跨主机的进程间通信,就需要通过 socket 通信了,可以实现基于 TCP 或者 UDP 协议的通信方式

补充:

  • 要说出每个通信方式的应用场景和优劣势。
    • 管道:分为 有名管道 和 匿名管道
      • 匿名管道顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中 shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失
      • 命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作
    • 消息队列:消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程
    • 共享内存:共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱
    • 信号量:信号量可以保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作
    • 信号:与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如:键盘 ctrl + C)和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即:SIGKILL 和 SIGSTOP ,这是为了方便我们能在任何时候结束或停止某个进程
    • socket 通信:前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式

详细链接:5.2 进程间有哪些通信方式? | 小林coding


哪个进程间通信效率最高的?

答:

共享内存 的通信效率最高,因为共享内存不涉及内核态和用户态之间的数据拷贝

补充:

  • 共享内存通信效率最高
  • 共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度

共享内存


有名管道和匿名管道的区别?

答:

  • 匿名管道 没有 文件实体,不能用于非亲缘进程间通信,只能用于在父子进程之间进行通信,并且匿名管道只存在于创建它的进程的内存中,进程退出后即被销毁
  • 有名管道 存在 文件实体,可以用于任意进程间的通信,并且数据可以持久化保存,即使创建它的进程退出,其他进程仍然可以使用该管道

补充:

说出 2 个区别点:

  1. 持久性:有名管道在文件系统中存在,可以持久化保存,即使创建它的进程退出,其他进程仍然可以使用该管道。而匿名管道只存在于创建它的进程的内存中,进程退出后即被销毁
  2. 进程间关系:有名管道可以用于任意进程间的通信,不受进程关系的限制。而匿名管道只能在父子进程之间进行通信,因为子进程会复制父进程的文件句柄,虽然这样子进程就拥有了匿名管道的文件句柄,因此就可以进行父子通信了

详细链接:Linux 进程间通信——匿名管道和有名管道 - Colourso


信号和信号量的区别?

答:

  • 信号用于 异步处理异常 或 特殊事件,而信号量用于同步进程以安全地访问共享资源
    • 信号是一种异步的通知机制,用于通知进程某个事件已经发生。当一个信号发送给一个进程时,操作系统会中断进程的正常流程来处理信号
    • 信号量是一种用于解决多进程同步和互斥问题的工具,主要用于保护共享资源,防止多个进程同时访问。信号量有一个整数值,该值的含义是可以同时访问某个资源的进程数量。当一个进程想要访问资源时,它会尝试递减信号量(称为P操作)。如果信号量的值大于零,这个操作会成功,否则进程就会阻塞,直到信号量变为正数。当进程完成对资源的访问后,它会递增信号量(称为V操作),允许其他阻塞的进程访问资源

补充:

  • 概念不同:信号是一种异步通知机制,用于通知进程发生了某个特定事件,例如:键盘输入、异常等。信号量是一种同步机制,用于实现多个进程之间的互斥访问和同步操作
  • 使用方式不同:信号通过发送信号的进程向接收信号的进程发送信号,接收进程通过注册信号处理函数来响应信号。信号量通过对共享资源进行加锁和解锁的操作,协调多个进程对共享资源的访问。
  • 应用场景不同:信号常用于处理异步事件,例如:捕获异常、处理用户输入等。信号量常用于解决多个进程对共享资源的并发访问问题,例如:控制进程的执行顺序、避免竞态条件等

详细链接:5.2 进程间有哪些通信方式? | 小林coding


内存管理