3、套接字层(Socket Layer):socket系统调用流程,struct socket与struct sock的关系,协议族注册与管理

套接字层,说白了就是用户态和内核协议栈之间的「接待前台」。你写网络程序时调用的 socket()、bind()、connect() 这些 API,最终都会落到这一层来处理。我刚开始啃内核源码时,总觉得这层就是个简单的分发器,后来深入一看才发现——里面的设计相当精巧。

3.1 socket系统调用的完整流程

我们先从最常用的 socket() 系统调用说起。你在应用层写这么一行:

int fd = socket(AF_INET, SOCK_STREAM, 0);

这行代码在内核里会经历什么?我把它拆成几个关键步骤:

  1. 用户态陷入内核:glibc 封装触发 sys_socketcall() 或直接调用 __sys_socket()。
  2. 分配 struct socket:内核调用 sock_create(),在 VFS 层创建一个 socket 对象。
  3. 协议族匹配:根据 AF_INET 找到对应的 inet_family_ops,调用其 create 方法。
  4. 创建 struct sock:在 inet_create() 内部,根据 SOCK_STREAM 查表找到 tcp_prot,分配并初始化 struct sock。
  5. 绑定文件描述符:将 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 创建流程 inet_init() sock_register(inet_family_ops) net_families[AF_INET] = ops socket(AF_INET, SOCK_STREAM, 0) __sock_create() 查找 net_families[AF_INET] 查找 inet_create() 分配 struct socket + struct sock 注册在模块初始化时完成 创建在每次 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 是怎么被创建和关联起来的。这种「跟着代码走一遍」的方式,比死记硬背结构体字段要有效得多。