socket / bind 内核实现 —— socket 四件套的前两步
上一篇:无(TCP 四件套从这里开始,网络栈总览),下一篇 accept-kernel-internals 讲 accept 怎么取连接。本篇聚焦服务端启动的前两步:
socket()在内核里分配什么、bind()做了什么、为什么这两步不改变 TCP 状态。全文呼应 TCP 四件套socket → bind → listen → accept的生命线(listen 的细节见 listen-kernel-internals)。
一、socket 和 bind 各自做了什么
一句话:socket() 造壳(分配结构 + 挂 vtable),bind() 贴标签(注册 IP:Port 到内核 hash 表)。两者都不改变 TCP 状态。
| 步骤 | 做了什么 | TCP 状态 | 关键数据结构 |
|---|---|---|---|
socket(AF_INET, SOCK_STREAM, 0) | 分配 struct socket + struct sock(含 inet_connection_sock),挂好 inet_stream_ops vtable(详见 §二 VFS 视角),返回 fd | TCP_CLOSE | socket.ops = &inet_stream_ops、socket.sk → struct sock |
bind(sockfd, &addr, ...) | 把 IP:Port 写入 sock,注册到全局 bind hash 表(端口冲突检查) | TCP_CLOSE(不变) | inet->inet_sport、inet->inet_num、bind hash 节点 |
后面的 listen() 才把状态推到 TCP_LISTEN 并创建两队列。在这之前,socket 只是一个"有名字但还不接客"的空壳。
二、socket 和 sock 的双层结构
这是理解 socket 内核实现的核心:两个结构,两层抽象。

用户态拿到的是整数 fd,但 fd 背后有五层沿路指针,各结构体之间的引用与继承关系如下:

关系分析:
fd 不是结构体,而是进程 fd 表(files_struct)中的数组下标。真正的数据结构链如下:
fd → struct file:进程的files_struct.fd_array[fd]拿到struct file *。这一步由sock_map_fd()分配 fd 并建立映射(进程 fd 表的结构详见files-struct)。file 是 VFS 层的通用对象,内核用引用计数(f_count)管理生命周期——fork 时父子共享同一 file,close 时计数减 1,归零才真正释放。file.private_data → struct socket:private_data是void *类型的"万能钩子"。对普通文件它指向 inode/目录等,对 socket 文件它指向struct socket。这一层把通用 VFS 接口和网络协议栈桥接起来——用户调用read(sockfd)时,VFS 通过file→f_op→read进入 socket 文件操作,再通过socket→ops→recvmsg进入协议栈。socket.sk → struct sock:socket 通过.sk指针指向 sock,两者是组合关系——socket 管接口、sock 管数据,sk_alloc()先分配 sock,sock_init_data()再把两层关联。这层分离使得同一套协议栈(sock)可以复用给不同 socket 接口。sock.sk_protinfo → struct inet_sock:sk_protinfo是协议族私有数据的入口,AF_INET下它指向inet_sock(通过inet_sk(sk)宏转换)。IP 和端口信息就存在这里,bind()写入时同步更新inet_sport和inet_num。inet_sock → inet_connection_sock(继承):inet_connection_sock内嵌在inet_sock之后,形成 C 风格继承——sk_alloc()一次性分配整块内存,包含 sock + inet_sock + inet_connection_sock 三个结构。listen()时在icsk_accept_queue里初始化半连接和全连接两队列。
为什么不能省略中间层:
| 如果省略... | 后果 |
|---|---|
struct file | fd 失去引用计数,fork/dup 后无法管理共享,close 时无法判断是否真正释放 |
struct socket | VFS 的 read/write/poll 无法找到网络协议栈的入口函数,每一类文件都要硬编码分发 |
struct sock | TCP/UDP/RAW 各自实现一套缓冲区管理和状态机,代码重复,无法复用 |
struct inet_sock | IPv4 和 IPv6 的地址信息混在 sock 里,协议族扩展需要改核心结构 |
struct inet_connection_sock | 连接队列、拥塞控制状态无处安放,只能靠条件编译或运行时判断是否"是 TCP" |
| 结构 | 职责 |
| ------ | ------ |
struct socket | 面向用户态 API,持有 ops 函数指针表(bind/listen/accept/sendmsg/recvmsg 全部指向 inet_*) |
struct sock | 面向内核网络栈,协议状态机、缓冲区、队列、bind hash 节点 |
struct inet_connection_sock | TCP 特有扩展,内嵌于 sock,含 icsk_accept_queue(两队列的容器) |
结构体成员详解
以上 PlantUML 和职责表给出了结构体的大框架,下面把每个结构体的关键成员逐一展开。
struct socket —— 接口层
| 成员 | 类型 | 描述 |
|---|---|---|
| type | short | socket 类型:SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW |
state | socket_state | socket 状态机:SS_UNCONNECTED → SS_CONNECTING → SS_CONNECTED → SS_DISCONNECTING |
ops | struct proto_ops * | 操作函数指针表(vtable),指向 inet_stream_ops,含 bind / listen / accept / sendmsg / recvmsg / poll 等 |
sk | struct sock * | 核心指针:指向 struct sock,是 socket 到网络协议栈的唯一桥梁 |
| file | struct file * | 反向指针:指向关联的 struct file(通过 sock_map_fd 建立) |
wq | wait_queue_head_t | 等待队列头,供 poll / epoll 阻塞等待事件 |
flags | unsigned long | socket 标志位(SOCK_NOSPACE / SOCK_ASYNC_NOSPACE 等) |
struct sock —— 实现层(核心)
| 成员 | 类型 | 描述 |
|---|---|---|
sk_prot | struct proto * | 协议操作函数表,SOCK_STREAM 时指向 tcp_prot:含 connect / sendmsg / recvmsg / hash / unhash / setsockopt 等 |
sk_state | __u8 | TCP 状态机:TCP_CLOSE → TCP_LISTEN → TCP_SYN_RECV → TCP_ESTABLISHED → TCP_FIN_WAIT1 → ... |
sk_family | sa_family_t | 地址族:AF_INET(IPv4) / AF_INET6(IPv6) / AF_UNIX(Unix Domain) |
sk_type | unsigned short | socket 类型(与 socket.type 一致) |
sk_protocol | __u8 | 协议号:IPPROTO_TCP(6) / IPPROTO_UDP(17) |
sk_rcvbuf | int | 接收缓冲区大小(字节),可由 SO_RCVBUF 设置(调优见 kernel-tuning-net) |
sk_sndbuf | int | 发送缓冲区大小(字节),可由 SO_SNDBUF 设置(调优见 kernel-tuning-net) |
sk_refcnt | refcount_t | 引用计数:fd 关闭 / fork 复制时增减,减到 0 才真正释放 |
sk_reuse | __u8 | SO_REUSEADDR 标志位(用法见 accept-bottleneck) |
sk_reuseport | __u8 | SO_REUSEPORT 标志位(Linux 3.9+,深入见 accept-bottleneck) |
sk_bound_dev_if | int | SO_BINDTODEVICE 绑定的网卡接口索引 |
sk_err | int | 异步错误码(SO_ERROR),连接失败 / RST 时置位 |
sk_shutdown | unsigned char | shutdown() 状态:SEND_SHUTDOWN / RCV_SHUTDOWN |
sk_backlog | struct sk_buff_head | backlog 队列:内核空间积压的数据包链表(两队列模型详见 listen-kernel-internals) |
sk_protinfo | void * | 指向协议族私有扩展——AF_INET 下指向 struct inet_sock |
struct inet_sock —— IPv4 地址信息
sock 通过 sk_protinfo / inet_sk(sk) 宏拿到:
| 成员 | 类型 | 描述 |
|---|---|---|
inet_sport | __be16 | 源端口(网络字节序),bind() 时由 htons(port) 赋值 |
inet_dport | __be16 | 目的端口(网络字节序),connect() / accept() 时赋值 |
inet_saddr | __be32 | 源 IP(网络字节序) |
inet_daddr | __be32 | 目的 IP(网络字节序) |
inet_num | __be16 | 本地端口号(主机字节序),与 inet_sport 对应,bind() 同时写入 |
tos | __u8 | IP TOS(Type of Service)字段 |
uc_ttl | __u8 | 单播 TTL |
inet_id | __u16 | IP 分片 ID |
struct inet_connection_sock(icsk)—— TCP 连接管理
继承自 inet_sock,是 TCP 特化扩展:
| 成员 | 类型 | 描述 |
|---|---|---|
icsk_accept_queue | struct request_sock_queue | 接受队列容器:内含半连接队列(syn_table)和全连接队列(accept_queue),listen() 时初始化 |
icsk_bind_hash | struct inet_bind_bucket * | bind hash 表节点指针:bind() 时注册,用于端口冲突检测;桶内以链表串联同 IP:Port 的所有 sock |
icsk_af_ops | struct inet_connection_sock_af_ops * | 地址族差异操作:queue_xmit / send_check / rebuild_header / mtu_reduced(IPv4 vs IPv6) |
icsk_syn_retries | __u8 | SYN 重试次数上限(tcp_syn_retries,默认 6) |
icsk_rto | __u32 | 重传超时(RTO),动态计算,初始约 1s |
icsk_pmtu_cookie | __u32 | 路径 MTU 发现缓存值 |
icsk_ca_state | __u8 | 拥塞控制状态:open / disorder / cwr / recovery / loss(详见 tcp-flow-congestion-control) |
icsk_retransmits | __u8 | 当前重传次数 |
icsk_backoff | __u8 | 退避指数 |
icsk_user_timeout | __u32 | TCP_USER_TIMEOUT(ms),用户态设置的最大等待时间 |
icsk_delack_max | __u32 | 延迟 ACK 最大超时(μs)(调优见 tcp-nodelay) |
struct file —— fd 与 socket 的桥梁
用户态 fd 背后的内核对象,socket() 的 sock_map_fd 负责绑定(进程 fd 表全貌见 ../process/task-resources/files-struct):
| 成员 | 类型 | 描述 |
|---|---|---|
private_data | void * | 关键指针:指向 struct socket,是 fd → socket 的唯一路径 |
f_op | struct file_operations * | 文件操作表:socket 文件指向 socket_file_ops,read / write / poll 最终转发到 sock->ops |
f_mode | fmode_t | 访问模式(读 / 写 / 读写) |
f_flags | fmode_t | 文件标志:O_NONBLOCK / O_CLOEXEC / O_APPEND |
f_count | refcount_t | 引用计数:fork / dup 时递增,close 时递减,减到 0 释放 |
f_pos | loff_t | 文件偏移量(对 socket 无意义,始终为 0) |
f_path | struct path | 文件路径(对 socket 显示为 socket:[inode_number]) |
关系总结:fd → struct file (private_data) → struct socket (sk) → struct sock (sk_protinfo) → struct inet_sock → struct inet_connection_sock,从 fd 到 icsk 共经过 5 层指针跳转。
整个 TCP 服务端四件套的函数指针都在 inet_stream_ops 这张 vtable 里——socket() 选协议族时挂好表,之后 bind/listen/accept 都通过 sock->ops->xxx() 分发到 inet_bind/inet_listen/inet_accept。
VFS 视角:socket 为什么是一个"文件"
socket 能通过 read() / write() / poll() / close() 操作,源自 Linux 的 VFS(Virtual File System) 多态框架。
| vtable | 所属层 | 指向对象 | 职责 |
|---|---|---|---|
file_operations | VFS 层 | socket_file_ops | 文件语义:read / write / poll / mmap / ioctl |
proto_ops | 协议族层 | inet_stream_ops | 协议语义:bind / listen / accept / sendmsg / recvmsg |
file_operations 让 socket 复用文件系统的全部基础设施——read() 通过 3 层函数指针跳转(file_operations → proto_ops → proto)进入 TCP 收包,close() 通过引用计数(f_count)确保 fork 后只有双方都 close 才触发四次挥手,epoll / sendfile / fcntl 全部因为 VFS 的 fd 抽象而能在 socket 上统一工作。
../vfs/vfs-overview—— VFS 架构全景:四大对象、多态分发
../vfs/vfs-file-operations——file_operationsvtable 逐字段详解
../vfs/vfs-and-socket—— socket 与 VFS 深度融合:两套 vtable、read()3 层分发、close()引用计数、三大好处
三、socket() 执行流程

socket() 的核心动作只有三件事:
- 选 vtable:根据协议族和类型——
AF_INET + SOCK_STREAM→inet_stream_ops,这一步决定了后续所有操作走 TCP 路径。 - 分配双层结构:先
sk_alloc出来struct sock(含inet_connection_sock),再sock_init_data把 socket 和 sock 双向关联。 - 分配 fd 并挂到进程 fd 表:
sock_map_fd把socket → file → fd这条链串起来。
三步走完,socket 还处于 TCP_CLOSE,不知道自己是哪个 IP、哪个端口——只是一个有数据结构但"无名无分"的空壳。
协议族怎么选
socket() 传入的 AF_INET 在 __sock_create 里通过 net_families[AF_INET] 查表,拿到 inet_family_ops,其 .create = inet_create。inet_create 再根据 socket 类型选 ops:
| socket 类型 | sock->ops | 协议 |
|---|---|---|
SOCK_STREAM | inet_stream_ops | TCP |
SOCK_DGRAM | inet_dgram_ops | UDP |
SOCK_RAW | inet_sockraw_ops | RAW |
四、bind() 执行流程

bind() 的核心是对全局 bind hash 表的查 + 写操作:
- 查冲突:遍历 hash 表,找相同 IP:Port 的已有绑定。如果精确匹配且无
SO_REUSEPORT,返回EADDRINUSE。 - 写入:如果通过检查,把新的 IP:Port 注册进 hash 表。
bind() 不改变 TCP 状态,socket 仍是 TCP_CLOSE——它只是给 socket 贴了个名字,让外界能"找到"它。真正的"开张"得等 listen() 标记 TCP_LISTEN。
bind 的端口冲突规则
| 标志 | 行为 |
|---|---|
| 无特殊标志 | 一个 IP:Port 只能被一个 socket 绑定 |
SO_REUSEADDR | TIME_WAIT 状态的端口可复用;非 TIME_WAIT 不行 |
SO_REUSEPORT(Linux 3.9+) | 多个 socket 可绑定同 IP:Port,内核做分发——是实现无锁 accept 的基础(详见 accept-bottleneck) |
端口=0 | 内核从 ip_local_port_range 自动分配(通常 32768~60999,参数详解见 kernel-tuning-net) |
五、观测:怎么看到 socket 和 bind
| 手段 | 命令 | 看什么 |
|---|---|---|
| 跟踪系统调用 | strace -e trace=socket,bind ./program(详见 ../../tools/code/strace) | 看到 socket(AF_INET, SOCK_STREAM, ...) 和 bind(...) |
| 查看已创建 socket | ss -tlnp(详见 ../../tools/network/ss) | State 列:LISTEN(listen 后)或空(socket 后 bind 前不显示) |
| 查看 socket 详细信息 | ss -tlnp -o | Send-Q / Recv-Q 显示全连接队列状态 |
| /proc | ls -l /proc/<PID>/fd | 看到 socket:[inode] 类型的 fd |
延伸阅读:
- listen-kernel-internals — 四件套第三步:
listen()建两队列、标记TCP_LISTEN - accept-kernel-internals — 四件套第四步:
accept()阻塞等待 → 取出连接 → 返回 fd - epoll — epoll 原理:红黑树 + 就绪队列、LT/ET 模式
- thread-models — Reactor / Proactor 线程模型
- zero-copy / zero-copy-deep — 零拷贝技术:mmap / sendfile / splice
- tcp-flow-congestion-control — TCP 流控与拥塞控制
- kernel-tuning-net — 内核网络参数调优
- accept-bottleneck — accept 瓶颈分析与 SO_REUSEPORT 解决方案
- read-write-process — VFS
read()完整 10 层调用链 - vfs-overview — VFS 架构全景:四大对象、多态分发
- vfs-file-operations —
file_operationsvtable 逐字段拆解 - vfs-and-socket — socket 与 VFS 深度融合
- files-struct — 进程 fd 表(
files_struct)结构 - strace — strace 用法详解
- ss — ss 命令详解
总结
socket() 分配双层结构(struct socket 挂 vtable + struct sock 管状态)并返回 fd,bind() 把 IP:Port 注册到内核全局 hash 表。这两步不改变 TCP 状态(始终 TCP_CLOSE),只是为服务端准备好"我是谁、我能做什么"的数据结构——真正的"开张"要等 listen() 标记 TCP_LISTEN 并建两队列。