I/O多路转接(一)——select函数
I/O的过程可以分成两步,等待和数据搬迁。
等待的过程等的是,读写事件就绪,比如说缓冲区有数据了说明读事件就绪,空了则说明写事件就绪。I/O的多路转接可以同时等待多个文件描述符,大大节省了I/O的等待时间,从而提高I/O效率。
多路转接函数要做的工作就是等待,等待读写或者异常事件就绪后通知用户。
第一篇介绍的是select函数。
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
返回值
大于0,有多少fd就绪;等于0,超出等待时间却没有一个fd就绪;小于0,出错。
参数
nfds:需要select等待的file descriptor数量。
readfds,writefds,exceptfds:分别代表了读事件集,写事件集,异常事件集。
timeout:代表了愿意等待的时间,timeout==NULL代表永远等待;timeout->tv_sec,
timeout->tv_usec分别代表秒与微妙,都等于0代表不等待检测后直接返回;不等于0代表愿意等待吃的时间。
对于读写异常事件集,用专门的操作函数。
void FD_CLR(int fd, fd_set *set);//清理一个fd
int FD_ISSET(int fd, fd_set *set);//检测一个fd是否就绪
void FD_SET(int fd, fd_set *set);//添加一个fd到set,代表你想关心哪个fd的哪个事件
void FD_ZERO(fd_set *set);//初始化
Select缺点
- select等待的file descriptor的数量是有上线的,默认为1024。
- 调用select时,需要将fd_array集合从用户态拷贝到内核态,同时也要在内核态中遍历所有传进来的fd。
- 由于三个事件集参数均是输入输出型参数,用户需要自己维护一个fd_array集合用来保存想要等待的file descriptor的事件,每次调用select前需要重新设置三个事件集,调用结束后又需要遍历检测 。
- 当select所等待的fd数量越来越多时,2与3的开销会越来越大,服务器的性能也随之越来越差。
下面是使用select编写的简单网路服务器
为了简单快速使用select这里只关心读事件。
server.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
int startup(char*ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){
perror("bind");
return 3;
}
if(listen(sock,10) < 0){
perror("listen");
return 4;
}
return sock;
}
int main(int argc,char *argv[])
{
if(argc!= 3){
printf("Usage:%s [ip] [port]\n",argv[0]);
return 1;
}
int listen_sock = startup(argv[1],atoi(argv[2]));
printf("listen_sock has been created,the value is %di\n",listen_sock);
//监听套接字创建完成,等待读事件发生。
fd_set rfds;//读事件描述副集。
int size = sizeof(rfds)*8;
int fd_array[size];//创建一个保存所有关心描述符的数组。
int i = 0;
for(;i<size;i++ ){//初始化数组。
fd_array[i] = -1;
}
fd_array[0] = listen_sock;//数组第一个位置永远保存监听套接字描述符。
struct timeval timeout = {5,1};
while(1){
int max = -1;
FD_ZERO(&rfds);//清空读事件描述符集。
for(i = 0;i < size; i++ ){
FD_SET(fd_array[i],&rfds);//将想要关心的读时间描述符添加
//到读事件描述符集。
if(max < fd_array[i])
max = fd_array[i];//找出最大的描述符
}
//设置读事件描述符集完成,开始调用select进行等待
int ret = select(max + 1,&rfds,NULL,NULL,NULL/*&timeout*/);
switch(ret){
case 0:
printf("timeout\n");
break;
case -1:
perror("select");
break;
default:
{
//一一查看所关心的读事件描述符是否发生状态改变
for(i= 0;i < size;i++){
if(fd_array[i] < 0)
continue;
if(i==0&&FD_ISSET(fd_array[i],&rfds)){ //listen_sock描述符状态改变说明有client请求连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct
sockaddr*)&client,&len);
if(new_sock < 0){
perror("accept");
continue;
}else{
printf("get a client!ip:%s,port:%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
for(i= 0;i<size;i++){//找到一个未被占用的位置存放新链接的描述符。
if(fd_array[i]< 0){
fd_array[i]= new_sock;
break;
}
}
if(i==size){
close(new_sock);
printf("server is full\n");
}
}
}else if(i!= 0&&FD_ISSET(fd_array[i],&rfds)){//其他关心描述符状态改变。
char buf[1024];
ssize_t s = read(fd_array[i],buf,sizeof(buf)-1);
if(s >0){
buf[s]= 0;
printf("client say# %s\n",buf);
}else if(s==0){
printf("client is quit!\n");
close(fd_array[i]);
fd_array[i]= -1;
}else{
perror("read");
close(fd_array[i]);
fd_array[i]= -1;
}
}
}
}
break;
}
}
return 0;
}