3. 系统调用入口:send/sendto/write 系统调用的内核入口路径分析

当用户态应用程序通过 Socket 发送数据时,首先触发的是系统调用。无论是 send()、sendto() 还是 write(),它们在 Linux 内核中的入口路径最终都会汇聚到同一个核心函数——sock_sendmsg()。本章将深入分析这三条路径的异同、内核入口的调用链以及关键数据结构的变化。

3.1 系统调用表与入口函数注册

Linux 内核在初始化时为每个系统调用分配一个唯一的编号,并在系统调用表中注册对应的处理函数。对于网络相关的系统调用,其注册关系如下表所示:

用户态 API 系统调用号 内核入口函数 glibc 封装行为
send() __NR_sendto (通常复用 sendto) __sys_sendto() 将 flags 参数置 0,调用 sendto
sendto() __NR_sendto __sys_sendto() 直接传递所有参数
write() __NR_write ksys_write() 通过 fd 找到 file 结构,调用 file->f_op->write
关键点: 在 Linux 内核 5.x 及更高版本中,send() 在 glibc 层被实现为 sendto() 的特例(目标地址为 NULL,地址长度参数为 0)。因此,内核中实际只有 sendto 和 write 两条独立的入口路径。

3.2 send/sendto 路径:__sys_sendto() 深度解析

__sys_sendto() 是 send() 和 sendto() 在内核中的统一入口。其核心流程分为三个阶段:参数校验与地址拷贝、socket 文件描述符解析、以及消息构造与发送。

3.2.1 参数校验与用户态地址拷贝

内核首先需要从用户态安全地拷贝目标地址结构。这一过程涉及 copy_from_user() 函数,确保用户提供的地址指针合法且数据完整。

// net/socket.c (简化内核源码)
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
                unsigned int, flags, struct sockaddr __user *, addr,
                int, addr_len)
{
    struct sockaddr_storage address;
    int err;

    // 如果用户提供了目标地址,则从用户态拷贝到内核态
    if (addr) {
        err = move_addr_to_kernel(addr, addr_len, &address);
        if (err < 0)
            return err;
    }
    // ... 后续调用 __sys_sendto()
    return __sys_sendto(fd, buff, len, flags, 
                        addr ? (struct sockaddr *)&address : NULL, 
                        addr_len);
}
安全注意: move_addr_to_kernel() 内部使用 copy_from_user() 进行安全拷贝。如果用户提供的地址指针指向非法内存区域,内核会返回 -EFAULT,不会导致内核崩溃。

3.2.2 socket 文件描述符解析

内核通过文件描述符 fd 查找对应的 struct socket 结构。这一过程通过 sockfd_lookup_light() 完成,它从当前进程的文件描述符表中获取 struct file,再通过 file->private_data 获取 struct socket 指针。

// __sys_sendto 核心逻辑
int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
                 struct sockaddr *addr, int addr_len)
{
    struct socket *sock;
    struct file *file;
    int err;

    // 1. 通过 fd 获取 socket 结构
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        return err;

    // 2. 构造消息头
    struct msghdr msg;
    struct iovec iov;
    // ... 初始化 msg 和 iov

    // 3. 调用核心发送函数
    err = sock_sendmsg(sock, &msg, len);

    // 4. 释放引用
    fput_light(file, fput_needed);
    return err;
}

3.3 write 路径:从 ksys_write() 到 sock_write_iter()

write() 系统调用走的是通用文件 I/O 路径。内核入口 ksys_write() 通过 VFS(虚拟文件系统)层调用文件操作函数表中的 write 方法。对于 socket 文件描述符,这个函数指针指向 sock_write_iter()。

3.3.1 VFS 层分发机制

当进程调用 write(fd, buf, len) 时,内核执行以下路径:

  1. ksys_write():获取 struct file,调用 file->f_op->write_iter()
  2. sock_write_iter():将 struct kiocb 转换为 struct msghdr
  3. sock_sendmsg():进入与 sendto 相同的核心路径
// net/socket.c - sock_write_iter 实现
static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
    struct file *file = iocb->ki_filp;
    struct socket *sock = file->private_data;
    struct msghdr msg = { .msg_iter = *from, .msg_flags = 0 };

    // 如果 socket 未连接,返回错误
    if (unlikely(!sock->sk))
        return -ENOTCONN;

    // 调用核心发送函数
    return sock_sendmsg(sock, &msg, iov_iter_count(from));
}
路径差异总结: send/sendto 路径在 __sys_sendto() 中构造 struct msghdr,而 write 路径在 sock_write_iter() 中构造。两者最终都调用 sock_sendmsg(),实现了路径的统一。

3.4 三条路径的统一汇聚点:sock_sendmsg()

无论从哪条路径进入,最终都会调用 sock_sendmsg()。这个函数是用户态到协议栈的桥梁,它完成以下关键操作:

  • 权限检查:验证进程是否有权限发送数据(如 CAP_NET_ADMIN 等)
  • 消息头验证:检查 struct msghdr 的合法性
  • 协议分发:调用 sock->ops->sendmsg(),进入具体的传输层协议(如 TCP 的 tcp_sendmsg())
// net/socket.c - sock_sendmsg 实现
int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
    struct sockaddr_storage *address = msg->msg_name;
    int err;

    // 安全审计(如果启用)
    if (unlikely(!sock->file))
        return -EBADF;

    // 调用协议层的 sendmsg 回调
    err = sock->ops->sendmsg(sock, msg, size);
    return err;
}

3.5 系统调用入口路径全景图

下面的 SVG 图展示了从用户态 API 到内核核心函数 sock_sendmsg() 的完整调用路径:

系统调用入口路径全景图 send() sendto() write() glibc 封装为 sendto __sys_sendto() ksys_write() move_addr_to_kernel() sockfd_lookup_light() sock_write_iter() sock_sendmsg() 统一入口点 tcp_sendmsg() / udp_sendmsg() 三条路径在此汇聚, 后续流程完全一致

3.6 关键数据结构对比

在系统调用入口阶段,不同路径操作的关键数据结构有所差异。下表总结了这些差异:

数据结构 send/sendto 路径 write 路径 说明
struct msghdr 在 __sys_sendto() 中构造 在 sock_write_iter() 中构造 包含消息头、地址、IO 向量等信息
struct iovec 由用户态 buff 和 len 构造 从 struct iov_iter 转换而来 描述用户态数据缓冲区
struct sockaddr_storage 从用户态拷贝到内核栈 不使用(依赖已建立的连接) 仅 sendto 需要目标地址
struct file 通过 sockfd_lookup_light() 获取 由 VFS 层直接传入 引用计数管理方式不同

3.7 性能开销分析

系统调用入口路径的性能开销主要来自以下几个方面:

  • 用户态/内核态切换:每次系统调用都需要 CPU 模式切换,涉及上下文保存与恢复
  • 数据拷贝:copy_from_user() 将用户态数据拷贝到内核态,对于大数据量传输是主要瓶颈
  • 文件描述符查找:sockfd_lookup_light() 需要遍历进程的文件描述符表
  • 权限检查:内核需要验证进程的权限和 socket 状态
优化建议: 对于高频发送场景,建议使用 sendmmsg() 系统调用(批量发送)或 io_uring 异步 I/O 框架,以减少用户态/内核态切换次数,降低系统调用开销。

3.8 本章小结

系统调用入口路径是数据包发送流程的第一道关卡。通过本章分析,可以得出以下核心结论:

  1. 路径统一性:send()、sendto() 和 write() 三条路径最终汇聚到 sock_sendmsg(),实现了代码复用
  2. 安全边界:内核通过 copy_from_user() 和 sockfd_lookup_light() 确保用户态数据的安全性
  3. 协议无关性:系统调用层不关心具体传输协议,通过 sock->ops->sendmsg() 实现多态分发
  4. 性能关键点:用户态/内核态切换和数据拷贝是主要性能瓶颈,后续章节将深入分析如何优化