各位老铁们好,相信很多人对linux源码分享阅读网站都不是特别的了解,因此呢,今天就来为大家分享下关于linux源码分享阅读网站以及linux源码阅读神器的问题知识,还望可以帮助大家,解决大家的一些困惑,下面一起来看看吧!
前言
笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情。今天笔者就来从Linux源码的角度看下Client端的Socket在进行Connect的时候到底做了哪些事情。由于篇幅原因,关于Server端的Accept源码讲解留给下一篇博客。(基于Linux3.10内核)
一个最简单的Connect例子
intclientSocket;\nif((clientSocket=socket(AF_INET,SOCK_STREAM,0))<0){\n\t//创建socket失败失败\n\treturn-1;\n}\n……\nif(connect(clientSocket,(structsockaddr*)&serverAddr,sizeof(serverAddr))<0){\n\t//connect失败\n\treturn-1;\n}\n…….\n\n
首先我们通过socket系统调用创建了一个socket,其中指定了SOCK_STREAM,而且最后一个参数为0,也就是建立了一个通常所有的TCPSocket。在这里,我们直接给出TCPSocket所对应的ops也就是操作函数。
如果你想知道上图中的结构是怎么来的,可以看下笔者以前的博客:
https://my.oschina.net/alchemystar/blog/1791017\n
值得注意的是,由于socket系统调用操作做了如下两个代码的判断
sock_map_fd\n\t|->get_unused_fd_flags\n\t\t\t|->alloc_fd\n\t\t\t\t|->expand_files(ulimit)\n\t|->sock_alloc_file\t\n\t\t|->alloc_file\n\t\t\t|->get_empty_filp(/proc/sys/fs/max_files)\n
第一个判断,ulmit超限:
intexpand_files(structfiles_struct*files,intnr\n{\n\t……\n\tif(nr>=current->signal->rlim[RLIMIT_NOFILE].rlim_cur)\n\t\treturn-EMFILE;\n\t……\n}\n
这边的判断即是ulimit的限制!在这里返回-EMFILE对应的描述就是&34;
第二个判断max_files超限
structfile*get_empty_filp(void)\n{\n……\n\t/*\n\t*由此可见,特权用户可以无视文件数最大大小的限制!\n\t*/\n\tif(get_nr_files()>=files_stat.max_files&&!capable(CAP_SYS_ADMIN)){\n\t\t/*\n\t\t*percpu_countersareinaccurate.Doanexpensivecheckbefore\n\t\t*wegoandfail.\n\t\t*/\n\t\tif(percpu_counter_sum_positive(&nr_files)>=files_stat.max_files)\n\t\t\tgotoover;\n\t}\n\t\n……\n}\n
所以在文件描述符超过所有进程能打开的最大文件数量限制(/proc/sys/fs/file-max)的时候会返回-ENFILE,对应的描述就是&34;,但是特权用户确可以无视这一限制,如下图所示:
connect系统调用
我们再来看一下connect系统调用:
intconnect(intsockfd,conststructsockaddr*serv_addr,socklen_taddrlen)\n
这个系统调用有三个参数,那么依据规则,它肯定在内核中的源码长下面这个样子
SYSCALL_DEFINE3(connect,……\n
笔者全文搜索了下,就找到了具体的实现:
socket.c\nSYSCALL_DEFINE3(connect,int,fd,structsockaddr__user*,uservaddr,\n\t\tint,addrlen)\n{\n……\n\terr=sock->ops->connect(sock,(structsockaddr*)&address,addrlen,\n\t\t\t\tsock->file->f_flags);\n\t……\n}\n
前面图给出了在TCP下的sock->ops==inet_stream_ops,然后再陷入到更进一步的调用栈中,即下面的:
SYSCALL_DEFINE3(connect\n\t|->inet_stream_ops\n\t\t|->inet_stream_connect\n\t\t\t|->tcp_v4_connect\n\t\t\t\t|->tcp_set_state(sk,TCP_SYN_SENT);设置状态为TCP_SYN_SENT\n\t\t\t\t|->inet_hash_connect\n\t\t\t\t|->tcp_connect\n
首先,我们来看一下inet_hash_connect这个函数,里面有一个端口号的搜索过程,搜索不到可用端口号就会导致创建连接失败!内核能够建立一个连接也是跋涉了千山万水的!我们先看一下搜索端口号的逻辑,如下图所示:
获取端口号范围
首先,我们从内核中获取connect能够使用的端口号范围,在这里采用了Linux中的顺序锁(seqlock)
voidinet_get_local_port_range(int*low,int*high)\n{\n\tunsignedintseq;\n\n\tdo{\n\t\t//顺序锁\n\t\tseq=read_seqbegin(&sysctl_local_ports.lock);\n\n\t\t*low=sysctl_local_ports.range[0];\n\t\t*high=sysctl_local_ports.range[1];\n\t}while(read_seqretry(&sysctl_local_ports.lock,seq));\n}\n
顺序锁事实上就是结合内存屏障等机制的一种乐观锁,主要依靠一个序列计数器。在读取数据之前和之后,序列号都被读取,如果两者的序列号相同,说明在读操作的时候没有被写操作打断过。这也保证了上面的读取变量都是一致的,也即low和high不会出现low是改前值而high是改后值得情况。low和high要么都是改之前的,要么都是改之后的!内核中修改的地方为:
cat/proc/sys/net/ipv4/ip_local_port_range\n3276861000\n
通过hash决定端口号起始搜索范围
在Linux上进行connect,内核给其分配的端口号并不是线性增长的,但是也符合一定的规律。先来看下代码:
int__inet_hash_connect(…)\n{\n\t\t//注意,这边是static变量\n\t\tstaticu32hint;\n\t\t//这边的port_offset是用对端ip:porthash的一个值\n\t\t//也就是说对端ip:port固定,port_offset固定\n\t\tu32offset=hint+port_offset;\n\t\tfor(i=1;i<=remaining;i++){\n\t\t\tport=low+(i+offset)%remaining;\n\t\t\t/*port是否占用check*/\n\t\t\t….\n\t\t\tgotook;\n\t\t}\n\t\t…….\nok:\n\t\thint+=i;\n\t\t……\n}\n
这里面有几个小细节,为了安全原因,Linux本身用对端ip:port做了一次hash作为搜索的初始offset,所以不同远端ip:port初始搜索范围可以基本是不同的!但同样的对端ip:port初始搜索范围是相同的!
在笔者机器上,一个完全干净的内核里面,不停的对同一个远端ip:port,其以2进行稳定增长,也即38742->38744->38746,如果有其它的干扰,就会打破这个规律。
端口号范围限制
由于我们指定了端口号返回ip_local_port_range是不是就意味着我们最多创建high-low+1个连接呢?当然不是,由于检查端口号是否重复是将(网络命名空间,对端ip,对端port,本端port,Socket绑定的dev)当做唯一键进行重复校验,所以限制仅仅是在同一个网络命名空间下,连接同一个对端ip:port的最大可用端口号数为high-low+1,当然可能还要减去ip_local_reserved_ports。如下图所示:
检查端口号是否被占用
端口号的占用搜索分为两个阶段,一个是处于TIME_WAIT状态的端口号搜索,另一个是其它状态端口号搜索。
TIME_WAIT状态端口号搜索
众所周知,TIME_WAIT阶段是TCP主动close必经的一个阶段。如果Client采用短连接的方式和Server端进行交互,就会产生大量的TIME_WAIT状态的Socket。而这些Socket由占用端口号,所以当TIME_WAIT过多,打爆上面的端口号范围之后,新的connect就会返回错误码:
C语言connect返回错误码为\n-EADDRNOTAVAIL,对应描述为Cannotassignrequestedaddress\n对应Java的异常为\njava.net.NoRouteToHostException:Cannotassignrequestedaddress(Addressnotavailable)\n
ip_local_reserved_ports。如下图所示:
由于TIME_WAIT大概一分钟左右才能消失,如果在一分钟内Client端和Server建立大量的短连接请求就容易导致端口号耗尽。而这个一分钟(TIME_WAIT的最大存活时间)是在内核(3.10)编译阶段就确定了的,无法通过内核参数调整。如下代码所示:
39;1'>/proc/sys/net/ipv4/tcp_tw_reuse\n
ESTABLISHED状态端口号搜索
ESTABLISHED的端口号搜索就简单了许多
\t/*Andestablishedpart…*/\n\tsk_nulls_for_each(sk2,node,&head->chain){\n\t\tif(INET_MATCH(sk2,net,hash,acookie,\n\t\t\t\t\tsaddr,daddr,ports,dif))\n\t\t\tgotonot_unique;\n\t}\n\n
以(网络命名空间,对端ip,对端port,本端port,Socket绑定的dev)当做唯一键进行匹配,如果匹配成功,表明此端口无法重用。
端口号迭代搜索
Linux内核在[low,high]范围按照上述逻辑进行port的搜索,如果没有搜索到port,即port耗尽,就会返回-EADDRNOTAVAIL,也即Cannotassignrequestedaddress。但还有一个细节,如果是重用TIME_WAIT状态的Socket的端口的话,就会将对应的TIME_WAIT状态的Socket给销毁。
__inet_hash_connect(……)\n{\n\t\t……\n\t\tif(tw){\n\t\t\tinet_twsk_deschedule(tw,death_row);\n\t\t\tinet_twsk_put(tw);\n\t\t}\n\t\t……\n}\n\t\t\n
寻找路由表
在我们找到一个可用端口号port后,就会进入搜寻路由阶段:
ip_route_newports\n\t|->ip_route_output_flow\n\t\t\t|->__ip_route_output_key\n\t\t\t\t|->ip_route_output_slow\n\t\t\t\t\t|->fib_lookup\n
这也是一个非常复杂的过程,限于篇幅,就不做详细阐述了。如果搜索不到路由信息的话,会返回。
-ENETUNREACH,对应描述为Networkisunreachable\n
Client端的三次握手
在前面一大堆前置条件就绪后,才进入到真正的三次握手阶段。
tcp_connect\n\t|->tcp_connect_init初始化tcpsocket\n\t|->tcp_transmit_skb发送SYN包\n\t|->inet_csk_reset_xmit_timer设置SYN重传定时器\n
tcp_connect_init初始化了一大堆TCP相关的设置,例如mss_cache/rcv_mss等一大堆。而且如果开启了TCP窗口扩大选项的话,其窗口扩大因子也在此函数里进行计算:
tcp_connect_init\n\t|->tcp_select_initial_window\ninttcp_select_initial_window(…)\n{\n\t……\n\t(*rcv_wscale)=0;\n\tif(wscale_ok){\n\t\t/*Setwindowscalingonmaxpossiblewindow\n\t\t*SeeRFC1323foranexplanationofthelimitto14\n\t\t*/\n\t\tspace=max_t(u32,sysctl_tcp_rmem[2],sysctl_rmem_max);\n\t\tspace=min_t(u32,space,*window_clamp);\n\t\twhile(space>65535&&(*rcv_wscale)<14){\n\t\t\tspace>>=1;\n\t\t\t(*rcv_wscale)++;\n\t\t}\n\t}\n\t……\n}\t\n\n
如上面代码所示,窗口扩大因子取决于Socket最大可允许的读缓冲大小和window_clamp(最大允许滑动窗口大小,动态调整)。搞完了一票初始信息设置后,才开始真正的三次握手。在tcp_transmit_skb中才真正发送SYN包,同时在紧接着的inet_csk_reset_xmit_timer里设置了SYN超时定时器。如果对端一直不发送SYN_ACK,将会返回-ETIMEDOUT。
重传的超时时间和
/proc/sys/net/ipv4/tcp_syn_retries\n
息息相关,Linux默认设置为5,建议设置成3,下面是不同设置的超时时间参照图。
在设置了SYN超时重传定时器后,tcp_connnect就返回,并一路返回到最初始的inet_stream_connect。在这里我们就等待对端返回SYN_ACK或者SYN定时器超时。
int__inet_stream_connect(structsocket*sock,…,)\n{\n\t//如果设置了O_NONBLOCK则timeo为0\n\ttimeo=sock_sndtimeo(sk,flags&O_NONBLOCK);\n\t……\n\t//如果timeo=0即O_NONBLOCK会立刻返回\n\t//否则等待timeo时间\n\tif(!timeo||!inet_wait_for_connect(sk,timeo,writebias))\n\t\tgotoout;\n}\n
Linux本身提供一个SO_SNDTIMEO来控制对connect的超时,不过Java并没有采用这个选项。而是采用别的方式进行connect的超时控制。仅仅就C语言的connect系统调用而言,不设置SO_SNDTIMEO,就会将对应用户进程进行睡眠,直到SYN_ACK到达或者超时定时器超时才将次用户进程唤醒。
如果是NON_BLOCK的话,则是通过select/epoll等多路复用机制去捕获超时或者连接成功事件。
对端SYN_ACK到达
在Server端SYN_ACK到达之后会按照下面的代码路径传递,并唤醒用户态进程:
tcp_v4_rcv\n\t|->tcp_v4_do_rcv\n\t\t|->tcp_rcv_state_process\n\t\t\t|->tcp_rcv_synsent_state_process\n\t\t\t\t|->tcp_finish_connect\n\t\t\t\t\t|->tcp_init_metrics初始化度量统计\n\t\t\t\t\t|->tcp_init_congestion_control初始化拥塞控制\n\t\t\t\t\t|->tcp_init_buffer_space初始化buffer空间\n\t\t\t\t\t|->inet_csk_reset_keepalive_timer开启包活定时器\n\t\t\t\t\t|->sk_state_change(sock_def_wakeup)唤醒用户态进程\n\t\t\t\t|->tcp_send_ack发送三次握手的最后一次握手给Server端\n\t\t\t|->tcp_set_state(sk,TCP_ESTABLISHED)设置为ESTABLISHED状态
原文:https://my.oschina.net/alchemystar/blog/4327484
作者:无毁的湖光-Al
文章到此结束,如果本次分享的linux源码分享阅读网站和linux源码阅读神器的问题解决了您的问题,那么我们由衷的感到高兴!
