select模型详解

1.select模型原理

使用select函数检查文件描述符上是否有io事件发生,包括可读,可写以及异常

 select参数和返回值意义如下:

int select (

 IN int nfds,                           //0,无意义

 IN OUT fd_set* readfds,      //检查可读性

 IN OUT fd_set* writefds,     //检查可写性

 IN OUT fd_set* exceptfds,  //例外数据

 IN const struct timeval* timeout);    //函数的返回时间

 参数说明:

第一个参数nfds在linux表示要监视的最大文件描述符+1,在windows下为0

第二个参数readfds检查文件描述符集合可读

第三个参数writefds检查文件描述符集合可写

第四个参数exceptfds检查文件描述符集合异常

第五个参数timeout结构如下

struct  timeval {

        long    tv_sec;        //秒

        long    tv_usec;     //毫秒

};

设置select函数返回的等待时间

如果参数timeout设为:

NULL,则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件。

0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。

特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

返回值:

执行成功则返回文件描述符状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足

select返回fd_set中可用的套接字个数。
系统调用:

 fd_set是一个SOCKET集合(数组),以下宏可以对该集合进行操作:

FD_CLR( s, *set) 从集合set删除句柄s;

FD_ISSET( s, *set) 检查句柄s是否存在与集合set中;

FD_SET( s, *set )把句柄s添加到集合set中;

FD_ZERO( *set ) 把set队列初始化集合成空.

2.select工作流程

1:用FD_ZERO宏来初始化我们感兴趣的fd_set。

也就是select函数的第二三四个参数。

2:用FD_SET宏来将套接字句柄分配给相应的fd_set。

如果想要检查一个套接字是否有数据需要接收,可以用FD_SET宏把套接接字句柄加入可读性检查集合中

3:调用select函数。

如果该套接字没有数据需要接收,select函数会把该套接字从可读性检查集合中删除掉,

4:用FD_ISSET对套接字句柄进行检查。

如果我们所关注的那个套接字句柄仍然在开始分配的那个fd_set里,那么说明马上可以进行相应的IO操 作。比如一个分配给select第一个参数的套接字句柄在select返回后仍然在select第一个参数的fd_set里,那么说明当前数据已经来了, 马上可以读取成功而不会被阻塞。


3.使用实例

下面给出一个基于udp组播在windows下的实现,linux下可能略有不同

#include <winsock2.h>
#include <mswsock.h>
#include <MSTcpIP.h>
#include <errno.h>
#include <ws2ipdef.h>
#include <process.h>

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "mswsock.lib")
#define MAX_FD 16
fd_set fdread;
SOCKET fd_arr[MAX_FD] = {0};
unsigned int __stdcall threadfunc(void *)
{
	while (1)
	{
		fd_set fd_tmp = fdread;
		timeval tmv;
		tmv.tv_sec = 1;
		tmv.tv_usec = 0;
		int ret = select(0, &fd_tmp, NULL, NULL, &tmv);
		if (ret < 0)
		{
			printf("select failed:%d\n",ret);
			break;
		}
		else
		{
			for (unsigned int i=0;i<fd_tmp.fd_count;i++)
			{
				SOCKET fd = fd_tmp.fd_array[i];
				if (fd > 0)
				{
					SOCKADDR_IN client_addr;
					int nlen_addr = sizeof(SOCKADDR_IN);
					char buff[40960]={0};
					int nrecv = recvfrom(fd, buff, 40960, 0, (SOCKADDR *)&client_addr, &nlen_addr);
					if (nrecv < 0)
					{
						printf("recvfrom error:%d\n", WSAGetLastError());
						continue;
					}
					printf("sock:%d src addr:%s:%d recv data:%s\n", 
						fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port),buff);
				}
			}
			/*for (int i=0;i<MAX_FD;i++)
			{
				SOCKET fd = fd_arr[i];
				if (FD_ISSET(fd, &fd_tmp))
				{
				}
			}*/
		}
	}
	return 0;
};

#define MULTI_IP "239.0.0.37"

#define RECV_START_PORT 16000

#define	LOCAL_BIND_PORT 20260

int _tmain(int argc, _TCHAR* argv[])
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2,2), &wsaData)!=0)
	{
		printf("init socket failed\n");
		return -1;
	}

	FD_ZERO(&fdread);
	for (int i=0;i<MAX_FD;i++)
	{
		SOCKADDR_IN udplocal;
		udplocal.sin_family = AF_INET;
		udplocal.sin_addr.s_addr = inet_addr("0.0.0.0");//htonl(ADDR_ANY)
		udplocal.sin_port = htons(RECV_START_PORT+2*i);

		SOCKET fd_local= socket(PF_INET, SOCK_DGRAM, IPPROTO_IP | IPPROTO_UDP);
		if (fd_local == INVALID_SOCKET)
		{
			printf("create socket() failed\n");
			return -1;
		}
		//int nbroadcast = 1;
		//setsockopt(fd_local, SOL_SOCKET, SO_BROADCAST, (char *)&nbroadcast, sizeof(int));
		int err = bind(fd_local, (SOCKADDR *)&udplocal, sizeof SOCKADDR_IN);
		if (err != 0)
		{
			printf("bind() socket failed\n");
			return -1;
		}
		//char buff[1024]={0};
		//memset(buff, 0x0, 1024);

		//add multicast group 
		IP_MREQ mreq;
		mreq.imr_interface.s_addr = htonl(ADDR_ANY);
		mreq.imr_multiaddr.s_addr = inet_addr(MULTI_IP);
		setsockopt(fd_local, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char*)&mreq, sizeof mreq);

		//set client socket ttl
		int ttl_value = 4;
		if (setsockopt(fd_local, IPPROTO_IP, IP_MULTICAST_TTL, (char *)&ttl_value, sizeof(ttl_value)) != 0)
		{
			printf("setsockopt multicast_ttl failed\n");
			return -1;
		}

		fd_arr[i] = fd_local;
		
		FD_SET(fd_local,&fdread);
	}
	
	::_beginthreadex(NULL,0,&threadfunc,NULL,0,NULL);
	printf("select io model server thread start running\n");
	getchar();
	for (int i=0;i<MAX_FD;i++)
	{
		//shutdown(fd_arr[i], 1);
		closesocket(fd_arr[i]);
	}
	WSACleanup();

	return 0;
}
linux下检查键盘stdio输入
    #include<sys/time.h>  
    #include<sys/types.h>  
    #include<unistd.h>  
    #include<string.h>  
    #include<stdlib.h>  
    #include<stdio.h>  
    int main()  
    {  
            char buf[10]="";  
            fd_set rdfds;  
            struct timeval tv;  
            int ret;  
            FD_ZERO(&rdfds);  
            FD_SET(0,&rdfds);   //文件描述符0表示stdin键盘输入  
            tv.tv_sec = 3;  
            tv.tv_usec = 500;  
            ret = select(1,&rdfds,NULL,NULL,&tv);  
            if(ret<0)  
                  printf("\n selcet error");  
            else if(ret == 0)  
                  printf("\n select timeout");  
            else  
                  printf("\n ret = %d",ret);  
      
            if(FD_ISSET(1,&rdfds))  //如果有输入,从stdin中获取输入字符  
            {  
                  printf("\n reading");  
                  fread(buf,9,1,stdin);  
             }  
             write(1,buf,strlen(buf));  
             printf("\n %d \n",strlen(buf));  
             return 0;  
    }  
    //执行结果ret = 1.  


4.总结讨论

1.select为何效率低

通过select的代码流程,我们发现首先要将文件描述符循环拷贝到select调用的临时集合,内核在调用select时要将用户态数组拷贝到内核态并执行轮询操作,轮询完成后将有事件发送的文件描述符拷贝到select轮询后的集合,并将内核太数据拷贝到用户态,处理时在循环处理返回集合,这里至少3次的循环和数据拷贝,并有用户态数据到内核态,内核态到用户态数据的拷贝,epoll为文件描述符建立一个以红黑树结构的文件系统,大大提高了搜索效率,另外在文件描述符通过epoll_ctl时已经将文件描述符拷贝到内核中,并没有文件描述符的在内核与用户之间的来回拷贝,epoll_wait后之间返回事件节点,不用轮询集合了,iocp也是在套接字和完成端口关联时拷贝到内核态,并在完成后放在完成队列中,用户直接取出处理,不用在循环集合,总结起来就两点,一是避免了大量文件描述符集合的拷贝及重复的处理流程,二是内核在搜索文件描述符可用事件的方法高效

2.如何突破64的限制

#ifndef FD_SETSIZE
#define FD_SETSIZE      64
#endif /* FD_SETSIZE */

typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;

方法1:修改宏定义
linux和windows下重定义FD_SETSIZE宏的大小,linux下修改内核FD_SETSIZE宏定义

方法2:分段轮询

使用一个select轮询多个文件描述符集合,完成一个集合轮询后,切换到下一个集合,轮询的数量可以达到n*64

方法3:多线程

使用多个线程select,每个线程中select轮询64个文件描述符,轮询的文件描述符数量可达到64*n

方法4:动态数组

 linux内核代码中有很多零长度数组的运用,只不过不是c/c++标准,由于轮询的是一个数组,那么也可以定义为动态数组;

这也是libevent的做法,这是libevent在win32下的实现

struct win_fd_set {
u_int fd_count;
SOCKET fd_array[1];
};
 使用时
win_fd_set * Set = (win_fd_set*)malloc(sizeof(win_fd_set) + sizoef(SCOEKT) * 10);
让fd_array动态变化
Set->fd_array 可以放11 个 SOCKET,因为动态开辟的内存控件足够存放11个SOCKET。


  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值