跳到主要内容

11.7 快速参考手册

代码读完了,协议看过了,现在让我们把散落在各处的零件拼回一张图纸。

这一节本质上是一张「作弊条」——当你下次自己在内核里游荡,试图搞清楚一个数据包到底是怎么被从网卡拽进 socket 缓冲区的时候,这张单子能帮你迅速定位到是哪个函数在干活。

我们把这些方法按协议归类,把最重要的几个宏和表格摊开来看。


核心方法速查

这里列出的不是所有东西,而是那些真正在驱动着协议运转的「轴心」。

通用 Socket 操作

这些是处理 socket 本身生命周期和通用属性的方法,不管是 TCP、UDP 还是 DCCP,都得经过这几关。

  • int sock_create(int family, int type, int protocol, struct socket **res); 这是用户态 socket() 系统调用在内核的投射。它做两件事:一是 sanity check(检查家族、类型是否合法),二是调用 sock_alloc() 分配一个 struct socket,紧接着调用协议家族的 create 方法。如果是 IPv4,那就是 inet_create()。没有这一步,后面的一切都无从谈起。

  • int sock_map_fd(struct socket *sock, int flags); Socket 结构有了,得把它变成一个文件描述符返回给用户。这个方法负责分配 fd,并填充 file entry。它是「一切皆文件」哲学的胶水层。

  • void sock_hold(struct sock *sk); / void sock_put(struct sock *sk); 内核对象的生命周期全靠引用计数。 sock_hold 是「钉住它」,计数加一,防止在使用过程中被释放; sock_put 是「松手」,计数减一,减到零的时候就触发释放。这是并发环境下的安全锁,少一次 put 会泄漏,少一次 hold 会 panic。

  • bool sock_flag(const struct sock *sk, enum sock_flags flag); struct sock 里有一堆标志位(比如 SOCK_DEAD 表示连接已死)。这个函数就是用来查某个位是不是被置位了。虽然简单,但它是判断连接状态最直接的方式。

IP 层辅助

  • int ip_cmsg_send(struct net *net, struct msghdr *msg, struct ipcm_cookie *ipc); 这是用户态通过辅助数据传递「非普通数据」的解析器。 当你用 sendmsg 并且附带了一些控制信息(比如指定源 IP 或者接口)时,内核会把这个 msghdr 解析成一个 ipcm_cookie 结构。后者才是 IP 层真正看得懂的「指令集」。

TCP (Transmission Control Protocol)

TCP 是最复杂的那块,函数签名也最长。

  • struct tcp_sock *tcp_sk(const struct sock *sk); 前面说过,内核有一套严格的继承体系。struct sock 是通用基类,struct tcp_sock 是 TCP 专用的派生类。这个宏通过 container_of 机制,从一个通用的 sock 指针拿到内嵌的 tcp_sock 指针。拿到它,你就能访问所有 TCP 的私有参数(比如拥塞窗口、拥塞状态等)。

  • void tcp_init_sock(struct sock *sk); 当 socket 刚被创建时,需要初始化。TCP 有很多自己的私有变量(比如延迟 ACK 定时器、保活定时器),这个方法负责把它们设置成初始值。

  • struct tcphdr *tcp_hdr(const struct sk_buff *skb); 一个 sk_buff 里有数据,有 IP 头,有 TCP 头。这个函数帮你跳过前面的网络层头,直接指向 TCP 头的起始位置。别自己去算偏移量,用这个宏。

  • int tcp_v4_rcv(struct sk_buff *skb); 这是 IPv4 上 TCP 包的总入口。网络层(L3)收到一个 TCP 包(IP 头协议号是 6)后,最终会交到这个函数手里。它会处理校验和、查 socket 哈希表、把包丢进接收队列或者状态机处理。

  • int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t size); 用户态的 send()write() 最终会走到这里。它负责把用户态的数据切成段,管理发送队列,触发重传定时器,并在必要时把包推送到 IP 层。

UDP (User Datagram Protocol)

UDP 相对清爽很多。

  • struct udphdr *udp_hdr(const struct sk_buff *skb);tcp_hdr 一样,指向 UDP 头。

  • int udp_rcv(struct sk_buff *skb); UDP 的主接收入口。虽然逻辑比 TCP 简单(没有连接状态),但它依然要做校验和检查,并且在 socket 的哈希表里找到对应的接收者,把 skb 扔进 sk_receive_queue

  • int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len); UDP 的发送入口。这里处理 UDP_CORK(软木塞)选项,如果设置了,数据会先堆积起来;否则直接构造 UDP 头并丢给 IP 层。当然,这里也会处理我们前面提到的 MSG_MORE 标志。

SCTP (Stream Control Transmission Protocol)

SCTP 的对象模型更复杂,有 Association,有 Endpoint。

  • struct sctp_sock *sctp_sk(const struct sock *sk);tcp_sk 一样,从通用 sock 获取 SCTP 专用 sctp_sock 结构。

  • struct sctp_association *sctp_association_new(...); void sctp_association_free(struct sctp_association *asoc); SCTP 的核心是「关联」。这两个函数负责创建和销毁一个关联。sctp_association_new 会把状态机初始化好,把传输控制块(TCB)准备好。

  • void sctp_chunk_hold(struct sctp_chunk *ch); / void sctp_chunk_put(struct sctp_chunk *ch); SCTP 里数据包的基本单位叫 Chunk。这两个函数管理 Chunk 的引用计数。put 到 0 的时候,会调用 sctp_chunk_destroy() 真正释放内存。

  • struct sctphdr *sctp_hdr(const struct sk_buff *skb); 定位 SCTP 头部。

  • int sctp_rcv(struct sk_buff *skb); SCTP 的接收总管。它不仅处理数据,还要处理各种控制 Chunk(比如 INIT、SHUTDOWN)。因为它支持多宿主,这里的查找逻辑比 TCP 要麻烦得多。

  • int sctp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t msg_len); SCTP 的发送接口。它得决定发到哪个目的地址(多宿主选择),把数据切分成 DATA Chunk,然后丢给 IP 层。

DCCP (Datagram Congestion Control Protocol)

作为后来者,DCCP 的接口命名习惯和 TCP 非常像。

  • static int dccp_v4_rcv(struct sk_buff *skb); DCCP 在 IPv4 上的接收函数。虽然它是数据报协议,但引入了类似 TCP 的握手和状态机,所以这个函数里也有状态机处理逻辑。

  • int dccp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len); DCCP 的发送入口。它既要像 UDP 那样不保证可靠,又要像 TCP 那样管理拥塞控制(CCID)。这里会根据 CCID 的特性来决定怎么发包。


核心宏

宏不多,但有一个是 SCTP 的标志:

  • sctp_chunk_is_data(chunk) SCTP 是控制消息和数据消息混传的协议。这个宏一眼就能看出当前拿到的是不是数据块。如果是 1,就是 Payload Data(SCTP_CID_DATA);否则就是 Init、Ack 之类的控制块。

关键数据表

这些表格是协议的「词汇表」。

表 11-1:TCP 与 UDP 的 proto_ops 对比

这个表展示了面向用户(POSIX 接口)的那层操作结构 proto_ops。注意看 TCP 和 UDP 的差异。

prot_ops 回调TCP 的实现 (inet_stream_ops)UDP 的实现 (inet_dgram_ops)
releaseinet_releaseinet_release
bindinet_bindinet_bind
connectinet_stream_connectinet_dgram_connect
acceptinet_acceptsock_no_accept
listeninet_listensock_no_listen
sendmsginet_sendmsginet_sendmsg
recvmsginet_recvmsginet_recvmsg
polltcp_polludp_poll

关键点:注意 acceptlisten。 UDP 是无连接的,所以它没有 listen 的概念,也没有 accept 的概念(直接 recvfrom 就行)。如果你试图在 UDP socket 上调 listen(),内核会直接返回 EOPNOTSUPP(操作不支持),因为对应的函数指针指向 sock_no_accept

表 11-2:SCTP Chunk 类型一览

SCTP 包不叫「包」,叫 Chunk。这是它的字典。

Chunk 类型Linux 内核符号说明
Payload DataSCTP_CID_DATA0真正的数据载荷。
InitiationSCTP_CID_INIT1握手的第一步,建立关联。
Initiation AckSCTP_CID_INIT_ACK2握手第二步,确认连接。
SACKSCTP_CID_SACK3选择性确认,告诉对方哪些包收到了。
HeartbeatSCTP_CID_HEARTBEAT4心跳包,检测链路存活(多宿主必备)。
AbortSCTP_CID_ABORT6立即终止关联,很暴力。
ShutdownSCTP_CID_SHUTDOWN7优雅关闭。
Cookie EchoSCTP_CID_COOKIE_ECHO10把状态 Cookie 发回服务器,防 SYN 泛洪。
Cookie AckSCTP_CID_COOKIE_ACK11收到 Cookie,握手完成。
ASCONFSCTP_CID_ASCONF0xC1动态修改 IP 地址(多宿主特性)。

注意:Chunk ID 并不是完全连续的,中间有一些预留,后面扩展的(比如 AUTH、FWD_TSN)数值跳得比较大。

表 11-3:DCCP 包类型

DCCP 的包类型定义了它是谁,在干什么。

Linux 符号描述
DCCP_PKT_REQUEST客户端发出的请求包(类似 TCP SYN)。
DCCP_PKT_RESPONSE服务器的响应(类似 TCP SYN/ACK)。
DCCP_PKT_DATA纯数据包。
DCCP_PKT_DATAACK最常见。捎带 ACK 的数据包,DCCP 鼓励用这个。
DCCP_PKT_ACK纯 ACK,确认收到。
DCCP_PKT_CLOSEREQ服务器请求关闭(类似 TCP 的 FIN?不完全是)。
DCCP_PKT_CLOSE关闭连接。
DCCP_PKT_RESET重置连接(异常终止或拒绝)。
DCCP_PKT_SYNC大量丢包后的序列号同步。
DCCP_PKT_SYNCACK对 SYNC 的确认。

你会发现 DCCP 的名字里带着「Control」,它确实是在尽力维持一个像样的连接状态,哪怕它不保证数据送达。


本章回响

回顾这一章,我们从最熟悉的 Socket API 一路拆到了协议栈的底层。

其实,内核网络子系统的设计并不神秘,它本质上是在处理**「差异」**。

  • Socket 层(struct socket)处理的是**「文件的差异」**——把网络包伪装成文件读写。
  • Transport 层(struct sock,TCP/UDP)处理的是**「传输模型的差异」**——有的要像水一样流(TCP),有的要像信封一样扔(UDP),有的想兼得(SCTP/DCCP)。

还记得我们在 DCCP 那一节遇到的那个困境吗? 为什么设计上更先进的 DCCP 在现实世界几乎无人问津,而老旧的 UDP 却无处不在? 答案不在代码里,而在生态里。 NAT 设备认不出 DCCP,运营商不支持 DCCP,导致 DCCP 被困在了内网的孤岛上。这再次证明了一个工程真理:最好的协议往往不是理论上最完美的那个,而是最能穿透现有基础设施的那个。 这就是 UDP 至今依然不可替代的原因,也是 QUIC 最终选择基于 UDP 实现的根本原因。

下一章,我们将跨越边界,离开主机本身,去看看邻居之间是怎么打招呼的——那是 ARP 和 ND 的地盘,是**Layer 2(链路层)**的世界。


练习题

练习 1:understanding

题目:在 Linux 内核实现中,struct socketstruct sock 是两个核心数据结构。请判断以下说法是否正确:struct socket 主要负责与用户空间交互(提供文件接口),而 struct sock 主要负责与网络层(L3)交互。请简述两者的主要区别。

答案与解析

答案:正确。

解析:根据章节中“Creating Sockets”部分的描述,struct socket 提供了与用户空间的接口(包含文件指针 file 和操作回调 ops),而 struct sock 代表网络层(L3)的套接字接口,包含队列、缓冲区大小和协议相关的回调(如 sk_receive_queue)。内核通过这种分离设计,将文件系统视图与网络协议栈视图解耦。

练习 2:application

题目:在使用 UDP 协议发送数据时,如果想将多次 send 调用的数据累积并在内核中组装成一个大包发送(以减少分片),可以采用哪两种机制?请结合内核代码实现(如 udp_sendmsg)简述其原理。

答案与解析

答案:使用 UDP_CORK socket 选项或在 sendmsg 调用中设置 MSG_MORE 标志位。

解析:根据“Sending Packets with UDP”章节,内核代码检测 corkreq = up->corkflag || msg->msg_flags&MSG_MORE。当 corkreq 为真时,内核会持有套接锁并调用 ip_append_data 将数据缓存到 sk_write_queue 中,直到取消该选项或发送不带 MSG_MORE 的数据,最后通过 udp_push_pending_frames 统一发送。这允许应用层将多个逻辑数据块合并为一个物理数据包。

练习 3:thinking

题目:为什么 SCTP(Stream Control Transmission Protocol)采用四次握手(Four-way handshake)而不是像 TCP 那样采用三次握手?请从安全性角度分析其设计意图。

答案与解析

答案:为了防止 SYN 泛洪攻击。

解析:虽然章节内容主要集中在 SCTP 的实现细节(如 Chunk 和 Association),但根据文中提到的“Setting Up an SCTP Association”部分,SCTP 使用了 State Cookie 机制。在四次握手中,服务器不分配资源而是发送一个 COOKIE(INIT-ACK),客户端必须回显这个 COOKIE(COOKIE-ECHO)。只有验证通过后,服务器才建立关联。这种机制使得攻击者无法通过伪造大量 INIT 请求耗尽服务器资源,从而比 TCP 的三次握手更具抗毁性。

练习 4:understanding

题目:当内核通过 UDP 协议接收数据包时(__udp4_lib_rcv),如果通过哈希表没有找到匹配的套接字(即目标端口没有监听程序),且数据包校验和正确,内核会发送什么类型的 ICMP 消息给发送方?

答案与解析

答案:ICMP Destination Unreachable(目标不可达,具体代码通常为 Port Unreachable)。

解析:根据“Receiving Packets from the Network Layer (L3) with UDP”部分的描述,当 socket 查找失败(sk == NULL)时,意味着本地没有应用在监听该端口。如果校验和正确,内核应该发送 ICMP 目标不可达消息通知发送方,表明该端口不可达。

练习 5:application

题目:DCCP(Datagram Congestion Control Protocol)结合了 TCP 和 UDP 的特性。假设你需要开发一个实时流媒体应用,既要求低延迟(允许丢包)又要求避免网络拥塞,你会选择 DCCP 还是 UDP?请结合 DCCP 的特性(如 CCID)解释原因。

答案与解析

答案:应选择 DCCP。

解析:根据“DCCP”章节的描述,DCCP 提供了拥塞控制机制,这是 UDP 所缺乏的。对于流媒体应用,虽然可以容忍一定的丢包(UDP 的特性),但如果使用纯 UDP 可能会导致网络拥塞崩溃。DCCP 通过内置的拥塞控制算法(如 CCID-3 类似 TCP 的平滑算法)来控制发送速率,既能保持低延迟(不保证可靠性,无需重传延迟),又能保证网络 fairness,是此类场景的最佳选择。


要点提炼

Linux 内核通过标准 POSIX socket API 暴露网络功能,其核心设计遵循“一切皆文件”的哲学。尽管用户空间接口统一,内核内部却维护着一套复杂的对象模型。这种双重结构主要由 struct socketstruct sock 构成:前者是面向用户空间的接口层,关联文件系统以便使用标准 I/O 操作;后者是面向网络协议栈的底层表示,负责管理发送/接收队列及协议状态。两者紧密协作,并在数据交换时利用 struct msghdr 作为容器,在内核与用户空间高效传输数据载荷与控制信息。

UDP 是理解内核网络栈数据流向的最佳切入点,其实现体现了极简与高效的平衡。在初始化阶段,UDP 协议通过 udp_protocoludp_prot 将处理函数注册到内核的协议表中。发送数据时,内核依据是否开启 UDP_CORK 选项区分快慢路径:快路径直接构造 sk_buff 发送,而慢路径则通过 ip_append_data 将数据积攒到写队列中,待合并后统一发送,这种设计在保证灵活性的同时优化了小包场景的网络性能。

TCP 的内核实现建立在复杂的状态机与严格的定时器机制之上,以确保传输的可靠性。当创建 Socket 时,tcp_v4_init_sock 会初始化包括重传、延迟 ACK、保活和零窗口探测在内的四种核心定时器,并设置缓冲区与初始拥塞窗口。在三次握手过程中,内核通过监听 Socket 管理半连接,为每个新连接请求生成 request_sock,仅在完成握手后才将其转换为完整的子 Socket 并放入接收队列,这种设计有效地将连接建立逻辑与数据传输路径解耦。

TCP 的数据收发流程包含针对性能与并发性的深度优化。在接收路径上,内核根据 Socket 是否被用户进程占用决定处理策略:若未被锁定,尝试利用 prequeue 队列批量处理以减少上下文切换;若已被锁定,则将数据包暂存于 backlog 队列,避免丢包。在发送路径上,tcp_sendmsg 不仅负责拷贝数据,还需处理 Nagle 算法、MSS(最大分段大小)计算等逻辑,最终通过 tcp_transmit_skb 将封装好的数据包移交至 IP 层,体现了协议栈对可靠性与流控制度的极致追求。

从 UDP 的“即发即忘”到 TCP 的“严丝合缝”,Linux 内核通过分离通用 Socket 层与特定协议实现(如 proto_opsproto 结构体),构建了一个高度可扩展的框架。这种架构允许 TCP、UDP 以及 SCTP 等不同协议复用相同的系统调用入口(如 sendmsg),同时又能根据各自特性实现差异化的底层逻辑(如 TCP 的复杂拥塞控制与 UDP 的简单校验)。理解这一抽象层级,是掌握 Linux 内核网络子系统如何平衡标准接口与内部复杂性的关键。