对epoll不够了解可以先看看它的底层实现:The Implementation of epoll (1)
多路IO Multiplex IO
多路IO的作用:
1、阻塞 I/O 只能阻塞一个 I/O 操作,而 I/O 复用模型能够阻塞多个 I/O 操作,所以才叫做多路复用
2、采用epoll模型时创建了一个共享的内存空间,操作系统采用事件通知的方式,使一个进程能同时等待多个文件描述符
3、这样就可以同时监听多个网络连接 IO, 相对于多进程、多线程切换的开销问题,IO 多路复用可以极大的提升系统效率。
谈到多路IO,Linux下最常用的也就是三种IO模型: Select、Poll、Epoll,简单介绍一下epoll的使用。
epoll API
作为三大IO复用函数中比较常用的epoll, 它在实现和使用上与select poll有很大差异.首先, epoll使用一组函数来完成任务, 而不是单个函数. 其次, epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中, 从而无须向 select 和 poll 那样每次调用都要重复传入文件描述符集或事件集, 但 epoll 需要使用一个i额外的文件描述符, 来唯一标识内核中的这个事件表, 这个文件描述符由以下的 epoll_create 函数来创建.
epoll_create
EPOLL_CREATE(2) Linux Programmer's Manual EPOLL_CREATE(2)
NAME
epoll_create, epoll_create1 - open an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_create(int size);
DESCRIPTION
epoll_create() creates an epoll(7) instance. Since Linux
2.6.8, the size argument is ignored, but must be greater than
zero; see NOTES below.
epoll_create() returns a file descriptor referring to the new
epoll instance. This file descriptor is used for all the
subsequent calls to the epoll interface. When no longer
required, the file descriptor returned by epoll_create()
should be closed by using close(2). When all file descrip‐
tors referring to an epoll instance have been closed, the
kernel destroys the instance and releases the associated
resources for reuse.
翻译一下就是:用于创建一个 epoll 实例,size参数自从Linux 2.6.8以来已经被忽略,只需要传入大于0的数即可,函数返回一个文件描述符绑定这个新的epoll实例,当不再使用时需要用close函数关闭它。当所有与epoll实例相关的文件描述符都被关闭的时候,内核会销毁这个实例并且释放对应的资源以便再次使用。
epoll_ctl
epoll_ctl 函数用于操作内核事件表,Linux手册中是这么定义它的:
EPOLL_CTL(2) Linux Programmer's Manual EPOLL_CTL(2)
NAME
epoll_ctl - control interface for an epoll descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
用于操作epoll实例中事件的注册与销毁,以及监听事件的操作类型:
DESCRIPTION
This system call performs control operations on the epoll(7) instance
referred to by the file descriptor epfd. It requests that the opera‐
tion op be performed for the target file descriptor, fd.
第一个参数 epfd:传入需要操作的epoll实例。
第二个参数 op,有三个常用的宏:
EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL,对应完成了事件(fd)的注册、事件的修改、事件的注销(删除)。
第三个参数 fd 传入需要操作的文件描述符
第四个参数***epoll_event***是一个结构体,定义如下:
The struct epoll_event is defined as:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
结构体定义了一个 表示事件类型的 uint32_t events,该参数有常用的几个宏:(只是在poll的宏前面加上了’E’)
EPOLLIN: 表示允许相关的文件描述符进行读操作。
EPOLLOUT: 表示允许相关的文件描述符进行写操作。
其余还有例如: EPOLLERR、EPOLLHUP、EPOLLET… 具体内容可以打开linux使用 man 2 epoll_ctl 查看。
这些宏都是位掩码,因此如果需要多种操作类型或者属性定义的话可以使用:
EPOLLIN | EPOLLOUT,进行位或。
结构体中另一个属性:
epoll_data_t data; epoll_data_t是一个联合,这里笔者只使用了fd这一属性。
深入了解epoll_data点这里
epoll_wait
这是epoll中最为主要的接口函数了,在一段超时时间内等待一组文件描述符上的事件,原型如下:
EPOLL_WAIT(2) Linux Programmer's Manual EPOLL_WAIT(2)
NAME
epoll_wait - wait for an I/O event on an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
DESCRIPTION
The epoll_wait() system call waits for events on the epoll(7) instance referred to by
the file descriptor epfd. The memory area pointed to by events will contain the
events that will be available for the caller. Up to maxevents are returned by
epoll_wait(). The maxevents argument must be greater than zero.
- struct epoll_event events
参数events指示了调用者可用的事件类型,如果epoll实例监视的文件描述符发生了IO事件且我们关心的操作可实施时,内核会将这些文件描述符的信息写入events,因此这是一个传入传出参数。 - int maxevents
参数指定了events数组的大小,如果需要监听的事件比较多,那么可以使用大数组,监听数量不受限。 - int timeout
参数指定了epoll_wait()阻塞的时长,单位是毫秒(milisecond),以下三种情况会解除阻塞:
Ⅰ. 一个文件描述符发生了事件( a file descriptor delivers an event)
Ⅱ. 函数被信号处理器中断(the call is interrupted by a signal handler)
Ⅲ. timeout爆发(the timeout expires)
epoll_wait 函数如果检测到事件, 就将所有就绪的事件从内核事件表(由 epfd 参数指定)中复制到它的第二个参数 events 指向的数组中, 这个数组只用于输出 epoll_wait 检测到的就绪事件, 而不像 select 和 poll 的数组参数那样既用于传入用户注册的事件, 又用于输出内核检测到的就绪事件, 这就极大地提高了应用程序索引就绪文件描述符的效率
下面的一段代码展示了这个区别:
int main(int argc, char* argv[]){
/* 如何索引 poll 返回的就绪文件描述符 */
int ret = poll( fds, MAX_EVENT_NUMBER, -1 );
/* 必须遍历所有已注册文件描述符并找到其中的就绪事件 */
for(int i = 0;i < MAX_EVENT_NUMBER; ++i){
if( fds[i].revents & POLLIN ){
int sockfd = fds[i].fd;
if(--ret){
break;//这里用ret做了一点优化
}
/* 处理sockfd */
}
}
/* 如何索引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;
/* sockfd肯定就绪,直接处理 */
}
return 0;
}
返回值
RETURN VALUE
When successful, epoll_wait() returns the number of file descriptors ready for the
requested I/O, or zero if no file descriptor became ready during the requested time‐
out milliseconds. When an error occurs, epoll_wait() returns -1 and errno is set
appropriately.
若成功调用,返回已经就绪的文件描述符数量,如果是时间爆发,那么返回0. 出错返回-1并且将errno设置成合适值。
epoll使用示例
笔者使用epoll实现了简单的多路IO,监听多个客户端的读事件,读取客户机发来的信息,将其显示在标准输出上,并写回客户端。
服务端代码
/**************************bin************************
******************************************************
******************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define MAXFD 1025
int main(int argc, char* argv[]){
//创建服务端套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr, clnt_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(6666);
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//绑定套接字
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//监听套接字
listen(serv_sock, 10);
socklen_t addrlen = sizeof(clnt_addr);
int epfd = epoll_create(MAXFD);
//将客户套接字挂载到epoll instance上
struct epoll_event epollEvent;
epollEvent.events = EPOLLIN;//申请注册读事件
epollEvent.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &epollEvent);
//传入参数,接收内核写入的被激活的事件
struct epoll_event events[MAXFD];
while(1){
//timeout 传入 -1, 无限阻塞 infinitely
int waitret = epoll_wait(epfd, events, MAXFD, -1);
if(waitret == -1){//出错
perror("epoll_wait error");
exit(1);//线程退出
}
else{
//对每个事件进行处理
for(int i = 0; i < waitret; ++i){
if(events[i].data.fd == serv_sock){
//与我们在外面监听的是同一个套接字
int cfd = accept(serv_sock,(struct sockaddr*)&clnt_addr, &addrlen);
char dst[64];
printf("client is OK, IP:%s, PORT:%d\n", inet_ntop(AF_INET, &clnt_addr.sin_addr.s_addr, dst, sizeof(dst)),ntohs(clnt_addr.sin_port));
epollEvent.data.fd = cfd;
epollEvent.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epollEvent);
}
else{
//保存文件描述符
int fd = events[i].data.fd;
char buf[1024];
//从fd中读取三个 byte
int rr = read(fd, buf, 3);
if(rr < 0){
perror("read error");
exit(1);
}
else if(rr == 0){
printf("\nclient is out\n"); close(fd);
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, NULL);
continue;
}
else {//读到数据
buf[rr] = '\n';
write(1, buf, rr);
write(fd, buf, rr);//写回
}
}
}
}
}
return 0;
}
/**************************bin************************
******************************************************
******************************************************
客户端代码
/**************************bin************************
******************************************************
******************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char* argv[]){
int clntSock = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(clntSock, (struct sockaddr*)&addr, sizeof(addr));
//写入数据
char buf[1024];
while(1){
memset(buf, '\0', 1024);
printf("请输入:\n");
scanf("%s", buf);
if(buf[0] == buf[1] && buf[0] == 'g')
break;
send(clntSock, buf, sizeof(buf), 0);
char newBuf[1024];
int rr = recv(clntSock,newBuf, 1024, 0);
newBuf[rr] = '\0';
printf("服务器回射信息:%s\n\n", newBuf);
}
close(clntSock);
return 0;
}
/**************************bin************************
******************************************************
******************************************************/
运行
参考 --------《Linux高性能服务器编程》