多路转接epoll原理详解
目录
从epoll接口入手
创建epoll模型
用户告诉内核关心的事件
内核告诉用户就绪的事件
epoll的原理
整体思路
如何判断事件是否就绪
事件就绪后如何实现将节点插入就绪队列
从epoll接口入手
本篇文章从epoll的三个接口入手介绍epoll的具体工作原理
创建epoll模型
#include <sys/epoll.h>
int epoll_create(int size);
创建一个epoll的句柄,从linux-2.6.8后,size参数是被忽略的,用完之后必须调用close关闭。
返回值实际上就是一个文件描述符
用户告诉内核关心的事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll作为多路转接的方案,依然要解决如何让操作系统知道用户关心哪些文件描述符的哪些事件的问题,这个接口解决的就是这个问题。epfd是epoll_create返回的文件描述符,op表示动作,用三个宏来表示。
EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
fd是用户关心的文件描述符,最后一个参数event告诉内核要监听fd上的哪些事件,再来看看epoll_event的结构
struct epoll_event
{uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;typedef union epoll_data
{void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;
events是事件类型,有以下几个宏
EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里.
关于epoll_data这个结构,在写代码时还会细谈
内核告诉用户就绪的事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epfd依然是epoll_create的返回值,events和maxevents确定了一个epoll_event的数组,这个数组包含了哪些文件描述符的哪些事件就绪的信息,timeout是超时时间,返回值与select和poll一样
为0表示超时返回;为-1表示有错误发生,并设置错误码errno;为正数表示在timeout时间内事件就绪的文件描述符个数
epoll的原理
整体思路
有了使用epoll的宏观认识后,再来详细谈谈工作原理
先来看看上面第一个接口所说的创建epoll模型,这个epoll模型到底是什么
以下代码来自linux-2.6.18的源码(使用vscode打开源码根目录可以搜索到下面的代码)
/** This structure is stored inside the "private_data" member of the file* structure and rapresent the main data sructure for the eventpoll* interface.*/
struct eventpoll {/* Protect the this structure access */rwlock_t lock;/** This semaphore is used to ensure that files are not removed* while epoll is using them. This is read-held during the event* collection loop and it is write-held during the file cleanup* path, the epoll file exit code and the ctl operations.*/struct rw_semaphore sem;/* Wait queue used by sys_epoll_wait() */wait_queue_head_t wq;/* Wait queue used by file->poll() */wait_queue_head_t poll_wait;/* List of ready file descriptors */struct list_head rdllist;/* RB-Tree root used to store monitored fd structs */struct rb_root rbr;
};
eventpoll结构体就是epoll模型,重点来看rdllist和rbr两个字段,rbr是一颗红黑树,节点存放了文件描述符和事件的映射关系,里面存放的都是用户传入的的文件描述符,而如果某个文件描述符上的事件就绪了,就会将这个节点插入到rdllist中
rdllist是就绪队列,里面的节点都是已经就绪的文件描述符和对应的事件
所以用户使用epoll_ctl实际就是对这颗红黑树进行增删查改,而一旦有文件描述符就绪了,就会被加入rdllist中,epoll_wait实际上就会得到rdllist中存放的就绪文件描述符的信息。
下面是节点的结构体,可以看到有所属的红黑树的节点的信息,所属的等待队列的节点的信息
实际上,前文所说插入到rdllist并不是复制一个节点插入到rdllist里,而是修改这个节点的rdllink字段,让这个节点属于红黑树的同时,还属于等待队列
struct epitem {/* RB-Tree node used to link this structure to the eventpoll rb-tree */struct rb_node rbn;/* List header used to link this structure to the eventpoll ready list */struct list_head rdllink;/* The file descriptor information this item refers to */struct epoll_filefd ffd;/* Number of active wait queue attached to poll operations */int nwait;/* List containing poll wait queues */struct list_head pwqlist;/* The "container" of this item */struct eventpoll *ep;/* The structure that describe the interested events and the source fd */struct epoll_event event;/** Used to keep track of the usage count of the structure. This avoids* that the structure will desappear from underneath our processing.*/atomic_t usecnt;/* List header used to link this item to the "struct file" items list */struct list_head fllink;/* List header used to link the item to the transfer list */struct list_head txlink;/** This is used during the collection/transfer of events to userspace* to pin items empty events set.*/unsigned int revents;
};
如何判断事件是否就绪
接下来再谈谈epoll如何知道某个文件描述符的事件是否就绪
要解决这个问题,先从下面这个结构体开始
struct file {.../* needed for tty driver, and maybe others */void *private_data;#ifdef CONFIG_EPOLL/* Used by fs/eventpoll.c to link all the hooks to this file */struct list_head f_ep_links;spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */struct address_space *f_mapping;
};
重点关注这个private_data
当我们创建tcp或者udp套接字时,返回的实际上是文件描述符,内核通过这个文件描述符找到了对应的struct file结构,而这个结构的private_data字段就会指向套接字
struct socket {socket_state state;unsigned long flags;const struct proto_ops *ops;struct fasync_struct *fasync_list;struct file *file;struct sock *sk;wait_queue_head_t wait;short type;
};
可以看到socket中还有一个字段是file,这个file就是socket所属的那个struct file的地址
socket下还有sock结构体
struct sock {/** Now struct inet_timewait_sock also uses sock_common, so please just* don't add nothing before this first member (__sk_common) --acme*/...struct sk_buff_head sk_receive_queue;struct sk_buff_head sk_write_queue;struct sk_buff_head sk_async_wait_queue;...
};
太长了省略了一些字段,sock中实际上是各种网络信息,这里最重要的两个就是接收队列和写队列,实际上就是接收缓冲区和发送缓冲区,至此,只需要判断接收缓冲区是否为空就能判断读事件是否就绪
之前说struct file的private_data字段指向struct socket其实还不够准确
我们在创建套接字时常常会指定创建tcp套接字或者udp套接字,private_data会指向这样的struct tcp_socket或者struct udp_socket,而这两个struct结构体实际上会间接包含strcut sock结构
struct tcp_sock {/* inet_connection_sock has to be the first member of tcp_sock */struct inet_connection_sock inet_conn;...
};
struct inet_connection_sock {/* inet_sock has to be the first member! */struct inet_sock icsk_inet;...
};
struct inet_sock {/* sk and pinet6 has to be the first two members of inet_sock */struct sock sk;...
};
struct udp_sock {/* inet_sock has to be the first member */struct inet_sock inet;...
};
struct inet_sock {/* sk and pinet6 has to be the first two members of inet_sock */struct sock sk;...
};
可以看到tcp比udp多套了一个管理连接的结构体 struct inet_connection_sock,这里所展现的tcp_sock和udp_sock是一种继承体系,其中strcut sock可看成是基类,剩下的都是子类
而前面的struct socket结构体中的struct sock字段也可能并不是指向一个struct sock的对象,而是指向一个tcp_sock或者udp_sock对象,在需要用到对应tcp或者udp的方法时进行类型强转
这实际上也有一种多态的思想
事件就绪后如何实现将节点插入就绪队列
内核底层提供了一种回调机制,用于处理事件就绪时进行什么动作,默认这个回调是为空的,在epoll这里,这个回调被设置为当事件就绪后把节点插入就绪队列,于是,可以自动的在事件就绪时将节点插入就绪队列。
epoll的优点
使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而 select/poll 都是每次循环都要进行拷贝)
事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度 O(1). 即使文件描述符数目很多, 效率也不会受到影响.
没有数量限制: 文件描述符数目无上限.