Q:epoll是什么,有哪些接口以及实现什么功能?
A:epoll是SUS标准之外,只在Linux平台上提供的多路复用I/O接口,普遍用于高并发的网络编程,用以代替select/poll,提高并发网络I/O效率。epoll不是一个函数,而是一组函数,由epoll_create、epoll_ctl和epoll_wait组成,包含在sys/epoll.h头文件中。
- epoll_create:
int epoll_create(int size);
调用此函数内核会产生一个epoll instance数据结构并返回一个文件描述符,这个特殊的描述符就是epoll instance的句柄,后面的两个接口都以它为中心。size参数表示所要监视文件描述符的最大值,不过在目前的Linux版本中已经被弃用(调用的时候不要用0,否则会报invalid argument)
- epoll_ctl:
typedef union epoll_data {
void *ptr; /* 指向用户自定义数据 */
int fd; /* 注册的文件描述符 */
uint32_t u32; /* 32-bit integer */
uint64_t u64; /* 64-bit integer */
} epoll_data_t;
struct epoll_event {
uint32_t events; /* 描述epoll事件 */
epoll_data_t data; /* 见第一个结构体 */
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
对于需要监视的文件描述符集合,epoll API使用interest list进行管理,list中每个成员由描述符值和所要监控的文件描述符指向的文件表项的引用等组成。epoll_ctl就是管理interest list的接口,op参数说明操作类型:
op value | meaning |
---|---|
EPOLL_CTL_ADD | 向interest list添加一个需要监视的描述符 |
EPOLL_CTL_DEL | 从interest list中删除一个描述符 |
EPOLL_CTL_MOD | 修改interest list中一个描述符 |
struct epoll_event结构描述一个文件描述符的epoll行为。在使用epoll_wait函数返回处于ready状态的描述符列表时,data域是唯一能给出描述符信息的字段,所以在调用epoll_ctl加入一个需要监测的描述符时,一定要在此域写入描述符相关信息;events域是bit mask,描述一组epoll事件,在epoll_ctl调用中解释为:描述符所期望的epoll事件。常用的事件描述如下:
epoll event | meaning |
---|---|
EPOLLIN | 描述符处于可读状态 |
EPOLLOUT | 描述符处于可写状态 |
EPOLLET | 将epoll event通知模式设置成edge triggered |
EPOLLONESHOT | 第一次进行通知,之后不再监测 |
EPOLLHUP | 本端描述符产生一个挂断事件,默认监测事件 |
EPOLLRDHUP | 对端描述符产生一个挂断事件 |
EPOLLPRI | 由带外数据触发 |
EPOLLERR | 描述符产生错误时触发,默认检测事件 |
EPOLLONESHOT:带有这个标志的描述符,在第一次处于ready状态并被epoll_wait返回之后,内核就会将此描述符标记为inactive状态,之后不会对它进行检测。
- epoll_wait
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
处于ready状态的那些文件描述符会被复制进ready list中,epoll_wait用于向用户进程返回ready list。evlist和maxevents两个参数描述一个由用户分配的struct epoll event数组,调用返回时,内核将ready list复制到这个数组中,并将实际复制的个数作为返回值。注意,如果ready list比evlist长,则只能复制前maxevents个成员;反之,则能够完全复制ready list。另外,struct epoll event结构中的events域在这里的解释是:在被监测的文件描述符上实际发生的事件。参数timeout描述在函数调用中阻塞时间上限,单位是ms:
- timeout = -1表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;
- timeout = 0用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;
- timeout > 0表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时。
Q:Edge triggered和Level triggered两种模式有什么不同?
- ET模式(边缘触发):在两次epoll_wait调用之间,如果所监测文件描述符没有状态上的变化(not ready到ready),即便其处于ready状态也不会被放进ready list中。
- LT模式(水平触发):每次调用epoll_wait的时候,只要被监测的文件描述符处于ready的状态就予以返回。
这就意味着,使用ET模式的描述符,如果没有读/写到使描述符状态发生变化(可读-》无数据可读、可写-》无空间可写),以后就收不到关于这个描述符ready的通知,也就是说这个描述符废掉了。ET模式与LT模式相比优点在于效率高,所以也称为高速模式。
Q:epoll API基本实现原理是什么?
下图是我对epoll实现机制的一些粗浅的理解:
当调用epoll_create的时候,内核会生成一张in-memery inode表和一张打开的文件表项,后者也就是所谓的epoll instance,并且在进程的文件描述符表中找到一个能用的描述符,使其指向epoll instance。在epoll instance中除了常规的文件状态标志等,还有指向interest list表和ready list表的指针,这两个表的成员结构相同,至少包含被监测的描述符值和指向此描述符对应文件表项的指针。epoll_create就是向interest list中添加、删除、修改成员的接口,而epoll_wait就是返回ready list中成员的接口。
当一个文件表项消失之后(最后一个指向这个表项的文件描述符被关闭),interest list中指向这个表项的成员就会被自动删除;反过来讲,只要文件表项没有消失,interest list中相应的成员就不会自动被删除,即使当初被加入insteret list的那个描述符已经被关闭(但指向的文件表项没关闭,因为还有其他描述符指向他),其对应的interest list中的成员仍然还在。
另外,复制epfd之后,复制的那个描述符指向同一个epoll instance,跟epfd有着相同的功能。更进一步,在fork一个子进程之后,两个进程中各有一个文件描述符epfd,因为epoll instance不会被复制,所以这两个epfd的指向也是一样的,他们都能对其进行操作。
Q:epoll为什么比select/poll快?
- select/poll是一次调用一次监测,每次调用的时候都需要向内核复制描述符列表,返回结果时内核也要向用户空间复制结果列表(两个表一样长),如果所要监测的描述符很多,这样的拷贝就会很耗时。相反,epoll接口是一次注册,多次监测,使用epoll_create将描述符加入interest list中之后,就可以多次使用epoll_wait返回结果,而且ready list相较于interest list一般都短很多,这样在描述符很多调用次数很多的情况下优势就很明显。另外,epoll使用mmap技术来和内核交换数据,这样内核和用户进程可以在同一块共享内存中操作两个list,I/O效率进一步提高。
- select/poll的查询是对每个描述符进行轮询,如果描述符很多,轮询就会很耗时。反观epoll API,它使用红黑树组织interest list,查询效率很高;另一方面,epoll会注册回调函数,在描述符就绪的时候主动把它复制到ready list中。这两方面的改进使得epoll比select/poll查询效率更高。
(关于mmap,红黑树,回调函数是怎么使用的还不了解)
实践
主进程fork10个子进程,各自通过pipe与主进程通信,兄弟进程之间互不相干。子进程循环的向pipe写消息,时间间隔为10s内的随机值;主进程使用epoll以ET模式监视十个pipe读端,epoll_wait的超时设为3s,读取子进程的消息并打印在stdout上。
子进程
/*****************************************************
* send a string to standard output at a random time pause
* until received quit from standard input
****************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#define BSZ 64
int main(void)
{
char buf[BSZ] = {0};
int sleep_time;
static int call_id = 0;
//random seed
srand((int)time(NULL));
while (1)
{
sleep_time = rand() % 10 + 1;
sleep(sleep_time);
sprintf(buf, "This is the %d call from client %d\n", ++call_id, getpid());
if (write(STDOUT_FILENO, buf, BSZ) < 0)
{
perror("write error:");
exit(1);
}
}
return 0;
}
父进程:
/************************************************
* fork 10 child process and communicate through
* pipe. use epoll to monitor 10 file descriptors
* and put the ready strings to the standard output
* *************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#define BSZ 64
void err_exit(const char *str)
{
perror(str);
exit(1);
}
//try to use the "read to the end way" to read ET-mode fds
int transmit_data(int in_fd, int out_fd)
{
char buf[5];
int len = 0;
int cnt = 0;
int fl = 0;
//set non-block of in_fd
fl = fcntl(in_fd, F_GETFL, 0);
fcntl(in_fd, F_SETFL, fl | O_NONBLOCK);
while ((len = read(in_fd, buf, 5)) > 0)
{
write(out_fd, buf, len);
cnt += len;
}
if (errno != EAGAIN)
err_exit("read error in transmit");
fcntl(in_fd, F_SETFL, fl);
return cnt;
}
int main(int argc, char **argv)
{
pid_t pids[10];
struct epoll_event evlist[10];
char buf[BSZ];
int fl, i, quit_flag = 0;
int fds[10][2], epfd = -1;
int cnt = 0;
int ready_cnt = 0;
//set stdin non-blocked
fl = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK);
//construct pipes
for (i = 0; i < 10; i++)
if (pipe(fds[i]) < 0)
err_exit("pipe error");
//init epoll_event
for (i = 0; i < 10; i++)
{
//set to ET mode
evlist[i].events = EPOLLIN | EPOLLET;
evlist[i].data.fd = fds[i][0];
}
//fork children
for (i = 0; i < 10; i++)
{
if ((pids[i] = fork()) < 0)
err_exit("fork error");
//child
else if (pids[i] == 0)
{
close(fds[i][0]);
if (fds[i][1] != STDOUT_FILENO)
{
dup2(fds[i][1], STDOUT_FILENO);
close(fds[i][1]);
}
execl("client", "client", NULL);
err_exit("execl error");
}
//parent
else
{
close(fds[i][1]);
}
}
//sign up epoll events
if ((epfd = epoll_create(7)) < 0)
err_exit("epoll_create error");
for (i = 0; i < 10; i++)
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fds[i][0], &evlist[i]) < 0)
err_exit("epoll_ctl error");
//start epoll monitoring
while (!quit_flag)
{
memset(buf, 0, BSZ);
if (read(STDIN_FILENO, buf, BSZ) >= 0)
{
if (strncmp(buf, "quit", 4) == 0)
quit_flag = 1;
else
printf("wrong code");
}
else if (errno != EAGAIN)
err_exit("read_error");
//clean evlist
for (i = 0; i < 10; i++)
{
evlist[i].events = 0;
evlist[i].data.fd = -1;
}
if (!quit_flag)
{
if ((ready_cnt = epoll_wait(epfd, evlist, 10, 3000)) < 0)
err_exit("epoll_wait error");
printf("This is the %d epoll report:\n%d fds are ready\n", ++cnt, ready_cnt);
// print those whose fd state are ready
for (i = 0; i < ready_cnt; i++)
{
if (evlist[i].events & EPOLLIN)
{
transmit_data(evlist[i].data.fd, STDOUT_FILENO);
//read(evlist[i].data.fd, buf, BSZ);
//write(STDOUT_FILENO, buf, BSZ);
}
}
printf("\n");
}
}
for (i = 0; i < 10; i++)
{
kill(pids[i], SIGKILL);
waitpid(pids[i], NULL, 0);
}
fcntl(STDIN_FILENO, F_SETFL, fl);
return 0;
}