目录
[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 file、struct socket、struct 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,并建立半连接队列 和全连接队列。
cppSYSCALL_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结构体里):
- 半连接队列(SYN Queue):存放只完成三次握手前两步的连接(收到 SYN,回复了 SYN+ACK,等待客户端最后的 ACK)。
- 全连接队列(Accept Queue) :存放已完成三次握手,等待应用程序调用
accept()取走的连接。backlog参数就是用来限制全连接队列长度的,防溢出。- 移位 :内核会将这个
sock从通用的绑定哈希表,移动到专门的监听哈希表(Listening Hash Table)中。此时,它真正成了一个服务端 socket。4.
connect(fd, {IP=10.0.0.1, Port=8080}):主动出击,三次握手用户态动作: 客户端调用
connect(),发起连接。
内核核心任务: 自动 Bind(如果没 Bind),构造 SYN 包发出去,阻塞等待三次握手完成。
cppSYSCALL_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_ESTABLISHED,connect()返回 0。5.
accept(fd, ...):提取成果,孵化新连接用户态动作: 服务端调用
accept()。
内核核心任务: 从全连接队列里取出一个已完成的连接,创建一个全新的 socket 返回给用户。
cppSYSCALL_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赋给新创建的newsock:newsock->sk = child_sock;- 状态更新 :
child_sock->sk_state = TCP_ESTABLISHED;- 返回新 fd :用户拿到的
newfd,对应着底层的newsock和child_sock。
把这五步串起来,内核的资源变化就像这样:
socket():造了一个空壳监听套接字(fd_listen->file_L->sock_L->sk_L(CLOSE))。bind():给sk_L贴上本地门牌号(IP:Port),放入绑定哈希表。listen():给sk_L升级状态(LISTEN),分配半连接/全连接队列,放入监听哈希表。connect():客户端自动 Bind,构造请求包发给sk_L,完成握手进入sk_L的全连接队列。accept():从sk_L的全连接队列里捞出已经握好手的底层sk_child,造一个全新的上层壳 (fd_new->file_C->sock_C),把sk_child装进去,返回fd_new。