【Linux 网络子系统深度拆解】Socket 层内核实现:从 VFS 到协议栈的桥梁
前面几篇我们从收包路径到 TCP/UDP 协议实现,始终有一个隐含的入口没有拆解——socket。用户态程序看到的是一个文件描述符,内核看到的是一组精心组织的分发表和数据结构。socket 层是 VFS 文件系统抽象和协议栈实现之间的翻译层。
当你写下 socket(AF_INET, SOCK_STREAM, 0) 时,内核做了这些事:
- 分配
struct socket(VFS 层)+struct inode——合并为一次内存分配 - 查找地址族工厂(
AF_INET→inet_create) - 根据
(SOCK_STREAM, 0)查找协议注册表 → 得到tcp_prot+inet_stream_ops - 从
tcp_prot->slab(专用 kmem_cache)分配struct sock - 互相关联:
socket->sk = sock,sock->sk_socket = socket - 创建 VFS file,绑定到进程的文件描述符表
之后每次 send() / recv() 调用,都经过两层分发:先通过 socket->ops(proto_ops)做 VFS 语义翻译,再通过 sock->sk_prot(proto)调用协议实现。
本文从内核源码出发,完整拆解这个双层架构。
下面这张图展示了 socket 的双层分发架构——从 VFS 文件操作到 proto_ops 再到 proto 的完整调用链:
Socket 双层分发架构
一、核心数据结构:双层分发的基石
struct socket——VFS 层的 socket 表示
struct socket(include/linux/net.h:117)是用户态 fd 直接对应的内核对象:
struct socket {
socket_state state; /* SS_UNCONNECTED/SS_CONNECTED/... */
short type; /* SOCK_STREAM/SOCK_DGRAM/SOCK_RAW */
unsigned long flags; /* SOCK_NOSPACE 等标志 */
struct file *file; /* 反向指针:VFS file → socket */
struct sock *sk; /* 前向指针:socket → 协议实现 */
const struct proto_ops *ops; /* VFS 层分发表 */
struct socket_wq wq; /* epoll/select/poll 的等待队列 */
};
socket_state 是 VFS 层面的连接状态(include/uapi/linux/net.h:45),只有五个值:
状态 含义 SS_FREE 未分配 SS_UNCONNECTED 刚创建,未连接 SS_CONNECTING connect() 进行中 SS_CONNECTED 已连接 SS_DISCONNECTING 断连中
注意这与 TCP 的 11 个状态完全独立——SS_CONNECTED 只是 VFS 层面的标记,TCP 层面的 ESTABLISHED、CLOSE_WAIT 等状态存在 sock->sk_state 中。
struct sock——协议层的 socket 表示
struct sock(include/net/sock.h:341)是协议栈的核心对象,190+ 个字段。最关键的几组:
struct sock {
struct sock_common __sk_common; /* 五元组、状态、哈希 */
/* 接收侧 */
struct sk_buff_head sk_receive_queue; /* 接收包队列 */
int sk_rcvbuf; /* SO_RCVBUF 限制 */
/* 发送侧 */
struct sk_buff_head sk_write_queue; /* 发送包队列 */
int sk_sndbuf; /* SO_SNDBUF 限制 */
refcount_t sk_wmem_alloc; /* 已分配写内存 */
/* 分发与关联 */
struct proto *sk_prot; /* 协议分发表 */
struct socket *sk_socket; /* 反向指针 → VFS socket */
/* 事件通知回调 */
void (*sk_data_ready)(struct sock *sk); /* 数据到达通知 */
void (*sk_write_space)(struct sock *sk); /* 写空间可用通知 */
void (*sk_error_report)(struct sock *sk); /* 错误通知 */
/* SO_REUSEPORT */
struct sock_reuseport __rcu *sk_reuseport_cb;
};
sock_common(include/net/sock.h:150)包含连接五元组:
struct sock_common {
__be32 skc_daddr; /* 目的 IP */
__be32 skc_rcv_saddr; /* 本地 IP */
__be16 skc_dport; /* 目的端口(网络字节序) */
__u16 skc_num; /* 本地端口(主机字节序) */
unsigned short skc_family; /* AF_INET / AF_INET6 */
unsigned char skc_state; /* 协议层状态(TCP_ESTABLISHED 等) */
unsigned char skc_reuse:4; /* SO_REUSEADDR */
unsigned char skc_reuseport:1; /* SO_REUSEPORT */
struct proto *skc_prot; /* 协议处理器 */
possible_net_t skc_net; /* 网络命名空间 */
};
两张分发表
proto_ops(include/linux/net.h:160)——VFS 到 socket 的翻译层:
struct proto_ops {
int (*bind)(struct socket *, struct sockaddr *, int);
int (*connect)(struct socket *, struct sockaddr *, int, int);
int (*accept)(struct socket *, struct socket *, int, bool);
int (*listen)(struct socket *, int);
int (*sendmsg)(struct socket *, struct msghdr *, size_t);
int (*recvmsg)(struct socket *, struct msghdr *, size_t, int);
__poll_t (*poll)(struct file *, struct socket *, poll_table *);
/* ... */
};
proto(include/net/sock.h:1223)——协议特定实现:
struct proto {
void (*close)(struct sock *, long);
int (*connect)(struct sock *, struct sockaddr *, int);
struct sock *(*accept)(struct sock *, int, int *, bool);
int (*sendmsg)(struct sock *, struct msghdr *, size_t);
int (*recvmsg)(struct sock *, struct msghdr *, size_t, int, int *);
int (*hash)(struct sock *);
void (*unhash)(struct sock *);
int (*get_port)(struct sock *, unsigned short);
struct kmem_cache *slab; /* 协议专用内存池 */
unsigned int obj_size; /* socket 对象大小 */
char name[32]; /* "TCP" / "UDP" / "SCTP" */
/* ... */
};
双层分发的调用路径:
用户态 send(fd, buf, len)
↓ VFS
file->f_op->write_iter()
↓ socket 文件操作
sock_sendmsg(socket, msg)
↓ proto_ops 分发
socket->ops->sendmsg(socket, msg, size)
↓ 对于 TCP: inet_sendmsg()
sock->sk_prot->sendmsg(sock, msg, size)
↓ proto 分发
tcp_sendmsg(sock, msg, size) ← 最终的协议实现
proto_ops 和 proto 的分离让同一协议能支持不同的 VFS 语义——例如 TCP 用 inet_stream_ops(面向连接),而 UDP 用 inet_dgram_ops(无连接),但底层的 tcp_prot / udp_prot 各自处理协议细节。
二、socket 创建路径
系统调用入口
socket(family, type, protocol) 系统调用经过 __sys_socket() 进入 __sock_create():
__sys_socket(family, type, protocol)
↓
__sock_create(current->nsproxy->net_ns, family, type, protocol, &sock, 0)
│
├── ① sock_alloc()
│ └── new_inode_pseudo(sock_mnt->mnt_sb)
│ → 分配 struct socket_alloc(= socket + inode 合体)
│ → 设置 inode->i_op = &sockfs_inode_ops
│
├── ② 查找地址族工厂
│ └── net_families[family]
│ → AF_INET: inet_family_ops
│ → AF_INET6: inet6_family_ops
│ → AF_UNIX: unix_family_ops
│
└── ③ family->create(net, sock, protocol, kern)
└── 对于 AF_INET: inet_create()
socket_alloc:一次分配两个对象
struct socket_alloc(include/net/sock.h:1555)把 struct socket 和 struct inode 合并为一个结构体:
struct socket_alloc {
struct socket socket;
struct inode vfs_inode;
};
sock_alloc() 通过 new_inode_pseudo() 从 sockfs 超级块分配这个合体对象。好处是缓存友好——socket 和 inode 在同一缓存行,VFS 操作和 socket 操作不会产生额外的缓存未命中。
转换宏: - SOCKET_I(inode) → 从 inode 取 socket(container_of) - SOCK_INODE(socket) → 从 socket 取 inode
inet_create:协议注册与匹配
inet_create() 是 AF_INET 地址族的 socket 工厂:
inet_create(net, sock, protocol, kern)
│
├── 遍历 inetsw[sock->type] 链表
│ └── 查找匹配 (type, protocol) 的 inet_protosw
│ ├── SOCK_STREAM + IPPROTO_TCP → tcp_prot + inet_stream_ops
│ ├── SOCK_DGRAM + IPPROTO_UDP → udp_prot + inet_dgram_ops
│ └── SOCK_RAW + IPPROTO_ICMP → raw_prot + inet_sockraw_ops
│
├── sock->ops = answer->ops 设置 proto_ops 分发表
│
├── sk = sk_alloc(net, AF_INET, priority, answer->prot, kern)
│ └── 从 prot->slab(kmem_cache)分配 struct sock
│ ├── TCP: kmem_cache("TCP", sizeof(tcp_sock)) ~2KB
│ ├── UDP: kmem_cache("UDP", sizeof(udp_sock)) ~400B
│ └── 设置 sk->sk_prot = prot
│
├── sock_init_data(sock, sk)
│ ├── socket->sk = sk VFS → 协议
│ ├── sk->sk_socket = socket 协议 → VFS
│ ├── sk_data_ready = sock_def_readable 默认回调
│ ├── sk_write_space = sock_def_write_space
│ ├── sk_rcvbuf = sysctl_rmem_default
│ └── sk_sndbuf = sysctl_wmem_default
│
└── sk->sk_prot->init(sk)
├── TCP: tcp_v4_init_sock() 初始化拥塞控制、定时器
└── UDP: (无 init)
inet_protosw 注册表
struct inet_protosw(include/net/protocol.h:76)是协议注册表条目:
struct inet_protosw {
struct list_head list;
unsigned short type; /* SOCK_STREAM / SOCK_DGRAM / SOCK_RAW */
unsigned short protocol; /* IPPROTO_TCP / IPPROTO_UDP */
struct proto *prot; /* tcp_prot / udp_prot */
const struct proto_ops *ops; /* inet_stream_ops / inet_dgram_ops */
unsigned char flags; /* INET_PROTOSW_PERMANENT 等 */
};
内核启动时,inet_init() 注册默认条目到 inetsw[] 数组。模块(如 SCTP)可以通过 inet_register_protosw() 添加新条目。
从 socket 到 fd
__sys_socket() 在 __sock_create() 返回后:
__sys_socket()
├── __sock_create() → 得到 struct socket
├── sock_map_fd(socket, flags)
│ ├── get_unused_fd_flags(flags) 分配 fd 号
│ ├── sock_alloc_file(socket, flags, NULL)
│ │ └── alloc_file_pseudo(inode, sock_mnt, dname, ...)
│ │ → 创建 struct file
│ │ → file->f_op = &socket_file_ops VFS 文件操作
│ └── fd_install(fd, file) 绑定到进程 fd 表
└── return fd
socket_file_ops 是 sockfs 文件系统的操作集,它把 VFS 的 read() / write() / poll() 转换为 socket 层的 sock_read_iter() / sock_write_iter() / sock_poll()。
三、proto_ops 分发:inet_stream_ops 与 inet_dgram_ops
TCP 的分发链
inet_stream_ops(include/net/inet_common.h:11)是 TCP socket 的 VFS 分发表:
inet_stream_ops:
.bind = inet_bind
.connect = inet_stream_connect
.listen = inet_listen
.accept = inet_accept
.sendmsg = inet_sendmsg → sk->sk_prot->sendmsg = tcp_sendmsg
.recvmsg = inet_recvmsg → sk->sk_prot->recvmsg = tcp_recvmsg
.poll = tcp_poll
.setsockopt = sock_common_setsockopt
.getsockopt = sock_common_getsockopt
inet_sendmsg() 的实现非常薄——本质上就是转发到 sk->sk_prot->sendmsg():
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct sock *sk = sock->sk;
/* 如果还没有绑定端口,先自动绑定 */
if (unlikely(inet_send_prepare(sk)))
return -EAGAIN;
return INDIRECT_CALL_2(sk->sk_prot->sendmsg, tcp_sendmsg,
udp_sendmsg, sk, msg, size);
}
INDIRECT_CALL_2 是 retpoline 优化——在 Spectre 缓解环境下,间接调用很慢,这个宏先比较目标地址是否是 tcp_sendmsg 或 udp_sendmsg,如果是就直接调用,避免间接跳转。
UDP 的分发链
inet_dgram_ops 类似,但面向无连接语义:
inet_dgram_ops:
.bind = inet_bind
.connect = inet_dgram_connect ← 不建立连接,只缓存目的地址
.sendmsg = inet_sendmsg → udp_sendmsg
.recvmsg = inet_recvmsg → udp_recvmsg
.poll = udp_poll
inet_dgram_connect() 对 UDP 的语义是”记住默认目的地址”——后续 send() 不需要每次指定目的地址,且内核能把这个 socket 放入二级哈希表(参见 UDP 内核实现)。
四、SO_REUSEPORT:多核 socket 分发
问题
单个 socket 的收包路径天然是串行的——所有包都进入同一个 sk_receive_queue,无论系统有多少 CPU。对高 PPS 的 UDP 服务(DNS/QUIC)或高连接率的 TCP 监听(Nginx),单 socket 成为瓶颈。
机制
SO_REUSEPORT(Linux 3.9+)允许多个 socket 绑定同一 (address, port),收包时按包的哈希值分发到不同 socket——每个 socket 对应一个处理线程/进程。
struct sock_reuseport(include/net/sock_reuseport.h:13)是分发组的核心容器:
struct sock_reuseport {
struct rcu_head rcu;
u16 max_socks; /* 数组容量 */
u16 num_socks; /* 当前活跃 socket 数 */
u16 num_closed_socks; /* 已关闭待清理的 socket 数 */
unsigned int reuseport_id; /* BPF 程序可见的组 ID */
unsigned int bind_inany:1; /* 绑定 INADDR_ANY? */
unsigned int has_conns:1; /* 是否有已建立的连接 */
struct bpf_prog __rcu *prog; /* eBPF 选择器程序 */
struct sock *socks[]; /* socket 数组——flexible array */
};
分发流程
收包路径(__udp4_lib_lookup / inet_lookup_listener)
↓ 找到一组 SO_REUSEPORT sockets
reuseport_select_sock(sk, hash, skb, hdr_len)
├── prog = rcu_dereference(reuse->prog)
├── prog 非空?
│ └── bpf_run_sk_reuseport(reuse, sk, prog, skb, ...)
│ → eBPF 程序返回选中的 socket 索引
└── prog 为空?
└── reciprocal_scale(hash, num_socks)
→ 用包哈希值选择 socket(均匀分布)
eBPF 选择器
reuseport_attach_prog()(include/net/sock_reuseport.h:44)挂载 BPF 程序。程序类型为 BPF_PROG_TYPE_SK_REUSEPORT,可以读取包头信息,实现自定义分发逻辑:
/* eBPF 伪代码:按 QUIC Connection ID 分发 */
SEC("sk_reuseport")
int select_by_cid(struct sk_reuseport_md *ctx)
{
/* 解析 QUIC 包头,提取 Connection ID */
__u64 cid = parse_quic_cid(ctx->data, ctx->data_end);
/* 按 CID 哈希选择 socket */
return bpf_sk_select_reuseport(ctx, &sock_map, &cid, 0);
}
这让同一 QUIC 连接的所有包始终路由到同一个处理线程,避免跨线程同步。
五、epoll 集成:事件通知机制
回调链
socket 与 epoll 的集成通过三个回调函数实现(include/net/sock.h:514-517):
sk_data_ready(sk) → 数据到达时调用
sk_write_space(sk) → 发送缓冲区有空间时调用
sk_error_report(sk) → 错误发生时调用
默认实现(sock_init_data() 中设置):
sk_data_ready = sock_def_readable()
└── 检查 socket_wq 是否有等待者
└── 有 → wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN)
→ 唤醒 epoll_wait 中阻塞的进程
sk_write_space = sock_def_write_space()
└── 检查发送缓冲区是否有空间
└── 有空间 → wake_up_interruptible_poll(&wq->wait, EPOLLOUT)
epoll_wait 的底层流程
① 用户态: epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event)
→ 内核注册 socket 到 epoll 实例
→ socket->ops->poll(file, socket, &epq.pt)
→ sock_poll_wait(file, socket_wq, wait)
→ 把当前进程添加到 socket_wq.wait 队列
② 用户态: epoll_wait(epfd, events, maxevents, timeout)
→ 进程睡眠在 epoll 的等待队列上
③ 网卡收包 → softirq → tcp_rcv_established()
→ sk_data_ready(sk)
→ sock_def_readable(sk)
→ wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN)
→ 唤醒 epoll_wait 中的进程
④ 进程返回: events[i].events = EPOLLIN
→ 用户调用 recv() 读取数据
等待队列标志
socket_wq 中的标志(include/linux/net.h:99)用于优化唤醒:
标志 含义 SOCKWQ_ASYNC_NOSPACE 发送缓冲区满,下次有空间时通知 SOCKWQ_ASYNC_WAITDATA 正在等待数据到达
这些标志避免了不必要的唤醒——如果没有进程在等,sk_data_ready() 就不会触发昂贵的唤醒操作。
六、socket 选项处理
三级选项处理
socket 选项按层级分发:
setsockopt(fd, level, optname, optval, optlen)
↓
__sys_setsockopt()
├── level == SOL_SOCKET?
│ └── sock_setsockopt(sock, level, optname, ...)
│ → 通用选项(SO_RCVBUF/SO_SNDBUF/SO_REUSEPORT/...)
│
├── level == SOL_TCP / SOL_UDP?
│ └── sock->ops->setsockopt()
│ → inet_stream_ops: sock_common_setsockopt()
│ → sk->sk_prot->setsockopt()
│ → TCP: do_tcp_setsockopt()
│ → UDP: udp_setsockopt()
│
└── level == SOL_IP?
└── ip_setsockopt()
→ IP 层选项(IP_TTL/IP_TOS/IP_MULTICAST_IF/...)
关键通用选项
sock_setsockopt() 处理的通用选项及其内核映射:
选项 sock 字段 行为 SO_RCVBUF sk->sk_rcvbuf 设置接收缓冲区大小(上限 sysctl_rmem_max) SO_SNDBUF sk->sk_sndbuf 设置发送缓冲区大小(上限 sysctl_wmem_max) SO_REUSEADDR sk->sk_reuse 允许绑定 TIME_WAIT 状态的地址 SO_REUSEPORT sk->sk_reuseport 启用多 socket 端口共享 SO_KEEPALIVE sock_set_flag(SOCK_KEEPOPEN) 启用 TCP 保活探测 SO_BINDTODEVICE sk->sk_bound_dev_if 绑定到指定网络设备 SO_RCVTIMEO sk->sk_rcvtimeo 接收超时 SO_SNDTIMEO sk->sk_sndtimeo 发送超时 SO_ZEROCOPY sock_set_flag(SOCK_ZEROCOPY) 启用零拷贝发送
SO_RCVBUF / SO_SNDBUF 有一个重要细节——内核把用户设的值翻倍(sk->sk_rcvbuf = min(val * 2, sysctl_rmem_max * 2)),因为内核需要额外空间存放 sk_buff 元数据。SO_RCVBUFFORCE / SO_SNDBUFFORCE(需要 CAP_NET_ADMIN)可以绕过 sysctl 上限。
七、可观测性实战
socket 创建追踪
# 追踪 socket 创建——谁创建了什么类型的 socket
bpftrace -e '
kprobe:__sock_create {
printf("%s pid=%d family=%d type=%d proto=%d\n",
comm, pid, arg1, arg2, arg3);
}'
SO_REUSEPORT 分发监控
# 追踪 reuseport 选择——查看分发是否均匀
bpftrace -e '
kprobe:reuseport_select_sock {
@select[tid] = nsecs;
}
kretprobe:reuseport_select_sock /@select[tid]/ {
$sk = (struct sock *)retval;
if ($sk != 0) {
@selected_port[$sk->__sk_common.skc_num] = count();
}
delete(@select[tid]);
}
interval:s:10 { print(@selected_port); clear(@selected_port); }'
epoll 唤醒延迟
# 测量从数据到达到 epoll_wait 返回的延迟
bpftrace -e '
kprobe:sock_def_readable {
@wake_start[arg0] = nsecs;
}
tracepoint:syscalls:sys_exit_epoll_wait /args->ret > 0/ {
/* 粗略估算——实际需要关联 sk */
@epoll_wake_us = hist((nsecs - @last_readable) / 1000);
}'
socket 缓冲区使用率
# 查看所有 TCP socket 的缓冲区使用率
ss -tmni | awk '/skmem/ {
match($0, /r([0-9]+)/, r);
match($0, /rb([0-9]+)/, rb);
if (rb[1] > 0) printf("recv: %d%%\n", r[1]*100/rb[1]);
}'
# bpftrace:实时追踪接收缓冲区使用
bpftrace -e '
kprobe:sock_rfree {
$sk = (struct sock *)arg0;
$rmem = $sk->sk_rmem_alloc.refs.counter;
$rcvbuf = $sk->sk_rcvbuf;
if ($rcvbuf > 0) {
@rcvbuf_pct = hist($rmem * 100 / $rcvbuf);
}
}'
proto_ops 分发开销
# 测量 inet_sendmsg → tcp_sendmsg 的分发开销
bpftrace -e '
kprobe:inet_sendmsg { @start[tid] = nsecs; }
kprobe:tcp_sendmsg /@start[tid]/ {
@dispatch_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
八、关键参数速查
参数 默认值 内核对应 调优建议 net.core.somaxconn 4096 listen() backlog 上限 高并发服务器建议 65535 net.core.rmem_default 212992 sk_rcvbuf 默认值 按业务需求调整 net.core.rmem_max 212992 SO_RCVBUF 上限 高 BDP 场景增大到 16MB+ net.core.wmem_default 212992 sk_sndbuf 默认值 按业务需求调整 net.core.wmem_max 212992 SO_SNDBUF 上限 高吞吐场景增大 net.core.optmem_max 20480 辅助数据缓冲区上限 大量 cmsg 场景增大 net.ipv4.ip_local_port_range 32768 60999 自动分配端口范围 高连接率扩大到 1024-65535 net.core.netdev_max_backlog 1000 softirq 队列深度 高 PPS 增大到 5000+
参考文献
- Linux 内核源码,
include/linux/net.h,6.8(struct socket 定义于第 117 行,struct proto_ops 于第 160 行) - Linux 内核源码,
include/net/sock.h,6.8(struct sock 定义于第 341 行,struct proto 于第 1223 行) - Linux 内核源码,
include/net/protocol.h,6.8(struct inet_protosw 定义于第 76 行) - Linux 内核源码,
include/net/sock_reuseport.h,6.8(struct sock_reuseport 于第 13 行) - Stevens, Fenner, Rudoff, “UNIX Network Programming, Volume 1”, 3rd Edition, 2003(socket API 参考)
- Linux 内核文档,
Documentation/networking/scaling.rst
上一篇:UDP 内核实现与 socket lookup 优化
下一篇:邻居子系统与 ARP:L2 地址解析的内核实现
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。