从Linux源码看Socket(TCP)的bind

从Linux源码看Socket(TCP)的bind 前言

笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情。 今天笔者就来从Linux源码的角度看下Server端的Socket在进行bind的时候到底做了哪些事情(基于Linux 3.10内核)。

一个最简单的Server端例子

众所周知,一个Server端Socket的建立,需要socket、bind、listen、accept四个步骤。

从Linux源码看Socket(TCP)的bind


代码如下:

void start_server(){ // server fd int sockfd_server; // accept fd int sockfd; int call_err; struct sockaddr_in sock_addr; sockfd_server = socket(AF_INET,SOCK_STREAM,0); memset(&sock_addr,0,sizeof(sock_addr)); sock_addr.sin_family = AF_INET; sock_addr.sin_addr.s_addr = htonl(INADDR_ANY); sock_addr.sin_port = htons(SERVER_PORT); // 这边就是我们今天的聚焦点bind call_err=bind(sockfd_server,(struct sockaddr*)(&sock_addr),sizeof(sock_addr)); if(call_err == -1){ fprintf(stdout,"bind error!\n"); exit(1); } // listen call_err=listen(sockfd_server,MAX_BACK_LOG); if(call_err == -1){ fprintf(stdout,"listen error!\n"); exit(1); } }

首先我们通过socket系统调用创建了一个socket,其中指定了SOCK_STREAM,而且最后一个参数为0,也就是建立了一个通常所有的TCP Socket。在这里,我们直接给出TCP Socket所对应的ops也就是操作函数。

从Linux源码看Socket(TCP)的bind


如果你想知道上图中的结构是怎么来的,可以看下笔者以前的博客:

https://my.oschina.net/alchemystar/blog/1791017 bind系统调用

bind将一个本地协议地址(protocol:ip:port)赋予一个套接字。例如32位的ipv4地址或128位的ipv6地址+16位的TCP活UDP端口号。

#include <sys/socket.h> // 返回,若成功则为0,若出错则为-1 int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

好了,我们直接进入Linux源码调用栈吧。

bind // 这边由系统调用的返回值会被glibc的INLINE_SYSCALL包一层 // 若有错误,则设置返回值为-1,同时将系统调用的返回值的绝对值设置给errno |->INLINE_SYSCALL (bind......); |->SYSCALL_DEFINE3(bind......); /* 检测对应的描述符fd是否存在,不存在,返回-BADF |->sockfd_lookup_light |->sock->ops->bind(inet_stream_ops) |->inet_bind |->AF_INET兼容性检查 |-><1024端口权限检查 /* bind端口号校验or选择(在bind为0的时候) |->sk->sk_prot->get_port(inet_csk_get_port) inet_bind

inet_bind这个函数主要做了两个操作,一是检测是否允许bind,而是获取可用的端口号。这边值得注意的是。如果我们设置需要bind的端口号为0,那么Kernel会帮我们随机选择一个可用的端口号来进行bind!

// 让系统随机选择可用端口号 sock_addr.sin_port = 0; call_err=bind(sockfd_server,(struct sockaddr*)(&sock_addr),sizeof(sock_addr));

让我们看下inet_bind的流程

从Linux源码看Socket(TCP)的bind


值得注意的是,由于对于<1024的端口号需要CAP_NET_BIND_SERVICE,我们在监听80端口号(例如启动nginx时候),需要使用root用户或者赋予这个可执行文件CAP_NET_BIND_SERVICE权限。

use root or setcap cap_net_bind_service=+eip ./nginx

我们的bind允许绑定到0.0.0.0即INADDR_ANY这个地址上(一般都用这个),它意味着内核去选择IP地址。对我们最直接的影响如下图所示:

从Linux源码看Socket(TCP)的bind


然后,我们看下一个比较复杂的函数,即可用端口号的选择过程inet_csk_get_port
(sk->sk_prot->get_port)

inet_csk_get_port 第一段,如果bind port为0,随机搜索可用端口号

直接上源码,第一段代码为端口号为0的搜索过程

// 这边如果snum指定为0,则随机选择端口 int inet_csk_get_port(struct sock *sk, unsigned short snum) { ...... // 这边net_random()采用prandom_u32,是伪(pseudo)随机数 smallest_rover = rover = net_random() % remaining + low; smallest_size = -1; // snum=0,随机选择端口的分支 if(!sum){ // 获取内核设置的端口号范围,对应内核参数/proc/sys/net/ipv4/ip_local_port_range inet_get_local_port_range(&low,&high); ...... do{ if(inet_is_reserved_local_port(rover) goto next_nonlock; // 不选择保留端口号 ...... inet_bind_bucket_for_each(tb, &head->chain) // 在同一个网络命名空间下存在和当前希望选择的port rover一样的port if (net_eq(ib_net(tb), net) && tb->port == rover) { // 已经存在的sock和当前新sock都开启了SO_REUSEADDR,且当前sock状态不为listen // 或者 // 已经存在的sock和当前新sock都开启了SO_REUSEPORT,而且两者都是同一个用户 if (((tb->fastreuse > 0 && sk->sk_reuse && sk->sk_state != TCP_LISTEN) || (tb->fastreuseport > 0 && sk->sk_reuseport && uid_eq(tb->fastuid, uid))) && (tb->num_owners < smallest_size || smallest_size == -1)) { // 这边是选择一个最小的num_owners的port,即同时bind或者listen最小个数的port // 因为一个端口号(port)在开启了so_reuseaddr/so_reuseport之后,是可以多个进程同时使用的 smallest_size = tb->num_owners; smallest_rover = rover; if (atomic_read(&hashinfo->bsockets) > (high - low) + 1 && !inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) { // 进入这个分支,表明可用端口号已经不够了,同时绑定当前端口号和之前已经使用此port的不冲突,则我们选择这个端口号(最小的) snum = smallest_rover; goto tb_found; } } // 若端口号不冲突,则选择这个端口 if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) { snum = rover; goto tb_found; } goto next; } break; // 直至遍历完所有的可用port } while (--remaining > 0); } ....... }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpwdzw.html