网络编程时内核究竟做了什么???

目录

[1. socket(AF_INET, SOCK_STREAM, 0):申请资源,搭建套娃](#1. socket(AF_INET, SOCK_STREAM, 0):申请资源,搭建套娃)

[2. bind(fd, {IP=192.168.1.100, Port=80}):贴门牌号,登记入册](#2. bind(fd, {IP=192.168.1.100, Port=80}):贴门牌号,登记入册)

[3. listen(fd, backlog):转换身份,搭起候客大厅](#3. listen(fd, backlog):转换身份,搭起候客大厅)

[4. connect(fd, {IP=10.0.0.1, Port=8080}):主动出击,三次握手](#4. connect(fd, {IP=10.0.0.1, Port=8080}):主动出击,三次握手)

[5. accept(fd, ...):提取成果,孵化新连接](#5. accept(fd, ...):提取成果,孵化新连接)

为了方便理解,我们以 TCP/IPv4 为例,且将内核代码极度简化,只保留最核心的骨架。

1. socket(AF_INET, SOCK_STREAM, 0):申请资源,搭建套娃

用户态动作: 调用 socket(),返回一个文件描述符 fd
内核核心任务: 创建 struct filestruct socketstruct sock,并把它们串联起来。

cpp 复制代码
// 系统调用入口
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    // 1. 创建 struct socket 结构体
    struct socket *sock;
    sock_create(family, type, protocol, &sock);

    // 2. 创建 struct file 结构体,并将 file->private_data 指向 sock
    struct file *file;
    sock_alloc_file(sock, &file, flags); 
    
    // 3. 为 struct file 分配 fd,并安装到当前进程的文件描述符表
    int fd = get_unused_fd_flags(flags);
    fd_install(fd, file); 

    return fd; // 返回给用户
}
  • 深度拆解:

  • sock_create :不仅分配了 struct socket,还因为传入的 family 是 AF_INET,底层调用了 inet_create(),分配了真正干活的 struct sock

  • 链接套娃

    • file->private_data = sock; (VFS 找到 Socket 的桥梁)
    • sock->file = file; (Socket 反向找到 VFS 的桥梁)
    • sock->sk 指向了底层的 struct sock
  • 初始化函数表

    • file->f_op = &socket_file_ops; (VFS 层操作表)
    • sock->ops = &inet_stream_ops; (INET 层操作表,因为是 TCP)
    • sock->sk->sk_prot = &tcp_prot; (TCP 具体协议操作表)
  • 此时的状态 :资源分配完毕,但 struct sock 里的本地 IP 和端口全是 0,处于 TCP_CLOSE 状态。

2. bind(fd, {IP=192.168.1.100, Port=80}):贴门牌号,登记入册

用户态动作: 调用 bind(),将 socket 绑定到指定的 IP 和端口。
内核核心任务: 将地址写入 struct sock,并将该 sock 对象加入内核的全局哈希表,以便后续网卡收包时能快速找到它。

cpp 复制代码
// 系统调用入口
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
    // 1. 通过 fd 找到 struct socket (回顾之前的降级过程)
    struct socket *sock = sockfd_lookup_light(fd, &err, &fput_needed);
    
    // 2. 拷贝用户态的地址结构到内核态
    struct sockaddr_storage address;
    copy_from_user(&address, umyaddr, addrlen);

    // 3. 调用 inet_stream_ops->bind,最终走到 inet_bind
    sock->ops->bind(sock, (struct sockaddr *)&address, addrlen);
    
    // ... 释放资源
}

深度拆解(进入 inet_bind):

  • 检查冲突 :内核去全局的 TCP 哈希表(tcp_hashinfo)里查,这个 IP+端口 是不是已经被别人占了(如果没有设置 SO_REUSEPORT 等特权,直接报错 EADDRINUSE)。
  • 写入结构体
    • sock->sk->sk_rcv_saddr = 192.168.1.100; (绑定本地 IP)
    • sock->sk->sk_num = 80; (绑定本地端口)
  • 挂入哈希表 :内核把这个 struct sock 挂到以端口号为 Key 的哈希桶里。从此,网卡收到目标端口是 80 的包,内核就能顺着哈希表找到这个 sock

3. listen(fd, backlog):转换身份,搭起候客大厅

用户态动作: 调用 listen(),声明这个 socket 要开始接收连接了。
内核核心任务: 修改 TCP 状态为 LISTEN,并建立半连接队列全连接队列

cpp 复制代码
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    struct socket *sock = sockfd_lookup_light(fd, ...);
    
    // 调用 inet_stream_ops->listen,最终走到 inet_listen
    sock->ops->listen(sock, backlog);
}

深度拆解(进入 inet_listen):

  • 状态变更sock->sk->sk_state = TCP_LISTEN; (从 CLOSE 变为 LISTEN)。
  • 分配队列 :这是 listen 最核心的动作。内核在 struct sock 内部初始化两个重要队列(准确说是 inet_connection_sock 结构体里):
    1. 半连接队列(SYN Queue):存放只完成三次握手前两步的连接(收到 SYN,回复了 SYN+ACK,等待客户端最后的 ACK)。
    2. 全连接队列(Accept Queue) :存放已完成三次握手,等待应用程序调用 accept() 取走的连接。
  • backlog 参数就是用来限制全连接队列长度的,防溢出。
  • 移位 :内核会将这个 sock 从通用的绑定哈希表,移动到专门的监听哈希表(Listening Hash Table)中。此时,它真正成了一个服务端 socket。

4. connect(fd, {IP=10.0.0.1, Port=8080}):主动出击,三次握手

用户态动作: 客户端调用 connect(),发起连接。
内核核心任务: 自动 Bind(如果没 Bind),构造 SYN 包发出去,阻塞等待三次握手完成。

cpp 复制代码
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr, int, addrlen)
{
    struct socket *sock = sockfd_lookup_light(fd, ...);
    
    // 调用 inet_stream_ops->connect,走到 inet_stream_connect
    sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags);
}

深度拆解(进入 inet_stream_connect):

  • 自动 Bind :如果发现 sock->sk->sk_num == 0(没有绑定端口),内核自动调用 inet_autobind(),随机分配一个可用端口并写入结构体、挂入哈希表。
  • 记录目标 :将对方的 IP 和端口写入 sock->sk
    • sock->sk->sk_daddr = 10.0.0.1;
    • sock->sk->sk_dport = 8080;
  • 发起握手 :修改状态为 TCP_SYN_SENT,调用 TCP 层的 tcp_v4_connect,构造 SYN 报文交给 IP 层发送。
  • 阻塞等待 :将当前进程挂起,挂在 sock->sk->sk_wq(等待队列)上,直到收到服务端的 SYN+ACK,并回复 ACK 后,内核唤醒进程,状态变为 TCP_ESTABLISHEDconnect() 返回 0。

5. accept(fd, ...):提取成果,孵化新连接

用户态动作: 服务端调用 accept()
内核核心任务: 从全连接队列里取出一个已完成的连接,创建一个全新的 socket 返回给用户。

cpp 复制代码
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr, ...)
{
    struct socket *sock = sockfd_lookup_light(fd, ...); // 这是监听 socket

    // 1. 调用 inet_stream_ops->accept,走到 inet_accept
    struct socket *newsock;
    sock_alloc_file(newsock, ...); // 【注意!】这里创建了一个全新的 struct socket 和 struct file
    
    sock->ops->accept(sock, newsock, ...); // 将握手完成的连接转移给 newsock

    // 2. 为新 socket 分配新的 fd
    int newfd = get_unused_fd_flags(flags);
    fd_install(newfd, newsock->file);

    return newfd; // 返回新 fd
}

深度拆解(进入 inet_accept):

  • 检查队列:检查监听 socket 的全连接队列是否为空。如果为空,且没有设置非阻塞,则将当前进程挂起,等待三次握手完成唤醒。
  • 提取子 sock :从全连接队列里弹出一个已经完成握手的 struct sock(我们叫它 child_sock)。
  • 乾坤大挪移 :把 child_sock 赋给新创建的 newsocknewsock->sk = child_sock;
  • 状态更新child_sock->sk_state = TCP_ESTABLISHED;
  • 返回新 fd :用户拿到的 newfd,对应着底层的 newsockchild_sock
    把这五步串起来,内核的资源变化就像这样:
  1. socket() :造了一个空壳监听套接字(fd_listen -> file_L -> sock_L -> sk_L(CLOSE))。
  2. bind() :给 sk_L 贴上本地门牌号(IP:Port),放入绑定哈希表。
  3. listen() :给 sk_L 升级状态(LISTEN),分配半连接/全连接队列,放入监听哈希表。
  4. connect() :客户端自动 Bind,构造请求包发给 sk_L,完成握手进入 sk_L 的全连接队列。
  5. accept() :从 sk_L 的全连接队列里捞出已经握好手的底层 sk_child造一个全新的上层壳fd_new -> file_C -> sock_C),把 sk_child 装进去,返回 fd_new
相关推荐
原来是猿1 小时前
腾讯云服务器端口开放完全指南
服务器·网络·腾讯云
你的保护色2 小时前
【无标题】
java·服务器·网络
楼兰公子2 小时前
RK3588 + Linux7.0.3 网络工程调试错误速查手册
linux·网络·3588
Elnaij2 小时前
Linux系统与系统编程(9)——自设计shell与基础IO
linux·服务器
IpdataCloud2 小时前
稳定的企业级IP数据接口怎么选?可用性指标+离线库高可用方案
运维·网络·tcp/ip
HMS工业网络2 小时前
如何解决使用TwinCAT时EtherCAT网络出现“Sync Manager Watchdog”报错
网络·网络协议·网络安全
IMPYLH2 小时前
Linux 的 unexpand 命令
linux·运维·服务器·bash
想唱rap3 小时前
IO多路转接之poll
服务器·开发语言·数据库·c++
|_⊙3 小时前
Linux 文件知识 补充
linux·运维·服务器