TCP/IP详解 卷2:实现

书籍概述

本书是W. Richard Stevens的经典之作,深入剖析了4.4BSD-Lite(简称Net/3)操作系统中TCP/IP协议栈的完整实现。全书包含约15,000行源代码,是理解网络协议内部工作原理的权威参考。

与卷1侧重协议规范不同,卷2从实现者视角出发,展示了协议代码如何组织、数据如何流动、系统调用如何到达内核。

核心架构:三层网络代码组织

Net/3内核联网代码分为三层(对应OSI模型):

  1. 插口层(Socket Layer) — 协议无关接口

    • 所有系统调用(socket、bind、connect、sendto等)的入口
    • 验证参数后调用协议相关代码
    • 对应OSI第5层(会话层)
  2. 协议层(Protocol Layer) — 四种协议族

    • TCP/IP(互联网协议族,本书重点)
    • XNS(Xerox网络系统,已淘汰)
    • OSI协议
    • Unix域协议(本地IPC)
    • 对应OSI第3-4层(网络/运输层)
  3. 接口层(Interface Layer) — 设备驱动

    • 以太网、SLIP、PPP、环回等
    • 对应OSI第1-2层(物理/数据链路层)

关键数据结构

描述符与socket结构链

进程调用socket()后,内核建立以下数据结构链:

进程表项 proc{}
  → p_fd → filedesc{}
    → fd_ofiles[] → file{}(DTYPE_SOCKET类型)
      → f_data → socket{}
        → so_pcb → inpcb{}(Internet协议控制块)
  • inpcb 包含本地/远端IP地址(inp_laddr/inp_faddr)和端口号(inp_lport/inp_fport)
  • 所有UDP PCB通过 inp_next/inp_prev 组成双向循环链表,表头为全局变量 udb
  • 数据到达时通过端口号(可能还需IP地址)反向查找对应的socket

mbuf:存储器缓存

mbuf是BSD网络代码的基本存储单元,所有网络数据都通过mbuf链传递。

  • 标准mbuf大小:128字节(20字节首部 + 108字节数据空间)
  • 带分组首部的mbuf:首部额外占用8字节,数据空间100字节
  • mbuf通过 m_next 指针链接成链表
  • 大数据使用簇(cluster):1024或2048字节的外部缓存,通过引用计数共享
  • m_flags:M_PKTHDR(分组首部)、M_EXT(外部簇)
  • m_type:MT_SONAME(插口地址)、MT_DATA(数据)等

数据发送时mbuf链的演变(UDP 150字节数据示例):

  1. 目标地址存入独立mbuf(MT_SONAME,16字节sockaddr_in)
  2. 用户数据分两个mbuf存储(100+50字节)
  3. 新增mbuf放IP+UDP首部(28字节),首部前移留出72字节
  4. 以太网层再添加14字节以太网帧首部

UDP数据报的完整旅程

输出路径(进程→网络)

进程 sendto()
  → 插口层:复制地址和数据到mbuf链
    → UDP输出:添加UDP首部(8字节),计算检验和
      → IP输出:填写IP首部(20字节),选路,必要时分片
        → 以太网输出:ARP解析MAC地址,添加14字节帧首部
          → 设备驱动:放入输出队列,复制到传输缓存,发送

关键细节:

  • 150字节数据在内核中被遍历3次(复制到mbuf、UDP检验和、复制到设备缓存)
  • IP检验和由IP层计算,UDP检验和是可选的(IPv4)
  • 数据到达驱动前不排队,高层只添加首部后立即传递

输入路径(网络→进程)

以太网硬件中断
  → 驱动程序:数据读入mbuf,提交通用以太网输入例程
    → 识别帧类型为IP,加入IP输入队列,触发软中断
      → IP输入(异步软中断):验证检验和、处理选项、转发或上交
        → UDP输入:验证UDP首部,遍历udb链表匹配PCB
          → 数据加入socket接收队列,唤醒进程
            → recvfrom():数据从mbuf复制到用户缓存

关键区别:输入是异步的(硬件中断驱动),输出是同步的(系统调用驱动)。

TCP核心机制

连接状态机

TCP通过11种状态管理连接生命周期,关键转换:

  • 三次握手:SYN_SENT → ESTABLISHED(客户端);LISTEN → SYN_RCVD → ESTABLISHED(服务端)
  • 连接关闭:FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT(2MSL等待)
  • RST用于异常终止和拒绝无效报文段

定时器体系

Net/3为每条TCP连接维护多个定时器:

  • 重传定时器:未收到ACK时重发数据
  • 持续定时器:窗口大小为0时定期探测
  • 保活定时器:检测空闲连接是否存活
  • 2MSL定时器:TIME_WAIT状态等待
  • 延迟ACK定时器:累积确认减少ACK流量

流量控制与拥塞控制

  • 滑动窗口:接收方通过通告窗口(rcv_wnd)控制发送速率
  • 慢启动:连接开始时拥塞窗口(cwnd)指数增长
  • 拥塞避免:到达慢启动门限(ssthresh)后线性增长
  • 快速重传:收到3个重复ACK立即重传,不等待超时
  • 快速恢复:重传后不进入慢启动,而是减半窗口继续

时间戳选项(RFC 1323)

  • 用于精确测量RTT(往返时间),每个报文段可获得独立样本
  • 时间戳时钟频率建议:1b/ms ~ 1b/s(Net/3采用2b/s)
  • 频率过高(1b/ms):32位时间戳24.8天回绕
  • 频率过低(500ms/bit):34年回绕
  • PAWS(防止序号回绕):利用时间戳丢弃重复的旧报文段

IP层关键实现

路由表查找

采用**基数树(radix tree)**结构,支持变长掩码:

  • 主机路由(掩码全1)优先于网络路由
  • 默认路由作为最后手段
  • PCB中缓存路由指针(ro_rt),避免每次发送都查询路由表
  • ICMP重定向可动态创建(RTF_DYNAMIC)或修改(RTF_MODIFIED)路由

IP分片与重装

  • 输出时:超过出接口MTU的数据报被分片,每片独立路由
  • 输入时:根据(源IP,目的IP,协议,ID)四元组组装
  • 只有偏移量为0的第一个分片携带完整IP选项
  • 重装超时后丢弃所有分片,只对第一个分片发送ICMP超时报文

广播与多播

  • 有限广播地址:255.255.255.255(不经过路由器转发)
  • 以太网多播映射:IP多播组224.0.0.1 → MAC 01:00:5e:00:00:01
  • IGMP管理组成员关系,报告延迟随机化以减少报文数量
  • 多播路由通过mrouted守护进程和隧道接口实现

ARP实现

  • 以太网输出时通过ARP将32位IP地址解析为48位MAC地址
  • ARP缓存条目正常超时20分钟
  • 不完整条目(已发送请求未收到应答)0~5分钟超时
  • 收到ARP请求时自动学习发送方的IP-MAC映射
  • 环回接口不需要ARP

ICMP协议要点

  • ICMP差错报文不对广播/多播数据报生成(避免广播风暴)
  • 不对ICMP差错报文再生成差错(防止无限循环)
  • 只对分片数据报的第一个分片生成差错
  • ICMP重定向只由路由器生成,主机据此更新路由表
  • 回显请求/应答(ping)是最常用的诊断工具

原始IP与BPF

原始IP插口(Raw Socket)

三种用途:

  1. 收发ICMP/IGMP报文(如ping)
  2. IP_HDRINCL选项自行构造IP首部
  3. 支持内核不处理的其他IP协议

接收过滤条件(可组合):协议字段、源IP(connect指定)、目的IP(bind指定)。

BPF(BSD分组过滤器)

  • 工作在数据链路层,比原始IP更底层
  • 可接收指定接口的所有数据帧(包括非IP协议)
  • 支持混杂模式,可抓取发往其他主机的数据帧
  • 通过内核态过滤器减少数据复制
  • 输出需自行构造完整数据链路层首部,不经过IP选路

插口层编程要点

关键系统调用

  • socket():创建插口,分配file{}、socket{}、inpcb{}
  • bind():绑定本地IP和端口(服务器通常绑定通配地址+熟知端口)
  • connect():TCP建立连接;UDP仅记录目的地址(可调用两次以”断开”)
  • listen():标记插口为被动接收,设置连接队列长度
  • accept():从完成队列取出连接,返回新的描述符
  • sendto()/recvfrom():无连接通信(UDP典型)
  • send()/recv():面向连接通信(TCP典型)

插口缓冲区

  • 每个socket有发送和接收两个缓冲区
  • 高水位标记(sb_hiwat)限制总字节数
  • 缓冲区满时进程阻塞(默认)或返回错误(非阻塞模式)
  • select()可同时监控多个描述符的可读性/可写性/异常

性能与设计启示

  1. mbuf设计权衡:小内存块减少碎片但增加遍历开销,簇提高大块数据效率但需引用计数管理
  2. 异步输入处理:硬件中断仅做最少工作,协议处理延迟到软中断,避免阻塞其他中断
  3. 路由缓存:PCB中保存路由指针避免重复查找,但路由变更时需失效处理
  4. 首部预留:mbuf在数据前后预留空间,各层添加首部时无需复制已有数据
  5. 检验和开销:UDP检验和需要遍历全部数据,是主要性能瓶颈之一
  6. 中断优先级:splimp(网络硬件中断)> splnet(网络软中断)> 进程级,保护共享数据结构

与卷1的关联阅读

本书应配合TCP-IP详解卷1-协议阅读:

  • 卷1说明协议”应该怎样”(规范、报文格式、状态机)
  • 卷2说明代码”实际怎样”(数据结构、函数调用、边界处理、实现bug)
  • 卷2中反复指出Net/3代码与RFC规范的偏差及实际兼容性考量

延伸阅读