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永久阻塞。
思考两个问题:
- 如何将用户关注的文件描述符设置到fd_set结构体变量
- 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的缺点:
- 只能关注三种事件类型
- select 在线修改fd_set的状态,每次需要重新设置。
- fd_set 最大1024位,也就是0-1023
- 返回值探测只返回就绪文件个数,用户探测就绪的文件描述符的时间复杂度位o(n)
- 内核使用轮询的方式检测就绪文件描述符,内核事件复杂度也是O(n)