计算机操作系统 网络入门(小白专版 · 深入浅出)
计算机操作系统 & 网络入门(小白专版 · 深入浅出)
为什么要学习这些?先给个直观比喻
把电脑想象成一个邮局:
- CPU 是邮局的员工(处理事务的工人);
- 内存 是邮局里放信件的货架(可以快速拿到);
- 磁盘(外存) 是外仓库(拿信较慢,需要搬进来);
- 网络包 是信封,协议 是信封上写的规则(写谁、怎样投递);
- 中断 就像门铃或报警器,告诉邮局“有新任务/有紧急事儿”。
有了这个比喻,下面的内容都会穿插类比,帮助记忆。
一、中断(Interrupt)与中断处理流程(通俗版)
什么是中断?
- 当外部设备(键盘、网卡、磁盘)或 CPU 自己发生异常(例如缺页)时,硬件/软件会“打断”当前正在做的事,转而处理紧急事件,这就是中断。
中断的分类
- 硬件中断(Hardware IRQ):网卡、键盘、硬盘控制器触发。
- 软件中断 / 异常(Exception):除零、缺页(page fault)等由CPU触发。
- 系统调用(trap):用户程序主动请求内核服务(比如
read
、write
)。
中断处理的一般步骤
- 保存现场(save context):把寄存器、程序计数器等状态保存到内存(以便处理完能恢复执行)。
- 确定原因:读取中断号/异常码,确认是哪种中断。
- 运行中断服务程序(ISR / handler):完成最关键的处理(尽量短)。
- 恢复现场并返回:把之前保存的寄存器恢复,继续被中断的指令或下一个指令执行。
关键注意(对内核/驱动代码非常重要)
- 中断上下文不能睡眠/阻塞:不能调用会阻塞的函数(不能
sleep
、不能wait
信号量)——否则系统死锁。 - 中断处理要做最小工作量:把耗时任务交给“下半部”(bottom half / workqueue / tasklet / deferred work)。
- 不要在中断里做长时间计算或分配会睡的内存。
小例子(键盘中断)
- 硬件发来中断 → ISR 读取 scancode 放到环形缓冲(ring buffer) → ISR 将“已读”标记后退出 → 用户进程/上半部稍后从缓冲取数据处理。
二、虚拟内存与缺页中断(Page Fault)
为什么要虚拟内存?
- 程序看到的是连续、独立的地址空间(虚拟地址),操作系统负责把虚拟地址映射到物理内存的“页帧”(frame)。好处:进程隔离、内存共享、地址空间扩展(超过物理内存)等。
页(page)与页表(page table)
- 常见页大小:4KB(还会有大页如2MB等)。页表保存每个虚拟页与物理页帧的映射,以及状态位(存在/不在、读写权限等)。
什么是缺页中断(page fault)?
- 当程序访问的虚拟页在页表中标记为“不在内存”(或权限不足),CPU 会触发 缺页异常(特殊的中断/异常),内核负责处理:把所需页从外存(磁盘)读入内存,更新页表,然后重试那条指令。
缺页处理的步骤(简化版)
- CPU 触发缺页异常并进入内核。
- 内核保存现场并查页表(或反向页表)定位该虚拟页的磁盘地址。
- 如果内存已满,选择一个页面进行置换(见下一节)。
- 把目标页从磁盘读入空闲内存帧(I/O,可能很慢)。
- 更新页表、TLB(必要时刷新)。
- 恢复现场,重新执行发生缺页的指令(大多数情况下从这条指令重新开始)。
与一般中断的区别
- 缺页中断通常会导致相同行指令被重试(重新执行),而不是直接执行下一条指令(这是关键区别)。
类比:你在图书馆找一本书(虚拟页),书不在阅览室(内存)而在外仓(磁盘),你叫管理员帮你从外仓调书(缺页中断),把书放到阅览室后你继续看(恢复指令)。
三、页面置换算法(Page Replacement)
当内存没空帧时,内核要把某页换出(写回磁盘如果需要),常见算法:
-
FIFO(先进先出)
- 最先进入内存的页先被置换。实现简单,但可能置换掉当前刚被频繁访问的页(Belady异常)。
-
LRU(最近最少使用,Least Recently Used)
- 置换“最近最长时间未被访问”的页面。直觉好,但实现成本高(需要追踪访问时间或用链表)。
-
Clock(近似LRU)
- 给每页一个访问位(reference bit),扫描“钟表指针”,如果位为0就换出,为1则清0并跳过。高效且近似LRU。
-
Optimal(最佳算法)
- 置换未来最长时间不会被访问的页(理论上最优,但需要未来信息,不可实现,只作参考)。
举例(LRU vs FIFO)
- 页面访问序列:
A B C A B D A B C D
,物理帧数 = 3 - 通过逐步演示可以看到两种算法的缺页次数不同,LRU 通常比 FIFO 更少(但不是绝对)。
## 四、进程上下文 vs 中断上下文(重要)
进程上下文(Process Context)
- 正常用户进程运行时的内核环境,允许睡眠、阻塞、分配可能阻塞的内存、获取信号量等。
中断上下文(Interrupt Context)
- CPU 在 ISR 中执行的代码,不能睡、不能阻塞。中断处理程序必须很短。
常见约束(中断上下文必须遵守)
- 不能执行会导致阻塞/睡眠的 API(例如不能直接调用会等待的文件 I/O、不能 down 一个不可用的信号量)。
- 不能做长时间工作(应 defer 到下半部)。
- 不要请求大型内存分配(可能会睡)。
- 不要调用会引起页错误的函数(因为页错误在中断中处理会复杂化)。
如何把工作分两步做
- 上半部(Top-half, ISR):快速响应并收集必要数据(如把收到的数据复制到缓冲区),尽快返回中断。
- 下半部(Bottom-half):在进程上下文或软中断环境执行耗时操作(比如解析数据、磁盘写入等)。Linux 里有
tasklet
、workqueue
等机制。
五、上下文切换与寄存器(简明)
为何要保存寄存器?
- 切换线程/进程或进入中断时必须保存当前 CPU 寄存器(PC、SP、一般寄存器)等,以便返回时能继续之前的工作。
上下文切换步骤(概念)
- 保存当前进程的寄存器上下文(到进程描述符或内核栈)。
- 更新进程状态(就绪/等待等),挑选下一个运行的进程。
- 恢复下一个进程的寄存器上下文,跳转执行。
上下文切换会消耗时间(寄存器保存/恢复、TLB 可能失效),因此频繁切换会降低性能。
六、网络基础 — TCP vs UDP(小白理解版)
两者最核心的区别
- TCP(Transmission Control Protocol):面向连接,可靠、有序、拥塞控制(适合文件传输、HTTP、邮件)。
- UDP(User Datagram Protocol):无连接、不可靠、不保证顺序,延迟低、开销小(适合实时语音/视频、DNS、一些游戏应用)。
举个场景
- 发快递(要签收、按顺序到达)→ TCP
- 发明信片(不保证到达,也不保证先后)→ UDP
UDP 的 connect()
有啥特殊?
-
调用
connect()
在 UDP 上并不会建立三次握手。它只是把对端 IP/端口记下:- 之后
send()
无需每次指定地址,内核只会把数据发到该地址; - 内核只会把来自该对端的数据投递到此 socket(相当于“过滤”);
- 异步错误(如 ICMP 错误)会返回给该 socket。
- 之后
-
优点:简化应用逻辑,少些函数参数;缺点:socket 只能与该对端通信(单一对端)。
七、Socket 编程(一个最小的 TCP 服务端/客户端示例 — C++ 风格)
下面给出最简单的示例,解释每一步。注意:这是教学示例,实际生产代码需加错误检查、并发处理、超时处理等。
服务器(伪 C++ / BSD socket):
// 这是示例,不完整的错误处理仅为教学
int listen_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP socket
bind(listen_fd, (sockaddr*)&addr, sizeof(addr)); // 绑定 IP 和端口
listen(listen_fd, 5); // 开始监听
int client_fd = accept(listen_fd, nullptr, nullptr);// 接受连接(阻塞)
send(client_fd, "Hello\n", 6, 0); // 发送数据
close(client_fd); close(listen_fd);
客户端:
int fd = socket(AF_INET, SOCK_STREAM, 0);
connect(fd, (sockaddr*)&server_addr, sizeof(server_addr)); // 建立三次握手
read(fd, buf, sizeof(buf)); // 读取服务器发送的数据
close(fd);
每个函数的直观解释
socket()
:创建一个“邮筒”;bind()
:把邮筒放在某个门牌(IP:端口);listen()
:开始排队等候“信件”;accept()
:把一个排队的连接取出来,得到一个新的连接句柄;connect()
:客户端向服务器发起连接(触发三次握手);send()/recv()
/read()/write()
:收发数据。
八、TCP 的可靠性(序列号、确认、窗口、重传)
为什么 TCP 可靠?
- 序列号(Sequence Number):每字节都有序号,接收方通过序号把数据重组成顺序流。
- 确认(ACK):接收方回送已收到的序号(下一个期待序号)。
- 重传:若发送方在超时内未收到 ACK,会重传。
- 滑动窗口(Flow Control):接收方告诉发送方当前还能接收多少字节(窗口大小),避免接收方被淹没。
- 拥塞控制(Congestion Control):调节发送速率以避免网络拥塞(后面会讲)。
快速重传(Fast Retransmit)
- 如果接收端连续发送了 3 个相同的 ACK(重复 ACK),发送端通常会认为对应的某个数据段丢失,立即重传而不是等待超时。
## 九、TCP 三次握手(建立连接)与四次挥手(断开连接)
三次握手(为何需要三次?)
- 客户端发送
SYN
(随机 Seq=X)→ 表示“我想建立连接并给出我的初始序号”。 - 服务器回复
SYN+ACK
(Seq=Y, Ack=X+1)→ 表示“我同意,给出我的序号,同时确认你的SYN”。 - 客户端再发
ACK
(Ack=Y+1)→ 确认服务器的SYN,连接建立。
为什么不是两次?
- 三次握手既能保证双方都能发送也能接收,而且能避免“旧的延迟包”误触发新连接的问题(防止资源浪费与连接错乱)。
四次挥手(连接关闭)
- TCP 是全双工的:两端都可以独立关闭各自的发送方向。
- 关闭需要双方分别发送
FIN
和对方确认,因此通常需要 4 次报文(两次FIN
、两次ACK
)。 - 发起主动关闭的一方会进入
TIME_WAIT
(等待 2×MSL,见下)。
TIME_WAIT 与 2×MSL(为什么要等待)
- MSL = Maximum Segment Lifetime(报文在网络中存在的最大时间)。
TIME_WAIT
的目的:确保最后发送的 ACK 能被对端收到(若丢失,对端会重发 FIN),以及防止旧重复的报文(来自已关闭连接)被新连接误解。TIME_WAIT
持续2 × MSL
(实现中通常是系统定值,比如 30s~2min)。
类比:你把信封拿到邮局交给对方并希望对方确认收到了最后一封,你会在门口等一会儿(TIME_WAIT),以便如果对方没收到你能再次确认,或者等过期时间让所有旧邮件从系统中消失。
十、TCP 拥塞控制(慢启动 / 拥塞避免 / 快重传 / 快恢复)
目标:在不精确知道网络容量的前提下,尽量提高吞吐量但不导致网络崩溃。
核心参数
cwnd
(拥塞窗口):发送端为避免拥塞自行控制的窗口大小(以字节为单位)。ssthresh
(慢启动阈值):当cwnd
超过它后,从指数增长变为线性增长(拥塞避免)。
算法流程(经典版本)
-
慢启动(Slow Start):连接开始时
cwnd = 1
(或 MSS),每收到一个 ACKcwnd
翻倍(指数增长),直到cwnd >= ssthresh
或发生丢包。 -
拥塞避免(Congestion Avoidance):
cwnd
以线性方式增长(每 RTT 增加 1 MSS),避免快速触发拥塞。 -
发生丢包(超时):将
ssthresh = cwnd/2
,然后把cwnd
设为 1(回到慢启动)。 -
快速重传 + 快速恢复(当收到 3 个重复 ACK):
- 触发快速重传(立即重传丢失段),将
ssthresh = cwnd/2
,cwnd = ssthresh + 3*MSS
(进入快速恢复以避免退回慢启动太多),随后进入拥塞避免阶段。
- 触发快速重传(立即重传丢失段),将
小数字示例
- 初始:
cwnd = 1
- 一个 RTT 后:
2
→ 再后4
→8
(指数)直到ssthresh
(假设为 16),到 16 后:17, 18, ...
(线性)
十一、面试/练习题(建议亲自动手)
- 手算题:给出页面访问序列和帧数,模拟 FIFO、LRU,比较缺页数。
- 动手题:写一个简单的 TCP 服务端和客户端(上文示例),跑一下
telnet
或nc
测试连接。 - 观察题:用
tcpdump
/wireshark
抓包(或 wireshark),观察 TCP 三次握手与四次挥手的报文序列。 - 思考题:为什么中断处理不能睡眠?列举两种把耗时处理下放的方法(Linux 中的实现)。
十二、小结(快速回顾)
- 中断:是硬件/异常通知处理机制,处理要快、不能阻塞。
- 缺页:是虚拟内存的重要机制,缺页会触发内核来把页调入内存并重试指令。
- 页面置换:常见有 FIFO、LRU、Clock,选择折中实现效率与开销。
- 上下文切换:要保存寄存器与状态,有成本。
- TCP/UDP:TCP可靠重传、顺序、拥塞控制;UDP轻量、低延迟适合实时应用。
- TCP 建立/关闭:三次握手保证双方能收发并避免旧包;四次挥手与 TIME_WAIT 保护连接正确关闭。
- 拥塞控制:慢启动、拥塞避免、快速重传/恢复让 TCP 在动态网络中稳定运行。