如果你想知道上图中的构造是怎么来的,可以看下笔者以前的博客:
https://my.oschina.net/alchemystar/blog/1791017
值得把稳的是,由于socket系统调用操作做了如下两个代码的判断
sock_map_fd|->get_unused_fd_flags|->alloc_fd|->expand_files (ulimit)|->sock_alloc_file|->alloc_file|->get_empty_filp (/proc/sys/fs/max_files)
第一个判断,ulmit超限:
int expand_files(struct files_struct files, int nr{......if (nr >= current->signal->rlim[RLIMIT_NOFILE].rlim_cur)return -EMFILE;......}
这边的判断即是ulimit的限定!
在这里返回-EMFILE对应的描述便是 "Too many open files"
第二个判断max_files超限
struct file get_empty_filp(void){ ....../ 由此可见,特权用户可以忽略文件数最大大小的限定!
/if (get_nr_files() >= files_stat.max_files && !capable(CAP_SYS_ADMIN)) {/ percpu_counters are inaccurate. Do an expensive check before we go and fail. /if (percpu_counter_sum_positive(&nr_files) >= files_stat.max_files)goto over;} ......}
以是在文件描述符超过所有进程能打开的最大文件数量限定(/proc/sys/fs/file-max)的时候会返回-ENFILE,对应的描述便是"Too many open files in system",但是特权用户确可以忽略这一限定,如下图所示:
connect系统调用
我们再来看一下connect系统调用:
int connect(int sockfd,const struct sockaddr serv_addr,socklen_t addrlen)
这个别系调用有三个参数,那么依据规则,它肯定在内核中的源码长下面这个样子
SYSCALL_DEFINE3(connect, ......
笔者全文搜索了下,就找到了详细的实现:
socket.cSYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user , uservaddr,int, addrlen){ ......err = sock->ops->connect(sock, (struct sockaddr )&address, addrlen, sock->file->f_flags);......}
前面图给出了在TCP下的sock->ops == inet_stream_ops,然后再陷入到更进一步的调用栈中,即下面的:
SYSCALL_DEFINE3(connect|->inet_stream_ops|->inet_stream_connect|->tcp_v4_connect|->tcp_set_state(sk, TCP_SYN_SENT);设置状态为TCP_SYN_SENT |->inet_hash_connect|->tcp_connect
首先,我们来看一下inet_hash_connect这个函数,里面有一个端口号的搜索过程,搜索不到可用端口号就会导致创建连接失落败!
内核能够建立一个连接也是跋涉了千山万水的!
我们先看一下搜索端口号的逻辑,如下图所示:
获取端口号范围
首先,我们从内核中获取connect能够利用的端口号范围,在这里采取了Linux中的顺序锁(seqlock)
void inet_get_local_port_range(int low, int high){unsigned int seq;do {// 顺序锁seq = read_seqbegin(&sysctl_local_ports.lock);low = sysctl_local_ports.range[0];high = sysctl_local_ports.range[1];} while (read_seqretry(&sysctl_local_ports.lock, seq));}
顺序锁事实上便是结合内存樊篱等机制的一种乐不雅观锁,紧张依赖一个序列计数器。在读取数据之前和之后,序列号都被读取,如果两者的序列号相同,解释在读操作的时候没有被写操作打断过。 这也担保了上面的读取变量都是同等的,也即low和high不会涌现low是改前值而high是改后值得情形。low和high要么都是改之前的,要么都是改之后的!
内核中修正的地方为:
cat /proc/sys/net/ipv4/ip_local_port_range 32768 61000
通过hash决定端口号起始搜索范围
在Linux上进行connect,内核给其分配的端口号并不是线性增长的,但是也符合一定的规律。 先来看下代码:
int __inet_hash_connect(...){// 把稳,这边是static变量static u32 hint;// 这边的port_offset是用对端ip:port hash的一个值// 也便是说对端ip:port固定,port_offset固定u32 offset = hint + port_offset;for (i = 1; i <= remaining; i++) {port = low + (i + offset) % remaining;/ port是否占用check /....goto ok;}.......ok:hint += i;......}
这里面有几个小细节,为了安全缘故原由,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返回缺点码为-EADDRNOTAVAIL,对应描述为Cannot assign requested address 对应Java的非常为java.net.NoRouteToHostException: Cannot assign requested address (Address not available)
ip_local_reserved_ports。如下图所示:
由于TIME_WAIT大概一分钟旁边才能消逝,如果在一分钟内Client端和Server建立大量的短连接要求就随意马虎导致端口号耗尽。而这个一分钟(TIME_WAIT的最大存活韶光)是在内核(3.10)编译阶段就确定了的,无法通过内核参数调度。 如下代码所示:
#define TCP_TIMEWAIT_LEN (60HZ) / how long to wait to destroy TIME-WAIT state, about 60 seconds/
Linux自然也考虑到了这种情形,以是供应了一个tcp_tw_reuse参数使得在搜索端口号时可以在某些情形下重用TIME_WAIT。代码如下:
__inet_hash_connect|->__inet_check_establishedstatic int __inet_check_established(......){....../ Check TIME-WAIT sockets first. /sk_nulls_for_each(sk2, node, &head->twchain) {tw = inet_twsk(sk2);// 如果在time_wait中找到一个match的port,就判断是否可重用if (INET_TW_MATCH(sk2, net, hash, acookie,saddr, daddr, ports, dif)) {if (twsk_unique(sk, sk2, twp))goto unique;elsegoto not_unique;}}......}
如上面代码中写的那样,如果在一堆TIME-WAIT状态的Socket里面能够有当前要搜索的port,则判断是否这个port可以重复利用。如果是TCP的话这个twsk_unique的实现函数是:
int tcp_twsk_unique(......){......if (tcptw->tw_ts_recent_stamp && (twp == NULL || (sysctl_tcp_tw_reuse && get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2......return 1;}return 0;}
上面这段代码逻辑如下所示:
在开启了tcp_timestamp以及tcp_tw_reuse的情形下,在Connect搜索port时只要比之前用这个port的TIME_WAIT状态的Socket记录的最近韶光戳>1s,就可以重用此port,即将之前的1分钟缩短到1s。同时为了防止潜在的序列号冲突,直接将write_seq加上在65537,这样,在单Socket传输速率小于80Mbit/s的情形下,不会造成序列号冲突。同时这个tw_ts_recent_stamp设置的机遇如下图所示:
以是如果Socket进入TIME_WAIT状态后,如果一贯有对应的包发过来,那么会影响此TIME_WAIT对应的port是否可用的韶光。我们可以通过下面命令开始tcp_tw_reuse:
echo '1' > /proc/sys/net/ipv4/tcp_tw_reuse
ESTABLISHED状态端口号搜索
ESTABLISHED的端口号搜索就大略了许多
/ And established part... /sk_nulls_for_each(sk2, node, &head->chain) {if (INET_MATCH(sk2, net, hash, acookie,saddr, daddr, ports, dif))goto not_unique;}
以(网络命名空间,对端ip,对端port,本端port,Socket绑定的dev)当做唯一键进行匹配,如果匹配成功,表明此端口无法重用。
端口号迭代搜索Linux内核在[low,high]范围按照上述逻辑进行port的搜索,如果没有搜索到port,即port耗尽,就会返回-EADDRNOTAVAIL,也即Cannot assign requested address。但还有一个细节,如果是重用TIME_WAIT状态的Socket的端口的话,就会将对应的TIME_WAIT状态的Socket给销毁。
__inet_hash_connect(......){......if (tw) {inet_twsk_deschedule(tw, death_row);inet_twsk_put(tw);}......}
探求路由表
在我们找到一个可用端口号port后,就会进入征采路由阶段:
ip_route_newports|->ip_route_output_flow|->__ip_route_output_key|->ip_route_output_slow|->fib_lookup
这也是一个非常繁芜的过程,限于篇幅,就不做详细阐述了。如果搜索不到路由信息的话,会返回。
-ENETUNREACH,对应描述为Network is unreachable
Client真个三次握手
在前面一大堆前置条件就绪后,才进入到真正的三次握手阶段。
tcp_connect|->tcp_connect_init 初始化tcp socket|->tcp_transmit_skb 发送SYN包|->inet_csk_reset_xmit_timer 设置SYN重传定时器
tcp_connect_init初始化了一大堆TCP干系的设置,例如mss_cache/rcv_mss等一大堆。而且如果开启了TCP窗口扩大选项的话,其窗口扩大因子也在此函数里进行打算:
tcp_connect_init|->tcp_select_initial_windowint tcp_select_initial_window(...){......(rcv_wscale) = 0;if (wscale_ok) {/ Set window scaling on max possible window See RFC1323 for an explanation of the limit to 14 /space = max_t(u32, sysctl_tcp_rmem[2], sysctl_rmem_max);space = min_t(u32, space, window_clamp);while (space > 65535 && (rcv_wscale) < 14) {space >>= 1;(rcv_wscale)++;}}......}
如上面代码所示,窗口扩大因子取决于Socket最大可许可的读缓冲大小和window_clamp(最大许可滑动窗口大小,动态调度)。搞完了一票初始信息设置后,才开始真正的三次握手。 在tcp_transmit_skb中才真正发送SYN包,同时在紧接着的inet_csk_reset_xmit_timer里设置了SYN超时定时器。如果对端一贯不发送SYN_ACK,将会返回-ETIMEDOUT。
重传的超时时间和
/proc/sys/net/ipv4/tcp_syn_retries
息息相关,Linux默认设置为5,建议设置成3,下面是不同设置的超时时间参照图。
在设置了SYN超时重传定时器后,tcp_connnect就返回,并一起返回到最初始的inet_stream_connect。在这里我们就等待对端返回SYN_ACK或者SYN定时器超时。
int __inet_stream_connect(struct socket sock,...,){// 如果设置了O_NONBLOCK则timeo为0timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);......// 如果timeo=0即O_NONBLOCK会急速返回// 否则等待timeo韶光if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))goto out;}
Linux本身供应一个SO_SNDTIMEO来掌握对connect的超时,不过Java并没有采取这个选项。而是采取别的办法进行connect的超时掌握。仅仅就C措辞的connect系统调用而言,不设置SO_SNDTIMEO,就会将对运用户进程进行就寝,直到SYN_ACK到达或者超时定时器超时才将次用户进程唤醒。
如果是NON_BLOCK的话,则是通过select/epoll等多路复用机制去捕获超时或者连接成功事宜。
对端SYN_ACK到达在Server端SYN_ACK到达之后会按照下面的代码路径通报,并唤醒用户态进程:
tcp_v4_rcv|->tcp_v4_do_rcv|->tcp_rcv_state_process|->tcp_rcv_synsent_state_process|->tcp_finish_connect|->tcp_init_metrics 初始化度量统计|->tcp_init_congestion_control 初始化拥塞掌握|->tcp_init_buffer_space 初始化buffer空间|->inet_csk_reset_keepalive_timer 开启包活定时器|->sk_state_change(sock_def_wakeup) 唤醒用户态进程|->tcp_send_ack 发送三次握手的末了一次握手给Server端|->tcp_set_state(sk, TCP_ESTABLISHED) 设置为ESTABLISHED状态
原文:https://my.oschina.net/alchemystar/blog/4327484
作者:无毁的湖光-Al