非阻塞connect卡select问题分析

本文探讨了非阻塞connect配合select实现高效网络连接的方法,分析了connect原理、select工作方式及多线程环境下可能产生的问题,并给出了相应的优化建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

非阻塞connect卡select问题分析

目录

非阻塞connect卡select问题分析

一、connect原理

1.1阻塞与非阻塞connect的区别

1.2 connect函数超时时间

二、select原理

三、问题原因

4.1 select的第四个参数建议加上。

4.2 select的第五个参数timeout的大小建议。

4.3 fd的多线程操作


一、connect原理

1.1阻塞与非阻塞connect的区别

为解决阻塞connect等待耗时过长,最长等待127秒问题,企业级开发长采用非阻塞connect加select方式进行连接,达到连接时间更短,连接时间可设置的目的。

connect主要使用套接字建立到指定网络地址的socket连接。套接字执行connect操作有两种模式,分别是阻塞和非阻塞。阻塞模式下,在连接建立完成之前,connect一直等待;在非阻塞模式下,connect会立即返回,不会等待连接建立,这种模式下,如果errno为EINPROGRESS,此时三次握手仍在继续,需要调用select检测非阻塞connect是否完成。Select阻塞时间可以比connect超时时间短,防止连接线程长时间阻塞在connect函数处。

阻塞与非阻塞connect调用区别如图所示。

 

在建立连接时,通过sock_sndtimeo函数,如果套接字标志位含O_NONBLOCK标志,也就是开启了非阻塞模式,sock_sndtimeo函数返回0,反之返回sk->sk_sndtimeo,表示connect超时时间。

sock_sndtimeo返回后做if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))判定。当是非阻塞模式,timeo为0,if直接退出。而当是阻塞模式,timeo是超时时间,!time就是0,此时执行inet_wait_for_connect函数。

inet_wait_for_connect函数中有个循环,等待连接,只有连接建立状态不为(TCPF_SYN_SENT | TCPF_SYN_RECV),或者收到信号,后者超时,timeo变为0,while循环才会退出。

超时时间sk_sndtimeo可以通过setsockopt进行设置。

 

1.2 connect函数超时时间

Connect函数调用tcp_transmit_skb发送SYN包后,通过inet_csk_reset_xmit_timer函数设置超时定时器。如果对端一直不发送SYN_ACK,将会返回-ETIMEDOUT。重传的超时时间跟net.ipv4.tcp_syn_retries有关,默认值是6。

当测试环境网络负载很高,TCP无法快速完成三次握手,发送端无法收到SYN_ACK确认包时,会触发TCP的重传机制。初始时会等待1秒,如果1秒后未收到TCP确认包,会重发SYNC报文,这个阶段会等待2秒。当发生多次重传时,本次超时时间是上一次超时时间的两倍,默认重传次数是6,所以最长会等待1+2+4+8+16+32+64=127秒。通过设置net.ipv4.tcp_syn_retries可以调整建立连接最长超时时间。

对于非阻塞connect,如果连接建立成功,套接口描述符变成可写,而如果连接建立出错,套接口描述符变成既可读又可写。一般情况下是调用select,等待连接建立。如果select返回值为0,表示在 select 的超时时间内未能成功建立连接,需要返回超时错误给用户,同时关闭连接,以防止 TCP 三次握手继续进行下去;如果 select 返回大于 0 的值,则说明检测到可读或可写或异常的套接字描述符存在,此时我们可以通过调用 getsockopt 来检测集合中的套接口上是否存在待处理的错误,如果连接建立是成功的,则通过 getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len) 获取的 error 值将是 0 ,如果建立连接时遇到错误,则 error 的值是连接错误所对应的 errno 值,比如 ECONNREFUSED,ETIMEDOUT 等。

一般情况下,调用非阻塞connect,由于连接建立最大延迟127秒,所以如果select函数设定的超时时间大于127秒,使用非阻塞connect就没有意义。使用非阻塞connect主要是函数可以不必等待立刻返回,如果超过select设定的超时时间没有建立连接,就退出本次连接,如果设定的超时时间太长,效果就与阻塞connect一致,都是必须等待连接建立才返回。

 

二、select原理

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select系统调用是用来让我们的程序监视多个文件句柄(file descriptor)的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有某一个或多个发生了状态改变。

首先描述一下Select函数中各变量的含义。

int nfds是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。

fd_set *readfds是指向fd_set结构的指针,这个变量是一个文件描述符的集合,select函数要监视这些文件描述符的读状态变化,如果这些文件描述符中有文件可读,那么select函数就会返回一个大于0的值,表示有文件状态变化。如果一直没有文件读状态变化,则根据最后一个变量timeout判断是否超时,超时select返回0;若发生错误,则返回负值。如果不关心任何文件读状态变化,可以将参数设定为NULL。

fd_set *writefds是指向fd_set结构的指针,这个变量是一个文件描述符的集合,select函数要监视这些文件描述符的写状态变化,如果这些文件描述符中有文件可写,那么select函数就会返回一个大于0的值,表示有文件状态变化。如果一直没有文件写状态变化,则根据最后一个变量timeout判断是否超时,超时select返回0;若发生错误,则返回负值。如果不关心任何文件写状态变化,可以将参数设定为NULL。

fd_set *exceptfds是指向fd_set结构的指针,这个变量是一个文件描述符的集合,当监控文件描述符出现读写意外的状态变化,select会返回一个大于0的值。如果一直没有状态变化,则根据最后一个变量timeout判断是否超时,超时select返回0;若发生错误,则返回负值。如果不关心文件其他状态变化,可以将参数设定为NULL。

struct timeval *timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

函数返回:

ret > 0,表示监视的文件描述符的集合有状态变化,此时ret的值代表有多少处变化。

ret = 0,表示监视的文件描述符状态没有变化,一般是timeval设定的时间到期,select函数会返回一个0值。

ret < 0,表示发生错误。

由于fd_set的限制,select默认情况下最多监控1024个文件,如果监控文件超过1024,建议使用poll/epoll函数。

 

Select函数调用select系统调用,select系统调用主要是遍历它所监测的一组文件描述符(fd_set)的集合,调用文件描述符对应的驱动程序的poll函数。驱动程序的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用,则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒继续往下执行。

所以,当非阻塞connect函数返回EINPROGRESS错误时,调用select函数等待connect连接建立。如果在调用select之前已经建立连接,select在遍历文件描述符时会有资源可用,select唤醒相应进程;当执行select时才建立连接,poll设定的等待队列会唤醒进程,退出select。

 

三、问题原因

多线程同时操作socket套接字的fd引发的问题。fd通过socket创建后,作为成员变量,多个线程都可以使用。当一个线程使用非阻塞connect建立连接,使用select去监听连接是否成功。
select正常运行,select函数会去遍历文件描述符表,在文件描述符表中查找对应fd,并调用fd的回调函数去查看自己可写缓冲区是否可写。
当连接尚未成功,select函数还在遍历文件描述符表的时候。另一个线程close了fd,fd对应的socket会从文件描述符表中删除。而连接线程的select再去遍历文件描述符表,文件描述符表中并没有了传入参数的fd,select进入空转,等待超时。

 

四、修改建议

4.1 select的第四个参数建议加上。

经过跟踪,程序调用select的方式为select( fd+1, NULL, &writefds, NULL, &timeout );即不关心socket的读状态和其他状态的变化,只关注socket的写状态变化。对于SO_STREAM类型套接口,远端造成的连接中止和KEEPALIVE错误都将被作为意味出错。如果套接口正在进行连接connect()(非阻塞方式),则连接试图的失败将会表现在exceptfds参数中。

由于非阻塞connect在连接正常或失败,都会改变socket的写状态,所以读状态可以忽略。忽略exceptfds,可能出现select一直等待的情况。

假定connect的目标IP主机是存在的且没开防火墙,只是端口是没打开的,在connect发出[SYN]后的1秒系统已经收到目标主机回复的[RST, ACK],也就是说系统此时已经知道这个端口是连接不上的了, 但是应用程序只关注writefds,没有关注exceptfds,select会一直等待下去。

所以考虑设置exceptfds,将socket加入到exceptfds,使用select同时监控writefds和exceptfds,当写状态变化或者出现异常时,select快速退出。

4.2 select的第五个参数timeout的大小建议。

select的第五个参数timeout的值建议小于connect最大连接时间127秒。一般情况下,调用非阻塞connect,由于连接建立最大延迟127秒,所以如果select函数设定的超时时间大于127秒,使用非阻塞connect就没有意义。使用非阻塞connect主要是函数可以不必等待立刻返回,如果超过select设定的超时时间没有建立连接,就退出本次连接,如果设定的超时时间太长,效果就与阻塞connect一致,都是必须等待连接建立才返回。

4.3 fd的多线程操作

对套接字的fd创建、连接、关闭操作应在同一个线程中,多线程操作fd时,应注意加锁。多线程操作同一个fd,应保证fd的线程安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值