> 文章列表 > 【Linux】I/O复用

【Linux】I/O复用

【Linux】I/O复用

 概述

         I/O复用使得程序能同时监听多个文件描述符,看文件上是否有用户感兴趣的事件发生,提高程序性能。

使用I/O复用技术的情况:

客户端同时处理多个套接字

客户端同时处理用户输入和网络连接

同时监听多个端口

同时处理TCP和UDP请求

TCP服务器同时处理监听连接套接字

I/O复用的方法有三种:select、poll、epoll; 


select系统调用

select可以监听:用户感兴趣的文件描述符上的可读、可写、异常等。

需要包含头文件:#include <sys/select.h>

函数声明:

maxfd:被监听的文件描述符总数,它通常被设置为 select 监听的所有文件描述符中的最大值+1一共1024个位,只用检查maxfd+1(下标从0开始)位;

readfds、 writefds 和 exceptfds 参数分别指向可读、可写和异常等事件对应的文件描述符集合

timeout:超时时间(毫秒)

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

 

文件描述符按位插入每一轮要重新填充描述符,文件描述符就绪了位为1,否则为0,需要查看位为1的

设置文件描述符集合的函数:

FD_ZERO(&fdset)清空

FD_SET(fd,&fdset)fd添加到集合置1.

FD_ISSET(fd,&fdset)//测试fdset的位fd是否被设置

用select达成I/O复用的服务器端

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/select.h>
#include<string.h>
#include<time.h>
//0:标准输入的文件描述符
#define STDIN 0 
int main()
{int fd = STDIN;//fd代表键盘输入的文件fd_set fdset;//创建一个文件描述符集合while(1){FD_ZERO(&fdset);//清空成0000 0000FD_SET(fd,&fdset);//将描述符fd添加到描述符集合struct timeval tv = {5,0};//设置超时时间int n=select(fd+1,&fdset,NULL,NULL,&tv);//select方法I/O复用if(n==-1)//里面没有描述符{printf("select err\\n");}else if(n==0)//超时了{printf("timeout\\n");}else//添加了文件描述符{if(FD_ISSET(fd,&fdset))//判断是否有关注的fd事件{//有事件char buff[128]={0};read(fd,buff,127);//read127个写入的内容到buff里printf("read:%s\\n",buff);}}}
}

poll系统调用

poll和select的本质是一样的:在一定时间内轮询一定数量的文件描述符,测试其是否就位。

但是它监测的文件描述符的个数比select多。

头文件:#include<poll.h>

poll(文件描述符上的事件,被监听事件集合fds的大小,超时时间)

int poll(struct pollfd* fds,nfds_t nfds,int timeout);struct pollfd
{int fd;//文件描述符short events;//注册的事件,用户添加的感兴趣的事件short revents;//实际发生的事件,由内核填充
}

事件有以下几种(比select多) 

 

用poll达成I/O复用的服务器端

由于poll里面需要传一个结构体fds,需要将结构体初始化:

#define  MAXFD    10
void fds_init(struct pollfd fds[])
{for( int i = 0; i < MAXFD; i++){fds[i].fd = -1;//文件描述符初始化成-1fds[i].events = 0;//用户想检测的事件fds[i].revents = 0;//内核返回的事件}
}

在文件描述符集合中添加、删除描述符:

select有FD_SET可以直接用,但是poll要自己写

//添加
void fds_add(struct pollfd fds[],int fd)
{for( int i = 0; i < MAXFD; i++){if ( fds[i].fd == -1)//没有被处理的文件描述符{fds[i].fd = fd;fds[i].events = POLLIN;//POLLIN输入fds[i].revents = 0;break;}}
}
//删除
void fds_del(struct pollfd fds[],int fd)
{for( int i = 0; i < MAXFD; i++){if ( fds[i].fd == fd){fds[i].fd = -1;//置回初始值fds[i].events = 0;//无感兴趣事件fds[i].revents = 0;break;}}
}

处理文件描述符:

收到监听套接字文件描述符sockfd:listen()产生的sockfd 用 accept()处理

收到连接套接字文件描述符c:accept()产生的c 用recv()处理

在接收到套接字并其事件为用户感兴趣的时需要判断其为监听套接字还是连接套接字,分别调用。

//处理监听套接字
void accept_client(struct pollfd fds[], int sockfd)
{struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//accept处理已完成监听队列中的内容if ( c < 0 )//接收失败{return;} fds_add(fds,c);//将该链接套接字文件描述符添加到描述符集合printf("c=%d\\n",c);//输出链接套接字文件描述符的值
}
//处理连接套接字
void recv_data(struct pollfd fds[], int c)
{char buff[128] = {0};int n = recv(c,buff,127,0);//把c里面的127个数据读到buff里if ( n <= 0 )//没有接受到数据{close(c);//关闭套接字fds_del(fds,c);//将该文件描述符从fds中删除printf("client close\\n");return;}printf("buff(%d)=%s\\n",c,buff);//接收到数据,输出send(c,"ok",2,0);//返回ok
}

创建套接字

int socket_init()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if ( sockfd == -1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if ( res == -1){return -1;}res = listen(sockfd,5);if ( res == -1){return -1;}return sockfd;
}

主函数

int main()
{//创建套接字int sockfd = socket_init();if ( sockfd == -1){printf("socket err\\n");exit(1);}//创建poll文件套接字集合fdsstruct pollfd fds[MAXFD];fds_init(fds);//初始化fds_add(fds,sockfd);//将套接字添加到套接字集合while( 1 ){int n = poll(fds,MAXFD,5000);//文件描述符,最多的个数,超时时间if ( n == -1){printf("poll err\\n");}else if ( n == 0 ){printf("time out\\n");}else{for(int i = 0;i < MAXFD; i++ )//把所有文件描述符遍历一遍-轮询找发生事件的{if ( fds[i].revents & POLLIN )//是POLLIN事件{if ( fds[i].fd == sockfd )//是监听套接字{accept_client(fds,fds[i].fd);}else//是连接套接字{recv_data(fds,fds[i].fd);}}}}}
}

epoll系列系统调用

        epoll是Linux特有的I/O复用函数。它将用户关心的文件描述符放到一个内核事件表中。

内核事件表的底层是

头文件:#include<sys/epoll.h>

文件描述符创建:

int epoll_create(int size);

size提示内核,这个事件表要多大;

操作内核事件表

int epoll_ctl(int epfd,int op,int fd,struct epoll_event*event);
//          (内核事件表, 操作, 文件描述符,类型)
struct epoll_event
{__uint32_t events;//epoll事件epoll_data_t data;//文件描述符
}

EPOLL_CTL_ADD往事件表中注册fd上的事件

EPOLL_CTL_MOD修改fd上的注册事件

EPOLL_CTL_DEL删除fd上的注册事件

epoll的添加、删除: 

void epoll_add(int epfd, int fd)
{struct epoll_event ev;ev.data.fd = fd;ev.events = EPOLLIN;if ( epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1){printf("epoll add err\\n");}
}void epoll_del(int epfd, int fd)
{if( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1){printf("epoll del err\\n");}
}

其他与poll的函数几乎一样(在poll前面加个e)

epoll_wait()

        epoll的主要接口,在一定时间内等待一组文件描述符上的事件

#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event*events,int maxevevts,int timeout);

当epoll_wait检测到事件,将所有就绪的事件从内核事件表epfd中复制到events指向的数组中,events数组用于输出检测到的就绪事件,不用轮询所有文件描述符,只用遍历这个包含就绪了的文件描述符的表,提升效率。

LT和ET模式

        epoll的两种模式:LT(平电触发)模式(默认),ET(边沿触发)模式。

LT模式当epoll_wait检测到事件并通知程序后,程序可以不立即处理事件,只要有事件没处理,epoll_wait就会一直通知程序。

ET模式当epoll_wait检测到事件并通知程序后,程序必须立即处理事件,它只会在检测到时通知一次,后续不会再通知这个事件。(降低重复触发次数,高效)

event.events |= RPOLLET;//fd启用ET模式

立即处理的方法

需要一次处理将所有数据读完:循环读数据,当:recv的返回值<0且

errno==EWOULDBLOCK||errno==EAGAIN表示数据全部读取完。

用errno需要 #include<errno.h>

三种复用的区别