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) |
|---|---|---|
| release | inet_release | inet_release |
| bind | inet_bind | inet_bind |
| connect | inet_stream_connect | inet_dgram_connect |
| accept | inet_accept | sock_no_accept ⛔ |
| listen | inet_listen | sock_no_listen ⛔ |
| sendmsg | inet_sendmsg | inet_sendmsg |
| recvmsg | inet_recvmsg | inet_recvmsg |
| poll | tcp_poll | udp_poll |
⛔ 关键点:注意 accept 和 listen。
UDP 是无连接的,所以它没有 listen 的概念,也没有 accept 的概念(直接 recvfrom 就行)。如果你试图在 UDP socket 上调 listen(),内核会直接返回 EOPNOTSUPP(操作不支持),因为对应的函数指针指向 sock_no_accept。
表 11-2:SCTP Chunk 类型一览
SCTP 包不叫「包」,叫 Chunk。这是它的字典。
| Chunk 类型 | Linux 内核符号 | 值 | 说明 |
|---|---|---|---|
| Payload Data | SCTP_CID_DATA | 0 | 真正的数据载荷。 |
| Initiation | SCTP_CID_INIT | 1 | 握手的第一步,建立关联。 |
| Initiation Ack | SCTP_CID_INIT_ACK | 2 | 握手第二步,确认连接。 |
| SACK | SCTP_CID_SACK | 3 | 选择性确认,告诉对方哪些包收到了。 |
| Heartbeat | SCTP_CID_HEARTBEAT | 4 | 心跳包,检测链路存活(多宿主必备)。 |
| Abort | SCTP_CID_ABORT | 6 | 立即终止关联,很暴力。 |
| Shutdown | SCTP_CID_SHUTDOWN | 7 | 优雅关闭。 |
| Cookie Echo | SCTP_CID_COOKIE_ECHO | 10 | 把状态 Cookie 发回服务器,防 SYN 泛洪。 |
| Cookie Ack | SCTP_CID_COOKIE_ACK | 11 | 收到 Cookie,握手完成。 |
| ASCONF | SCTP_CID_ASCONF | 0xC1 | 动态修改 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 socket 和 struct 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 socket 和 struct sock 构成:前者是面向用户空间的接口层,关联文件系统以便使用标准 I/O 操作;后者是面向网络协议栈的底层表示,负责管理发送/接收队列及协议状态。两者紧密协作,并在数据交换时利用 struct msghdr 作为容器,在内核与用户空间高效传输数据载荷与控制信息。
UDP 是理解内核网络栈数据流向的最佳切入点,其实现体现了极简与高效的平衡。在初始化阶段,UDP 协议通过 udp_protocol 和 udp_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_ops 和 proto 结构体),构建了一个高度可扩展的框架。这种架构允许 TCP、UDP 以及 SCTP 等不同协议复用相同的系统调用入口(如 sendmsg),同时又能根据各自特性实现差异化的底层逻辑(如 TCP 的复杂拥塞控制与 UDP 的简单校验)。理解这一抽象层级,是掌握 Linux 内核网络子系统如何平衡标准接口与内部复杂性的关键。