2021- 十个问题理解Linux epoll 工作原理
十个问题理解Linux epoll 工作原理
Question 1:是否所有的文件类型都可以被epoll 监视?
答案:不是。看下面这个实验代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define MAX_EVENTS 1
int main (void)
{
int epfd;
epfd = epoll_create(100); /* 创建epoll实例,预计监听100个fd */
if (epfd < 0) {
perror ("epoll_create");
}
struct epoll_event *events;
int nr_events, i;
events = malloc (sizeof (struct epoll_event) * MAX_EVENTS);
if (!events) {
perror("malloc");
return 1;
}
/* 打开一个普通文本文件 */
int target_fd = open ("./11.txt", O_RDONLY);
printf("target_fd %d\n", target_fd);
int target_listen_type = EPOLLIN;
for (i = 0; i < 1; i++) {
int ret;
events[i].data.fd = target_fd; /* epoll调用返回后,返回给应用进程的fd号 */
events[i].events = target_listen_type; /* 需要监听的事件类型 */
ret = epoll_ctl (epfd, EPOLL_CTL_ADD, target_fd, &events[i]); /* 注册fd到epoll实例上 */
if (ret) {
printf("ret %d, errno %d\n", ret, errno);
perror ("epoll_ctl");
}
}
/* 应用进程阻塞在epoll上,超时时长置为-1表示一直等到有目标事件才会返回 */
nr_events = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nr_events < 0) {
perror ("epoll_wait");
free(events);
return 1;
}
for (i = 0; i < nr_events; i++) {
/* 打印出处于就绪状态的fd及其事件 */
printf("event=%d on fd=%d\n", events[i].events, events[i].data.fd);
}
free (events);
close(epfd);
return 0;
}
编译、运行上面的代码,会打印出下列信息:
gcc epoll_test.c -o epdemo
./epdemo
target_fd 4
ret -1, errno 1
epoll_ctl: Operation not permitted
正常打开了
那什么样的
只有底层驱动实现了
Question 2:ep->wq 的作用是什么?
答案:
一个进程调用
多个进程关注同一个
Question 3:什么是epoll 惊群?
答案:多个进程等待在
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netdb.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
#define PROCESS_NUM 10
static int create_and_bind (char *port)
{
int fd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(atoi(port));
bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
return fd;
}
static int make_socket_non_blocking (int sfd)
{
int flags, s;
flags = fcntl (sfd, F_GETFL, 0);
if (flags == -1)
{
perror ("fcntl");
return -1;
}
flags |= O_NONBLOCK;
s = fcntl (sfd, F_SETFL, flags);
if (s == -1)
{
perror ("fcntl");
return -1;
}
return 0;
}
#define MAXEVENTS 64
int main (int argc, char *argv[])
{
int sfd, s;
int efd;
struct epoll_event event;
struct epoll_event *events;
sfd = create_and_bind("8001");
if (sfd == -1)
abort ();
s = make_socket_non_blocking (sfd);
if (s == -1)
abort ();
s = listen(sfd, SOMAXCONN);
if (s == -1)
{
perror ("listen");
abort ();
}
efd = epoll_create(MAXEVENTS);
if (efd == -1)
{
perror("epoll_create");
abort();
}
event.data.fd = sfd;
//event.events = EPOLLIN | EPOLLET;
event.events = EPOLLIN;
s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event);
if (s == -1)
{
perror("epoll_ctl");
abort();
}
/* Buffer where events are returned */
events = calloc(MAXEVENTS, sizeof event);
int k;
for(k = 0; k < PROCESS_NUM; k++)
{
int pid = fork();
if(pid == 0)
{
/* The event loop */
while (1)
{
int n, i;
n = epoll_wait(efd, events, MAXEVENTS, -1);
printf("process %d return from epoll_wait!\n", getpid());
for (i = 0; i < n; i++)
{
if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN)))
{
/* An error has occured on this fd, or the socket is not ready for reading (why were we notified then?) */
fprintf (stderr, "epoll error\n");
close (events[i].data.fd);
continue;
}
else if (sfd == events[i].data.fd)
{
/* We have a notification on the listening socket, which means one or more incoming connections. */
struct sockaddr in_addr;
socklen_t in_len;
int infd;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
in_len = sizeof in_addr;
infd = accept(sfd, &in_addr, &in_len);
if (infd == -1)
{
printf("process %d accept failed!\n", getpid());
break;
}
printf("process %d accept successed!\n", getpid());
/* Make the incoming socket non-blocking and add it to the list of fds to monitor. */
close(infd);
}
}
}
}
}
int status;
wait(&status);
free (events);
close (sfd);
return EXIT_SUCCESS;
}
将服务端的监听

为了解决
Question 4:ep->poll_wait 的作用是什么?
答案:
在阅读内核代码过程中,
/* If the file is already "ready" we drop it inside the ready list */
if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
/* Notify waiting tasks that events are available */
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
spin_unlock_irqrestore(&ep->lock, flags);
atomic_long_inc(&ep->user->epoll_watches);
/* We have to call this outside the lock */
if (pwake)
ep_poll_safewake(&ep->poll_wait);
查阅很多资料后才搞明白其实
因此如果一个

epollfd1 监视了2 个“非epoll ”类型的fd epollfd2 监视了epollfd1 和2 个“非epoll ”类型的fd
如果
Question 5:ep->rdllist 的作用是什么?
答案:
通过扫描
看到这里你可能又产生了一个小小的疑问:为什么
Question 6:ep->ovflist 的作用是什么?
答案:在
当
/**
* ep_scan_ready_list - Scans the ready list in a way that makes possible for the scan code, to call f_op->poll(). Also allows for O(NumReady) performance.
* @ep: Pointer to the epoll private data structure.
* @sproc: Pointer to the scan callback.
* @priv: Private opaque data passed to the @sproc callback.
* Returns: The same integer error code returned by the @sproc callback.
*/
static int ep_scan_ready_list(struct eventpoll *ep,
int (*sproc)(struct eventpoll *,
struct list_head *, void *),
void *priv)
由于
但加锁期间很可能有新事件源源不断地产生,进而调用
看代码时会发现,还有一个

Question 7:epitem->pwqlist 队列的作用是什么?
答案:用来保存这个
首先介绍下什么是
struct epitem {
struct rb_node rbn; // 用于加入红黑树
struct list_head rdllink; // 用于加入rdllist
struct epoll_filefd ffd; // 包含被监视文件的文件指针和fd信息
struct list_head pwqlist; // poll等待队列
struct eventpoll *ep; // 所属的epoll实例
struct epoll_event event; // 关注的事件
/* 其他成员省略 */
};
回忆一下上文说到,每当用户调用
当调用
pwqlist、epitem、fd、epoll_entry、

Question 8:epmutex、ep->mtx、ep->lock 3 把锁的区别是?
答案:锁的粒度和使用目的不同。
epmutex 是一个全局互斥锁,epoll 中一共只有3 个地方用到这把锁。分别是ep_free() 销毁一个epoll 实例时、eventpoll_release_file() 清理从epoll 中已经关闭的文件时、epoll_ctl() 时避免epoll 间嵌套调用时形成死锁。我的理解是epmutex 的锁粒度最大,用来处理跨epoll 实例级别的同步操作。ep->mtx 是一个epoll 内部的互斥锁,在ep_scan_ready_list() 扫描就绪列表、eventpoll_release_file() 中执行ep_remove() 删除一个被监视文件、ep_loop_check_proc() 检查epoll 是否有循环嵌套或过深嵌套、还有epoll_ctl() 操作被监视文件增删改等处有使用。可以看出上述的函数里都会涉及对epoll 实例中rdllist 或红黑树的访问,因此我的理解是ep->mtx 是一个epoll 实例内的互斥锁,用来保护epoll 实例内部的数据结构的线程安全。ep->lock 是一个epoll 实例内部的自旋锁,用来保护ep->rdllist 的线程安全。自旋锁的特点是得不到锁时不会引起进程休眠,所以在ep_poll_callback 中只能使用ep->lock ,否则就会丢事件。
Question 9:epoll 使用红黑树的目的是什么?
答案:用来维护一个
用户态调用

从时间
/* Compare RB tree keys */
static inline int ep_cmp_ffd(struct epoll_filefd *p1, struct epoll_filefd *p2)
{
return (p1->file > p2->file ? +1 : (p1->file < p2->file ? -1 : p1->fd - p2->fd));
}
epoll、epitem、和红黑树间的组织关系是这样:

Question 10:什么是水平触发、边缘触发?
答案:水平触发
边缘触发:关注点是变化,只有监视的文件上有数据变化发生(读操作关注有数据写进缓冲区,写操作关注数据从缓冲区取走
看一个实验
水平触发时,客户端输入

边缘触发时,客户端同样输入

小结
本文通过
