Linux的三种I/O复用方式——select

I/O复用函数

select
poll
epoll<Linux的独有的I/O复用>

接下来我们分三次进行介绍I/O复用

select

select的原型是

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

maxfd:监听的最大文件描述符的值+1//轮询每次要遍历的位数,大于文件描述符总数
readfds,writefds,exceptfds:分别记录关注的可读,可写,异常三种事件的文件描述符的集合
timeout:设置select一次监听的时间,当没有文件描述符就绪,则超时,select返回0,NULL永久阻塞。
思考两个问题:

  1. 如何将用户关注的文件描述符设置到fd_set结构体变量
  2. select返回之后如何判断哪些文件描述符有事件
    我们先来看看fd_set原型
struct
{
	long int fds_bits[32];
}fd_set

fd_set使用按bit存储消息,32*32 = 1024位,也就是可以存储1024个文件描述符,为什么能存储1024个文件标识符?因为long int 是4字节32位,每一位上初始值为0,当某一个文件描述符有改动,则那一位上置1,正好1024个,每次select返回有多少个文件描述符有改动(也就是事件被触发),无法得知那些被改动。如果select在等待期间收到信号,则会立即返回-1,并设置error为EINTR。
这里使用了位运算,我们进行一些拓展:
fd_set(指针)/32就是获得结构体中fds_bits的某一位地址+fd_set%32 = fds_bits响应文件描述符的位置
也就是说数组的某一位地址下标 | 1<<fd%32
fds_bits[1]|1<<23 就是第55位的值

因为位运算过于复杂,系统为我们提供了一些宏,如下
**FD_ZERO(fd_set *fds);//初始化,清空
FD_SET(int fd,fd_set *fds);将fd设置到fds上
FD_CLR(int fd,fd_set fds);清除fds上的fd
FD_ISSET(int fd,fd_set fds);判断fds上的fd文件描述符是否有事件发生(有修改)

其中,FD_ISSET返回值为1表明有改动,0表示没有改动
举个例子

fd_set read;//声明fd_set
FD_ZERO(&read);
int n = select(&read);
n == 1 FD_ISSET(3,&read) == 0;/判断3这个fd有没有被修改,返回0表示3这个文件描述符没有被修改

使用select时,内核会在线修改三个结构体,每次调用select,都必须重置readfds,writefds,exceptfds三个值,也就是说我们每次最多只能关注三种事件,局限性较大。并且有一点需要注意,scokfd和c都是文件描述符,虽然没有区别,但是在响应事件的时候要有区分,一个是建立连接一个是进行I/O操作。I/O复用中单线程单进程可以做到多客户链接,不会一直为客户端服务,有需求的时候才会分配accept(),这正是I/O复用比起多线程多进程的优势所在。

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>

#define FDMAX 100
int fdall[FDMAX];
int maxfd = -1;

void Init_fdall()
{
	int i = 0;
	for(;i <FDMAX;++i)
	{
		fdall[i] = -1;
	}
}

void AddFd(int fd)
{
	int i = 0;
	for(;i <FDMAX;++i)
	{
		if(fdall[i] == -1)
		{
			fdall[i] = fd;
			break;
		}
	}
}

void DelFd(int fd)
{
	int i = 0;
	for(;i <FDMAX;++i)
	{
		if(fdall[i] == fd )
		{
			fdall[i] = -1;
			break;
		}
	}
}

void DealClientLink(int fd,struct sockaddr_in cli)
{
	int len = sizeof(cli);
	int c = accept(fd,(struct sockaddr*)&cli,&len);
	if(c <= 0)
	{
		printf("Link Error\n");
		return;
	}
	AddFd(c);
}

void DealClinkData(int fd)
{
	char buff[128] = {0};
	int n = recv(fd,buff,127,0);
	if(n <= 0)
	{
		close(fd);
		printf("Client can't accept");
		DelFd(fd);
		return;
	}
	printf("%s:%d\n", buff,fd);
	send(fd,"OK",2,0);
}
int main(int argc, char const *argv[])
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd != -1);

	struct sockaddr_in ser,cli;
	memset(&ser,0,sizeof(ser));

	ser.sin_family = AF_INET;
	ser.sin_port = htons(6000);
	ser.sin_addr.s_addr = inet_addr("127.0.0.1");

	int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
	assert(res != -1);

	listen(sockfd,5);
	
	fd_set read;
	Init_fdall();
	AddFd(sockfd);
	
	while(1)
	{
		maxfd = -1;
		FD_ZERO(&read);//重置read
		int i = 0;
		for(;i <FDMAX;++i)
		{
			if(fdall[i] != -1)
			{
				if(fdall[i] > maxfd)
					maxfd = fdall[i];
				FD_SET(fdall[i],&read);
				printf("for Init\n");
			}
		}	
		int n = select(maxfd+1,&read,NULL,NULL,NULL);//返回已就绪的文件描述符个数
		if(n <= 0)
		{
			printf("Error\n");
			continue;
		}
		i = 0;
		for(;i <FDMAX;++i)
		{
			if(fdall[i] != -1 && FD_ISSET(fdall[i], &read))
			{
				if(fdall[i] == sockfd)
				{
					DealClientLink(fdall[i],cli);
				}
				else
				{
					DealClinkData(fdall[i]);
				}
			}
		}	
	}
	return 0;
}

select的缺点:

  1. 只能关注三种事件类型
  2. select 在线修改fd_set的状态,每次需要重新设置。
  3. fd_set 最大1024位,也就是0-1023
  4. 返回值探测只返回就绪文件个数,用户探测就绪的文件描述符的时间复杂度位o(n)
  5. 内核使用轮询的方式检测就绪文件描述符,内核事件复杂度也是O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值