Linux 网络内核协议栈深度剖析(六):Socket 层与系统调用实现
Socket 是用户态程序与内核网络协议栈之间的唯一接口。它不仅是一个文件描述符,更是一套精心设计的多层抽象体系——从 VFS 层的 struct socket,到协议无关的 struct sock,再到 IPv4 专用的 struct inet_sock 和 TCP 专用的 struct tcp_sock,每一层都有其清晰的职责边界。本文基于 Linux 6.4-rc1 源码,深入剖析 socket 系统调用的完整实现路径、epoll 的内核机制,以及缓冲区管理与内存压力控制。
本系列前五篇分别覆盖了网卡驱动收发、NAPI、协议栈 IP/TCP 层、路由子系统和 netfilter,这一篇聚焦于最接近用户态的 socket 接口层,梳理从系统调用入口到数据进出内核缓冲区的全链路,并以 epoll 的内部实现为重点,阐释高并发服务器底层事件通知的工作机制。
一、Socket 数据结构层次
Linux socket 采用四层结构,从上到下依次为 VFS 层、BSD socket 层、网络协议无关层和协议专用层。
在深入代码之前,先理解这套抽象的设计动机:Linux 将”网络连接”和”文件”统一为相同的接口(read/write/poll),这要求存在一个既能被 VFS 理解、又能分发给各协议族(AF_INET、AF_UNIX、AF_NETLINK 等)的通用抽象层。struct socket 承担 VFS 侧的职责,struct sock 承担协议无关的网络核心状态,两者通过指针双向持有,各司其职。
1.1 struct socket:VFS 层抽象
struct socket 定义在 include/linux/net.h,是 VFS(虚拟文件系统)对网络连接的抽象载体:
1 | /* include/linux/net.h */ |
socket_wq 中的 wait 字段是整个 epoll 机制的关键入口——网络数据到达时,内核通过它唤醒等待的进程。state 字段记录 BSD socket 语义的状态机(SS_UNCONNECTED → SS_CONNECTING → SS_CONNECTED),与 TCP 状态机(TCP_SYN_SENT 等)相互独立但存在映射关系。flags 字段中的 SOCK_NOSPACE 标志由 sk_stream_wait_memory 设置,告知发送路径”当前缓冲区已满”,ACK 到达后 tcp_write_space 会清除它。
1.2 struct proto_ops:socket 操作集
struct proto_ops 定义了从 socket 文件描述符到协议实现的分发接口,每个协议族(AF_INET、AF_UNIX 等)都注册一套自己的实现:
1 | /* include/linux/net.h */ |
对于 TCP,这套接口对应 inet_stream_ops,其中 sendmsg → inet_sendmsg,recvmsg → inet_recvmsg。proto_ops 工作在 socket 粒度(持有 struct socket *),而下一层的 struct proto(tcp_prot 等)工作在 sock 粒度(持有 struct sock *),两层分工清晰:前者处理用户态接口语义(地址解析、socket 状态检查),后者负责协议逻辑(序列号、重传、窗口)。
1.3 struct sock:协议无关核心与锁模型
struct sock 是内核网络层最核心的结构,几乎包含所有通用的 socket 状态。关键字段(来自 include/net/sock.h):
1 | /* include/net/sock.h(精简展示关键字段) */ |
sk_receive_queue 存放经 TCP 重组后可供用户读取的数据;sk_backlog 则是软中断持有锁时暂存的包,进程释放锁后会被消化进 sk_receive_queue。
socket_lock_t 的双锁模型:sk_lock 由一个 spinlock(slock)和一个整数语义的 owned 字段组成。进程上下文调用 lock_sock(sk) 时,先获取 spinlock,然后设置 owned = 1,表示”用户上下文已拥有该 socket”,最后释放 spinlock。软中断(BH)上下文调用 bh_lock_sock(sk) 时,若发现 owned = 1 则把包挂进 sk_backlog 而非直接处理。这样既避免了进程与软中断间的死锁,又保证了 socket 处理的原子性。
1.4 inet_sock 与 tcp_sock:逐层扩展
1 | struct sock |
访问时通过辅助宏完成层间转换:
1 | inet_sk(sk) /* struct sock * → struct inet_sock * */ |
这种”嵌套继承”设计是 C 语言实现面向对象多态的经典方式:子类型的第一个字段必须是父类型,container_of 宏负责从子字段地址还原出父结构地址。struct tcp_sock 中的 copied_seq 字段记录了”已经拷贝给用户进程的字节序列号位置”,tcp_recvmsg 每次拷贝完数据都会推进它,而 TCP 的接收窗口通知(window advertisement)正是基于 copied_seq 和当前接收队列大小计算出来的。
二、socket() 系统调用
2.1 调用链:__sys_socket → sock_create → __sock_create
用户态 socket(AF_INET, SOCK_STREAM, 0) 进入内核后的路径(net/socket.c):
1 | /* net/socket.c */ |
sock_create 最终调用 __sock_create,其核心逻辑是根据 family 参数在全局 net_families[] 数组中查找已注册的协议族处理器,然后调用其 create 回调:
1 | /* net/socket.c */ |
2.2 inet_create:AF_INET socket 的实例化
inet_create(net/ipv4/af_inet.c)完成 AF_INET socket 的真正创建:
1 | /* net/ipv4/af_inet.c */ |
inetsw[] 是在 af_inet.c 初始化阶段(inet_init)注册的静态表,将 {SOCK_STREAM, IPPROTO_TCP} 映射到 tcp_prot 和 inet_stream_ops。
注意 inet_create 的几个细节:首先,sk_alloc 从协议专属的 slab(对 TCP 是 tcp_prot.slab)分配内存,这使得 tcp_sock 的分配对齐到 cacheline,并便于 slab 内存统计;其次,sock_init_data(sock, sk) 同时完成 sock→sk 和 sk→sk_socket 的双向指针设置,并初始化 sk_data_ready = sock_def_readable,这正是 TCP 数据到达时触发 epoll 的回调入口;最后,sk_backlog_rcv 被设置为 sk_prot->backlog_rcv(即 tcp_v4_do_rcv),这是从 sk_backlog 消化报文时的处理函数。
2.3 sock_map_fd:socket 与文件系统绑定
socket 必须对应一个文件描述符,才能参与 poll/epoll 和 read/write:
1 | /* net/socket.c */ |
sock_alloc_file 创建的 file 对象使用 socket_file_ops 作为文件操作集,其中 read_iter/write_iter 均委托给 socket 层的 sock_recvmsg/sock_sendmsg。sock->file 和 file->private_data 互相持有引用,形成双向绑定。值得注意的是,sock_alloc() 中分配的 socket 对象实际上嵌入在 socket_alloc 结构(包含一个 struct inode)中,因此 socket 和 inode 共享同一块内存,SOCK_INODE(sock) 宏可以直接从 socket 指针得到对应的 inode,无需额外分配。
三、connect() 与 bind()
3.1 inet_bind:端口绑定与分配
1 | /* net/ipv4/af_inet.c */ |
__inet_bind 在验证地址合法性后,调用 sk->sk_prot->get_port(sk, snum) 即 inet_csk_get_port,完成端口号在内核哈希表(tcp_hashinfo.bhash)中的注册。端口 < 1024 时需要 CAP_NET_BIND_SERVICE 能力。
__inet_bind 的地址验证分两步:一是通过 inet_addr_type_table 检查目标地址是否属于本机(RTN_LOCAL)或允许绑定非本机地址(IP_FREEBIND / IP_TRANSPARENT);二是检查是否已被其他 socket 占用(SO_REUSEADDR / SO_REUSEPORT 规则在此生效)。若端口为 0,inet_csk_get_port 会自动在 ip_local_port_range 范围内选取一个可用端口——这与客户端 connect() 时隐式绑定的行为相同。
3.2 inet_stream_connect → tcp_v4_connect
connect() 系统调用经 proto_ops->connect 分发到 inet_stream_connect,后者调用 __inet_stream_connect:
1 | /* net/ipv4/af_inet.c */ |
对于非阻塞 socket,connect() 立即返回 -EINPROGRESS,之后通过 epoll 监听 EPOLLOUT 事件获知连接完成。连接建立后,再调用 getsockopt(SO_ERROR) 确认是否成功(值为 0)还是遇到了错误(如 ECONNREFUSED)。
源端口自动选择:tcp_v4_connect 内部调用 inet_hash_connect,它在 net.ipv4.ip_local_port_range 范围内(默认 32768–60999)用哈希算法随机选择一个未占用的源端口,避免顺序遍历带来的时序攻击风险。选择算法利用 (saddr ^ daddr ^ dport) 的哈希值作为起始偏移,在该范围内线性探测可用端口,因此同一五元组(src_ip, src_port, dst_ip, dst_port, proto)最多只能存在一个连接。当 ip_local_port_range 范围内的端口全部耗尽时,connect() 返回 EADDRNOTAVAIL,这是高频短连接场景(TIME_WAIT 堆积)的常见故障根因。
四、send/recv 数据路径
4.1 发送路径:sendto → tcp_sendmsg
1 | 用户态: sendto(fd, buf, len, flags, addr, addrlen) |
__sys_sendto 的实现(net/socket.c)将用户态缓冲区封装为 msghdr,然后一路向下:
1 | /* net/socket.c */ |
sock_sendmsg 在完成 LSM 检查后,通过 INDIRECT_CALL_INET 宏(优化间接调用预测)调用 inet_sendmsg,最终进入 tcp_sendmsg_locked。INDIRECT_CALL_INET 是 5.x 内核引入的 Spectre/Retpoline 优化,将最常见的两个函数指针(inet6_sendmsg、inet_sendmsg)内联为直接调用,减少间接分支预测失败的惩罚。
4.2 tcp_sendmsg_locked:发送缓冲区管理
tcp_sendmsg_locked(net/ipv4/tcp.c)是 TCP 发送的核心,它将用户数据追加到 sk_write_queue 中的 SKB:
1 | /* net/ipv4/tcp.c(核心逻辑简化) */ |
sk_stream_wait_memory(net/core/stream.c)是阻塞等待的关键:
1 | /* net/core/stream.c */ |
当对端的 ACK 回来,TCP 从 sk_write_queue 释放已确认的 SKB,调用 sk_write_space → tcp_write_space,唤醒在此睡眠的进程。
零拷贝发送(MSG_ZEROCOPY):tcp_sendmsg_locked 在检测到 MSG_ZEROCOPY 标志且硬件支持 scatter-gather(NETIF_F_SG)时,会通过 msg_zerocopy_realloc 建立用户页到 SKB 的引用映射,避免 copy_from_user 的内存拷贝。完成后通过 MSG_ERRQUEUE 异步通知应用程序何时可以安全释放用户缓冲区。这对于大吞吐量场景(单次 send 超过 1MB)能显著减少 CPU 时间。
4.3 接收路径:recvfrom → tcp_recvmsg
1 | 用户态: recvfrom(fd, buf, len, flags, addr, addrlen) |
tcp_recvmsg_locked(net/ipv4/tcp.c)从 sk_receive_queue 中提取数据:
1 | /* net/ipv4/tcp.c(核心循环简化) */ |
sk_wait_data(net/core/sock.c)等待 sk_receive_queue 有新数据:
1 | /* net/core/sock.c */ |
当网卡中断处理完毕、TCP 将新的 SKB 放入 sk_receive_queue 后,调用 sk->sk_data_ready(sk) 唤醒睡眠中的进程。默认情况下 sk_data_ready = sock_def_readable,它会调用 wake_up_interruptible_sync_poll 唤醒 sk->sk_wq 上的等待者,这正是 epoll 的回调挂载点。
SO_RCVLOWAT 的作用:target = sock_rcvlowat(sk, flags & MSG_WAITALL, len) 决定了阻塞接收时的最小字节阈值。默认值为 1(到达 1 字节即返回),通过 setsockopt(SO_RCVLOWAT, val) 可以提高到任意值,使接收调用在缓冲区中积累足够数据后才返回,减少系统调用次数,但会增加延迟。
五、epoll 实现原理
epoll 是高并发服务器的核心机制,相比 select/poll,它以 O(1) 的代价获取就绪事件。select/poll 的根本缺陷在于每次调用都需要将全部被监控的 fd 集合从用户态拷贝到内核,扫描后再将结果拷贝回来,时间复杂度是 O(n);同时,select 还有 FD_SETSIZE(通常为 1024)的 fd 数量限制。epoll 的设计通过以下三点解决了这些问题:一是用红黑树在内核中持久化存储被监控的 fd 集合,避免每次调用的拷贝;二是用就绪链表只返回真正有事件的 fd,扫描代价为 O(ready_count) 而非 O(total_count);三是通过在 socket 的等待队列上注册回调(而非每次 poll 扫描),将事件检测的开销摊薄到零。
5.1 核心数据结构
1 | /* fs/eventpoll.c */ |
红黑树(rbr)存放所有被监控的 epitem,提供 O(log n) 的 CRUD。
就绪链表(rdllist)存放当前有事件的 epitem,epoll_wait 只需扫描这个链表。
ovflist 是一个无锁单链表,用于在 ep_send_events 持有 ep->mtx 向用户空间传递事件期间,临时存放新到达的 ep_poll_callback 回调产生的就绪项,待传递完成后再合并回 rdllist,确保这段窗口期内不丢失事件。
5.2 epoll_create1:创建 epoll 实例
1 | /* fs/eventpoll.c */ |
epoll 实例本质上是一个持有 eventpoll 结构的匿名文件,/proc/<pid>/fd/ 中会显示 anon_inode:[eventpoll]。注意旧版 epoll_create(size) 的 size 参数在现代内核中已被忽略(仅保留 > 0 的检查以兼容旧程序),推荐使用 epoll_create1(EPOLL_CLOEXEC) 以避免 fd 泄漏到子进程。
5.3 epoll_ctl:注册 fd 并挂入 poll 等待队列
EPOLL_CTL_ADD 的核心是 ep_insert(fs/eventpoll.c):
1 | /* fs/eventpoll.c */ |
ep_ptable_queue_proc 是关键的 poll 回调安装函数:
1 | /* fs/eventpoll.c */ |
这样,当目标 socket 上有数据到达时,内核调用 sk->sk_data_ready(sk) 最终会唤醒 whead 上的所有等待者,其中就包含我们挂入的 ep_poll_callback。
注意 ep_item_poll 调用的是 file->f_op->poll(file, &epq.pt),对于 socket 文件来说这是 sock_poll,它会调用 tcp_poll,在内部调用 poll_wait(file, sk_sleep(sk), wait),从而触发 ep_ptable_queue_proc 将 ep_poll_callback 注册到 socket 的 sk_wq->wait 队列。这是一个巧妙的间接机制:epoll 不是直接操作 socket,而是通过标准 VFS poll 接口”借道”完成等待队列的注册。
5.4 ep_poll_callback:事件到达时的回调
1 | /* fs/eventpoll.c */ |
此函数在中断上下文中执行,因此必须无锁或持自旋锁(ep->lock 是 rwlock_t)。wake_up(&ep->wq) 唤醒调用 epoll_wait 阻塞在 ep->wq 上的进程。
注意 ep_poll_callback 的无锁设计:list_add_tail_lockless 使用 cmpxchg 原子操作将 epi->rdllink 加入 ep->rdllist,而不需要持有 ep->lock 写锁,这对于多 CPU 并发触发同一 epoll 实例上多个 fd 事件的场景至关重要。EPOLLEXCLUSIVE 标志(4.5+ 内核)允许多个 epoll 实例共享监听同一 fd,仅唤醒其中一个(避免”惊群”),其实现就在 ep_poll_callback 中对 EPOLLEXCLUSIVE 的特殊处理分支。
5.5 epoll_wait:等待并收割事件
1 | /* fs/eventpoll.c */ |
5.6 LT vs ET:水平触发与边缘触发的实现区别
两者的差异体现在 ep_send_events → ep_scan_ready_list 的处理逻辑中(fs/eventpoll.c:1754):
1 | /* fs/eventpoll.c */ |
LT(水平触发,默认):只要 socket 接收缓冲区中还有数据,每次 epoll_wait 都会返回该 fd 的 EPOLLIN 事件。实现上,ep_send_events 处理完后若 revents 仍非零,则把 epitem 重新加回 rdllist。
ET(边缘触发,EPOLLET):仅在状态从”无数据”变为”有数据”的瞬间触发一次,epitem 不会被重新放回就绪链表。因此使用 ET 模式时,应用必须循环读取直到 EAGAIN,否则剩余数据永远不会触发新事件。
ET 模式减少了 epoll_wait 的系统调用次数和 rdllist 上的竞争,但对应用的正确性要求更高。
EPOLLONESHOT:与 ET 类似但更进一步——事件触发后会从 epoll 实例中”屏蔽”该 fd(清除其关注的事件掩码),直到显式调用 EPOLL_CTL_MOD 重新激活。这在多线程 epoll 场景中非常有用:每次事件仅由一个线程处理,处理完成后再重新注册,彻底避免多线程同时处理同一 fd 的竞争问题。
EPOLLIN / EPOLLOUT 的 socket 实现:tcp_poll(net/ipv4/tcp.c)根据 tp->rcv_nxt != tp->copied_seq(接收队列有未读数据)返回 EPOLLIN,根据 sk_stream_is_writeable(sk)(发送缓冲区有空间)返回 EPOLLOUT。epoll 收到 EPOLLOUT 事件意味着可以继续调用 send(),而不是意味着数据已经到达对端。
六、Socket 缓冲区与内存压力
6.1 SO_SNDBUF / SO_RCVBUF 的”2 倍”规则
用户通过 setsockopt(SO_SNDBUF, val) 设置发送缓冲区,但内核实际使用的是 val * 2(net/core/sock.c:1154):
1 | /* net/core/sock.c */ |
内核注释解释:额外的空间用于存储 SKB 的内部元数据(skb->truesize 包含 SKB 头部开销)。getsockopt(SO_SNDBUF) 返回的是实际的 sk_sndbuf 值(即用户设置值的 2 倍),这常常让开发者感到困惑。
6.2 tcp_rmem / tcp_wmem 三段参数
通过 sysctl net.ipv4.tcp_rmem 和 net.ipv4.tcp_wmem 可以设置 TCP 缓冲区的动态范围:
| 位置 | 含义 |
|---|---|
tcp_rmem[0] |
接收缓冲区最小值(即使在内存压力下也保证) |
tcp_rmem[1] |
接收缓冲区初始值(新连接默认值) |
tcp_rmem[2] |
接收缓冲区最大值(自动调整上限) |
tcp_wmem[0] |
发送缓冲区最小值 |
tcp_wmem[1] |
发送缓冲区初始值 |
tcp_wmem[2] |
发送缓冲区最大值 |
Linux 的 TCP 自动调整(autotuning)机制在 tcp_rcv_space_adjust 和 tcp_new_space 中实现:根据实际吞吐量动态将 sk_rcvbuf/sk_sndbuf 在 [min, max] 范围内调整。
如果显式调用了 SO_SNDBUF/SO_RCVBUF,会设置 sk_userlocks |= SOCK_SNDBUF_LOCK,此后自动调整机制会跳过该 socket。
实践建议:除非有明确的基准测试证明手动设置更优,否则建议依赖内核自动调整(保持 tcp_moderate_rcvbuf = 1,不显式设置 SO_RCVBUF)。手动固定缓冲区大小会使内核失去根据 RTT 和带宽动态优化的能力,在高延迟网络(BDP 较大)上可能导致严重的吞吐量损失。
6.3 内存压力检测:sk_stream_is_writeable
发送路径在两个层面检测内存是否充足:
per-socket 层面:
1 | /* include/net/sock.h */ |
sk_wmem_queued 是 sk_write_queue 中排队等待发送的字节总量(含 SKB 元数据),当它超过 sk_sndbuf 时,tcp_sendmsg 进入 sk_stream_wait_memory 睡眠。
全局层面:TCP 有全局内存计数器 tcp_memory_allocated,当它超过 sysctl_tcp_mem[1](pressure 阈值)时,进入”内存压力”模式,sk_under_memory_pressure(sk) 返回 true,新的 SKB 分配会更保守。当超过 sysctl_tcp_mem[2](hard limit)时,内核会拒绝新的内存分配请求,tcp_sendmsg 返回 ENOMEM。sysctl_tcp_mem 的单位是 pages(4KB),默认值由系统总内存按比例计算,通常为系统内存的 1/4 ~ 1/2。
orphaned socket 内存:/proc/net/sockstat 中的 orphan 计数是已关闭但还在 FIN_WAIT_1/CLOSING/LAST_ACK 等状态等待最终确认的 socket,它们不再与任何进程关联但仍占用内存。内核通过 sysctl_tcp_max_orphans 限制其数量,超出时新的 close() 会直接发 RST 而非正常四次挥手。
七、诊断方法
7.1 ss:完整 socket 统计
1 | ss -tmenop |
各选项含义:
-t:只显示 TCP socket-m:显示内存信息(skmem)-e:显示详细扩展信息(timer、retransmits、uid 等)-n:不解析服务名-o:显示 timer 信息-p:显示关联的进程
输出中的 skmem 字段格式为 (r<rmem_alloc>,rb<sk_rcvbuf>,t<wmem_alloc>,tb<sk_sndbuf>,f<fwd_alloc>,w<wmem_queued>,o<omem_alloc>,bl<backlog>,d<drops>):
1 | Recv-Q Send-Q Local Address:Port Peer Address:Port |
rb131072 表示 sk_rcvbuf=128KB,tb87040 表示 sk_sndbuf(约 85KB)。
7.2 strace 追踪网络系统调用
1 | strace -e trace=network -p <pid> |
典型输出:
1 | epoll_wait(5, [{events=EPOLLIN, data={u32=7, u64=7}}], 1024, -1) = 1 |
7.3 /proc/net/sockstat:全局统计
1 | cat /proc/net/sockstat |
示例输出:
1 | sockets: used 1024 |
TCP mem 34 表示 TCP 全局内存使用了 34 个 page(tcp_memory_allocated),对照 sysctl net.ipv4.tcp_mem 的三个阈值可判断是否处于内存压力。tw 是 TIME_WAIT 状态的连接数,过高说明短连接过多。
7.4 bpftrace 追踪 tcp_sendmsg/tcp_recvmsg 延迟
1 | # 追踪 tcp_sendmsg 调用延迟(纳秒) |
7.5 perf stat 分析 socket 系统调用开销
1 | # 统计 sendto/recvfrom 系统调用次数和 CPU 周期 |
典型优化发现:若 epoll_wait 系统调用次数远高于 recvfrom,说明存在频繁的虚假唤醒(spurious wakeup)或事件处理不彻底(LT 模式未及时读完数据导致反复触发)。
7.6 常见 socket 性能问题速查
| 现象 | 诊断命令 | 根因 |
|---|---|---|
connect() 返回 EADDRNOTAVAIL |
ss -s 查看 TIME_WAIT 数量 |
本地端口耗尽,调大 ip_local_port_range 或开启 tcp_tw_reuse |
send() 返回 EAGAIN 且 Send-Q 不增长 |
ss -tmenop 查看 tb 字段 |
发送缓冲区满,对端接收窗口为 0 或网络拥塞 |
epoll_wait 返回但 recv 无数据 |
strace + ss 查看 Recv-Q |
多线程竞争同一 fd(LT 模式 “intra-thread” 问题),或 EPOLLHUP 误触发 |
高 %si(软中断 CPU) |
cat /proc/net/softnet_stat |
网卡接收包速率过高,考虑 RSS/RPS 多队列分散 |
| socket 内存压力 OOM | /proc/net/sockstat mem 值接近 tcp_mem[2] |
降低单连接缓冲区或限制连接总数 |
小结
Linux socket 层是用户态与内核网络协议栈之间精心设计的多层抽象。struct socket 负责 VFS 集成;struct sock 提供协议无关的通用状态;struct inet_sock 和 struct tcp_sock 在其上叠加协议专用字段。__sys_socket 到 inet_create 的创建链把三者组装为一个整体,而 sock_map_fd 完成与 VFS 的最终绑定。
发送路径(tcp_sendmsg)和接收路径(tcp_recvmsg)均围绕 sk_write_queue/sk_receive_queue 工作,sk_stream_wait_memory 和 sk_wait_data 提供阻塞语义。epoll 通过在 socket 等待队列上挂载 ep_poll_callback,实现了 O(1) 的事件通知,LT/ET 差异仅在于就绪后是否重新入队。理解这些机制,是排查高并发 I/O 问题、调优缓冲区参数的前提。
相关源文件速查:
net/socket.c— socket 系统调用入口net/ipv4/af_inet.c— AF_INET socket 创建与连接net/ipv4/tcp.c— TCP 发送/接收核心net/core/stream.c—sk_stream_wait_memorynet/core/sock.c—sk_wait_data、SO_SNDBUF/SO_RCVBUFfs/eventpoll.c— epoll 完整实现include/linux/net.h—struct socket、struct proto_opsinclude/net/sock.h—struct sock、struct sock_commoninclude/net/inet_sock.h—struct inet_sockinclude/linux/tcp.h—struct tcp_sock