前言
上一篇文章我们讲了socket()系统调用:它仅仅在内核创建一个空的套接字对象,分配 fd、初始化sock结构体等等,但是没有 IP、没有端口,没有任何地址信息,内核不知道要从哪个网卡、哪个端口收发数据。今天继续系列第二篇,拆解函数bind():
- bind到底干了什么?
- 为什么客户端通常不需要 bind?
- 内核端口绑定原理?
一、一行代码,看懂入口
1.1 函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数详解:
- sockfd:socket()创建的套接字文件描述符
- addr:地址结构体,包含协议族、IP、端口
- addrlen:地址结构体长度
返回值:
- 成功:返回0
- 失败:返回-1,并设置errno
1.2 极简示例(TCP 服务端)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_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 连接查找的关键数据结构
struct inet_hashinfo
{
// 所有建立连接的socket,哈希key:四元组
struct inet_ehash_bucket *ehash;
// 所有绑定端口的socket,哈希key:端口
struct inet_bind_hashbucket *bhash;
// 所有监听scoket,哈希key:端口
struct inet_listen_hashbucket listening_hash[INET_LHTABLE_SIZE];
};
四元组:网络通信中唯一标识一条 TCP 连接的四个要素。
- 源 IP 地址(Source IP)
- 源端口号(Source Port)
- 目的 IP 地址(Destination IP)
- 目的端口号(Destination Port)
有时也会提到五元组,就是在四元组基础上加上传输层协议号(TCP/UDP),用于区分不同协议的连接。
2.2 bind()完整执行链路
socket的相关函数都在net\socket.c中,bind()方法主要内容如下:
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
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主要做的工作包括:
int inet_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_rcv_saddr | 收包时用于四元组匹配 | 始终等于用户 bind 的地址 |
inet_saddr | 发包时填充 IP 源地址 | 多播/广播时置 0,由出包路径决定 |
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 触发隐式绑定
1078



被折叠的 条评论
为什么被折叠?



