前言
上一篇文章我们讲了socket()系统调用:它仅仅在内核创建一个空的套接字对象,分配 fd、初始化sock结构体等等,但是没有 IP、没有端口,没有任何地址信息,内核不知道要从哪个网卡、哪个端口收发数据。今天继续系列第二篇,拆解函数bind():
一、一行代码,看懂入口
1.1 函数原型
intbind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数详解:
- sockfd:socket()创建的套接字文件描述符
返回值:
1.2 极简示例(TCP 服务端)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);structsockaddr_inserv_addr;serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(8080);// 绑定所有网卡serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);int ret = bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
二、调用 bind()时,内核默默做了什么事情
2.1 关键结构体
// struct inet_hashinfo 是 Linux 内核网络协议栈中用于管理 TCP/UDP 连接查找的关键数据结构structinet_hashinfo{// 所有建立连接的socket,哈希key:四元组structinet_ehash_bucket *ehash;// 所有绑定端口的socket,哈希key:端口structinet_bind_hashbucket *bhash;// 所有监听scoket,哈希key:端口structinet_listen_hashbucketlistening_hash[INET_LHTABLE_SIZE];};
四元组:网络通信中唯一标识一条 TCP 连接的四个要素。
有时也会提到五元组,就是在四元组基础上加上传输层协议号(TCP/UDP),用于区分不同协议的连接。
2.2 bind()完整执行链路
socket的相关函数都在net\socket.c中,bind()方法主要内容如下:
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen){structsocket *sock;structsockaddr_storageaddress;int err, fput_needed;// 由 fd 获取 监听的 socket sock = sockfd_lookup_light(fd, &err, &fput_needed);if (sock) {// 将用户态地址拷贝到内核中 err = move_addr_to_kernel(umyaddr, addrlen, &address);// 实际调用 inet_stream_ops 中的 inet_bind(详见第一篇文章,socket已绑定操作集) err = sock->ops->bind(sock, (struct sockaddr *)&address, addrlen); }return err;}
inet_bind主要做的工作包括:
intinet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len){// 确保 sock 处于 TCP_CLOSE 状态且尚未绑定过端口if (sk->sk_state != TCP_CLOSE || inet->inet_num) goto out_release_sock;// inet_rcv_saddr:收包是谁// inet_saddr:发包是谁// 在普通单播 bind 时,它们是同一个值,此时确定了四元组中的源IP inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;// 绑定端口,TCP协议实际调用的是 inet_csk_get_port sk->sk_prot->get_port(sk, snum);// 到这里,四元组中的 源地址 源端口 确定好了 inet->inet_sport = htons(inet->inet_num); inet->inet_daddr = 0; inet->inet_dport = 0;}
rcv_saddr和saddr的双地址设计:
inet_csk_get_port比较复杂,主要内容总结如下:
- 若用户没指定端口或指定端口为0,则从系统临时端口范围(由
ip_local_port_range 控制)内自动选取一个可用的端口。
- 通过查找哈希表(
bhash),检查目标端口是否已被占用。 - 若端口已被其他套接字使用,则根据是否允许地址复用(
SO_REUSEADDR / SO_REUSEPORT)以及绑定的具体地址(INADDR_ANY 或特定 IP)判断是否冲突。 - 若无冲突,则将当前套接字加入bhash,并更新套接字的本地端口字段。
三、总结
socket()创建空白套接字;bind()为套接字分配本地 IP + 端口,在内核建立地址映射- bind 内核流程:状态校验→拷贝地址→端口冲突检查→填充 socket 地址→注册bhash
- 服务端必须 bind 固定端口,方便客户端访问;客户端默认依靠 connect 触发隐式绑定
四、后续预告
本文基于 Linux 内核源码梳理 bind 的底层实现,文中如有错误或者理解偏差,欢迎大家在评论区留言指正,一起交流探讨。下一篇继续拆解:listen()到底完成了什么? 深入解析 TCP 半连接队列、全连接队列、backlog 参数真实作用。