Linux操作系统相关知识总结
第1章 Linux概述
本章是理解Linux工作原理的基石,系统地阐述了程序、进程、内核、系统调用和库这些核心概念及其相互关系。
一、核心概念:程序 vs. 进程 vs. 内核
| 概念 | 定义 | 关系与比喻 |
|---|---|---|
| 程序 | 存储在存储设备上的一系列指令和数据的静态集合。如可执行文件、源代码文件。 | 剧本。包含所有台词和动作说明。 |
| 进程 | 正在执行的程序的一个实例。是系统进行资源分配和调度的基本单位。 | 一场演出。根据剧本(程序)进行动态表演。 |
| 内核 | 一种特殊的程序 ,永远运行在内核模式下。它是操作系统的核心,负责管理硬件资源(CPU、内存、设备)并为所有进程提供服务。 | 舞台总监。管理整个剧场(计算机)的硬件资源(灯光、音响),协调所有演出(进程),并确保它们不会互相干扰。 |
关键结论 :计算机启动后,首先加载内核 ,然后由内核负责创建和运行其他进程 来执行程序。
二、为什么需要内核?------ 特权模式与系统调用
1. 问题:没有内核的直接访问
如果进程可以直接访问硬件(如存储设备),当多个进程并发操作时,会因指令交错的竞态条件而导致数据被破坏,甚至硬件损坏。
2. 解决方案:内核作为"看门人"
现代CPU提供了不同级别的特权模式(如x86架构的Ring 0-Ring 3)。Linux主要使用两种:
- 内核模式 :CPU可以执行所有指令 ,包括操作硬件的特权指令。只有内核运行在此模式。
- 用户模式 :CPU禁止执行 特权指令。几乎所有应用程序进程都运行在此模式。
3. 桥梁:系统调用
进程如何请求内核服务?通过系统调用。
- 定义 :进程向内核请求服务的唯一接口。例如创建进程、操作文件、网络通信等。
- 工作流程 :
- 进程在用户模式执行特殊指令(如
syscall),触发陷入。 - CPU切换到内核模式,跳转到内核中对应的系统调用处理函数。
- 内核执行请求(如写入数据到磁盘),并进行安全校验。
- 处理完毕,CPU切换回用户模式,进程继续执行。
- 进程在用户模式执行特殊指令(如
核心价值 :内核通过特权模式和系统调用,实现了对硬件的安全、有序访问,避免了混乱。
三、实践:观察系统调用
1. 工具:strace
- 命令 :
strace -o <logfile> <command> - 作用 :跟踪命令执行过程中发生的所有系统调用。
- 结论 :无论程序用何种语言(Go, Python, C)编写,只要涉及底层操作(如打印输出、读写文件),最终都会发出相同的系统调用(如
write)。这证明了系统调用是跨语言的统一接口。
2. 工具:sar
- 命令 :
sar -P 0 1 1 - 作用:查看CPU时间的使用分布。
- 关键字段 :
- %user:CPU处于用户模式的时间占比(执行应用程序代码)。
- %system :CPU处于内核模式的时间占比(处理系统调用)。
- %idle:CPU空闲时间占比。
- 应用 :如果
%system过高,说明系统正在频繁处理系统调用,可用strace -T进一步分析是哪些系统调用耗时过长。
四、库:系统调用的"包装层"
直接使用汇编指令发起系统调用非常复杂且不可移植。因此,操作系统提供了库 来封装这些底层细节。
1. 核心库:C标准库
- Linux实现 :glibc。
- 作用 :
- 提供C语言标准函数。
- 提供系统调用的包装函数 (如C函数
getppid()内部会发起getppid系统调用)。
- 价值:程序员只需调用简单的高级语言函数,库会处理不同CPU架构的汇编差异,极大提升了开发效率和可移植性。
- 查看工具 :
ldd <program>命令可查看程序依赖了哪些库。
2. 库的链接方式:静态库 vs. 共享库
| 特性 | 静态库 (.a) | 共享库 (.so) |
|---|---|---|
| 链接时机 | 编译时 | 运行时 |
| 结果 | 库代码被复制到最终可执行文件中。 | 可执行文件只记录库信息,运行时动态加载。 |
| 优点 | 移植简单(单文件)、启动快、避免"DLL地狱"。 | 节省磁盘和内存(多进程共享一份代码)、易更新。 |
| 缺点 | 文件大、浪费空间、更新需重新编译。 | 依赖环境、可能存在库版本冲突(DLL地狱)。 |
| 趋势 | 早期少用,现在因硬件资源丰富而复兴(如Go语言默认使用)。 | 传统上应用广泛,是Linux系统的主流方式。 |
第2章 进程管理(基础篇)
本章是理解Linux多任务环境如何运作的核心,详细阐述了进程从创建、管理到通信的完整生命周期。
2.1 进程的创建:fork() 与 execve()
这是进程诞生的基础。
fork():核心机制是"复制"。调用后,内核创建当前进程(父进程)的一个几乎完全相同的副本(子进程),两者拥有独立的内存空间。通过写时复制(Copy-on-Write) 技术优化性能。关键区别在于返回值:父进程获得子进程的PID,子进程获得0。这使得程序可以区分父子并执行不同代码。execve():核心机制是"替换"。它将当前进程的内存映像(代码、数据等)替换为一个全新的程序文件。- 典型模式 :Shell运行命令的经典模式就是先
fork()创建一个子进程,然后在子进程中调用execve()来执行新程序(如ls)。
2.2 进程的层次关系:进程树
Linux中的进程呈树状结构,所有进程都是PID=1的 init进程 (现代系统多为systemd)的后代。使用 pstree -p 命令可以直观地查看整个进程树。
bash
$ pstree -p
systemd(1)─┬─sshd(960)───sshd(19260)───bash(19262)───pstree(19777)
├─go_build(19555)
└─...
这种关系确保了进程管理的秩序性,特别是为处理孤儿进程奠定了基础。
2.3 进程的状态
进程在其生命周期中会处于以下几种主要状态(可通过 ps aux 或 ps l 的 STAT 列查看):
- 运行(R):正在或即将在CPU上执行。
- 可中断睡眠(S):等待某个事件(如I/O操作完成),在等待期间可被信号唤醒。
- 不可中断睡眠(D) :同样在等待,但在此期间不响应信号,通常发生在等待磁盘I/O等关键操作时,无法被强制杀死。
- 停止(T) :被信号(如
SIGSTOP)暂停执行,可被信号(如SIGCONT)恢复。 - 僵尸(Z) :进程已终止,但其退出状态尚未被父进程读取(通过
wait()系统调用)。它占用的内核资源已被释放,仅在进程表中保留一个条目。如果父进程异常退出,其子进程会被init收养并由init负责清理,避免僵尸进程累积。
2.4 进程间通信:信号(Signal)
信号是异步通信机制,用于通知进程某个事件的发生。
- 常见信号 :
SIGINT(2):终端中断信号,通常由Ctrl+C触发,发送给前台进程组。SIGKILL(9):强制终止信号,无法被捕获、忽略或阻塞。SIGSTOP(19):暂停进程执行。SIGCONT(18):继续执行已暂停的进程。SIGCHLD(17):子进程状态改变(终止、暂停等)时发给父进程的通知。SIGHUP(1):1)终端挂断(如用户关闭终端窗口);2)守护进程常用其重新读取配置文件。
- 进程可以自定义信号处理函数来响应大部分信号(除
SIGKILL和SIGSTOP)。
2.5 进程的组织单位:会话(Session)与进程组(Process Group)
这是Shell实现作业控制(Job Control)的基础。
1. 核心定义
- 会话(Session) :一个用户登录后的一次完整工作环境 。例如,当你打开一个终端窗口,你就创建了一个会话。每个会话有一个唯一的会话ID(SID) ,通常由会话的领头进程(如登录Shell)的PID担任。一个会话关联一个控制终端(Controlling Terminal)。
- 进程组(Process Group) / 作业(Job) :一个会话包含一个或多个进程组。Shell执行的每一条命令 都会形成一个独立的进程组。每个进程组有一个唯一的进程组ID(PGID),通常是组长的PID。
2. 实践解析:ps ajx 命令输出
命令 ps ajx 的示例输出:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
19261 19262 19262 19262 pts/0 19653 Ss 1000 0:05 -bash
19262 19653 19653 19262 pts/0 19653 R+ 1000 0:00 ps ajx
19262 19654 19653 19262 pts/0 19653 S+ 1000 0:00 less
- PPID : 父进程ID。
ps和less的PPID都是bash的PID(19262)。 - PID: 进程ID。
- PGID : 进程组ID。
ps(19653) 和less(19654) 拥有相同的PGID (19653) ,说明它们属于同一个进程组(作业),即管道命令ps ajx | less。 - SID : 会话ID。三者SID都是19262(即
bash的PID),属于同一会话。 - TTY : 控制终端。
pts/0表示伪终端0。 - TPGID : 前台进程组的PGID 。此值为19653,与
ps和less的PGID相同,说明它们是前台作业。 - STAT : 进程状态。其中的
+号表示进程属于前台进程组 。bash的状态是Ss(会话领头进程,可中断睡眠),没有+,说明它是后台进程组。
3. 前台与后台进程组
- 前台进程组 :在每个会话中,有且只有一个 。它能直接从控制终端接收输入和信号(如
Ctrl+C产生的SIGINT)。 - 后台进程组 :会话中的其他进程组。如果后台进程尝试从终端读取输入,它会收到
SIGTTIN信号而被暂停。 - 控制命令 :
command &:将命令置于后台启动。Ctrl+Z:发送SIGSTOP暂停前台作业。bg:将最近暂停的作业在后台继续运行。fg:将作业切换到前台。kill -<信号> -<PGID>:向整个进程组发送信号。例如kill -KILL -12345。
2.6 守护进程(Daemon)
守护进程是在后台运行、提供系统服务的特殊进程,如sshd、nginx等。
1. 核心特征
- 脱离终端(TTY=?) :通过调用
setsid()创建新的会话并脱离原始终端。在ps输出中,TTY字段为?。 - 成为会话领头进程(PID=SID) :其PID等于SID。
- 父进程为
init(PPID=1) :创建守护进程的父进程通常会先结束,使得守护进程被init进程收养。这确保了它不会受到原登录会话结束的影响,从而常驻系统。
2. 判断实践:以sshd为例
sshd的ps ajx输出:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
...
1 960 960 960 ? -1 Ss 0 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
...
PPID=1:父进程是init。PID=960:进程ID。SID=960:PID等于SID,自身是会话领头进程。TTY=?:没有关联任何终端。STAT=Ss:会话领头进程,处于可中断睡眠。
结论 :sshd是一个典型的守护进程。
3. SIGHUP信号的新用途
由于守护进程已脱离终端,原本表示"终端挂断"的SIGHUP信号对它失去了原意。因此,守护进程通常将SIGHUP信号用于重新读取配置文件 ,实现不重启服务即可应用新配置。例如,执行 kill -HUP <nginx_pid> 可让Nginx重载配置。
第3章 进程调度
本章通过一系列精心设计的实验,将抽象的进程调度原理可视化,深入揭示了Linux调度器的工作机制、性能表现及其对系统设计的影响。
3.1 预备知识:运行时间和执行时间
本章开篇明确了两个关键概念,这是理解后续所有实验的基石:
- 运行时间(Real Time) :进程从开始到结束所耗费的总时间 ,即墙上时钟走过的时间。对应
time命令输出中的real值。 - 执行时间(CPU Time) :进程实际占用CPU 的时间总和。它分为:
- 用户时间(User Time) :进程在用户态 执行代码的时间,对应
time命令输出中的user值。 - 系统时间(System Time) :进程在内核态 执行系统调用的时间,对应
time命令输出中的sys值。 - 总CPU时间 = user + sys
- 用户时间(User Time) :进程在用户态 执行代码的时间,对应
实验对比:
- CPU密集型程序 :
real≈user,sys≈ 0。因为程序几乎一直在占用CPU计算。 - 睡眠程序(如
sleep 3) :real≈ 3秒,user和sys几乎为0。因为进程大部分时间在睡眠,不占用CPU。
3.2 单逻辑CPU的调度:验证时间片轮转
- 单个进程的运行时间(Real)为基准值 T。
- 当同时运行 N 个进程时,总运行时间变为 N * T,但每个进程的执行时间(User+Sys)仍约为 T。
结论 :这完美验证了在单个逻辑CPU上,调度器采用时间片轮转算法。N个进程分享一个CPU,每个进程只能得到 1/N 的CPU资源,因此总完成时间变为原来的N倍。
3.3 多逻辑CPU的调度:负载均衡
- 如果进程数 ≤ 逻辑CPU数 :每个进程可以独占一个CPU核心。此时,总运行时间(Real)不再成倍增加,而是略高于单个进程的运行时间。
- 如果进程数 > 逻辑CPU数 :调度器会进行负载均衡,将进程平均分配到所有可用的CPU核心上。超出核心数的进程仍需通过时间片轮转共享CPU。
3.4 理解 user + sys > real
在多核环境下,会出现 user+sys > real 的情况。
- 原因 :
time命令会统计父进程及其所有子进程 的CPU时间并累加。如果父进程创建了多个子进程,并且这些子进程在不同的CPU核心上同时运行,那么在这些核心上消耗的CPU时间就会被叠加。
3.5 时间片的原理与可视化
调度过程可视化,创建多个CPU密集型子进程并记录其运行。
实验结果:
- 1个进程:图表显示一条平滑的斜线。
- 2个进程 :图表显示两条阶梯状斜线,进程以大约10毫秒为间隔交替运行,清晰展示了时间片轮转。
- 时间片动态调整机制 :Linux调度器基于 Latency Target 动态计算时间片。单个进程获得整个Latency Target,N个就绪进程则每个分得约
Latency Target / N,以平衡响应速度和吞吐量。
3.6 上下文切换
这是对调度机制的关键深化。
- 核心定义 :当调度器需要从一个进程切换到另一个进程运行时,必须执行上下文切换,即保存当前进程状态、加载下一进程状态。
- 触发时机 :只要进程的时间片用完,就会强制执行上下文切换。这是一个非协作式的抢占式调度,与进程正在执行什么代码无关。
- 纠正常见误解 :
- 误解 :认为函数
foo()执行完后会立刻执行bar()。 - 现实 :如果
foo()执行完后时间片刚好用完,会发生上下文切换,bar()必须等待当前进程再次被调度才能执行。 - 实践意义:当某个进程运行时间过长时,应考虑到可能是因上下文切换导致其他进程运行所致,而非一定是该进程本身有问题。
- 误解 :认为函数
3.7 性能:周转时间与吞吐量
本章从原理转向实践,探讨如何量化评估调度器性能。
-
核心性能指标:
- 周转时间 :任务从提交到完成的总时间(即
real值)。关注用户体验(响应速度)。 - 吞吐量 :单位时间内完成的任务数量(
进程数 / 总运行时间)。关注系统效率(处理能力)。
- 周转时间 :任务从提交到完成的总时间(即
-
性能测量实验与结论:
-
场景一:单逻辑CPU
- 平均周转时间 :随着进程数增加,线性增长。
- 吞吐量 :当进程数超过CPU数量(>1)后,基本保持稳定,不再提升。
- 核心结论 :当进程数比逻辑CPU的数量多时,增加进程数只会增加平均周转时间,并不能提升吞吐量。
-
场景二:利用所有逻辑CPU(如4核)
- 平均周转时间 :进程数 ≤ 4 时增长缓慢;进程数 > 4 时急剧增加。
- 吞吐量 :进程数 ≤ 4 时快速提升 ;进程数 > 4 后增长趋于平缓并达到峰值。
- 核心结论 :
- 没有足够多的进程,无法充分利用多核提升吞吐量。
- 盲目增加进程数超过CPU核心数,对提升吞吐量无益,只会恶化响应时间。
-
对系统设计的启示:
- 重视响应速度的系统(如Web服务器):必须控制CPU使用率,留出余量。
- 重视吞吐量的系统(如批处理计算):应让进程数略多于CPU核心数,但不宜过多。
-
3.8 程序并行执行的重要性
本节提升到硬件与软件协同设计的层面。
- 背景:CPU发展模式的转变
- 过去 :提升单核性能。软件无需修改也能受益。
- 现在 :通过增加核心数来提升总体性能。
- 对软件的要求 :要充分利用现代硬件性能,程序必须被设计成多线程或多进程的,能够并行执行。串行程序无法享受多核红利。
第4章 内存管理系统
本章系统地阐述了Linux内核如何管理内存,从用户空间查看内存信息的工具,到核心的虚拟内存、内存分配机制,以及页表等高级主题。
4.1 获取内存的相关信息
核心工具:free 命令
- 作用:快速获取系统内存的总量和使用概况
- 关键字段 :
used:已使用的内存量,包含进程内存和内核使用的部分内存buff/cache:缓冲区缓存和页缓存占用的内存量,可被内核回收以满足新需求available:估算的可用内存量,比free字段更能反映真实的内存余量
持续监控工具:sar -r 命令
- 可以按指定时间间隔持续采集内存统计信息,观察内存使用趋势
4.2 内存回收
内存回收机制:
- 当可用内存减少时,内核优先回收可回收内存(如未修改的页缓存)
- 若回收后仍无法满足需求,系统陷入OOM状态,触发OOM Killer强制终止进程以释放内存
- 使用
dmesg查看OOM日志,用ps aux监控RSS字段排查内存泄漏
4.3 虚拟内存
三大挑战:
- 内存碎片化:频繁分配释放导致不连续的小空闲区域
- 多进程地址冲突:程序使用固定内存地址导致冲突
- 非法内存访问:进程可随意访问其他进程或内核内存
核心机制:
- 为每个进程提供独立的虚拟地址空间
- 通过页表 由MMU完成虚拟地址到物理地址的转换
- 缺页中断 :当访问的虚拟页无有效物理映射时触发,内核处理程序负责分配物理内存或发送
SIGSEGV信号
解决方案:
- 碎片化:通过页表将不连续的物理页映射为连续的虚拟空间
- 多进程:各自独立的页表实现地址空间隔离
- 非法访问:页表由内核控制,进程只能访问已建立映射的区域
4.4 为进程分配新内存
Linux的内存分配分为两个步骤,实现按需分配,提高效率。
4.4.1 分配内存区域:系统调用 mmap()
- 作用:在进程的虚拟地址空间中开辟新的连续内存区域,更新页表
- 实验验证 (
mmap.go程序):- 调用
mmap()申请1 GiB内存 - 通过对比调用前后
/proc/<pid>/maps的内容,可见虚拟地址空间扩展约1 GiB - 结论 :
mmap()只分配虚拟空间,未分配物理内存
- 调用
4.4.2 分配物理内存:按需调页
-
机制 :物理内存分配推迟到进程首次访问内存区域时进行
-
过程:
- 进程调用
mmap(),内核创建页表项但标记为"未分配物理内存" - 进程首次访问该区域地址
- CPU触发缺页中断
- 内核缺页处理程序分配物理页框,建立完整页表映射
- 进程恢复执行
- 进程调用
-
实验验证:
- 申请100 MiB内存区域,逐页访问,同时用
sar -r 1监控 - 实验结果 :
- 获取内存区域后:
vsz(虚拟内存)增加100 MiB,rss(物理内存)不变 - 访问内存期间:
rss逐步增加,min_flt(次缺页中断)次数显著上升 - 访问完成后:
rss稳定在约100 MiB
- 获取内存区域后:
- 结论:物理内存是按需分配的,每次访问未映射页都触发缺页中断
- 申请100 MiB内存区域,逐页访问,同时用
编程语言的内存管理:
- 语言运行时系统通过
mmap()预先申请大块内存(堆),由自身内存分配器管理 - 不是每次分配都调用
mmap(),提高效率
4.5 多级页表
问题:x86_64架构下,虚拟地址空间128 TiB,使用单级页表将占用256 GiB内存,不现实
解决方案 :多级页表
- 将页表分成多级,像书籍目录一样
- 只为实际使用的虚拟内存区域分配页表项,大大节省内存
- 现实应用:x86_64架构使用4级页表
查看页表内存占用 :使用 sar -r ALL 命令,关注 kbpgtbl 字段
4.5.1 大页
- 概念:使用大于标准4 KiB的页面(如2 MiB, 1 GiB)
- 优势 :
- 减少页表项数量,降低页表内存开销
- 减少
fork()时复制页表的开销 - 提升TLB命中率,提高地址转换效率
- 使用 :向
mmap()传递MAP_HUGETLB标志显式申请
4.5.2 透明大页
- 概念:内核自动将连续小页面合并成大页,无需程序显式请求
- 控制 :通过
/sys/kernel/mm/transparent_hugepage/enabled文件设置always:全局启用madvise:仅对通过madvise()提示的区域启用(Ubuntu 20.04默认)never:禁用
4.6 内存保护
- 虚拟内存机制和页表天然提供强大内存保护
- 进程只能访问自身页表中已建立的映射区域
- 任何非法访问尝试都会触发缺页中断,内核发送
SIGSEGV信号终止进程
第5章 进程管理(进阶篇)
本章深入探讨了Linux进程管理的高级主题,包括进程创建优化、进程间通信、并发编程的核心概念,以及多线程实现的深入原理。
5.1 进程创建的高速化
5.1.1 fork()函数的高速化:写时复制
- 核心机制 :当调用
fork()创建子进程时,现代Linux系统采用写时复制 技术进行优化,而非立即复制父进程的全部内存空间。 - 工作流程 :
- 初始化状态 :
fork()调用后,内核仅为子进程复制父进程的页表 ,而非物理内存页。父子进程的页表项均被设置为只读权限,但它们指向相同的物理内存页,从而实现内存共享。 - 写入时复制 :当父进程或子进程尝试写入 共享的内存页时,会触发以下步骤:
- 由于页表项为只读,CPU引发缺页中断,陷入内核态。
- 内核的缺页中断处理程序识别到这是写时复制场景。
- 内核为该进程分配一个新的物理页,并将原共享页的数据复制到新页。
- 更新该进程的页表项,使其指向新的物理页,并设置页表项为可写。
- 另一进程的页表项保持不变,仍然指向原物理页(仍为只读或可写,取决于其自身操作)。
- 初始化状态 :
- 优势 :避免了创建子进程时不必要的内存复制开销。如果子进程随后立即调用
exec()加载新程序,则共享的物理内存页完全无需复制,极大地提升了fork()的效率。
5.1.2 execve()函数的高速化:按需调页
- 核心机制 :在调用
execve()加载新程序后,新程序的代码和数据并非立即全部载入物理内存,而是利用按需调页机制。 - 工作流程 :
- 映射阶段 :
execve()主要工作是为新程序设置好虚拟地址空间布局,并将程序文件的代码段、数据段等映射到进程的虚拟地址空间。此时,多数页表项标记为"未加载"。 - 执行时加载 :当进程开始执行,访问到尚未加载的虚拟页(如程序入口点)时,触发缺页中断。
- 内核处理:缺页中断处理程序从程序文件中将对应的代码或数据页读入物理内存,并建立正确的页表映射。
- 继续执行:进程从中断点恢复执行。
- 映射阶段 :
- 优势:加快了程序的启动速度,因为不需要等待整个程序文件加载完毕即可开始执行。同时,节省了内存,因为程序可能不会用到所有代码路径(如错误处理代码)。
5.2 进程间通信
5.2.1 共享内存
- 原理:允许两个或多个进程访问同一块物理内存区域,从而实现高效的数据共享。这是最快的IPC形式,因为数据不需要在进程间拷贝。
- 问题 :需要程序员自行处理同步问题,防止多个进程同时写入导致数据不一致。
5.2.2 信号
- 概述:一种异步通信机制,用于通知进程某个事件已发生。
- 特点 :
- 除了
SIGKILL和SIGSTOP,进程可以捕获、忽略或为信号指定处理函数。 - 信号机制非常原始,它只能传递信号编号,不能携带复杂数据。
- 除了
- 限制:不适用于复杂的进程间数据交换。
5.2.3 管道
- 原理:提供一个单向数据流。一个进程(写端)写入数据,另一个进程(读端)读取数据。
- 常见用法 :在Shell中使用
|符号连接多个命令。 - 特点:数据是字节流,遵循FIFO(先进先出)顺序。管道容量有限。
5.2.4 套接字
- 概述:功能强大的IPC机制,不仅可用于同一台主机的进程间通信,还可用于网络上的不同主机间的通信。
- 类型 :
- UNIX域套接字:用于同一台主机上的高效进程间通信。
- TCP/UDP套接字:遵循网络协议,可用于跨网络通信。
5.3 互斥锁
- 背景 :当多个进程(或线程)需要访问共享资源时,可能会发生竞争条件,导致数据不一致。
- 错误尝试与问题 :试图通过检查一个"锁文件"是否存在来实现互斥。但检查文件是否存在 和创建文件这两个操作不是原子的,在并发环境下,多个进程可能同时检查到锁文件不存在,然后都认为自己获得了锁,从而同时进入临界区,导致数据错误。
- 正确方案 :需要使用操作系统提供的原子性同步原语,如文件锁 (通过
fcntl系统调用)或专门的互斥锁。
5.4 互斥锁的实现与硬件支持
本节深入探讨了互斥锁在硬件层面的实现原理。
-
问题根源 :实现锁的关键操作------检查锁状态 和设置锁状态 ------必须是原子操作,即不可被中断的操作。
-
错误示例分析:书中通过一段汇编伪代码说明问题:
load r0, mem(从内存地址mem读取值到寄存器r0)test r0(测试r0的值)jmpz enter(如果为0,跳转到加锁入口)jmp start(如果不为0,跳回开始重试)enter: store mem, 1(向mem写入1,表示加锁)
即使使用汇编语言,步骤1(读)和步骤5(写)之间也可能被中断。如果两个执行流同时执行到步骤1,都可能读到0,然后都执行步骤5,导致两个执行流都进入了临界区。
-
硬件解决方案 :大部分CPU架构提供了原子操作指令 ,如
compare and exchange或compare and swap。这类指令将"比较内存值"和"条件写入新值"这两个操作在一条指令内完成,由CPU保证其原子性,从而可以用于正确实现互斥锁。 -
软件实现的挑战 :在高级语言中理论上可以实现互斥锁(如 Peterson算法),但这通常耗费大量时间与内存资源,且正确性难以保证。因此,实践中均依赖硬件提供的原子指令或操作系统封装好的同步原语。
5.5 多进程与多线程
-
并行化动机:为了充分利用多核CPU的性能,需要将任务并行化。
-
两种主要并行化模型:
- 多进程 :
- 通过
fork()和execve()创建多个进程。 - 进程间拥有独立的地址空间,稳定性高(一个进程崩溃通常不影响其他进程)。
- 进程间通信(IPC)开销相对较大。
- 通过
- 多线程 :
- 在一个进程内创建多个执行流(线程)。
- 所有线程共享进程的绝大部分资源,如内存空间、打开的文件描述符等。
- 线程间通信(通过共享内存)非常高效。
- 但需要谨慎处理同步问题,因为数据是共享的,一个线程的错误可能破坏整个进程。
- 多进程 :
-
多线程的优缺点:
- 优点 :
- 创建速度快(无需复制页表等)。
- 资源消耗少(共享内存等资源)。
- 线程间共享内存,更容易实现数据共享和协作。
- 缺点 :
- 稳定性差:一个线程的异常(如段错误)会导致整个进程终止。
- 编程复杂 :程序员必须确保所有操作是线程安全的,需要熟练使用互斥锁等同步机制,编写正确的多线程程序非常困难。
- 优点 :
5.6 内核级线程与用户级线程
-
内核级线程:
- 由操作系统内核直接管理和调度。在Linux中,通过
clone()系统调用创建线程时,内核会为其创建一个对应的内核级线程。 - 优点 :内核知晓所有线程,因此可以将不同线程调度到不同的CPU核心上真正并行执行。
- 缺点:线程的创建、调度、同步都需要陷入内核态,存在一定的性能开销。
- 由操作系统内核直接管理和调度。在Linux中,通过
-
用户级线程:
- 在用户空间由线程库(如早期的Pthreads用户级实现)实现,内核对此不知情。
- 优点:线程切换等操作在用户态完成,开销极小。
- 致命缺点 :内核的调度单位是进程。如果一个进程有多个用户级线程,内核依然将其视为一个调度单位。这意味着所有这些用户级线程只能运行在同一个CPU核心上,无法实现真正的并行。此外,如果一个用户级线程发起阻塞式系统调用,会导致整个进程被阻塞,从而阻塞了该进程内的所有用户级线程。
-
现代Linux的实现 :现代Linux采用的Pthreads线程实现(NPTL)是一种混合模型。它使用内核级线程(在Linux中,常通过轻量级进程实现)来映射用户线程,从而既能让内核参与调度以实现真正的并行,又通过优化减少了部分开销。
-
简化多线程编程的努力 :由于多线程编程难度大,出现了很多简化方案。Go语言中的 goroutine 就是一个成功范例,它由Go运行时管理,在用户态进行调度,创建开销极小,并通过通信(Channel)来共享内存,而非直接共享内存,大大简化了并发编程。
第6章 设备访问
本章系统地阐述了Linux系统中应用程序如何通过内核访问和控制硬件设备,涵盖了从用户空间可见的设备文件到内核空间的设备驱动程序、通信机制等一系列核心概念。
6.1 设备文件
设备文件是用户空间进程与硬件设备交互的桥梁。
- 基本概念 :在Linux中,一切皆文件。每个硬件设备(或设备的一部分,如硬盘分区)在
/dev/目录下都有一个对应的设备文件 。例如,存储设备可能对应/dev/sda,/dev/sdb等。 - 设备文件信息 :设备文件不是普通的磁盘文件,它不存储数据,而是包含了访问底层设备所需的信息:
- 设备类型 :
- 字符设备 :以字节流方式进行读写,不支持随机访问(寻址)。例如终端 (
/dev/tty*)、键盘、鼠标。 - 块设备 :以数据块为单位进行读写,支持随机访问。例如硬盘 (
/dev/sda*)、SSD。
- 字符设备 :以字节流方式进行读写,不支持随机访问(寻址)。例如终端 (
- 主设备号:用于标识设备的类型,对应特定的设备驱动程序。
- 次设备号:用于标识同一类型下的不同设备实例或不同分区。
- 设备类型 :
- 访问权限 :通常只有
root用户有权限直接访问设备文件,这是重要的安全机制。 - 操作方式 :进程可以像操作普通文件一样,对设备文件使用
open(),read(),write(),close()等系统调用。例如,向终端设备文件/dev/pts/9执行write()操作,字符串就会显示在对应的终端上。
6.2 设备驱动程序
当进程访问设备文件时,实际工作由内核中的设备驱动程序完成。
- 角色:设备驱动程序是内核的一部分,它知道如何与特定的硬件设备通信,充当了进程与硬件之间的翻译官。
- 操作流程 :
- 进程通过设备文件发出请求(如
read)。 - CPU切换到内核模式,执行对应的设备驱动程序。
- 设备驱动程序通过操作硬件设备的寄存器,将请求传达给硬件。
- 硬件设备执行操作,驱动程序获取结果并返回给进程。
- 进程通过设备文件发出请求(如
6.2.1 内存映射 I/O
这是CPU与设备寄存器通信的主流方式。
- 原理 :将设备寄存器映射到内核的虚拟地址空间中。这样,设备驱动程序就可以像访问普通内存地址一样,使用
load(读)和store(写)指令来操作设备寄存器,从而控制硬件。 - 示例:本章通过一个虚拟的存储设备,详细演示了驱动程序如何通过向不同偏移量的映射内存(对应不同功能的寄存器)写入参数(如起始内存地址、设备地址、数据大小),然后写入一个"开始"指令来发起读取请求。
6.2.2 轮询
设备驱动程序需要知道硬件何时完成了请求。轮询 是其中一种机制。
- 原理 :驱动程序在发出请求后,主动地、周期性地读取设备的状态寄存器,检查操作是否完成。
- 两种方式 :
- 简单轮询 :驱动程序不间断地检查状态,直到完成。这种方式严重浪费CPU资源,因为CPU在等待期间无法执行其他任务。
- 复杂轮询:驱动程序每隔一段时间(如每次被调度运行时)检查一次状态。这减少了CPU浪费,但难以设定合适的时间间隔:间隔太短仍浪费资源,间隔太长则响应延迟高。同时,实现逻辑也变得更复杂。
6.2.3 中断
这是更高效、更常用的机制。
- 原理 :
- 驱动程序发出请求后,CPU立即返回,可以处理其他任务。
- 硬件设备完成操作后,主动通过中断信号线通知CPU。
- CPU暂停当前工作,转而执行预先注册的中断处理程序(属于设备驱动程序的一部分)。
- 中断处理程序从设备读取结果,并唤醒正在等待该结果的进程。
- 优势 :
- 高效利用CPU:在设备工作期间,CPU可以执行其他任务。
- 低延迟:设备完成操作后能立即得到响应。
- 观察中断 :可以通过查看
/proc/interrupts文件来了解系统中各种中断的发生次数。每个硬件设备通常有一个对应的IRQ号。
6.3 设备文件名
本节探讨了设备文件命名可能带来的问题及解决方案。
- 问题根源 :对于同类型设备(如多个SATA硬盘),设备名(如
/dev/sda,/dev/sdb)的分配取决于内核在启动时识别设备的顺序。这个顺序可能因设备插槽更换、新增设备、或某个设备故障无法识别而改变,导致设备名发生变化。 - 后果 :如果脚本或配置文件(如
/etc/fstab)中使用了类似/dev/sda1这样的名称,设备名变化可能导致挂载错误、数据损坏等严重问题。 - 解决方案:持久化命名
- UUID :每个文件系统都有一个全局唯一的标识符。在
/etc/fstab中使用UUID=<文件系统UUID>来代替设备名是推荐的最佳实践。 - udev 规则 :Linux的
udev服务可以根据设备的稳定属性(如序列号、总线位置)在/dev/disk/目录下创建永久的符号链接(如/dev/disk/by-uuid/,/dev/disk/by-id/)。
- UUID :每个文件系统都有一个全局唯一的标识符。在
技术专栏
- 回环设备 :允许将一个普通文件 (如
loopdevice.img)当作块设备文件来使用。可以在这个文件上创建文件系统并挂载,非常适合实验而无需动用真实硬盘分区。 - 用户空间I/O:一种允许在用户空间编写设备驱动程序的技术,可以减少内核态/用户态切换的开销,提升性能,但实现更复杂。
第7章 文件系统
本章系统性地阐述了Linux文件系统的工作原理,从基本概念、访问方法,到高级功能如Btrfs的存储池、数据损坏修复,以及各种特殊文件系统。
7.1 文件系统的必要性与基本概念
为什么需要文件系统?
如果没有文件系统,用户或程序将不得不直接管理磁盘块:
- 需要手动记住每个数据的确切位置 和大小
- 需要自行管理磁盘的空闲区域,避免数据覆盖
- 这种管理方式极其繁琐且容易出错
文件系统的作用就是代替用户完成所有这些管理工作。它以文件为单位组织对用户有意义的数据块。
文件系统包含两层含义:
- 磁盘上的数据结构 :在存储设备上划出特定区域,用于保存元数据 (管理信息)和数据(文件内容本身)
- 内核中的软件代码:负责处理对这些数据结构的操作,如读写、创建、删除等
文件与元数据
- 数据:用户创建的实际内容,如文本文档、图片、程序等
- 元数据 :文件系统用于支持文件管理的数据,包括:
- 文件的名称、类型、位置、大小
- 时间信息(创建、访问、修改时间)
- 权限信息
- 目录内容
树形结构
文件系统通过树形结构 组织文件。最顶端是根目录,之下是各级子目录和文件,形成了清晰的层次关系。
7.2 访问文件系统的方法
标准接口:POSIX API
POSIX标准定义了一系列用于操作文件和目录的函数:
- 文件操作 :
creat(),open(),close(),read(),write(),unlink()等 - 目录操作 :
mkdir(),rmdir(),opendir(),readdir(),chdir()等
处理流程:
- 程序调用这些函数(最终触发系统调用)
- 内核中的虚拟文件系统(VFS) 层被激活
- VFS调用具体的文件系统(如ext4, XFS)的实现
- 文件系统的处理程序可能会调用设备驱动程序来操作硬件设备
内存映射文件
Linux提供了 mmap() 系统调用,允许将文件的一部分直接映射到进程的虚拟地址空间。
- 优势 :进程可以像访问内存一样读写文件数据,无需调用
read()和write(),性能更高 - 机制 :当进程访问映射的内存区域时,如果数据不在物理内存中,会触发缺页中断,由内核将文件对应的数据页从磁盘加载到内存
7.3 文件系统的种类与常见功能
常见的文件系统
Linux支持多种文件系统,各有特点和适用场景:
- ext4:Linux的传统默认选择,稳定可靠
- XFS:适用于大文件和高并发访问,具有良好的可扩展性
- Btrfs:功能丰富,支持写时复制、快照、子卷等高级功能
磁盘配额
当系统被多个用户或任务共享时,可以使用磁盘配额功能来限制其可用容量:
- 用户配额:限制特定用户的可用容量
- 目录配额(项目配额):限制某个目录的可用容量
- 子卷配额:在Btrfs等文件系统中,以子卷为单位进行限制
7.4 文件系统的一致性
一致性问题
文件系统的操作(如移动目录)可能包含多个步骤。如果在多个步骤之间发生系统崩溃或断电,就会导致文件系统处于不一致状态。
保障一致性的技术
1. 日志技术
被ext4和XFS等文件系统广泛采用。
- 原理 :
- 日志写入 :在真正更新文件系统元数据之前,先将操作概要写入日志区域
- 数据提交:将日志标记为提交后,开始更新实际元数据
- 日志清理:实际更新完成后,清理日志区域
- 优势 :保证了操作的原子性,崩溃后可以通过重放日志恢复一致性
2. 写时复制技术
被Btrfs和ZFS等文件系统采用。
- 原理 :更新数据时,不覆盖 原有数据块,而是将数据块复制到新的位置进行修改
- 优势 :天然支持一致性,是实现快照等功能的基础
首要之事是备份
尽管有日志和写时复制技术,文件系统仍可能因漏洞或硬件故障而损坏。定期备份是保护数据的最终手段。
7.5 Btrfs提供的高级功能
快照
快照是文件系统在某个时间点的只读副本。
- 原理:利用写时复制机制,创建时只复制元数据,与原文件系统共享数据块
- 用途 :
- 数据版本保护:升级软件或编辑重要文件前创建快照
- 高效备份的基础:先创建快照,然后从容备份快照中的数据
存储池与软件RAID
Btrfs支持将多个存储设备合并为存储池,并内置软件RAID功能。
实际配置示例:
bash
# 创建存储池
$ sudo mkfs.btrfs -f /dev/sda /dev/sdb
# 创建子卷
$ sudo btrfs subvolume create /mnt/btrfs-pool/vol1
# 查看子卷列表
$ sudo btrfs subvolume list /mnt/btrfs-pool
ID 256 gen 7 top level 5 path vol1
RAID配置:
bash
# 创建RAID 1配置
$ sudo mkfs.btrfs -m raid1 -d raid1 /dev/sda /dev/sdb
# 查看RAID状态
$ sudo btrfs filesystem usage /mnt/btrfs-pool
Data,RAID1: Size:128.00MiB, Used:0.00B
/dev/sda 128.00MiB
/dev/sdb 128.00MiB
7.6 数据损坏的检测与修复
校验和检测
Btrfs为所有数据计算并存储校验和来检测数据损坏。
自动修复(配合RAID)
当配置了RAID时,Btrfs可以自动修复损坏的数据:
- 从损坏设备读取数据校验失败
- 自动从健康设备读取正确数据
- 用正确数据修复损坏设备
系统日志示例:
bash
$ sudo dmesg | grep -i btrfs | tail -3
[ 1234.567890] BTRFS error (device sda): checksum error at logical 109445120
[ 1234.567891] BTRFS: attempting to read from good copy
[ 1234.567892] BTRFS: repaired by copy from dev /dev/sdb
7.7 基于内存的文件系统:tmpfs
特点:
- 数据存储在内存中
- 重启后数据丢失
- 访问速度极快
实际应用:
bash
# 查看系统中的tmpfs挂载
$ mount | grep ^tmpfs
tmpfs on /run type tmpfs (rw,size=1535936k,mode=755)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
# 创建自定义tmpfs
$ sudo mount -t tmpfs -o size=1G tmpfs /mnt/tmpfs
# 查看内存使用情况(shared字段反映tmpfs占用)
$ free -h
total used free shared buff/cache available
Mem: 7.6G 2.1G 3.2G 345M 2.3G 5.0G
7.8 网络文件系统
主要类型:
- NFS:UNIX/Linux系统间文件共享
- CIFS/SMB:与Windows系统文件共享
- 分布式文件系统(如CephFS):多机存储整合
架构:
本地主机 → 网络文件系统客户端 → 网络 → 文件服务器 → 实际文件系统
7.9 进程文件系统(procfs)
挂载点: /proc
进程信息查看:
bash
# 查看进程信息
$ ls /proc/1234/
cmdline cwd environ exe fd maps stat status
# 查看进程命令行
$ cat /proc/1234/cmdline
bash
# 查看系统信息
$ cat /proc/cpuinfo | grep "model name"
model name: Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz
$ cat /proc/meminfo | head -3
MemTotal: 16230644 kB
MemFree: 10256356 kB
MemAvailable: 13888056 kB
系统工具的数据源:
ps、top、free等命令实际读取/proc下的文件。
7.10 系统文件系统(sysfs)
挂载点: /sys
块设备信息查看:
bash
# 查看块设备列表
$ ls /sys/block/
loop0 loop1 sda sdb nvme0n1
# 查看设备属性
$ cat /sys/block/nvme0n1/dev
259:0
$ cat /sys/block/nvme0n1/size
1000215216
# 判断设备类型(HDD=1,SSD=0)
$ cat /sys/block/nvme0n1/queue/rotational
0
# 查看设备是否可移动
$ cat /sys/block/sda/removable
0
7.11 文件系统选择指南
选择考虑因素:
- 功能需求:是否需要快照、压缩、去重等高级功能
- 性能要求:针对大文件还是小文件,读多还是写多
- 数据安全性:对数据一致性和可靠性的要求
- 硬件环境:HDD还是SSD,单盘还是多盘
性能测试建议:
bash
# 简单的文件系统性能测试
$ dd if=/dev/zero of=testfile bs=1G count=1 oflag=direct
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB) copied, 2.34 s, 458 MB/s
第8章 存储层次
本章系统性地阐述了计算机存储层次结构,从寄存器、高速缓存到内存和存储设备,详细讲解了Linux如何利用这种层次结构来优化系统性能,包括缓存机制、交换机制和直接I/O等高级主题。
8.1 存储器的层次结构
金字塔形层次结构
计算机存储器呈现典型的金字塔形层次结构:
- 顶层:寄存器 - 容量最小,速度最快,价格最高
- 中层:高速缓存(多级缓存) - 容量和速度介于寄存器和内存之间
- 底层:内存和存储设备 - 容量最大,速度最慢,单位容量价格最低
访问速度对比示例
bash
# 寄存器访问:< 1纳秒
# 内存访问:数十纳秒
# SSD访问:数十微秒(比内存慢1000倍)
# HDD访问:数毫秒(比内存慢100,000倍)
8.2 高速缓存的工作原理
CPU工作流程
CPU的基本工作循环:
- 从内存读取指令和数据到寄存器
- 在寄存器上执行计算
- 将结果写回内存
高速缓存的引入
为了解决内存访问速度瓶颈,引入了高速缓存机制:
缓存行概念
bash
# 查看系统缓存行大小
$ getconf LEVEL1_DCACHE_LINESIZE
64
# 或者通过sysfs查看
$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64
高速缓存工作示例
以虚拟CPU为例说明:
- 2个10字节寄存器:R0、R1
- 50字节高速缓存
- 10字节缓存行
数据读取过程
- 首次读取:内存地址300 → 高速缓存 → 寄存器R0
- 再次读取:直接高速缓存 → 寄存器R1(无需访问内存)
脏数据机制
- 脏标记:标识缓存行数据已修改但未写回内存
- 写入策略 :
- 直写:同时写入高速缓存和内存
- 回写:先写入高速缓存,延迟写入内存
缓存替换策略
当缓存空间不足时,需要替换缓存行:
bash
# 如果要被清除的数据是脏数据,需要先写回内存
# 频繁替换导致"颠簸",性能下降
8.3 局部性原理
时间局部性
近期访问的内存很可能被再次访问
go
// 循环是时间局部性的典型例子
for i := 0; i < 1000; i++ {
sum += array[i] // array[i]被重复访问
}
空间局部性
访问某个内存位置后,很可能访问相邻位置
go
// 数组遍历是空间局部性的典型例子
for i := 0; i < n; i++ {
process(array[i]) // 连续访问相邻内存
}
8.4 多级高速缓存
缓存层次
现代CPU采用多级缓存架构:
- L1缓存:最接近寄存器,速度最快,容量最小(通常32-64KB)
- L2缓存:速度较快,容量较大(通常256KB-1MB)
- L3缓存:共享缓存,容量最大(通常2-32MB)
查看缓存信息
bash
# 查看CPU缓存信息
$ ls /sys/devices/system/cpu/cpu0/cache/
index0 index1 index2 index3
# 查看L1数据缓存信息
$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
$ cat /sys/devices/system/cpu/cpu0/cache/index0/type
Data
$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64
# 查看L1指令缓存
$ cat /sys/devices/system/cpu/cpu0/cache/index1/size
32K
$ cat /sys/devices/system/cpu/cpu0/cache/index1/type
Instruction
8.5 回写的时间点
脏页回写机制
脏页(已修改但未写入磁盘的页面)的回写操作在以下两种情况下运行:
1. 周期性运行
默认每5秒运行一次回写操作
bash
# 查看当前回写周期设置(单位:厘秒,1厘秒=0.01秒)
$ sysctl vm.dirty_writeback_centisecs
vm.dirty_writeback_centisecs = 500
# 修改回写周期为10秒(1000厘秒)
$ sudo sysctl vm.dirty_writeback_centisecs=1000
# 禁用周期性回写(实验环境使用,生产环境不推荐)
$ sudo sysctl vm.dirty_writeback_centisecs=0
2. 基于脏页比例触发
bash
# 当脏页比例超过dirty_background_ratio时触发后台回写
$ sysctl vm.dirty_background_ratio=10
# 当脏页比例超过dirty_ratio时,会阻塞写入操作并同步刷盘
$ sysctl vm.dirty_ratio=20
# 也可以使用字节单位设置(优先级高于百分比)
$ sysctl vm.dirty_background_bytes=10485760 # 10MB
$ sysctl vm.dirty_bytes=20971520 # 20MB
参数调优建议
在脏页容易增多的系统中,合理调整这些参数可以防止因内存不足导致的系统问题。
8.6 Direct I/O
适用场景
在以下情况下,禁用页缓存可能更优:
- 数据只使用一次:如备份到可移动设备的数据
- 应用自行实现缓存:如数据库系统
启用方式
bash
# 方式1:在open()函数中使用O_DIRECT标志
int fd = open("file.txt", O_RDWR | O_DIRECT);
# 方式2:使用dd命令的direct标志
$ free -h
total used free shared buff/cache available
Mem: 15G 379MB 14.4G 1.5MB 522MB 14.7G
$ dd if=/dev/zero of=testfile bs=1G count=1 oflag=direct,sync
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB) copied, 2.34 s, 458 MB/s
$ free -h
total used free shared buff/cache available
Mem: 15G 388MB 14.3G 1.5MB 612MB 14.6G
注意事项
- 纯direct I/O会立即返回,不等待I/O完成
- 需要配合
sync标志确保数据同步写入 - 详细说明参见
man 2 open中关于O_DIRECT的说明
8.7 交换机制
交换机制原理
当物理内存耗尽时,交换机制将部分内存数据转移到存储设备(交换分区),避免立即触发OOM。
页调出过程
bash
# 查看交换分区信息
$ swapon --show
NAME TYPE SIZE USED PRIO
/dev/nvme0n1p3 partition 15G 0B -2
# 查看内存和交换分区使用情况
$ free -h
total used free shared buff/cache available
Mem: 15G 380MB 15.3G 1.5MB 1.4GB 14.7G
Swap: 15G 0B 15G
缺页中断处理
当进程访问已调出的页面时:
- 触发缺页中断
- 内核将数据从交换分区读回内存
- 更新页表项
- 进程继续执行
8.8 交换监控工具
sar命令监控
bash
# 监控页调入/调出情况
$ sar -B 1
23:30:00 pgpgin/s pgpgout/s fault/s majflt/s
23:30:01 0.00 0.00 123.4 0.0
23:30:02 0.00 0.00 145.6 0.0
# 监控交换活动
$ sar -W 1
23:30:00 pswpin/s pswpout/s
23:30:01 0.00 0.00
23:30:02 0.00 0.00
# 监控交换分区使用情况
$ sar -S 1
23:29:00 kbswpfree kbswpused %swpused kbswpcad %swpcad
23:29:01 976892 0 0.00 0 0.00
23:29:02 976892 0 0.00 0 0.00
关键指标说明
pgpgin/s:每秒页调入数据量(KiB)pgpgout/s:每秒页调出数据量(KiB)majflt/s:主要缺页中断次数(需要磁盘I/O)pswpin/s:每秒换入页面数pswpout/s:每秒换出页面数kbswpused:已用交换空间(关键监控指标)
8.9 性能优化实践
缓存友好编程
c
// 好的访问模式(空间局部性)
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
array[i][j] = 0; // 连续内存访问
}
}
// 优化数据结构布局
struct Person {
char name[32];
int age;
float salary;
}; // 结构体数组比数组结构体更缓存友好
交换优化策略
- 监控
kbswpused趋势,持续增长时需警惕 - 合理设置交换分区大小(一般为内存的1-2倍)
- 使用SSD作为交换设备提升性能
第9章 通用块层
本章系统性地阐述了Linux通用块层的工作原理,包括I/O调度器、预读机制、性能测试方法,以及针对HDD和SSD的性能优化策略。
9.1 通用块层概述
通用块层是Linux内核中处理块设备I/O请求的核心组件,主要功能包括:
- I/O调度:对I/O请求进行排序、合并,优化磁盘访问性能
- 预读机制:预测并提前读取可能需要的数据
- 设备抽象:为上层文件系统提供统一的块设备接口
9.2 性能测试工具与方法
fio工具使用示例
bash
# 测试随机写性能
fio --name=test --filename=/dev/sdb --direct=1 --rw=randwrite \
--bs=4k --size=1G --numjobs=16 --runtime=60 --group_reporting
# 测试顺序读性能
fio --name=test --filename=/dev/sdb --direct=1 --rw=read \
--bs=1M --size=1G --runtime=60 --group_reporting
关键性能指标
- IOPS:每秒I/O操作数,衡量随机访问性能
- 吞吐量:数据传输速率(MB/s或GB/s),衡量顺序访问性能
- 延迟:I/O操作完成时间,影响响应速度
9.3 HDD性能测试与分析
测试配置
bash
# HDD测试配置文件示例
DEVICE_NAME="sdb"
SCHEDULERS="mq-deadline none"
READ_AHEAD_KB="128 0"
BLOCK_SIZES="4k 1M"
情景A:I/O调度器效果测试
结果分析:
- 启用mq-deadline调度器:IOPS提升明显,特别是在高并发场景
- 禁用调度器(none):延迟较低但吞吐量受限
- 最佳实践:对于HDD,推荐启用I/O调度器
数据对比表:
| 调度器 | 并发数 | IOPS | 延迟(ms) |
|---|---|---|---|
| mq-deadline | 16 | 3500 | 4.5 |
| none | 16 | 2800 | 5.2 |
情景B:预读机制效果测试
表9-4 预读的效果(针对HDD)
| I/O调度器 | 预读 | 吞吐量(MB/s) |
|---|---|---|
| 启用 | 启用 | 34.1 |
| 启用 | 禁用 | 13.5 |
| 禁用 | 启用 | 34.8 |
| 禁用 | 禁用 | 13.5 |
结论: 预读机制对HDD性能影响显著,启用预读后吞吐量提升约2.5倍。
9.4 SSD性能测试与分析
NVMe SSD特性
- 基于闪存技术,无机械部件
- 访问速度比HDD快100倍以上
- 对I/O调度开销更敏感
测试配置
bash
# NVMe SSD测试配置
DEVICE_NAME="nvme0n1"
SCHEDULERS="none mq-deadline"
READ_AHEAD_KB="128 0"
情景A:I/O调度器效果测试
结果分析:
- 低并发场景:禁用调度器性能更优(IOPS更高,延迟更低)
- 高并发场景:启用调度器可能改善延迟
- 总体趋势:NVMe SSD上调度器开销相对明显
性能变化率分析:
- IOPS变化率:-15% 到 +8%(因并发数而异)
- 延迟变化率:-10% 到 +12%
情景B:预读机制效果测试
表9-5 预读的效果(针对NVMe SSD)
| I/O调度器 | 预读 | 吞吐量(GB/s) |
|---|---|---|
| 启用 | 启用 | 1.92 |
| 启用 | 禁用 | 0.186 |
| 禁用 | 启用 | 2.15 |
| 禁用 | 禁用 | 0.201 |
关键发现:
- 预读对SSD性能影响极大(提升10倍以上)
- I/O调度器在SSD上可能带来性能下降
- NVMe SSD吞吐量可达HDD的50-100倍
9.5 现实中的性能测试实践
Web应用性能分析案例
系统架构:
客户端 → 网络 → Web服务器 → 数据库 → 存储设备
性能问题诊断流程:
- 明确测试范围:识别系统组件及其依赖关系
- 分层测量:网络延迟、应用处理、数据库查询、存储I/O
- 瓶颈定位:使用工具逐层分析性能数据
- 优化实施:针对瓶颈点进行针对性优化
常用诊断工具:
bash
# 网络诊断
ping target_server
traceroute target_server
# 系统监控
top/htop
iostat -x 1
vmstat 1
# 存储性能
fio --rw=randread --bs=4k --numjobs=16
9.6 最佳实践建议
HDD环境优化:
- 启用合适的I/O调度器(如mq-deadline)
- 设置合理的预读大小(通常128-256KB)
- 考虑RAID配置提升性能
SSD环境优化:
- 评估是否需要I/O调度器(多数场景建议none)
- 充分利用预读机制
- 注意TRIM功能的使用
- 监控SSD磨损程度
通用建议:
- 基准测试:在任何优化前建立性能基线
- 监控告警:设置关键指标监控和告警阈值
- 容量规划:根据业务增长预测存储需求
- 备份策略:确保数据安全性和可恢复性
第10章 虚拟化
本章系统性地阐述了Linux虚拟化技术的核心原理、实现机制和性能特性,涵盖了CPU虚拟化、内存管理、存储设备虚拟化等关键技术。
10.1 虚拟化基础概念
虚拟化的核心价值
- 资源隔离:在同一物理机上运行多个相互隔离的虚拟机
- 硬件抽象:为应用程序提供统一的硬件接口
- 灵活部署:快速创建、迁移和销毁虚拟机实例
虚拟化软件组成
- KVM(Kernel-based Virtual Machine):Linux内核内置的虚拟化模块
- QEMU:硬件设备仿真和虚拟机管理
- libvirt:虚拟化管理工具集,提供统一API接口
- virt-manager:图形化虚拟机管理界面
10.2 CPU虚拟化与调度机制
硬件虚拟化支持
现代CPU通过VT-x(Intel)和SVM(AMD)技术提供硬件级虚拟化支持:
- VMX root模式:宿主操作系统运行模式
- VMX non-root模式:客户操作系统运行模式
- VM Entry/Exit:两种模式间的切换机制
VCPU调度原理
每个虚拟CPU(VCPU)对应宿主系统中的一个线程:
bash
# 查看VCPU线程
ps -eLf | grep qemu
调度性能实验
测试调度性能:
单VCPU运行结果:
- 运行时间:约200毫秒
- CPU使用率:接近100%
- 进程调度:正常交替执行
竞争环境下的性能分析 :
当物理CPU(PCPU0)同时运行宿主和客户OS进程时:
- 运行时间延长至近400毫秒(约2倍)
- 进程0和进程1出现执行停滞
- VCPU0与PCPU0存在资源竞争
性能监控工具使用
bash
# 物理机CPU监控
sar -P 0 1
# 输出:CPU使用率100%,主要被qemu-system-x86进程占用
# 虚拟机内部监控
top
# 显示:inf-loop.py进程占用99.9% CPU资源
关键发现:
- 物理机视角:qemu-system-x86进程占用CPU
- 虚拟机视角:用户进程正常显示CPU占用
- steal时间:反映虚拟机被宿主机抢占CPU的时间
10.3 内存虚拟化管理
内存映射关系
物理机与虚拟机的内存对应关系:
- 宿主OS内存:包含内核内存、进程内存、虚拟机内存
- 虚拟机内存:作为qemu-system-x86进程的内存存在
- 内存组成 :
- 虚拟机管理内存(代码、数据)
- 客户OS分配内存(内核、进程)
内存消耗测量实验
通过系统命令精确测量虚拟机内存使用:
测量步骤:
- 清空页缓存:
echo 3 > /proc/sys/vm/drop_caches - 记录初始内存:
free命令 - 启动虚拟机,记录变化
- 分析进程内存:
ps -eo pid,comm,rss
实验结果分析:
- 宿主OS内存增加:约766MiB(对应管理开销+客户OS内存)
- qemu进程RSS:约745MiB(虚拟机总内存占用)
- 客户OS实际使用:约110MiB(used + buff/cache)
- 内存开销:约635MiB(管理开销+未使用内存)
按需调页机制
- 虚拟机启动时并不立即分配全部内存
- 随着客户OS内存分配,宿主进程内存逐步增长
- 支持内存过量分配(Overcommit)
10.4 存储设备虚拟化
虚拟磁盘架构
虚拟机存储设备通常映射到物理文件(磁盘映像):
xml
<!-- libvirt磁盘配置 -->
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='/var/lib/libvirt/images/ubuntu2004.qcow2'/>
<target dev='vda' bus='virtio'/>
</disk>
I/O性能分析
全虚拟化设备性能:
- 写入流程复杂,涉及多次模式切换
- 性能显著低于物理设备(350MB/s vs 1100MB/s)
- 受宿主文件系统其他I/O操作影响
性能测试方法:
bash
# 清空缓存确保测试准确性
echo 3 > /proc/sys/vm/drop_caches
# 同步写入测试
dd if=/dev/zero of=testfile bs=1G count=1 oflag=direct,sync
页缓存与I/O缓存选项
libvirt提供多种缓存策略:
- writeback:启用页缓存,异步写入(默认)
- writethrough:启用页缓存,同步写入
- none:绕过页缓存,直接I/O
缓存引起的性能逆转现象
在某些场景下,虚拟机I/O性能可能超过物理机:
- 物理机:直接从存储设备读取(202MB/s)
- 虚拟机:从宿主页缓存读取(327MB/s)
- 原因:虚拟机复用宿主的缓存数据
10.5 半虚拟化与性能优化
virtio-blk半虚拟化机制
传统全虚拟化问题:
- 每次I/O需要3次设备访问
- 频繁的VMX模式切换
- 性能开销大
virtio-blk优化原理:
- 客户OS驱动向共享队列插入多个指令
- 批量提交给宿主OS处理
- 减少模式切换次数
- 提升并发处理能力
性能对比结果:
- 物理机:1100MB/s
- 全虚拟化:350MB/s
- 半虚拟化:663MB/s(性能提升89%)
设备命名规范:
- 全虚拟化:
/dev/sd*(SCSI设备) - 半虚拟化:
/dev/vd*(virtio设备)
10.6 高级虚拟化技术
PCI直通(PCI Passthrough)
直接将物理PCI设备分配给虚拟机:
- 优势:获得接近物理机的I/O性能
- 应用场景:高性能计算、GPU虚拟化
- 限制:设备不能共享,迁移复杂
技术对比总结
| 技术方案 | 性能 | 灵活性 | 适用场景 |
|---|---|---|---|
| 全虚拟化 | 低 | 高 | 通用计算 |
| 半虚拟化 | 中 | 中 | I/O密集型 |
| PCI直通 | 高 | 低 | 高性能计算 |
10.7 虚拟化环境下的性能监控
关键监控指标
- CPU:steal时间、VCPU调度延迟
- 内存:宿主/客户OS内存使用对比
- 存储:I/O缓存命中率、队列深度
- 网络:虚拟交换机性能
监控最佳实践
- 分层监控:同时监控宿主和客户OS
- 基线建立:记录正常性能基线
- 趋势分析:长期跟踪性能变化
- 告警设置:针对关键指标设置阈值
第11章 容器
本章系统性地阐述了Linux容器技术的核心原理、实现机制以及与虚拟机的对比,深入分析了命名空间、安全机制等关键技术。
11.1 容器技术概述
容器与虚拟机的本质区别
容器技术通过Linux内核提供的命名空间和控制组(cgroups)机制,实现进程级别的资源隔离,与传统的虚拟机存在根本性差异:
架构对比:
- 虚拟机:每个虚拟机运行完整的操作系统,包含独立的内核,通过Hypervisor进行硬件虚拟化
- 容器:所有容器共享宿主机的操作系统内核,仅包含应用程序及其依赖
代表性工具:
- Docker:容器应用程序管理平台
- Kubernetes:容器编排系统,用于大规模容器部署和管理
11.2 性能对比分析
启动时间测试
通过严格的实验对比虚拟机与容器的启动性能:
测试条件:
- 系统:Ubuntu 20.04
- 虚拟机启动:
virsh start --console ubuntu2004 - 容器启动:
time docker run ubuntu:20.04 - 排除缓存影响:均进行两次启动,记录第二次时间
测试结果:
| 环境 | 启动时间 | 相对性能 |
|---|---|---|
| 虚拟机 | 14.0秒 | 1x |
| 容器 | 0.67秒 | 约21倍 |
性能优势分析:
- 启动流程简化:容器省略了引导程序加载、内核启动等步骤
- 硬件访问优化:直接使用宿主内核,无需虚拟化层转换
- 资源开销小:无需为每个实例分配完整操作系统资源
11.3 容器类型分类
系统容器与应用容器
系统容器特征:
- 模仿完整Linux环境,可运行多种应用程序
- 初始进程通常为轻量级init程序(非systemd)
- 代表性运行时:LXD
- 使用场景:需要完整操作系统环境的场景
应用容器特征:
- 专注于运行单个应用程序及其依赖
- 初始进程为应用程序本身
- 代表性工具:Docker
- 使用场景:微服务、云原生应用部署
11.4 命名空间机制详解
命名空间类型:
- PID命名空间(pidns):进程ID隔离
- 用户命名空间(userns):用户和组ID隔离
- 挂载命名空间(mountns):文件系统挂载点隔离
- 网络命名空间(netns):网络设备、端口等隔离
- UTS命名空间(utsns):主机名和域名隔离
- IPC命名空间(ipcns):System V IPC对象隔离
PID命名空间深度分析
层次结构:
bash
# 查看进程所属PID命名空间
ls -l /proc/$$/ns/pid
# 输出:lrwxrwxrwx 1 user user 0 Jan 1 10:00 /proc/1234/ns/pid -> 'pid:[4026531836]'
命名空间创建实验:
bash
# 创建新的PID命名空间并运行bash
sudo unshare --fork --pid --mount-proc bash
# 在新命名空间中查看进程信息
echo $$ # 输出:1(在新命名空间中的PID)
ps ax # 只能看到新命名空间内的进程
可见性规则:
- 父命名空间:可以看到所有子命名空间中的进程
- 子命名空间:无法看到父命名空间及其他平行命名空间中的进程
- PID映射:同一进程在不同命名空间中具有不同的PID
11.5 容器安全机制
安全风险分析
共享内核风险:
- 内核漏洞可能影响所有容器
- 容器逃逸攻击可能危及宿主机安全
- 资源隔离不完全可能导致干扰攻击
安全增强技术
容器运行时安全机制:
1. Kata Containers
- 基于轻量级虚拟机的容器运行时
- 每个容器运行在独立的微型虚拟机中
- 提供硬件级别的隔离保障
2. gVisor
- 用户空间内核,拦截和处理系统调用
- 提供额外的安全隔离层
- 兼容性较好,性能开销可控
3. runC(Docker默认运行时)
- 直接使用宿主内核
- 性能最优,隔离性相对较弱
- 依赖Linux内核的安全特性
系统调用处理对比:
- runC:直接调用宿主内核
- Kata Containers:通过虚拟机监控器转发
- gVisor:通过用户空间内核处理
11.6 故障诊断与监控
容器环境下的故障排查
局限性分析:
- 容器内无法查看宿主机或其他容器的进程
- 资源竞争问题难以在容器内诊断
- 需要宿主机级别的监控工具支持
监控策略:
bash
# 宿主机视角监控容器
docker stats [容器名]
# 显示CPU、内存、网络等使用情况
# 容器内进程监控(受限视图)
top/htop # 只能看到容器内进程
11.7 实际应用建议
技术选型指南
选择容器的场景:
- 需要快速启动和弹性伸缩
- 资源利用率要求高
- 应用基于Linux环境
- 安全要求可通过其他手段保障
选择虚拟机的场景:
- 需要运行不同内核的操作系统
- 安全隔离要求极高
- 遗留系统迁移
- 硬件设备直通需求
安全最佳实践:
- 最小权限原则:容器以非root用户运行
- 资源限制:使用cgroups限制资源使用
- 漏洞管理:定期更新基础镜像,扫描漏洞
- 网络隔离:使用网络策略限制容器间通信
第12章 cgroup
本章系统性地阐述了Linux控制组(cgroup)技术的核心原理、资源限制机制以及实际应用场景,深入分析了cgroup如何实现对系统资源的精确控制。
12.1 cgroup的基本概念与重要性
cgroup的核心价值
cgroup是Linux内核提供的一种机制,用于精确限制分配给进程的系统资源(如内存、CPU等)。其名称来源于"control group",即通过对进程进行分组并对资源进行控制。
技术演进背景
- 早期限制机制 :类UNIX系统很早就提供了
setrlimit()系统调用,但功能基础 - 商业系统领先:大型机和商用UNIX服务器早已实现完善的资源限制机制
- Linux引入挑战:需要大量代码修改,可能带来性能开销,早期并非迫切需求
应用场景分析
-
云服务多租户环境
- 防止某个用户独占系统资源
- 确保付费用户获得承诺的服务质量
- 避免资源竞争导致的性能波动
-
业务优先级管理
- 后台任务(如数据备份)不影响关键业务
- 保证高优先级应用的资源可用性
- 实现资源分配的精细化管控
12.2 cgroup的核心机制与控制器
控制器架构
cgroup通过不同的控制器实现对各类资源的限制:
主要控制器类型:
| 控制器 | 功能 | 应用场景 |
|---|---|---|
| cpu控制器 | 限制CPU使用时间 | 防止CPU密集型任务影响系统响应 |
| memory控制器 | 限制内存使用量 | 避免内存耗尽触发OOM killer |
| blkio控制器 | 限制块设备I/O带宽 | 控制磁盘I/O,如备份任务限速 |
| 网络控制器 | 限制网络带宽 | 需结合tc等工具实现 |
文件系统接口
cgroup通过特殊的cgroupfs文件系统提供用户接口:
- 每个控制器有专属的cgroupfs文件系统
- 在Ubuntu 20.04中挂载于
/sys/fs/cgroup/目录 - 仅存在于内存中,root用户才有访问权限
分层结构设计
cgroup支持进程分组和分层的分组结构,实现复杂的资源控制策略。
12.3 CPU资源限制实战
CPU带宽控制器原理
通过操作cpu.cfs_period_us和cpu.cfs_quota_us文件实现对CPU时间的精确控制:
参数说明:
cpu.cfs_period_us:时间周期长度(默认100毫秒)cpu.cfs_quota_us:在周期内允许使用的CPU时间(-1表示无限制)
实际操作示例:
bash
# 创建测试分组
mkdir /sys/fs/cgroup/cpu/test
# 查看默认值
cat /sys/fs/cgroup/cpu/test/cpu.cfs_period_us # 输出:100000
cat /sys/fs/cgroup/cpu/test/cpu.cfs_quota_us # 输出:-1
# 运行测试进程
./inf-loop.py &
echo $! > /sys/fs/cgroup/cpu/test/tasks
# 设置限制:100毫秒周期内最多使用50毫秒
echo 50000 > /sys/fs/cgroup/cpu/test/cpu.cfs_quota_us
效果验证:
- 限制前:进程可使用100% CPU资源
- 限制后:进程只能使用50% CPU资源
- 机制:进程在耗尽配额后必须等待下一个周期
12.4 cgroup的实际应用生态
系统级管理工具
- systemd:自动为每个服务和用户创建分组(system.slice、user.slice)
- Docker/Kubernetes:通过容器编排平台管理资源限制
- libvirt:虚拟机资源管理的基础机制
间接使用模式
大多数用户通过上层工具间接使用cgroup,而无需直接操作cgroupfs:
- Kubernetes清单中指定资源请求和限制
- docker run命令通过参数设置资源限制
- virt-manager图形界面配置虚拟机资源
12.5 cgroup v2的技术演进
v1版本的局限性
- 各控制器独立实现,难以协同工作
- 块设备I/O限制仅在direct I/O时有效
- 缺乏统一的资源控制视图
v2版本的改进
- 统一层级结构:所有控制器位于单一层级
- 协同处理:解决控制器间的协作问题
- 功能增强:改进块设备I/O限制等关键功能
版本共存现状
由于软件生态支持度差异,当前cgroup v1和v2处于共存状态,v2将逐步成为主流。
第13章 操作系统面试知识点大全
一、用户态和内核态
1. 用户态和内核态的区别?
概念:
- 用户态:CPU执行用户程序时的状态,权限有限,不能直接访问硬件和内核空间
- 内核态:CPU执行操作系统内核代码时的状态,拥有最高权限,可以执行所有指令
详细区别:
| 方面 | 用户态 | 内核态 |
|---|---|---|
| 权限 | 有限权限,只能执行非特权指令 | 完全权限,可以执行所有指令 |
| 访问资源 | 只能访问自己的内存空间 | 可以访问所有内存和硬件资源 |
| 执行内容 | 应用程序代码 | 操作系统内核代码 |
| 切换时机 | 系统调用、中断、异常时切换到内核态 | 处理完系统调用、中断后返回用户态 |
内核态负责的底层操作:
- 内存管理(分配/回收内存)
- 进程管理(创建/调度进程)
- 设备驱动控制
- 系统调用处理
划分原因:
- 安全性:防止用户程序破坏系统
- 稳定性:用户程序错误不会导致系统崩溃
- 隔离性:各程序相互独立,互不干扰
2. 用户态切换到内核态的方式?
三种方式:
- 系统调用:程序主动请求操作系统服务(如读写文件)
- 中断:外部设备请求CPU处理(如键盘输入)
- 异常:程序执行出错(如除零错误)
二、进程管理
1. 线程和进程的区别是什么?
本质区别:
- 进程是资源分配的基本单位
- 线程是CPU调度的基本单位
详细对比:
| 特性 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 独立的内存空间和系统资源 | 共享进程的资源 |
| 切换开销 | 大(切换页表、上下文) | 小(切换寄存器和栈) |
| 通信方式 | 需要IPC机制(管道、消息队列等) | 直接读写共享内存 |
| 创建开销 | 大(分配内存、建立数据结构) | 小(复用进程资源) |
| 稳定性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
| 包含关系 | 可以包含多个线程 | 必须属于某个进程 |
2. 进程、线程、协程的区别是什么?
进程:
- 独立的内存空间和资源
- 通信需要IPC机制
- 上下文切换开销大
- 稳定性高(一个进程崩溃不影响其他进程)
线程:
- 共享进程的内存和资源
- 通信方便(共享内存)
- 上下文切换开销较小
- 稳定性较低(一个线程崩溃可能影响整个进程)
协程:
- 用户态的轻量级线程
- 调度由程序控制,不依赖操作系统
- 切换开销最小(只需保存寄存器)
- 需要程序员显式管理调度
- 适合高并发IO密集型任务
3. 为什么进程崩溃不会对其他进程产生很大影响?
两个主要原因:
- 进程隔离性:每个进程有独立的虚拟地址空间
- 资源独立性:进程间不共享关键资源(除非显式共享)
具体机制:
- 内存隔离:一个进程无法访问另一个进程的内存
- 错误隔离:一个进程的错误不会传播到其他进程
- 资源回收:崩溃进程的资源会被操作系统回收
4. 进程是分配资源的基本单位,资源指什么?
进程分配的资源包括:
- 虚拟内存:独立的地址空间
- 文件描述符:打开的文件、网络连接等
- 信号量:同步和通信机制
- CPU时间:调度执行时间
- I/O设备:访问硬件设备的权限
5. 为什么要有线程?
解决进程的两个问题:
- 进程间通信复杂:需要专门的IPC机制
- 进程切换开销大:需要保存和恢复大量上下文
线程的优势:
- 共享进程资源,通信简单
- 切换开销小,响应速度快
- 适合多核并行计算
历史演进:
单进程 → 多进程 → 多线程
问题: 问题: 解决:
顺序执行 通信复杂 共享内存
效率低 切换开销大 切换快
6. 多线程的优势和劣势?
优势:
- 提高CPU利用率(多核并行)
- 提高程序响应速度(一个线程阻塞,其他可运行)
- 资源共享方便(不需要IPC)
- 经济(创建线程比创建进程开销小)
劣势:
- 编程复杂(需要处理线程同步)
- 调试困难(竞争条件难复现)
- 死锁风险(资源竞争可能导致死锁)
- 资源消耗(每个线程需要栈空间)
7. 多线程是不是越多越好?太多有什么问题?
不是越多越好,原因:
问题1:切换开销
- 线程数超过CPU核心数时,频繁切换消耗CPU时间
- 每个线程的上下文切换需要保存/恢复寄存器状态
问题2:资源竞争
- 线程越多,竞争共享资源越激烈
- 锁竞争导致大部分时间在等待
问题3:内存消耗
- 每个线程需要独立的栈空间(通常几MB)
- 线程过多可能导致内存不足
经验法则:
- CPU密集型任务:线程数 ≈ CPU核心数
- IO密集型任务:线程数可多于CPU核心数
- 通常不要创建超过1000个线程
8. 进程切换和线程切换的区别?
进程切换:
- 切换页表(虚拟内存映射)
- 切换内核栈
- 切换文件描述符表
- 切换信号处理表
- 切换寄存器状态
线程切换:
- 切换栈指针
- 切换程序计数器
- 切换寄存器状态
- 不切换内存空间(共享同一地址空间)
关键区别: 线程切换不需要切换页表,因此比进程切换快得多。
9. 线程切换为什么比进程切换快?节省了什么资源?
节省的资源:
- 页表切换:线程共享同一页表,不需要切换
- TLB刷新:不需要清空TLB缓存
- 内存映射:不需要重新建立内存映射
- 内核数据结构:共享进程的内核数据结构
具体节省:
- 时间:线程切换只需微秒级,进程切换需毫秒级
- CPU周期:减少TLB刷新和页表加载
- 内存带宽:减少内存访问次数
10. 线程切换详细过程?上下文保存在哪里?
线程切换步骤:
1. 保存当前线程上下文
- 寄存器值保存到TCB
- 栈指针保存到TCB
2. 调度器选择下一个线程
- 根据调度算法选择
- 检查线程状态(就绪/运行)
3. 恢复新线程上下文
- 从TCB加载寄存器值
- 恢复栈指针
4. 切换到新线程
- 设置程序计数器
- 开始执行
上下文保存位置:
- TCB(Thread Control Block):操作系统维护的数据结构
- 包含:线程ID、状态、优先级、寄存器值、栈信息等
- 存储在操作系统内核空间
11. 进程的五种状态及切换?
五种状态:
- 创建:进程正在被创建
- 就绪:进程准备好运行,等待CPU
- 运行:进程正在CPU上执行
- 阻塞:进程等待某事件(如I/O完成)
- 终止:进程执行完毕或被终止
状态转换:
创建 → 就绪:初始化完成
就绪 → 运行:被调度器选中
运行 → 就绪:时间片用完
运行 → 阻塞:等待I/O或资源
阻塞 → 就绪:等待的事件发生
运行 → 终止:执行完毕或出错
12. 进程上下文有哪些?
进程上下文包含:
-
用户级上下文:
- 用户寄存器状态
- 用户栈信息
-
寄存器上下文:
- 通用寄存器
- 程序计数器
- 栈指针
- 状态寄存器
-
系统级上下文:
- 页表指针
- 文件描述符表
- 信号处理表
- 内核栈
上下文切换流程:
保存当前进程上下文 → 选择下一个进程 → 恢复新进程上下文 → 执行新进程
13. 进程间通信有哪些方式?
6种主要方式:
1. 管道(Pipe)
- 匿名管道:父子进程间,单向通信
- 命名管道:任意进程间,有文件名
- 特点:先进先出,无格式字节流
2. 消息队列(Message Queue)
- 消息链表,支持结构体
- 异步通信,内核持久化
- 需要内核空间拷贝
3. 共享内存(Shared Memory)
- 映射同一块物理内存
- 速度最快,无需内核拷贝
- 需要同步机制(信号量)
4. 信号(Signal)
- 异步事件通知
- 简单但功能有限
- 如:SIGINT(Ctrl+C)
5. 信号量(Semaphore)
- 计数器,控制资源访问
- P操作(申请资源),V操作(释放资源)
- 用于进程同步
6. Socket
- 网络通信,也可本地通信
- 支持TCP/UDP协议
- 跨主机通信
选择建议:
- 少量数据:管道、信号
- 大量数据:共享内存
- 结构化数据:消息队列
- 网络通信:Socket
14. 管道有几种方式?
两种管道:
匿名管道:
- 使用
pipe()系统调用创建 - 只能用于有亲缘关系的进程(父子、兄弟)
- 单向通信
- 生命周期随进程结束
- Shell中的
|就是匿名管道
命名管道(FIFO):
- 使用
mkfifo()创建,有文件名 - 可用于任意进程间通信
- 双向通信(需要两个管道)
- 文件系统持久化
- 更像普通文件
15. 信号和信号量有什么区别?
信号(Signal):
- 目的:异步事件通知
- 本质:软件中断
- 使用:kill命令发送,signal函数处理
- 示例:SIGTERM(终止)、SIGSEGV(段错误)
信号量(Semaphore):
- 目的:进程同步和互斥
- 本质:计数器+等待队列
- 使用:P/V操作控制资源访问
- 示例:控制最多N个进程访问资源
关键区别:
- 信号是通知机制,信号量是同步机制
- 信号异步处理,信号量同步等待
- 信号简单(发送/接收),信号量复杂(P/V操作)
16. 共享内存怎么实现的?
实现步骤:
1. 创建共享内存
c
shmget(key, size, flags) // 创建或获取共享内存
2. 映射到进程地址空间
c
shmat(shmid, addr, flags) // 映射共享内存
3. 读写共享内存
- 像访问普通内存一样读写
- 需要同步机制(如信号量)
4. 解除映射
c
shmdt(addr) // 解除映射
5. 删除共享内存
c
shmctl(shmid, IPC_RMID, NULL) // 删除共享内存
优势:
- 最快IPC方式(直接内存访问)
- 减少数据拷贝次数
缺点:
- 需要显式同步
- 可能产生竞态条件
17. 线程间通信有什么方式?
5种主要方式:
1. 互斥锁(Mutex)
- 最基本的锁,保证互斥访问
- 加锁失败时线程阻塞
- 适合保护临界区
2. 读写锁(Read-Write Lock)
- 读锁可共享,写锁独占
- 适合读多写少场景
- 提高并发性能
3. 条件变量(Condition Variable)
- 等待特定条件成立
- 必须与互斥锁配合使用
- 用于线程间协调
4. 自旋锁(Spinlock)
- 忙等待,不放弃CPU
- 适合短临界区
- 避免上下文切换开销
5. 信号量(Semaphore)
- 计数器控制资源访问
- 可用于线程同步
- 控制并发数量
18. 除了互斥锁还有什么锁?应用场景?
1. 读写锁
- 场景:读多写少的数据结构
- 示例:缓存系统,配置读取
2. 自旋锁
- 场景:临界区执行时间很短
- 示例:内核中断处理,短时间锁
3. 条件变量
- 场景:等待特定条件发生
- 示例:生产者-消费者问题
4. 信号量
- 场景:控制资源并发访问数
- 示例:连接池,线程池
5. 屏障(Barrier)
- 场景:等待多个线程到达某点
- 示例:并行计算,多阶段任务
19. 进程调度算法有哪些?
6种常见算法:
1. 先来先服务(FCFS)
- 按到达顺序执行
- 优点:简单公平
- 缺点:短作业等待长作业
- 适合:批处理系统
2. 最短作业优先(SJF)
- 优先执行运行时间短的作业
- 优点:平均等待时间最小
- 缺点:长作业可能饿死
- 适合:批处理系统
3. 高响应比优先(HRRN)
- 响应比 = (等待时间 + 服务时间) / 服务时间
- 优点:兼顾长短作业
- 缺点:需要预估运行时间
4. 时间片轮转(RR)
- 每个进程分配固定时间片
- 优点:公平,响应快
- 缺点:上下文切换开销
- 适合:分时系统
5. 最高优先级(HPF)
- 静态或动态优先级
- 优点:重要任务优先
- 缺点:低优先级可能饿死
- 变种:可抢占/不可抢占
6. 多级反馈队列(MFQ)
- 多个优先级队列
- 进程可在队列间移动
- 优点:综合各种策略优点
- 缺点:实现复杂
- 适合:通用操作系统
20. 进程调度算法比较?
| 算法 | 抢占性 | 开销 | 响应时间 | 吞吐量 | 公平性 |
|---|---|---|---|---|---|
| FCFS | 不可抢占 | 低 | 长 | 一般 | 公平 |
| SJF | 不可抢占 | 中 | 短 | 高 | 不公平 |
| RR | 可抢占 | 高 | 短 | 一般 | 公平 |
| HPF | 可/不可抢占 | 中 | 短 | 高 | 不公平 |
| MFQ | 可抢占 | 高 | 短 | 高 | 较公平 |
三、内存管理
1. 什么是虚拟内存和物理内存?
物理内存:
- 实际的RAM硬件
- 容量有限(如16GB)
- 直接由CPU访问
虚拟内存:
- 操作系统提供的抽象
- 每个进程有独立的虚拟地址空间
- 大小可超过物理内存
虚拟内存实现机制:
- 分页:内存分成固定大小的页(通常4KB)
- 页表:记录虚拟页到物理页的映射
- MMU:硬件完成地址转换
- 交换:不常用的页换出到磁盘
2. 讲一下页表?
页表作用:记录虚拟页到物理页的映射关系
地址转换过程:
虚拟地址 → 页号 + 页内偏移
↓
查页表得到物理页号
↓
物理页号 + 页内偏移 = 物理地址
页表项内容:
- 物理页号
- 存在位(是否在内存中)
- 访问位(是否被访问过)
- 修改位(是否被修改过)
- 保护位(读/写/执行权限)
多级页表:
- 解决页表过大的问题
- 类似目录结构,只加载需要的部分
- 典型:x86-64使用4级页表
3. 虚拟地址怎么转换到物理地址?
转换步骤:
1. CPU发出虚拟地址
2. MMU接收虚拟地址
3. 查询TLB(快表)
- 命中:直接得到物理地址
- 未命中:继续步骤4
4. 查询页表
- 在内存中:得到物理地址,更新TLB
- 不在内存中:触发缺页异常
5. 缺页异常处理:
a. 从磁盘加载页面到内存
b. 更新页表
c. 重新执行指令
TLB作用:
- 缓存常用的页表项
- 加速地址转换
- 命中率通常>95%
4. 程序的内存布局是怎么样的?
Linux进程内存布局(从低地址到高地址):
0x00000000
┌─────────────┐
│ 保留区 │ ← 禁止访问的小地址区域
├─────────────┤
│ 代码段 │ ← 程序指令(只读)
│ (text) │
├─────────────┤
│ 数据段 │ ← 已初始化的全局/静态变量
│ (data) │
├─────────────┤
│ BSS段 │ ← 未初始化的全局/静态变量
│ │
├─────────────┤
│ 堆 │ ← 动态分配内存(向上增长)
│ (heap) │ malloc/new分配
│ │
├─────────────┤
│ 内存映射段 │ ← 动态库、共享内存
│ (mmap) │ mmap分配
│ │
├─────────────┤
│ 栈 │ ← 局部变量、函数调用(向下增长)
│ (stack) │ 大小固定(通常8MB)
└─────────────┘
0x7fffffffffff
各段特点:
- 代码段:只读,可执行
- 数据段:读写,不可执行
- BSS段:初始化为0,不占磁盘空间
- 堆段:动态分配,手动管理
- 栈段:自动分配/释放,大小有限
5. 堆和栈的区别?
| 特性 | 堆 | 栈 |
|---|---|---|
| 管理方式 | 程序员手动分配/释放 | 编译器自动分配/释放 |
| 分配方式 | 动态分配,运行时决定 | 静态分配,编译时确定 |
| 大小 | 较大,受系统内存限制 | 较小,固定大小(如8MB) |
| 生长方向 | 向高地址增长 | 向低地址增长 |
| 分配效率 | 较慢(需要系统调用) | 很快(移动栈指针) |
| 碎片问题 | 可能有外部碎片 | 无碎片问题 |
| 访问方式 | 通过指针访问 | 直接通过变量名访问 |
| 生命周期 | 直到手动释放 | 函数执行期间 |
| 典型错误 | 内存泄漏、悬空指针 | 栈溢出、缓冲区溢出 |
6. fork()会复制哪些东西?
复制的内容:
- 进程控制块(PCB):复制一份,修改PID等字段
- 虚拟内存:复制页表,但不复制物理内存(写时复制)
- 文件描述符表:复制表项,共享相同的打开文件
- 信号处理表:复制信号处理函数
- 寄存器状态:复制寄存器上下文
不复制的内容:
- PID:新的进程ID
- 父进程ID:设置为调用者PID
- 运行统计信息:清零
- 挂起信号:清零
- 文件锁:不继承
写时复制优化:
- 初始时父子进程共享物理页
- 页表项标记为只读
- 写入时触发缺页异常,再复制物理页
7. 介绍copy on write(写时复制)
基本思想:推迟复制,直到真正需要写入时
工作流程:
1. fork()创建子进程
2. 复制父进程页表
3. 页表项标记为只读
4. 父子进程共享物理页
5. 任一进程尝试写入
6. 触发写保护异常
7. 操作系统复制物理页
8. 修改页表映射
9. 完成写入操作
优点:
- 节省内存:只读页不复制
- 加快fork速度:避免立即复制内存
- 减少开销:实际写入时才复制
应用场景:
- fork()系统调用
- 进程创建
- 某些内存分配策略
8. malloc 1KB和1MB有什么区别?
分配策略差异:
小内存(<128KB):
- 使用
brk()系统调用 - 扩展堆的结束地址
- 从空闲链表分配
- 适合频繁分配释放的小对象
大内存(≥128KB):
- 使用
mmap()系统调用 - 在内存映射区分配
- 直接映射匿名页面
- 适合大块内存分配
区别对比:
| 方面 | brk()分配 | mmap()分配 |
|---|---|---|
| 大小 | <128KB | ≥128KB |
| 系统调用 | brk() | mmap() |
| 位置 | 堆区 | 内存映射区 |
| 管理 | 空闲链表 | 独立映射 |
| 碎片 | 可能有 | 无 |
| 释放 | 可能合并 | 直接unmap |
9. 介绍一下brk和mmap
brk():
-
作用:调整堆的结束地址
-
参数:新的堆结束地址
-
工作方式 :
cvoid *brk(void *addr); // 设置堆顶地址 -
特点 :
- 连续分配
- 适合小内存
- 可能产生碎片
mmap():
-
作用:创建内存映射
-
参数:地址、长度、保护、标志、文件描述符、偏移
-
工作方式 :
cvoid *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); -
特点 :
- 可映射文件或匿名内存
- 适合大内存
- 独立映射,无碎片
内存分配流程:
malloc(size)
↓
if size < 128KB
↓
使用brk()扩展堆
↓
从空闲链表分配
↓
返回指针
malloc(size)
↓
if size >= 128KB
↓
使用mmap()映射内存
↓
直接返回映射地址
10. 操作系统内存不足时会发生什么?
内存不足处理流程:
进程申请内存
↓
触发缺页异常
↓
检查空闲内存
├── 足够 → 分配内存
└── 不足 → 内存回收
↓
后台回收(kswapd)
├── 成功 → 分配内存
└── 失败 → 直接回收
↓
同步回收内存
├── 成功 → 分配内存
└── 失败 → OOM Killer
↓
选择进程杀死
↓
释放内存
回收策略:
-
后台回收(kswapd)
- 内核线程异步回收
- 不阻塞进程
- 维护活跃/非活跃链表
-
直接回收
- 同步回收内存
- 阻塞当前进程
- 可能触发磁盘I/O
-
OOM Killer
- 选择
badness值最高的进程 - 计算规则:内存占用×CPU时间
- 发送SIGKILL信号杀死
- 选择
可回收内存类型:
- 文件页:磁盘缓存,可直接释放
- 脏页:修改过的文件页,需写回磁盘
- 匿名页:进程堆栈数据,需swap到磁盘
11. 页面置换有哪些算法?
常见算法:
1. 最佳置换(OPT)
- 置换未来最长时间不会访问的页面
- 理论最优,无法实现(需要预知未来)
- 用作性能比较基准
2. 先进先出(FIFO)
- 置换最早进入内存的页面
- 实现简单,性能一般
- 可能出现Belady异常
3. 最近最久未使用(LRU)
- 置换最长时间未被访问的页面
- 接近最优,但实现复杂
- 需要硬件支持或软件模拟
4. 时钟算法(Clock)
- 环形链表存储页面
- 访问位为1:清0,跳过
- 访问位为0:置换该页
- FIFO的改进,近似LRU
5. 最不经常使用(LFU)
- 置换访问次数最少的页面
- 需要计数器,开销大
- 可能误伤新页面
算法比较:
| 算法 | 实现难度 | 开销 | 性能 | 备注 |
|---|---|---|---|---|
| OPT | 不可能 | - | 最优 | 理论基准 |
| FIFO | 简单 | 低 | 差 | Belady异常 |
| LRU | 复杂 | 高 | 好 | 需要硬件 |
| Clock | 中等 | 中 | 较好 | 近似LRU |
| LFU | 复杂 | 高 | 一般 | 计数开销 |
四、I/O多路复用
1. 你了解过哪些I/O模型?
5种I/O模型:
1. 阻塞I/O
- 调用read/write时阻塞等待
- 简单易用,效率低
- 适合低并发场景
2. 非阻塞I/O
- 调用立即返回,需要轮询
- 避免阻塞,但CPU占用高
- select/poll的基础
3. I/O多路复用
- select/poll/epoll
- 一个线程监控多个fd
- 适合高并发网络服务
4. 信号驱动I/O
- 内核通知进程fd就绪
- 异步通知,但信号处理复杂
- 使用较少
5. 异步I/O
- 发起I/O请求立即返回
- 完成后内核通知进程
- 真正的异步,但实现复杂
模型对比:
| 模型 | 阻塞 | 非阻塞 | 多路复用 | 异步 |
|---|---|---|---|---|
| 并发能力 | 低 | 中 | 高 | 高 |
| 编程复杂度 | 简单 | 中等 | 中等 | 复杂 |
| 适用场景 | 简单应用 | 专用场景 | 网络服务 | 高性能应用 |
2. 服务器处理并发请求有哪几种方式?
4种架构:
1. 单线程
- 一次处理一个请求
- 简单但性能差
- 适合低负载场景
2. 多进程
- 每个进程处理一个连接
- 进程隔离,稳定性好
- 开销大,适合Apache
3. 多线程
- 每个线程处理一个连接
- 共享内存,通信方便
- 需要同步,可能死锁
4. I/O多路复用+多线程
- Reactor模式
- 少量线程处理大量连接
- Nginx、Redis使用
- 高性能,复杂度高
选择建议:
- C10K以下:多进程/多线程
- C10K以上:I/O多路复用
- 极致性能:异步I/O
3. 讲一下I/O多路复用
核心思想:一个线程监控多个文件描述符
三种实现:
1. select
c
int select(int nfds, fd_set *readfds,
fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
- 限制:最多1024个fd
- 原理:轮询所有fd,标记就绪状态
- 缺点:每次调用需要重置fd_set
2. poll
c
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 改进:无fd数量限制
- 原理:链表存储,仍然轮询
- 缺点:仍然需要遍历所有fd
3. epoll
c
// 创建epoll实例
int epoll_create(int size);
// 添加/修改/删除fd
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
// 等待事件
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
- 优势 :
- 红黑树存储fd,查找O(log n)
- 就绪链表,只返回就绪事件
- 边缘触发模式
- 适合:Linux平台,高并发
4. select、poll、epoll的区别是什么?
详细对比:
| 特性 | select | poll | epoll |
|---|---|---|---|
| fd数量限制 | 1024 | 无 | 无 |
| 数据结构 | bitmap | 链表 | 红黑树 |
| 时间复杂度 | O(n) | O(n) | O(1)事件通知 |
| 内存拷贝 | 每次调用拷贝fd_set | 每次传递pollfd | 注册时拷贝一次 |
| 触发模式 | 水平触发 | 水平触发 | 水平/边缘触发 |
| 平台支持 | 所有平台 | 所有平台 | Linux特有 |
| 适用场景 | 小规模并发 | 中等并发 | 大规模并发 |
性能差异:
- 连接数<1000:三者差异不大
- 连接数1000-10000:epoll优势明显
- 连接数>10000:必须使用epoll
5. epoll的边缘触发和水平触发有什么区别?
水平触发(LT):
- 默认模式
- fd就绪时,每次epoll_wait都返回
- 类似:有数据就一直通知
- 优点:编程简单,不易丢失事件
- 缺点:可能重复通知,效率略低
边缘触发(ET):
- 需要设置EPOLLET标志
- 只有fd状态变化时通知一次
- 类似:状态变化时通知
- 优点:减少epoll_wait调用次数
- 缺点:必须一次读完所有数据
使用建议:
c
// 水平触发(默认)
event.events = EPOLLIN;
// 边缘触发
event.events = EPOLLIN | EPOLLET;
注意事项:
- ET模式必须使用非阻塞fd
- 需要循环read/write直到EAGAIN
- 可能丢失事件,需要谨慎处理
6. Redis、Nginx、Netty依赖什么高性能?
共同基础:Reactor模式 + I/O多路复用
Redis(单Reactor单进程):
客户端连接 → Reactor接收 → 事件分发 → 命令处理 → 返回结果
- 特点:单线程处理所有请求
- 优势:无锁,无上下文切换
- 局限:无法利用多核
Nginx(多Reactor多进程):
主进程 → 监听端口
↓
工作进程(多个) → 每个进程一个Reactor
↓
事件循环 → 处理请求
- 特点:一个进程一个Reactor
- 优势:利用多核,进程隔离
- 避免惊群:accept使用锁
Netty(多Reactor多线程):
主Reactor(1个) → 接受连接 → 分发给子Reactor
↓
子Reactor(多个) → 处理I/O事件
↓
业务线程池 → 处理业务逻辑
- 特点:主从Reactor,线程池
- 优势:职责分离,扩展性好
- 适用:Java高并发网络应用
7. 零拷贝是什么?
传统文件传输:
1. 磁盘 → 内核缓冲区(DMA拷贝)
2. 内核缓冲区 → 用户缓冲区(CPU拷贝)
3. 用户缓冲区 → Socket缓冲区(CPU拷贝)
4. Socket缓冲区 → 网卡(DMA拷贝)
- 4次拷贝:2次DMA,2次CPU
- 4次上下文切换:用户态↔内核态切换
零拷贝(sendfile):
1. 磁盘 → 内核缓冲区(DMA拷贝)
2. 内核缓冲区 → Socket缓冲区(DMA拷贝)
3. Socket缓冲区 → 网卡(DMA拷贝)
- 3次拷贝:全部DMA拷贝
- 2次上下文切换:大大减少
进一步优化(支持scatter/gather的DMA):
1. 磁盘 → 内核缓冲区(DMA拷贝)
2. 内核缓冲区描述符 → 网卡(DMA直接从内核读)
- 2次拷贝:全部DMA
- 2次上下文切换
优势:
- 减少CPU拷贝次数
- 减少上下文切换
- 提升传输性能(30%-100%)
应用场景:
- 文件下载服务器
- 静态资源服务器
- 大数据传输
五、锁机制
1. 为什么并发执行线程要加锁?
竞态条件:多个线程访问共享资源,结果依赖执行时序
不加锁的问题:
c
// 两个线程同时执行 count++
// 可能结果:count只加1,而不是2
线程A:读取count=0
线程B:读取count=0
线程A:计算0+1=1,写入count=1
线程B:计算0+1=1,写入count=1
// 最终count=1,而不是2
锁的作用:
- 互斥访问:一次只允许一个线程访问
- 可见性:确保修改对其他线程可见
- 有序性:防止指令重排序
2. 自旋锁是什么?应用在哪些场景?
自旋锁:
- 忙等待锁,不放弃CPU
- 通过原子指令实现(CAS)
- 适合短临界区
实现原理:
c
// 加锁
while (test_and_set(&lock) == 1) {
// 忙等待
}
// 解锁
lock = 0;
应用场景:
- 内核中断处理:不能睡眠的上下文
- 短临界区:等待时间小于线程切换时间
- 多核系统:单核自旋浪费CPU
与互斥锁对比:
| 特性 | 自旋锁 | 互斥锁 |
|---|---|---|
| 等待方式 | 忙等待 | 睡眠等待 |
| 开销 | CPU空转 | 上下文切换 |
| 适用场景 | 短临界区 | 长临界区 |
| 单核适用 | 不适用(需抢占调度) | 适用 |
3. 死锁发生条件是什么?
四个必要条件(同时满足):
-
互斥条件
- 资源一次只能被一个进程使用
- 如:打印机、锁
-
占有并等待
- 进程持有资源,同时请求新资源
- 如:持有A锁,请求B锁
-
不可剥夺
- 资源只能由持有者释放
- 不能被强制剥夺
-
循环等待
- 进程间形成等待环
- 如:P1等P2的资源,P2等P1的资源
死锁示例:
进程A:持有锁1,请求锁2
进程B:持有锁2,请求锁1
结果:互相等待,死锁
4. 如何避免死锁?
破坏四个条件之一:
-
破坏互斥
- 使用共享资源(如只读数据)
- 不总是可行
-
破坏占有并等待
- 一次性申请所有资源
- 资源利用率低
-
破坏不可剥夺
- 强制剥夺资源
- 实现复杂
-
破坏循环等待
- 资源有序分配:按固定顺序申请资源
- 银行家算法:预分配检查安全性
实用建议:
- 锁顺序:所有线程按相同顺序获取锁
- 锁超时:获取锁失败时超时返回
- 死锁检测:定期检测并恢复
5. 讲一下银行家算法
核心思想:预分配检查,避免进入不安全状态
数据结构:
- Available:可用资源向量
- Max:进程最大需求矩阵
- Allocation:已分配矩阵
- Need:还需资源矩阵(Need = Max - Allocation)
安全序列算法:
1. 初始化Work = Available, Finish[i] = false
2. 寻找满足条件的进程i:
a. Finish[i] = false
b. Need[i] ≤ Work
3. 如果找到:
Work = Work + Allocation[i]
Finish[i] = true
重复步骤2
4. 如果所有Finish[i] = true,系统安全
资源请求算法:
1. 如果Request[i] ≤ Need[i],继续;否则错误
2. 如果Request[i] ≤ Available,继续;否则等待
3. 尝试分配:
Available = Available - Request[i]
Allocation[i] = Allocation[i] + Request[i]
Need[i] = Need[i] - Request[i]
4. 检查安全性:
如果安全,完成分配
如果不安全,恢复原状,让进程等待
示例计算(简化):
资源:A=10, B=5, C=7
进程P1:最大需要(7,5,3),已分配(0,1,0)
进程P2:最大需要(3,2,2),已分配(2,0,0)
进程P3:最大需要(9,0,2),已分配(3,0,2)
进程P4:最大需要(2,2,2),已分配(2,1,1)
进程P5:最大需要(4,3,3),已分配(0,0,2)
计算安全序列...
6. 乐观锁和悲观锁有什么区别?
悲观锁:
-
思想:假定会发生冲突,先加锁再访问
-
实现:数据库行锁、Java synchronized
-
场景:写多读少,冲突概率高
-
示例 :
sqlBEGIN TRANSACTION; SELECT * FROM table WHERE id=1 FOR UPDATE; UPDATE table SET value=value+1 WHERE id=1; COMMIT;
乐观锁:
-
思想:假定不会冲突,更新时检查版本
-
实现:版本号、时间戳、CAS
-
场景:读多写少,冲突概率低
-
示例 :
sqlUPDATE table SET value=new_value, version=version+1 WHERE id=1 AND version=old_version;
对比:
| 特性 | 悲观锁 | 乐观锁 |
|---|---|---|
| 并发性能 | 低 | 高 |
| 冲突处理 | 阻塞等待 | 回滚重试 |
| 适用场景 | 写多读少 | 读多写少 |
| 实现复杂度 | 简单 | 复杂 |
| 数据一致性 | 强 | 最终一致 |
六、中断机制
1. 什么是中断?
中断:CPU暂停当前任务,处理紧急事件,然后返回
中断分类:
外部中断(硬件产生):
- 可屏蔽中断:可暂时忽略(如键盘输入)
- 不可屏蔽中断:必须立即处理(如电源故障)
内部中断(软件产生,又称异常):
- 陷阱:有意触发(系统调用、调试断点)
- 故障:可修复错误(缺页异常、除零)
- 终止:严重错误(硬件故障)
2. 讲讲中断的流程
中断处理流程:
1. 中断发生
↓
2. 保存现场
- 压栈保存寄存器
- 保存程序计数器
↓
3. 关中断(可选)
- 防止嵌套中断
↓
4. 识别中断源
- 查询中断向量表
- 获取处理程序地址
↓
5. 执行中断处理程序
- 处理具体中断
↓
6. 恢复现场
- 出栈恢复寄存器
- 恢复程序计数器
↓
7. 开中断(如果之前关闭)
↓
8. 中断返回
- iret指令返回原程序
3. 中断的类型有哪些?
详细分类:
1. 外部中断
- 可屏蔽中断:通过INTR引脚,可被IF标志屏蔽
- 不可屏蔽中断:通过NMI引脚,用于严重硬件错误
2. 内部中断(异常)
- 故障:执行指令前检测到的错误(如缺页)
- 陷阱:执行特定指令(如int 0x80系统调用)
- 中止:严重错误,无法恢复(如硬件故障)
常见中断号:
- 0x00:除零错误
- 0x0E:缺页异常
- 0x80:Linux系统调用
- 0x20-0x2F:可屏蔽硬件中断
4. 中断的作用是什么?
主要作用:
-
提高CPU利用率
- 不用轮询设备状态
- 设备就绪时通知CPU
-
实现多任务
- 时钟中断实现时间片轮转
- 进程切换的基础
-
处理紧急事件
- 硬件错误及时处理
- 防止数据丢失
-
系统调用接口
- 用户程序请求内核服务
- 如文件读写、网络通信
没有中断的问题:
- CPU需要轮询所有设备
- 响应延迟高
- 无法处理紧急事件
- 多任务难以实现