bksczm头像
关注
0基础面试3封面图

0基础面试3

08 进程上下文切换

Q:什么是进程上下文切换?切换过程中内核主要做了哪些工作?为什么线程上下文切换的开销远小于进程?

一、什么是进程上下文切换

进程上下文切换是指内核剥夺当前正在运行进程的 CPU 使用权,将其置回就绪态或睡眠态,再通过调度器从就绪队列中选出另一个进程,把 CPU 使用权交给它的过程。切换的核心是保存旧进程的硬件上下文、恢复新进程的硬件上下文,保证新进程能够从上次被打断的指令位置继续执行,且整个过程对进程透明。

触发切换的典型时机包括:进程的时间片耗尽被抢占、进程因等待 IO 或锁而主动阻塞、进程调用 sched_yield 主动让出、系统调用或中断处理结束时调度器发现有更高优先级进程、进程退出等。

需要区分两个概念:用户态通过系统调用/中断陷入内核态称为模式切换,此时只保存当前进程的陷入现场,并不一定发生进程切换;只有调度器决定换一个进程运行时,才是进程上下文切换。

二、切换过程中内核的主要工作

  1. 保存现场并陷入内核:切换由时钟中断、系统调用或异常引发,CPU 进入内核态,把当前的程序计数器、程序状态字、通用寄存器等压入该进程的内核栈(保存为 pt_regs 结构)。
  2. 保存旧进程的硬件上下文:调度器把与进程恢复相关的寄存器(栈指针、指令指针、callee-saved 寄存器,以及浮点/向量寄存器 FPU/SSE/AVX 状态、TLS 基址 fsbase 等)保存到旧进程 task_struct 的 thread 结构中,并切换记录内核栈指针。
  3. 调度选择新进程:调用 schedule(),由调度算法(如 Linux 的 CFS 完全公平调度器)从就绪队列中选出下一个运行的进程,完成当前任务指针(current)的更换。
  4. 切换地址空间:调用内存管理的切换逻辑,把新进程的页表基址(物理地址)加载到 CR3 寄存器;如果新旧进程属于同一进程(线程切换),地址空间相同则跳过这一步。
  5. 切换内核栈并恢复上下文:切换到新进程自己的内核栈,从其 task_struct->thread 中恢复栈指针、指令指针和通用寄存器,必要时恢复 FPU 与 TLS 状态。
  6. TLB 与缓存的处理:加载新 CR3 意味着地址空间改变,旧的 TLB 翻译失效(带全局位 G 的内核页除外),后续地址翻译需要重新走多级页表,造成一段时间的 TLB miss;现代 CPU 通过 PCID/ASID 等机制可以部分避免全量刷新。
  7. 返回用户态继续执行:从内核栈恢复现场,跳转到新进程上次被打断的程序计数器位置继续执行。

三、为什么线程切换开销远小于进程

进程切换必须更换整个地址空间,而同一进程内的线程共享同一个地址空间(同一个 mm_struct),因此:

  1. 不切换页表、不加载 CR3:线程切换时内核发现新旧任务的 mm 相同,直接跳过地址空间切换;
  2. 不引发 TLB 刷新:没有 CR3 更换,TLB 中缓存的虚拟地址到物理地址的翻译继续有效,避免了切换后大量内存访问因 TLB miss 而变慢;
  3. 保存恢复的数据量更少:线程切换只需更换内核栈并保存恢复各自私有的栈指针、寄存器、线程局部存储等少量上下文,而进程切换还要处理页表、内存映射等全套状态。

09 TCP 流量控制与拥塞控制

Q:请说明 TCP 的流量控制和拥塞控制的核心区别,以及各自的基本实现机制。

一、二者的核心区别

流量控制解决的是"发送方不能把接收方灌爆"的问题:接收方的应用程序可能来不及读取数据,内核接收缓冲区会被填满,它是端到端、由接收方主导的,手段是接收方在确认报文中显式通告自己的接收窗口。

拥塞控制解决的是发送方不能把网络灌爆"的问题:网络中的路由器、链路带宽和缓存是所有连接共享的,发送过快会导致中间设备排队、丢包甚至全网瘫痪,它是全局性、由发送方主导的,发送方无法直接知道网络容量,只能通过 ACK 返回速率、RTT 变化和丢包等隐含信号推断网络拥塞程度并自我约束。

二者共同决定发送速率,关系为:实际发送窗口 = min(接收窗口 rwnd, 拥塞窗口 cwnd)。

二、流量控制的实现机制:滑动窗口

TCP 头部携带 16 位窗口字段(在窗口扩大选项 Window Scale 支持下可按倍数放大),接收方在每个 ACK 中通告自己当前剩余的接收缓冲区空间,即接收窗口 rwnd。发送方已发送但未确认的数据量不得超过该窗口,从而保证接收缓冲区不会溢出。

当接收缓冲区被占满时,接收方通告窗口为 0,发送方停止发送数据但仍保持连接。为防止"窗口更新通告丢失"导致双方永久僵持,发送方会启动持续定时器(persist timer),定期向接收方发送单字节的零窗口探测报文,强制接收方回报当前窗口,直到窗口恢复非零再继续发送。此外接收方还会通过延迟 ACK、避免"糊涂窗口综合征"(窗口刚腾出一点就通告、发送方发极小报文)等策略保证效率。

三、拥塞控制的实现机制

发送方维护拥塞窗口 cwnd(单位为 MSS),经典实现(Tahoe/Reno/NewReno)分为以下阶段:

  1. 慢启动:连接建立后 cwnd 从初始值开始,每收到一个 ACK,cwnd 增加 1 个 MSS,效果是每经过一个 RTT,cwnd 翻倍的指数增长,快速探测网络可用容量;当 cwnd 增长到慢启动阈值 ssthresh 时,转入拥塞避免阶段。(注:经典教材中初始值为 1,现代实现按 RFC 6928 初始 cwnd 约为 10 个 MSS。)
  2. 拥塞避免:cwnd 改为线性增长,每个 RTT 约增加 1 个 MSS(具体为每收到一个 ACK 增加 MSS²/cwnd),谨慎地逼近网络容量。
  3. 拥塞的判定与响应,分两种情况:
    • 超时重传(RTO 超时):认为发生了严重拥塞,处理最严厉——ssthresh 设为 cwnd/2,cwnd 重置为 1,重新进入慢启动;
    • 收到 3 个重复 ACK:说明个别报文丢失但后续报文仍能到达,拥塞较轻,触发快速重传(立即重传丢失报文,不等超时)和快速恢复,不回到 cwnd=1。
  4. 快速恢复(快速重传后的阶段):先令 ssthresh = cwnd/2,然后 cwnd 取 ssthresh(经典实现为 ssthresh+3,之后每收到一个重复 ACK 再临时 +1 以维持管道),期间允许继续发送新数据;当收到确认了新数据的 ACK 后,把 cwnd 降为 ssthresh,平滑进入拥塞避免阶段。注意:并不是"cwnd 线性增长到 ssthresh",而是直接从 ssthresh 附近继续,避免速率骤降。

10 缺页中断

Q:什么是缺页中断?当进程访问一个没有对应物理内存的虚拟地址时,内核的完整处理流程是什么?

一、什么是缺页中断

缺页中断(Page Fault)是内存管理中的一种 CPU 异常:进程访问某个虚拟地址时,MMU 按多级页表进行翻译,发现该地址对应的页表项无效(页不在物理内存中),或权限校验不通过(如写只读页、用户态访问内核页),CPU 就会触发缺页异常并陷入内核态,由内核的缺页处理程序决定是"把页补上"还是"杀死进程"。它是按需分页、写时复制、swap 等虚拟内存机制得以工作的基础。

二、内核的完整处理流程

  1. 异常触发与现场保存:CPU 发现页表项无效后触发缺页异常,把触发异常的虚拟地址记入专用寄存器(x86 上为 CR2),并生成一个错误码标明原因(读/写/执行、用户态/内核态、是页不存在还是权限违例),随后陷入内核态并保存被中断指令的现场。
  2. 查找内存区域(VMA 校验):内核缺页处理程序(如 handle_mm_fault)在进程的 mm_struct 中查找该虚拟地址所属的虚拟内存区域 VMA(即通过 mmap/brk 等建立的合法区间)。
  3. 合法性判断:如果该地址不属于任何 VMA(野指针、越界访问),或访问类型违反 VMA 与页表项的权限(写只读区域、执行不可执行页、用户态访问内核地址),内核不会补页,而是直接向进程发送 SIGSEGV 信号,即常见的段错误。
  4. 按页的类型准备物理页:
    • 匿名页首次访问(按需分配):malloc 只预留虚拟区间,读未写入的页时映射到全局共享的零页;首次写入时才通过伙伴系统分配真实页框,这是次要缺页;
    • 写时复制缺页(COW):fork 后父子共享标记为只读的物理页,任一方写入时触发缺页,内核分配新页框、拷贝旧页内容并建立私有可写映射;
    • 文件映射页:先查页缓存 page cache,命中则直接建立映射(次要缺页);未命中则从磁盘文件(可执行文件、mmap 文件)中读入页数据(主要缺页,进程在此期间进入 D 状态等待 IO);
    • swap 换出页:该页此前被换出到交换区,内核分配页框并从 swap 读回(主要缺页);
    • 栈自动增长:访问地址恰好在栈 VMA 边界下方一个保护页范围内时,内核自动扩展栈映射而非报段错误。
  5. 建立页表映射:补齐多级页表中缺失的中间层页表,分配/填充最终页表项 PTE,写入物理页框号、读写执行权限及存在位。
  6. 恢复执行:刷新与该映射相关的 TLB,返回到用户态,重新执行当初触发缺页的那同一条指令(触发异常时程序计数器并未前进);此时翻译成功,指令正常完成,进程对整个过程无感知。

11 阻塞/非阻塞 IO 与同步/异步 IO

Q:阻塞 IO 和非阻塞 IO 的核心区别是什么?它们和同步 IO、异步 IO 是什么关系?

一、阻塞 IO 与非阻塞 IO 的区别

二者区分的是第一阶段——等待数据就绪期间,调用是否阻塞当前线程:

  • 阻塞 IO:线程发起 read/recvfrom 后,若内核数据尚未就绪,线程会被挂起(让出 CPU 进入睡眠),一直等到数据就绪、并且数据从内核缓冲区拷贝到用户缓冲区完成,调用才返回。期间线程不占 CPU 但无法做别的事。
  • 非阻塞 IO:fd 被设置为非阻塞后,若数据尚未就绪,调用立即返回错误码 EAGAIN/EWOULDBLOCK,线程不会被挂起;线程必须自己反复轮询调用,直到某次调用时数据已就绪。纯轮询会空转浪费 CPU,因此实践中非阻塞 IO 几乎总是配合 IO 多路复用使用,而不是盲目轮询。

二、同步 IO 与异步 IO 的区别

这组概念区分的是第二阶段——数据从内核缓冲区拷贝到用户缓冲区期间,调用者是否需要亲自参与(是否阻塞),这也是 POSIX 对同步/异步 IO 的定义:

  • 同步 IO:请求发出后,调用者需要亲自等待或轮询直到整个 IO 完成;即使第一阶段不阻塞(如非阻塞 IO、IO 多路复用),在数据就绪后真正执行 read 的拷贝阶段,调用仍然会阻塞当前线程,因此都属于同步 IO。
  • 异步 IO:调用者发起 IO 请求后立即返回,两个阶段都不参与:由内核负责等待数据就绪、再把数据拷贝到用户事先提供的缓冲区,全部完成后通过信号、回调或完成事件通知调用者;调用者在等待期间可以执行其他工作。

三、五种 IO 模型的归类

  1. 阻塞 IO:两个阶段都阻塞,同步 IO;
  2. 非阻塞 IO:第一阶段不阻塞但要轮询,第二阶段阻塞,同步 IO;
  3. IO 多路复用(select/poll/epoll):线程阻塞在多路复用调用上等待任一 fd 就绪,就绪后再调用 read 完成拷贝(拷贝阶段仍阻塞),同步 IO;它的价值是一个线程同时管理大量连接,而非异步;
  4. 信号驱动 IO(SIGIO):数据就绪时内核给进程发信号,进程在信号处理中执行 read,但拷贝阶段仍阻塞,仍属同步 IO;
  5. 异步 IO(POSIX AIO、Linux 的 io_uring/libaio):就绪等待与数据拷贝全由内核完成,完成后才通知,是唯一真正的异步 IO。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/bksczm/article/details/167390069

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--