从Linux源码看Socket(TCP)的bind

前言

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

一个最简单的Server端例子

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



代码如下:

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也就是操作函数。



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

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的流程



值得注意的是,由于对于<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地址。对我们最直接的影响如下图所示:



然后,我们看下一个比较复杂的函数,即可用端口号的选择过程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);
}
.......
}

由于,我们在使用bind的时候很少随机端口号(在TCP服务器来说尤其如此),这段代码笔者就注释一下。一般只有一些特殊的远程过程调用(RPC)中会使用随机Server端随机端口号。

第二段,找到端口号或已经指定

have_snum:
inet_bind_bucket_for_each(tb, &head->chain)
if (net_eq(ib_net(tb), net) && tb->port == snum)
goto tb_found;
}
tb = NULL;
goto tb_not_found
tb_found:
// 如果此port已被bind
if (!hlist_empty(&tb->owners)) {
// 如果设置为强制重用,则直接成功
if (sk->sk_reuse == SK_FORCE_REUSE)
goto success;
}
if (((tb->fastreuse > 0 &&
sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
(tb->fastreuseport > 0 &&
sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
smallest_size == -1) {
// 这个分支表明之前bind的port和当前sock都设置了reuse同时当前sock状态不为listen
// 或者同时设置了reuseport而且是同一个uid(注意,设置了reuseport后,可以同时listen同一个port了)
goto success;
} else {
ret = 1;
// 检查端口是否冲突
if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) {
if (((sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
(tb->fastreuseport > 0 &&
sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
smallest_size != -1 && --attempts >= 0) {
// 若冲突,但是设置了reuse非listen状态或者设置了reuseport且出在同一个用户下
// 则可以进行重试
spin_unlock(&head->lock);
goto again;
} goto fail_unlock;
}
// 不冲突,走下面的逻辑
}
tb_not_found:
if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
net, head, snum)) == NULL)
goto fail_unlock;
// 设置fastreuse
// 设置fastreuseport
success:
......
// 将当前sock链入tb->owner,同时tb->num_owners++
inet_bind_hash(sk, tb, snum);
ret = 0;
// 返回bind(绑定)成功
return ret;

判断端口号是否冲突

在上述源码中,判断端口号时否冲突的代码为

inet_csk(sk)->icsk_af_ops->bind_conflict 也即 inet_csk_bind_conflict
int inet_csk_bind_conflict(const struct sock *sk,
const struct inet_bind_bucket *tb, bool relax){
......
sk_for_each_bound(sk2, &tb->owners) {
// 这边判断表明,必须同一个接口(dev_if)才进入下内部分支,也就是说不在同一个接口端口的不冲突
if (sk != sk2 &&
!inet_v6_ipv6only(sk2) &&
(!sk->sk_bound_dev_if ||
!sk2->sk_bound_dev_if ||
sk->sk_bound_dev_if == sk2->sk_bound_dev_if))
{
if ((!reuse || !sk2->sk_reuse ||
sk2->sk_state == TCP_LISTEN) &&
(!reuseport || !sk2->sk_reuseport ||
(sk2->sk_state != TCP_TIME_WAIT &&
!uid_eq(uid, sock_i_uid(sk2))))) {
// 在有一方没设置reuse且sock2状态为listen 同时
// 有一方没设置reuseport且sock2状态不为time_wait同时两者的uid不一样的时候
const __be32 sk2_rcv_saddr = sk_rcv_saddr(sk2);
if (!sk2_rcv_saddr || !sk_rcv_saddr(sk) ||
// ip地址一样,才算冲突
sk2_rcv_saddr == sk_rcv_saddr(sk))
break;
}
// 非放松模式,ip地址一样,才算冲突
......
return sk2 != NULL;
}
......
}

上面代码的逻辑如下图所示:

SO_REUSEADDR和SO_REUSEPORT

上面的代码有点绕,笔者就讲一下,对于我们日常开发要关心什么。

我们在上面的bind里面经常见到sk_reuse和sk_reuseport这两个socket的Flag。这两个Flag能够决定是否能够bind(绑定)成功。这两个Flag的设置在C语言里面如下代码所示:

 setsockopt(sockfd_server, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int));
setsockopt(sockfd_server, SOL_SOCKET, SO_REUSEPORT, &(int){ 1 }, sizeof(int));

在原生JAVA中

 // java8中,原生的socket并不支持so_reuseport
ServerSocket server = new ServerSocket(port);
server.setReuseAddress(true);

在Netty(Netty版本 >= 4.0.16且Linux内核版本>=3.9以上)中,可以使用SO_REUSEPORT。

SO_REUSEADDR

在之前的源码里面,我们看到判断bind是否冲突的时候,有这么一个分支

(!reuse || !sk2->sk_reuse ||
sk2->sk_state == TCP_LISTEN) /* 暂忽略reuseport */){
// 即有一方没有设置
}

如果sk2(即已bind的socket)是TCP_LISTEN状态或者,sk2和新sk两者都没有设置_REUSEADDR的时候,可以判断为冲突。

我们可以得出,如果原sock和新sock都设置了SO_REUSEADDR的时候,只要原sock不是Listen状态,都可以绑定成功,甚至ESTABLISHED状态也可以!



这个在我们平常工作中,最常见的就是原sock处于TIME_WAIT状态,这通常在我们关闭Server的时候出现,如果不设置SO_REUSEADDR,则会绑定失败,进而启动不来服务。而设置了SO_REUSEADDR,由于不是TCP_LISTEN,所以可以成功。



这个特性在紧急重启以及线下调试的非常有用,建议开启。

SO_REUSEPORT

SO_REUSEPORT是Linux在3.9版本引入的新功能。

1.在海量高并发连接的创建时候,由于正常的模型是单线程listener分发,无法利用多核优势,这就会成为瓶颈。
2.CPU缓存行丢失

我们看下一般的Reactor线程模型,



明显的其单线程listen/accept会存在瓶颈(如果采用多线程epoll accept,则会惊群,加WQ_FLAG_EXCLUSIVE可以解决一部分),尤其是在采用短链接的情况下。

鉴于此,Linux增加了SO_REUSEPORT,而之前bind中判断是否冲突的下面代码也是为这个参数而添加的逻辑:


if(!reuseport || !sk2->sk_reuseport ||
(sk2->sk_state != TCP_TIME_WAIT &&
!uid_eq(uid, sock_i_uid(sk2))

这段代码让我们在多次bind的时候,如果设置了SO_REUSEPORT的时候不会报错,也就是让我们有个多线程(进程)bind/listen的能力。如下图所示:



而开启了SO_REUSEPORT后,代码栈如下:

tcp_v4_rcv
|->__inet_lookup_skb
|->__inet_lookup
|->__inet_lookup_listener
/* 用打分和伪随机数等挑选出一个listen的sock */
struct sock *__inet_lookup_listener(......)
{
......
if (score > hiscore) {
result = sk;
hiscore = score;
reuseport = sk->sk_reuseport;
if (reuseport) {
phash = inet_ehashfn(net, daddr, hnum,
saddr, sport);
matches = 1;
}
} else if (score == hiscore && reuseport) {
matches++;
if (((u64)phash * matches) >> 32 == 0)
result = sk;
phash = next_pseudo_random32(phash);
}
......
}

直接在内核层面做负载均衡,将accept的任务分散到不同的线程的不同socket上(Sharding),毫无疑问可以多核能力,大幅提升连接成功后的socket分发能力。

Nginx已经采用SO_REUSEPORT

Nginx在1.9.1版本的时候引入了SO_REUSEPORT,配置如下:

http {
server {
listen 80 reuseport;
server_name localhost;
# ...
}
} stream {
server {
listen 12345 reuseport;
# ...
}
}





在压测场景下,性能提升了3倍!详情见下面链接。

https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/

总结

Linux内核源码博大精深,一个看起来简单的bind系统调用竟然牵涉这么多,在里面可以挖掘出各种细节。在此分享出来,希望对读者有所帮助。

欢迎大家关注我公众号,里面有各种干货,还有大礼包相送哦!

从Linux源码看Socket(TCP)的bind的更多相关文章

  1. 从linux源码看socket(tcp)的timeout

    从linux源码看socket(tcp)的timeout 前言 网络编程中超时时间是一个重要但又容易被忽略的问题,对其的设置需要仔细斟酌.在经历了数次物理机宕机之后,笔者详细的考察了在网络编程(tcp ...

  2. 从Linux源码看Socket(TCP)Client端的Connect

    从Linux源码看Socket(TCP)Client端的Connect 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的 ...

  3. 从Linux源码看Socket(TCP)的listen及连接队列

    从Linux源码看Socket(TCP)的listen及连接队列 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的角度看 ...

  4. 从Linux源码看Socket(TCP)的accept

    从Linux源码看Socket(TCP)的accept 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就从Linux源码的角度看下Serve ...

  5. 从linux源码看socket的阻塞和非阻塞

    从linux源码看socket的阻塞和非阻塞 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 大部分高性能网络框架采用的是非阻塞模式.笔者这次就从linux ...

  6. 从linux源码看epoll

    从linux源码看epoll 前言 在linux的高性能网络编程中,绕不开的就是epoll.和select.poll等系统调用相比,epoll在需要监视大量文件描述符并且其中只有少数活跃的时候,表现出 ...

  7. 从Linux源码看TIME_WAIT状态的持续时间

    从Linux源码看TIME_WAIT状态的持续时间 前言 笔者一直以为在Linux下TIME_WAIT状态的Socket持续状态是60s左右.线上实际却存在TIME_WAIT超过100s的Socket ...

  8. mysql-5.5 for linux源码安装

    mysql-5.5 for linux源码安装 1.使用Yum安装依赖软件包 # yum install -y gcc gcc-c++ gcc-g77 autoconf automake bison  ...

  9. 如何从Linux源码获知版本信息

    /*************************************************************************** * 如何从Linux源码获知版本信息 * 声明 ...

随机推荐

  1. 一些免费的API

    Github 接口 Github 为我们提供了一些免费的 API 接口,利用这些接口我们可以开发一些工具. 接口文档地址为 https://docs.github.com/en/rest 下面是一个例 ...

  2. App测试理论简介

    一.App测试常见关注点 1.App的功能测试 功能测试都是我们首要测试的,只有功能实现了才算符合上线发布的最低标准.我们需要检测产品功能是否已实现.产品功能是否符合设计要求.产品功能是否有重复.产品 ...

  3. Spring AOP-用代理代替繁琐逻辑

    Spring AOP 基础概念 AOP 是一种面向切面的编程思想,通俗来讲,这里假如我们有多个方法. @Component public class Demo { public void say1() ...

  4. docker中重启某个服务命令

    docker ps------查看正在运行的cotainners docker ps -a --------查看所有的containners docker restart 容器id docker lo ...

  5. xss原理解析

    xss->跨站脚本攻击 xss是指攻击者在网页中嵌入客户端脚本.通常是指javascript编写的一个危险代码,当用户使用浏览器浏览网页时,脚本就会在用户的浏览器上执行,从而达到攻击者的目的. ...

  6. Linux rndis_host 驱动的一个BUG与解决方案

    关键字 rndis_host, linux, kernel, modem 综述 rndis 是微软定义的一套通讯方案.类似的协议还有 qmi/mbim/ecm/ncm. rndis 协议足够简单,可靠 ...

  7. Hadoop入门学习整理(一)

    今天是2020年4月8日,是一个平凡而又特殊的日子,武汉在经历了77天的封城之后,于今日0点正式解封.从1月14日放寒假离开武汉,到今天已近3个月,学校的花开了又谢了.随着疫情好转,春回大地,万物复苏 ...

  8. 透过 Cucumber 学习 BDD

    在需求的开发过程中,最令人困惑的地方就在于需求模糊.需求是解决业务的问题,那么验收的方式应该是由业务方提出,但是往往业务方(可能是产品经理,也可能是直接是客户)只能给出比较模糊的一个验收标准,而程序却 ...

  9. Linux实战(13):Ubuntu被远程

    此方法采用的xrdp原生方案,优点兼容性比较好. 安装xrdp sudo apt install xrdp #最高权限安装xrdp 修改配置 nano /etc/xrdp/startwm.sh #使用 ...

  10. Boolean.valueOf(String)

    Boolean.valueOf(String) a. 当 String 的参数值在不区分大小写的时候等于 "true" ,则 Boolean.valueOf(String) 返回值 ...