select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。
一张图总结一下select,poll,epoll的区别:
| select | poll | epoll |
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
I/O多路复用
I/O多路复用(multiplexing)的本质是通过系统内核缓冲I/O数据,让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
相信大家都了解了Unix五种IO模型,不了解的可以 => 查看这里
同步I/O,在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。反之是异步IO
阻塞I/O, 等待读写事件是否是阻塞的。
blocking IO | 同步 | 阻塞IO |
nonblocking IO | 同步 | 非阻塞IO |
IO multiplexing - IO多路复用 | 同步 | 非阻塞IO |
signal driven IO - 信号驱动IO | 同步 | 非阻塞IO |
asynchronous IO | 异步IO | 非阻塞IO |
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
在介绍select、poll、epoll之前,首先介绍一下Linux操作系统中基础的概念:
- 用户空间 / 内核空间现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
- 进程切换为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的,并且进程切换是非常耗费资源的。
- 进程阻塞正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得了CPU资源),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
- 文件描述符文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
- 缓存I/O缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
Select
select()函数与recv()函数和send()函数不同的是,recv()函数和send()函数可直接操作文件的描述符。但是使用select()函数时,需要先对所要操作的文件描述符进行查询,查看目标文件的描述符是否可以进行读、写、或者错误操作,然后当文件的描述符满足操作的条件时才进行真正的IO操作,即读和写操作。
select()函数监视的文件描述符可分为3类,分别是readfds,writefds,exceptfds。调用后,select()函数会阻塞,直到有描述符就绪(有数据、可读、可写或者有错误)时,或者超时时才返回,当select()函数返回后,可以通过便利fdset来找到就绪的描述符。需要注意的是,当声明了一个文件描述符集合后,必须用FD_ZERO()函数来将所有位置为0。
函数select()的原型为:
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
各参数含义为:
nfds:整型变量,它比所有文件描述符集合中的最大值大1。使用select的时候必须计算最大值的文件描述符的值,将值通过nfds传入。
readfds:这个文件描述符集合监视文件集合中的任何文件是否有数据可读,当select()函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符,即可以被recv()函数、read()函数等进行读数据的操作。
writefds:这个文件描述符集合监视文件集合中的任何文件是否有数据可写,当select()函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符,即可以被send()函数、write()函数等进行写数据的操作。
exceptfds:这个文件描述符集合监视文件集合中的任何文件是否发生错误,其实它可以用于其他的用途,例如,监视带外数据OOB,带外数据使用MSG_OOB标志发送到套接字上。当select()函数返回的时候,readfds将清除其中的其他文件描述符,只留下可读OOB数据。
timeout:用来描述等待描述符就绪需要的事件。设置在select()函数文件集合中的事件没有发生时,最长的等待时间,当超过此时间时,函数会返回。当超时时间为NULL时,表示阻塞操作,会一直等待,直到某个监视的文件集合中的某个文件描述符符合返回条件。当timeout的值为0时,select()会立即返回。timeout告知系统内核等待指定描述符中的任何一个就绪可花费多少时间。其timeval结构体用于指定这段时间的秒数和微秒数。
struct timeval
{
time_t tv_sec; //秒
long tv_usec; //微秒
};
返回值:成功执行时返回就绪描述符的数目;经过了timeout时长后仍无设备准备好,即超时,返回0;如果出错,返回-1并设置相应的errno,如果select()执行过程中被某个信号中断,返回-1并设置errno为EINTR。
errorno的取值及含义:
EBADF:文件描述符无效或该文件已被关闭
EINVAL:传递了不合法参数
EINTR:接收到中断信号
ENOMEM:没有足够内存
readset,writeset,exceptset都是值-结果参数,即传入指针进去,函数根据指针可以修改对应fd_set。
通常,操作系统通过宏FD_SETSIZE来声明一个进程中select所能操作的文件描述符的最大数目。在/usr/include/linux/posix_types.h中关于FD_SETSIZE是这样定义的:
#undef _FD_SETSIZE
#define _FD_SETSIZE 1024
除此之外,还有4个宏可以用来操作文件描述符的集合:
FD_ZERO(fd_set* fdset):将指定的文件描述符集合清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于系统在分配内存空间后通常不作清空处理,所以结果是不可知的。
FD_SET(fd_set* fdset):用于在文件描述符集合中增加一个新的文件描述符
FD_CLR(fd_set* fdset):用于在文件描述符集合中删除一个文件描述符
FD_ISSET(int fd,fd_set* fdset):用于检测指定的文件描述符是否在该文件描述符集合中
在内核中,socket对应struct socket结构,但在返回给用户空间之前,内核做了一个关联,调用get_unused_fd_flags从当前进程中获取一个可用的文件描述符fd,将struct socket结构关联到该fd,并返回fd给用户空间。所以在用户空间中,socket为文件描述符。另外,进程可以打开的文件数是有限的,为1024,所以socket的取值要小于1024。
select()函数的优缺点:
优点:select()目前几乎在所有的平台上支持,其良好的跨平台支持也是它的一个优点。
缺点:select最大的缺陷就是单个进程所打开的fd的数量是有一定限制的,它由FD_SETSIZE设置,默认值是1024。一般来说,这个数目和系统的内存关系很大,32位机默认是1024,64位机默认是2048;对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个socket来完成调度,不管哪个socket是活跃的,都得遍历一遍,这会在无形中浪费很多CPU时间,如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询;需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void handle(int clientfds[],int max_fd,fd_set* rset,fd_set* allset)
{
int nread;
int i;
char buffer[1024];
for(i = 0;i < max_fd;i++)
{
if(clientfds[i] != -1)
{
if(FD_ISSET(clientfds[i],rset))
{
//读取客户端socket流
nread = read(clientfds[i],buffer,1024);
if(nread < 0)
{
perror("read:");
close(clientfds[i]);
FD_CLR(clientfds[i],allset);
clientfds[i] = -1;
continue;
}
if(nread == 0)
{
printf("客户端关闭了连接!\n");
close(clientfds[i]);
FD_CLR(clientfds[i],allset);
clientfds[i] = -1;
continue;
}
write(clientfds[i],buffer,nread);
}
}
}
}
int main()
{
//设置服务器的端口号为6888
short s_port = 6888;
//设置默认监听队列的长度为1024
int backlog = 1024;
//地址结构
struct sockaddr_in clientaddr; //客户端地址
struct sockaddr_in serveraddr; //服务器地址
//存放客户端通信描述符的数组
int clientfds[FD_SETSIZE];
//所监听的描述符
int listen_fd;
//描述符的集合
fd_set allset,rset;
//用来记录select()函数的返回值
int nselect;
//用来记录最大的描述符
int max_fd;
//用来记录客户端的socket描述符
int client_fd;
//缓冲区长度
char buffer[1024];
//地址结构的长度
int socketlength = sizeof(struct sockaddr_in);
//创建TCPsocket
listen_fd = socket(PF_INET,SOCK_STREAM,0);
//如果创建失败
if(listen_fd < 0)
{
perror("socket:");
return -1;
}
int opt = 1;
//将监听的端口设置为可以复用的
if(setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)) < 0)
{
perror("setsockopt:");
}
//socket地址结构
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(s_port);
//绑定
int nbind = bind(listen_fd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr_in));
if(nbind == -1)
{
perror("bind:");
return -1;
}
//监听
int nlisten = listen(listen_fd,backlog);
if(nlisten < 0)
{
perror("listen:");
return -1;
}
//初始化存放客户端通信描述符的数组
int i = 0;
for(;i < FD_SETSIZE;i++)
{
clientfds[i] = -1;
}
//清理描述符集合
FD_ZERO(&allset);
//将监听的socket描述符加入集合
FD_SET(listen_fd,&allset);
max_fd = listen_fd;
printf("服务器正在监听端口%d......\n",s_port);
while(1)
{
rset = allset;
//等待select事件发生
nselect = select(max_fd+1,&rset,NULL,NULL,NULL);
if(nselect < 0)
{
perror("select:");
return -1;
}
//处理客户端的连接
if(FD_ISSET(listen_fd,&rset)) //检测监听的描述符是否存在于描述符集合中
{
client_fd = accept(listen_fd,(struct sockaddr*)&clientaddr,&socketlength);
if(client_fd < 0)
{
perror("accept:");
continue;
}
sprintf(buffer,"接受来自%s:%d\n",inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
printf(buffer);
//将客户端的socket描述符加入数组中
for(i = 0;i < FD_SETSIZE;i++)
{
if(clientfds[i] == -1)
{
clientfds[i] = client_fd;
break;
}
}
//如果达到了最大连接数
if(i == FD_SETSIZE)
{
printf("已达到最大连接数!\n");
close(client_fd);
}
if(client_fd > max_fd)
{
max_fd = client_fd;
}
//将socket加入集合中
FD_SET(client_fd,&allset);
if(--nselect <= 0)
{
continue;
}
}
//处理客户端收据的收发
handle(clientfds,max_fd,&rset,&allset);
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define max(a,b) ((a) > (b) ? (a) : (b))
void handle(int client_fd)
{
FILE* fp = stdin;
//发送队列和接收队列,设置为1024
char sendqueue[1024],receivequeue[1024];
fd_set rset;
FD_ZERO(&rset);
int max_fd = max(fileno(fp),client_fd) + 1;
for(;;)
{
FD_SET(fileno(fp),&rset);
FD_SET(client_fd,&rset);
int nselect = select(max_fd,&rset,NULL,NULL,NULL);
if(nselect == -1)
{
perror("select:");
continue;
}
if(FD_ISSET(client_fd,&rset))
{
//接收到服务器的响应
int nread = read(client_fd,receivequeue,1024);
if(nread == 0)
{
printf("服务器关闭了连接!\n");
break;
}
else if(nread == -1)
{
perror("read:");
break;
}
else
{
write(STDOUT_FILENO,receivequeue,nread);
}
}
if(FD_ISSET(fileno(fp),&rset))
{
//标准输入可读
if(fgets(sendqueue,1024,fp) == NULL)
{
break;
}
else
{
write(client_fd,sendqueue,strlen(sendqueue));
}
}
}
}
int main()
{
char* s_iaddr = "127.0.0.1";
int s_port = 6888;
int client_fd;
char buffer[1024];
struct sockaddr_in serveraddr;
client_fd = socket(AF_INET,SOCK_STREAM,0);
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(s_port);
inet_pton(AF_INET,s_iaddr,&serveraddr.sin_addr);
//建立连接
if(connect(client_fd,(struct sockaddr*)&serveraddr,sizeof(serveraddr)) < 0)
{
perror("connect:");
return -1;
}
printf("---回射服务器的客户端---\n");
handle(client_fd);
close(client_fd);
printf("exit\n");
exit(0);
}
Poll
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。
下面是pll的函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
typedef struct pollfd {
int fd; // 文件描述符,如果小于0。这个文件描述符将会被进程忽略
short events; // 注册的事件,即感兴趣的事件。可以是一个事件,也可以是多个事件的按位或
short revents; // 实际发生的事件,由内核来赋值。需要与某种类型的事件按位与来确定某种事件是否发生
} pollfd_t;
poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制远大于select的1024
【参数说明】
struct pollfd *fds fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;
一个pollfd结构体表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域
nfds_t nfds 记录数组fds中描述符的总数量
【返回值】int 函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;
poll事件类型如下表:
自Linux内核2.6.17开始,GUN为poll系统调用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后触发,使用POLLRDHUP时,需要在代码最开始处定义 _GUN_SOURCE。
当然我们也可以通过read来判断对方是否关闭了连接,当read返回0时,表示对方请求关闭连接。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
int main(){
struct pollfd fdset[1];
fdset[0].fd = 0;
fdset[0].events = POLLIN;
int ret;
char buf[100];
while(1){
ret = poll(fdset, 1, -1);
if(fdset[0].revents & POLLIN){
ret = read(0, buf, 100);
printf("读取到%d个字节的数据%s", ret, buf);
}
}
return 0;
}
Epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll操作过程需要三个接口,分别如下:
1 2 3 4 |
|
(1) int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
1 2 3 4 |
|
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
编写一个服务器回射程序echo,练习epoll过程。
//
// Created by ken.li on 2021/5/20.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/types.h>
#define IPADDRESS "127.0.0.1"
#define PORT 8787
#define MAXSIZE 1024
#define LISTENQ 5
#define FDSIZE 1000
#define EPOLLEVENTS 100
//函数声明
//创建套接字并进行绑定
static int socket_bind(const char* ip,int port);
//IO多路复用epoll
static void do_epoll(int listenfd);
//事件处理函数
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf);
//处理接收到的连接
static void handle_accpet(int epollfd,int listenfd);
//读处理
static void do_read(int epollfd,int fd,char *buf);
//写处理
static void do_write(int epollfd,int fd,char *buf);
//添加事件
static void add_event(int epollfd,int fd,int state);
//修改事件
static void modify_event(int epollfd,int fd,int state);
//删除事件
static void delete_event(int epollfd,int fd,int state);
int main(int argc,char *argv[])
{
int listenfd;
listenfd = socket_bind(IPADDRESS,PORT);
listen(listenfd,LISTENQ);
do_epoll(listenfd);
return 0;
}
static int socket_bind(const char* ip,int port)
{
int listenfd;
struct sockaddr_in servaddr;
listenfd = socket(AF_INET,SOCK_STREAM,0);
if (listenfd == -1)
{
perror("socket error:");
exit(1);
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET,ip,&servaddr.sin_addr);
servaddr.sin_port = htons(port);
if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)
{
perror("bind error: ");
exit(1);
}
return listenfd;
}
static void do_epoll(int listenfd)
{
int epollfd;
struct epoll_event events[EPOLLEVENTS];
int ret;
char buf[MAXSIZE];
memset(buf,0,MAXSIZE);
//创建一个描述符
epollfd = epoll_create(FDSIZE);
//添加监听描述符事件
add_event(epollfd,listenfd,EPOLLIN);
for ( ; ; )
{
//获取已经准备好的描述符事件
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
handle_events(epollfd,events,ret,listenfd,buf);
}
close(epollfd);
}
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
int i;
int fd;
//进行选好遍历
for (i = 0;i < num;i++)
{
fd = events[i].data.fd;
//根据描述符的类型和事件类型进行处理
if ((fd == listenfd) &&(events[i].events & EPOLLIN))
handle_accpet(epollfd,listenfd);
else if (events[i].events & EPOLLIN)
do_read(epollfd,fd,buf);
else if (events[i].events & EPOLLOUT)
do_write(epollfd,fd,buf);
}
}
static void handle_accpet(int epollfd,int listenfd)
{
int clifd;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
if (clifd == -1)
perror("accpet error:");
else
{
printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
//添加一个客户描述符和事件
add_event(epollfd,clifd,EPOLLIN);
}
}
static void do_read(int epollfd,int fd,char *buf)
{
int nread;
nread = read(fd,buf,MAXSIZE);
if (nread == -1)
{
perror("read error:");
close(fd);
delete_event(epollfd,fd,EPOLLIN);
}
else if (nread == 0)
{
fprintf(stderr,"client close.\n");
close(fd);
delete_event(epollfd,fd,EPOLLIN);
}
else
{
printf("read message is : %s",buf);
//修改描述符对应的事件,由读改为写
modify_event(epollfd,fd,EPOLLOUT);
}
}
static void do_write(int epollfd,int fd,char *buf)
{
int nwrite;
nwrite = write(fd,buf,strlen(buf));
if (nwrite == -1)
{
perror("write error:");
close(fd);
delete_event(epollfd,fd,EPOLLOUT);
}
else
modify_event(epollfd,fd,EPOLLIN);
memset(buf,0,MAXSIZE);
}
static void add_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}
static void delete_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}
static void modify_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}
客户端也用epoll实现,控制STDIN_FILENO、STDOUT_FILENO、和sockfd三个描述符
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#define MAXSIZE 1024
#define IPADDRESS "127.0.0.1"
#define SERV_PORT 8787
#define FDSIZE 1024
#define EPOLLEVENTS 20
static void handle_connection(int sockfd);
static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_write(int epollfd,int fd,int sockfd,char *buf);
static void add_event(int epollfd,int fd,int state);
static void delete_event(int epollfd,int fd,int state);
static void modify_event(int epollfd,int fd,int state);
int main(int argc,char *argv[])
{
int sockfd;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);
connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
//处理连接
handle_connection(sockfd);
close(sockfd);
return 0;
}
static void handle_connection(int sockfd)
{
int epollfd;
struct epoll_event events[EPOLLEVENTS];
char buf[MAXSIZE];
int ret;
epollfd = epoll_create(FDSIZE);
add_event(epollfd,STDIN_FILENO,EPOLLIN);
for ( ; ; )
{
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
handle_events(epollfd,events,ret,sockfd,buf);
}
close(epollfd);
}
static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf)
{
int fd;
int i;
for (i = 0;i < num;i++)
{
fd = events[i].data.fd;
if (events[i].events & EPOLLIN)
do_read(epollfd,fd,sockfd,buf);
else if (events[i].events & EPOLLOUT)
do_write(epollfd,fd,sockfd,buf);
}
}
static void do_read(int epollfd,int fd,int sockfd,char *buf)
{
int nread;
nread = read(fd,buf,MAXSIZE);
if (nread == -1)
{
perror("read error:");
close(fd);
}
else if (nread == 0)
{
fprintf(stderr,"server close.\n");
close(fd);
}
else
{
if (fd == STDIN_FILENO)
add_event(epollfd,sockfd,EPOLLOUT);
else
{
delete_event(epollfd,sockfd,EPOLLIN);
add_event(epollfd,STDOUT_FILENO,EPOLLOUT);
}
}
}
static void do_write(int epollfd,int fd,int sockfd,char *buf)
{
int nwrite;
nwrite = write(fd,buf,strlen(buf));
if (nwrite == -1)
{
perror("write error:");
close(fd);
}
else
{
if (fd == STDOUT_FILENO)
delete_event(epollfd,fd,EPOLLOUT);
else
modify_event(epollfd,fd,EPOLLIN);
}
memset(buf,0,MAXSIZE);
}
static void add_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}
static void delete_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}
static void modify_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}
总结
epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。