4. 套接字层实现:struct socket 与 struct sock 的关系、socket 系统调用流程、协议族注册与查找
套接字层是用户态网络编程与内核协议栈之间的桥梁。理解 struct socket 与 struct sock 的双层抽象、系统调用在内核中的完整流转路径,以及协议族的注册与查找机制,是掌握 Linux 网络协议栈的基石。本章将深入剖析这三个核心主题。
4.1 双层抽象:struct socket 与 struct sock
Linux 内核将套接字抽象为两个层次,分别服务于不同的关注点:
- struct socket:通用套接字层,面向 VFS(虚拟文件系统),负责与用户态文件描述符交互。它不关心具体的协议细节。
- struct sock:协议相关的套接字层,面向具体的传输协议(如 TCP、UDP)。它包含了协议状态机、缓冲区、拥塞控制等核心数据。
这种设计实现了“通用接口 + 协议特定实现”的解耦,使得添加新协议族时无需修改 VFS 层代码。
面试高频点: 面试官常问“为什么需要两个结构体?” 核心答案是:
struct socket 提供统一的文件操作接口(如 read/write),而 struct sock 封装了协议特有的状态和逻辑。一个 struct socket 通过 sk 指针唯一关联一个 struct sock。
4.1.1 核心数据结构对比
| 特性 | struct socket | struct sock |
|---|---|---|
| 所属层次 | 通用套接字层(VFS 层) | 协议特定层(传输层) |
| 主要字段 | state, type, ops, sk |
sk_state, sk_receive_queue, sk_write_queue, sk_prot |
| 与用户态交互 | 通过 file->private_data 关联 |
不直接与用户态交互 |
| 生命周期 | 由 sock_alloc 创建,sock_release 销毁 |
由 sk_alloc 创建,sk_free 销毁 |
| 协议操作集 | struct proto_ops *ops |
struct proto *sk_prot |
4.1.2 关联关系示意图
下图展示了 struct socket 与 struct sock 之间的 1:1 映射关系,以及它们如何通过 VFS 和协议层与外部交互。
4.2 socket 系统调用完整流程
当用户态调用 socket(AF_INET, SOCK_STREAM, 0) 时,内核经历了一条从 VFS 到具体协议实现的完整路径。理解这条路径是分析网络性能问题和调试内核的基础。
4.2.1 核心调用链
- 系统调用入口:
__x64_sys_socket()处理用户态传入的参数,进入内核通用套接字层。 - 套接字创建:
sock_create()根据协议族(AF_INET)和类型(SOCK_STREAM)查找对应的net_family,并调用其create方法。 - 协议族回调:对于 AF_INET,调用
inet_create(),该函数负责分配struct sock并初始化 TCP/UDP 协议控制块。 - 文件描述符绑定:
sock_map_fd()将struct socket与一个新的文件描述符关联,并设置file->f_op为套接字文件操作集。
工程实战提示: 在调试网络问题时,可以通过
strace -e socket 观察系统调用的返回值和参数。内核中可以通过 trace_sock_create 或 kprobe 挂载 __sys_socket 函数来追踪创建流程。
4.2.2 socket 系统调用流程图
4.3 协议族注册与查找机制
内核支持多种协议族(如 AF_INET、AF_INET6、AF_UNIX、AF_PACKET 等)。这些协议族通过一个全局数组 net_families 进行管理,实现了可插拔的协议族架构。
4.3.1 协议族注册
协议族在模块初始化或内核启动时通过 sock_register() 函数注册到 net_families 数组中。每个协议族需要提供一个 struct net_proto_family 结构体,其中最重要的是 create 回调函数。
// net/socket.c
#define NPROTO 256 // 最大支持的协议族数量
static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
int sock_register(const struct net_proto_family *ops)
{
int err;
if (ops->family >= NPROTO)
return -ENOBUFS;
// 检查是否已注册
if (rcu_dereference_protected(net_families[ops->family], 1) != NULL)
return -EEXIST;
// 写入数组
rcu_assign_pointer(net_families[ops->family], ops);
err = 0;
pr_info("NET: Registered protocol family %d\n", ops->family);
return err;
}
EXPORT_SYMBOL(sock_register);
4.3.2 协议族查找
当 sock_create() 被调用时,内核通过 __sock_create() 函数在 net_families 数组中查找对应的协议族,并调用其 create 方法。
// net/socket.c
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
// 1. 分配 struct socket
sock = sock_alloc();
if (!sock)
return -ENOMEM;
// 2. 查找协议族
rcu_read_lock();
pf = rcu_dereference(net_families[family]);
if (unlikely(!pf)) {
rcu_read_unlock();
err = -EAFNOSUPPORT;
goto out_release;
}
// 3. 调用协议族的 create 方法
err = pf->create(net, sock, protocol, kern);
rcu_read_unlock();
// ...
}
EXPORT_SYMBOL(__sock_create);
4.3.3 常见协议族注册示例
| 协议族 | family 值 | 注册函数 | create 回调 |
|---|---|---|---|
| AF_UNIX | 1 | unix_proto_init() |
unix_create() |
| AF_INET | 2 | inet_init() |
inet_create() |
| AF_INET6 | 10 | inet6_init() |
inet6_create() |
| AF_PACKET | 17 | packet_init() |
packet_create() |
| AF_NETLINK | 16 | netlink_proto_init() |
netlink_create() |
4.3.4 协议族注册与查找架构图
避坑指南: 在编写内核模块或自定义协议族时,必须确保
family 值不与已有协议族冲突。建议使用 AF_MAX 以上的值(如 40+)进行实验。同时,注册时需注意 net_families 数组使用 RCU 保护,读取时必须在 RCU 读锁内进行。
4.4 本章小结
- 双层抽象:
struct socket面向 VFS,struct sock面向协议,通过sk指针双向关联,实现通用接口与协议实现的解耦。 - 系统调用流程:从
sys_socket到sock_create,再到协议族特定的create回调,最后通过sock_map_fd绑定文件描述符,形成完整的创建链路。 - 协议族管理:通过
net_families数组实现 O(1) 查找,sock_register负责注册,__sock_create负责查找和调用,支持动态加载和卸载协议族模块。