好久没用I/O复用了,感觉差不多都快忘完了,记得当初刚学I/O复用的时候花了好多时间,但是由于那会不太爱写博客,导致花很多时间搞明白的东西,依然很容易忘记。俗话说眼过千遍不如手过一遍,的确,在以后的学习中,无论知识的难易亦或是重要程度如何,我都会尽量义博客的形式记录下来,这样即能用博客来督促自己学习,也能加深对知识的理解俩全其美,好了废话不说了。
I/O复用的基本概述
I/O复用技术主要是用来同时监听多个套接字描述符,使得我们的程序大幅度的提高性能,一般如下情况会用到I/O复用技术
(1)程序需要同时处理多个socket
(2)客户端程序需同时处理用户输入和网络连接
(3)TCP服务器要同时处理监听socket和连接socket
(4)服务器要同时处理TCP和UDP请求
(5)服务器要同时处理多个端口
1.select系统调用
#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
.ndf参数指定被监听文件描述符个数,它通常被设为select监听的所有文件描述符加1。
.readfds,writefds,exceptfds参数分别指向可读,可写和异常事件,应用程序通过将自己感兴趣的文件描述符加入到对应的集合中去,select调用返回时,内核将修改他们来通知应用程序哪些文件描述符已经就绪,timeout为超时时间,select调用成功返回就绪的文件描述符个数
我们一般使用如下宏来访问fd_set中的位
#include<sys/select.h>
FD_ZERO(fd_set *fdset); //清除fdset的所有位
FD_SET(int fd,fd_set *fdset);//设置fdset的fd位
FD_CLR(int fd,fd_set *fdset);//清除fdset的位fd
int FD_ISSET(int fd,fd_set *fdset); //测试fdset的位是否被设置
文件描述符就绪条件
(1)socket内核接收缓冲区大于或等于其低水位标志SO_RCVLOWAT.此时我们可以无阻塞的读该socket
(2)socket通信的对方关闭连接,此时对该socket的读操作将返回0
(3)监听socket上有新的连接请求
(4)socket上有未处理的错误
(5)socket的内核发送缓冲区大于其低水位字节SO_SNDLOWAT
(6)socket使用非阻塞connect连接成功或失败之后
(7)socket上有未处理的错误
具体实例
参考伪代码如下
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<sys/select.h>
int main(void)
{
if(argc < 3)
{
cout<<"参数有误"<<endl;
}
char *ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address,sizeof(adddress));
address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(AF_INET,SOCK_STREAM,0);
assert(ret != -1);
ret = bind(listenfd,(struct sockaddr *)&address,sizeof(address));
assert(ret != -1);
ret = listen(listenfd,5);
assert(ret != -1);
struct sockaddr_in client_address;
socklen_t len = sizeof(client_address);
int connfd = accept(listenfd,(struct sockaddr *)&client_address,&len);
if(connfd < 0)
{
cout<<"error"<<endl;
close(listenfd);
}
char buf[1024];
fd_set readfds;
FD_ZERO(&readfds);
while(1)
{
bzero(buf,1024);
FD_SET(connfd,&readfds);
ret = select(connfd + 1,&readfds,NULL,NULL,NULL);
if(ret < 0)
{
cout<<"error"<<endl;
}
//判读可读事件是否发生
if(FD_ISSET(connfd,&readfds))
{
ret = recv(connfd,buf,sizeof(buf) - 1,0);
if(ret <= 0)
{
break;
}
cout<<buf<<endl;
}
}
close(listenfd);
close(connfd);
return 0;
}
select的特点
select的内部实现调用了poll(下面会写道,所以读者可以先跳过这里,去读poll,然后在回头一起看这个),所有它和poll的特点相同,只是函数接口有所不同,本质一样
poll的特点如下
(1)将用户传入的pollfd数据(对应select的描述符集合)拷贝到内核空间,这个拷贝过程事件复杂度为O(N)
(2)挨个查询每一个文件描述符的状态,如果无就绪的文件描述符,则进程就会挂起等待,知道发生超时或设备驱动再次唤醒它,然后它再次遍历所有的文件描述符,找出发生事件的文件描述符。由于其共遍历2次文件文件描述符,所以其事件复杂度为O(N)
(3)将获得的数据拷贝至用户空间。时间复杂度又是O(N)
2.poll的使用
poll的原型如下
#include<poll.h>
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
其中fds为pollfd类型的结构体数组其结构体定义如下
struct pollfd
{
int fd; //要监听文件描述符
short events;//注册的事件
short revents;//实际发生的事件,内核填充
}
poll可监听的事件类型如下(只列出了常用的)
事件 | 描述 |
---|---|
POLLIN | 数据可读 |
POLLOUT | 数据可写 |
POLLRDHUB | TCP连接被对方关闭,或对方关闭了写操作 |
POLLHUB | 挂起,比如管道的写端被关闭 |
POLLERR | 错误 |
nfds为监听的文件描述符个数
timeout为超时事件
索引Poll返回的文件描述符
int ret = poll(fds,MAX_EVENT_NUMBER,-1);
//必须遍历所有文件描述符找到其中的就绪事件(也可以根据已知的就绪个数进行简单的优化)
for(int i = 0;i<MAX_EVENT_NUMBER,i++)
{
if(fds[i].revents & POLLIN)
{
int sock = fds[i];
}
}
关于poll的特点读者可以回到select去看,前面有写到,因为实在说其和select的内部实现机制是一样的所以没必要多余写
3.epoll的使用
epoll是linux特有的I/O复用函数,他在实现和使用上与其他I/O复用有所不同。它是使用一组函数来完成任务的,其次epoll把用户关心的文件描述符上的事件放在一个内核事件表中。epoll需要使用一个额外的文件描述符来来唯一的标识内核中的这个事件表,文件描述符使用epoll_create()来创建
#include<sys/epoll.h>
int epoll_create(int size);
size参数告诉内核事件表需要多大,该函数返回的文件描述符,将用于接下来所有函数的第一个参数
下面函数用来操作内核事件表
#include<sys/epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
fd为要操作的文件描述符,op参数则指定操作类型,操作类型有如下几种
操作类型 | 具体描述 |
---|---|
EPOLL_CTL_ADD | 往事件表中注册fd上的事件 |
EPOLL_CTL_MOD | 修改fd上的注册事件 |
EPOLL_CTL_DEL | 删除fd上的注册事件 |
epoll_event结构体的定义如下
struct epoll_event
{
_uint32_t events //epoll事件
epoll_data_t //用户数据
}
其中epoll_data_t是个联合体其定义如下
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t
epoll_wait()函数
epoll事件调用的主要接口就是epoll_wait函数,它在一段超时事件内等待一组文件描述符上的事件
#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
该函数成功时返回就绪的文件描述符个数
如果epoll_wait检测到就绪事件,就将所有的就绪事件赋值到第二个参数epoll_event数组当中,它只用于输出检测到的就绪事件。不想poll的数组参数既用于传入用户注册的事件,又用于输出内核检测的事件,这会极大的降低应用程序索引文件描述符的效率
epoll的使用
int ret = epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
//只需遍历ret个文件描述符
for(int i = 0;i<ret;i++)
{
int sockfd = events[i].data.fd;
}
特别注意epoll对文件描述符有俩中模式LT和ET,ET是epoll的高效模式
epoll的特点
epoll在内核实现中是根据每个fd上的俄callback函数来实现的,只有活跃的fd才会主动调用callback,其他的fd则不会。如果所监控的所有文件描述符基本上都是活跃的那么epoll和select或poll差距不是太大,但是要是所监控的文件描述符只有少数活跃,epoll的效率要远高于他俩