3. 套接字层架构
套接字层,说白了就是用户态和内核协议栈之间的「接待前台」。你写网络程序时调用的 socket()、bind()、connect() 这些 API,最终都会落到这一层来处理。我刚开始啃内核源码时,总觉得这层就是个简单的分发器,后来深入一看才发现——里面的设计相当精巧。
3.1 socket系统调用的完整流程
我们先从最常用的 socket() 系统调用说起。你在应用层写这么一行:
int fd = socket(AF_INET, SOCK_STREAM, 0);
这行代码在内核里会经历什么?我把它拆成几个关键步骤:
- 用户态陷入内核:glibc 封装触发
sys_socketcall()或直接调用__sys_socket()。 - 分配 struct socket:内核调用
sock_create(),在 VFS 层创建一个 socket 对象。 - 协议族匹配:根据
AF_INET找到对应的inet_family_ops,调用其create方法。 - 创建 struct sock:在
inet_create()内部,根据SOCK_STREAM查表找到tcp_prot,分配并初始化 struct sock。 - 绑定文件描述符:将 struct socket 挂到当前进程的文件描述符表上,返回 fd。
核心要点:socket() 系统调用返回的不是 socket 对象本身,而是一个文件描述符。后续所有操作都通过这个 fd 来间接访问内核中的 socket 结构体。
这里有个细节我当年踩过坑——socket() 调用并不会触发三次握手。它只是把数据结构准备好,真正的连接建立要等到 connect() 或 listen()/accept() 组合。嗯,这个区分很重要,很多人一开始容易搞混。
3.2 struct socket 与 struct sock:一对「表兄弟」
这两个结构体是套接字层的核心,但它们的职责完全不同。我习惯这么理解:
- struct socket:面向 VFS 层的「门面」。它负责跟文件系统打交道,管理
file_operations。 - struct sock:面向协议栈的「内核」。它负责真正的网络通信,管理连接状态、接收/发送缓冲区、拥塞控制等。
它们之间的关系可以用一句话概括:一个 socket 包含一个 sock,一个 sock 被一个 socket 引用。具体看代码:
// include/linux/net.h
struct socket {
socket_state state; // 连接状态
short type; // SOCK_STREAM / SOCK_DGRAM
unsigned long flags;
struct file *file; // 关联的 VFS file 对象
struct sock *sk; // 指向真正的协议层 sock
const struct proto_ops *ops; // 协议族操作函数表
};
// include/net/sock.h
struct sock {
struct sock_common __sk_common;
struct sk_buff_head sk_receive_queue; // 接收队列
struct sk_buff_head sk_write_queue; // 发送队列
int sk_rcvbuf; // 接收缓冲区大小
int sk_sndbuf; // 发送缓冲区大小
const struct proto_ops *sk_ops; // 协议操作
void *sk_protinfo; // 协议私有数据
// ... 还有几十个字段
};
我的经验:调试网络问题时,我经常用 ss -i 或 cat /proc/net/tcp 来查看 struct sock 的详细信息。比如 sk_rcvbuf 和 sk_sndbuf 的值,能直接反映当前 socket 的缓冲区水位,对排查性能瓶颈很有帮助。
为什么内核要设计两个结构体而不是合二为一?我个人觉得有两点考虑:
- 解耦 VFS 和协议栈:文件系统不需要知道 TCP 的滑动窗口怎么实现,协议栈也不需要关心文件描述符怎么分配。
- 支持多种协议族:同一个 struct socket 可以指向不同类型的 struct sock(inet、unix、netlink 等),通过
ops虚函数表实现多态。
我曾经在项目中遇到过一个诡异的问题:某个 socket 明明已经 close 了,但 ss 看到它还在 CLOSE_WAIT 状态。后来发现是 struct sock 的引用计数没归零,导致内核没有真正释放它。嗯,这种问题排查起来确实头疼。
3.3 协议族注册与管理
Linux 支持几十种协议族(AF_INET、AF_UNIX、AF_NETLINK 等),内核怎么管理它们?答案是通过一个全局的 net_families 数组。
// net/socket.c
#define AF_MAX 43 // 最大协议族数量
static const struct net_proto_family *net_families[AF_MAX] __read_mostly;
每个协议族在初始化时调用 sock_register() 把自己注册进去:
// net/socket.c
int sock_register(const struct net_proto_family *ops)
{
int err;
if (ops->family >= AF_MAX) {
pr_err("protocol family %d not supported\n", ops->family);
return -EINVAL;
}
// 检查是否重复注册
if (net_families[ops->family])
return -EEXIST;
// 写入全局数组
net_families[ops->family] = ops;
return 0;
}
以 AF_INET 为例,它的注册发生在 inet_init() 中:
// net/ipv4/af_inet.c
static int __init inet_init(void)
{
struct net_proto_family *inet_family_ops = &inet_family_ops;
sock_register(inet_family_ops);
// ... 注册传输层协议(TCP、UDP)
// ... 注册网络层(IP)
}
当用户调用 socket(AF_INET, ...) 时,内核通过 __sock_create() 查找 net_families[AF_INET],然后调用其 create 方法:
// net/socket.c
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
struct net_proto_family *pf;
// 1. 查找协议族
pf = rcu_dereference(net_families[family]);
if (unlikely(!pf))
return -EAFNOSUPPORT;
// 2. 分配 struct socket
err = pf->create(net, socket, protocol, kern);
// 3. 设置协议族操作表
socket->ops = pf->ops;
return err;
}
注意:net_families 数组的访问受 RCU 保护,因为协议族注册通常发生在模块加载时,而查找发生在每次 socket 创建时。用 RCU 可以避免加锁带来的性能开销。
下面这张图展示了协议族注册与 socket 创建的整体流程:
从图中可以清楚看到,协议族注册是一次性的初始化动作,而 socket 创建是每次系统调用都会触发的运行时路径。两者通过 net_families[] 这个全局数组连接起来。
3.4 协议族操作表:多态的实现
每个协议族都有一张操作函数表 struct proto_ops,定义了该协议族支持的所有操作:
// include/linux/net.h
struct proto_ops {
int family;
int (*bind) (struct socket *sock, struct sockaddr *addr, int len);
int (*connect) (struct socket *sock, struct sockaddr *addr, int len, int flags);
int (*listen) (struct socket *sock, int len);
int (*accept) (struct socket *sock, struct socket *newsock, int flags, bool kern);
int (*sendmsg) (struct socket *sock, struct msghdr *m, size_t total_len);
int (*recvmsg) (struct socket *sock, struct msghdr *m, size_t total_len, int flags);
int (*poll) (struct file *file, struct socket *sock, poll_table *wait);
int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg);
// ...
};
以 AF_INET 为例,它的 proto_ops 是这样定义的:
// net/ipv4/af_inet.c
const struct proto_ops inet_stream_ops = {
.family = PF_INET,
.bind = inet_bind,
.connect = inet_stream_connect,
.listen = inet_listen,
.accept = inet_accept,
.sendmsg = inet_sendmsg,
.recvmsg = inet_recvmsg,
.poll = tcp_poll,
.ioctl = inet_ioctl,
// ...
};
而 AF_UNIX 的 proto_ops 则完全不同:
// net/unix/af_unix.c
const struct proto_ops unix_stream_ops = {
.family = PF_UNIX,
.bind = unix_bind,
.connect = unix_stream_connect,
.listen = unix_listen,
.accept = unix_accept,
.sendmsg = unix_sendmsg,
.recvmsg = unix_recvmsg,
.poll = unix_poll,
.ioctl = unix_ioctl,
// ...
};
避坑指南:我曾经在写内核模块时,想当然地以为所有协议族的 sendmsg 行为都一样。结果在 AF_NETLINK 上调用时,发现它的语义跟 TCP 完全不同——netlink 的 sendmsg 是面向消息的,不会做流量控制。所以,千万别假设不同协议族的操作行为一致,一定要查对应的 proto_ops 实现。
3.5 关键数据结构关系总结
最后,我用一个表格来总结本章涉及的核心数据结构及其关系:
| 数据结构 | 所属层次 | 主要职责 | 关键字段 |
|---|---|---|---|
| struct socket | 套接字层(VFS 侧) | 面向文件系统的接口封装 | sk, ops, file, state |
| struct sock | 套接字层(协议侧) | 协议栈核心状态管理 | sk_receive_queue, sk_write_queue, sk_rcvbuf |
| struct proto_ops | 套接字层 | 协议族操作函数表 | bind, connect, sendmsg, recvmsg 等 |
| struct net_proto_family | 协议族注册层 | 协议族创建接口 | family, create, owner |
| net_families[] | 全局注册表 | 存储所有已注册协议族 | 数组索引为 family 号 |
理解这些结构体的关系,是深入阅读网络协议栈源码的基础。我个人建议你在看代码时,先找到 socket() 系统调用的入口,然后一步步跟踪下去,亲眼看看 struct socket 和 struct sock 是怎么被创建和关联起来的。这种「跟着代码走一遍」的方式,比死记硬背结构体字段要有效得多。