epoll在現在的軟件中佔據了很大的分量,nginx,libuv等單線程事件循環的軟件都使用了epoll。之前分析過select,今天分析一下epoll。
們按照epoll三部曲的順序進行分析。
epoll_create
asmlinkage long sys_epoll_create(int size)
{
int error, fd;
struct inode *inode;
struct file *file;
error = ep_getfd(&fd, &inode, &file);
error = ep_file_init(file);
return fd;
}
我們發現create函數似乎很簡單。
1 操作系統中,進程和文件系統是通過fd=>file=>node聯繫起來的。ep_getfd就是在建立這個聯繫。
static int ep_getfd(int *efd, struct inode **einode, struct file **efile)
{
// 獲取一個file結構體
file = get_empty_filp();
// epoll在底層本身對應一個文件系統,從這個文件系統中獲取一個inode
inode = ep_eventpoll_inode();
// 獲取一個文件描述符
fd = get_unused_fd();
sprintf(name, "[%lu]", inode->i_ino);
this.name = name;
this.len = strlen(name);
this.hash = inode->i_ino;
// 申請一個entry
dentry = d_alloc(eventpoll_mnt->mnt_sb->s_root, &this);
dentry->d_op = &eventpollfs_dentry_operations;
file->f_dentry = dentry;
// 建立file和inode的聯繫
d_add(dentry, inode);
// 建立fd=>file的關聯
fd_install(fd, file);
*efd = fd;
*einode = inode;
*efile = file;
return 0;
}
形成一個這種的結構。
2 通過ep_file_init建立file和epoll的關聯。
static int ep_file_init(struct file *file)
{
struct eventpoll *ep;
ep = kmalloc(sizeof(struct eventpoll), GFP_KERNEL)
memset(ep, 0, sizeof(*ep));
// 一系列初始化
file->private_data = ep;
return 0;
}
epoll_create函數主要是建立一個數據結構。並返回一個文件描述符供後面使用。
2 epoll_ctl
asmlinkage long
sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event)
{
int error;
struct file *file, *tfile;
struct eventpoll *ep;
struct epitem *epi;
struct epoll_event epds;
error = -EFAULT;
// 不是刪除操作則複製用戶數據到內核
if (
EP_OP_HASH_EVENT(op) &&
copy_from_user(&epds, event, sizeof(struct epoll_event))
)
goto eexit_1;
// 根據一種的圖,拿到epoll對應的file結構體
file = fget(epfd);
// 拿到操作的文件的file結構體
tfile = fget(fd);
// 通過file拿到epoll_event結構體,見上面的圖
ep = file->private_data;
// 看這個文件描述符是否已經存在,epoll用紅黑樹維護這個數據
epi = ep_find(ep, tfile, fd);
switch (op) {
// 新增
case EPOLL_CTL_ADD:
// 還沒有則新增,有則報錯
if (!epi) {
epds.events |= POLLERR | POLLHUP;
// 插入紅黑樹
error = ep_insert(ep, &epds, tfile, fd);
} else
error = -EEXIST;
break;
// 刪除
case EPOLL_CTL_DEL:
// 存在則刪除,否則報錯
if (epi)
error = ep_remove(ep, epi);
else
error = -ENOENT;
break;
// 修改
case EPOLL_CTL_MOD:
// 存在則修改,否則報錯
if (epi) {
epds.events |= POLLERR | POLLHUP;
error = ep_modify(ep, epi, &epds);
} else
error = -ENOENT;
break;
}
}
epoll_ctl函數看起來也沒有很複雜,就是根據用戶傳進來的信息去操作紅黑樹。對於紅黑樹的增刪改查,查和刪除就不分析了。就是去操作紅黑樹。增和改是類似的邏輯,所以我們只分析增操作就可以了。在此之前,我們先了解一些epoll中其他的數據結構。
當我們新增一個需要監聽的文件描述符的時候,系統會申請一個epitem去表示。epitem是保存了文件描述符、事件等信息的結構體。然後把epitem插入到eventpoll結構體維護的紅黑樹中。
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
struct file *tfile, int fd)
{
int error, revents, pwake = 0;
unsigned long flags;
struct epitem *epi;
struct ep_pqueue epq;
// 申請一個epitem
epi = EPI_MEM_ALLOC()
// 省略一系列初始化工作
// 記錄所屬的epoll
epi->ep = ep;
// 在epitem中保存文件描述符fd和file
EP_SET_FFD(&epi->ffd, tfile, fd);
// 監聽的事件
epi->event = *event;
epi->nwait = 0;
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
revents = tfile->f_op->poll(tfile, &epq.pt);
// 把epitem插入紅黑樹
ep_rbtree_insert(ep, epi);
// 如果監聽的事件在新增的時候就已經觸發,則直接插入到epoll就緒隊列
if ((revents & event->events) && !EP_IS_LINKED(&epi->rdllink)) {
// 把epitem插入就緒隊列rdllist
list_add_tail(&epi->rdllink, &ep->rdllist);
// 有事件觸發,喚醒阻塞在epoll_wait的進程隊列
if (waitqueue_active(&ep->wq))
wake_up(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
}
新增操作的大致流程是
1 申請了一個新的epitem表示待觀察的實體。他保存了文件描述符、感興趣的事件等信息。
2 插入紅黑樹
3 判斷新增的節點中對應的文件描述符和事件是否已經觸發了,是則加入到就緒隊列(由eventpoll->rdllist維護的一個隊列)
下面具體看一下如何判斷感興趣的事件在對應的文件描述符中是否已經觸發。相關代碼在ep_insert中。下面單獨拎出來。
/*
struct ep_pqueue {
// 函數指針
poll_table pt;
// epitem
struct epitem *epi;
};
*/
struct ep_pqueue epq;
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
revents = tfile->f_op->poll(tfile, &epq.pt);
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
pt->qproc = qproc;
}
上面的代碼是定義了一個struct ep_pqueue 結構體,然後設置他的一個字段爲ep_ptable_queue_proc。然後執行tfile->f_op->poll。poll函數由各個文件系統或者網絡協議實現。我們以管道爲例。
static unsigned int
pipe_poll(struct file *filp, poll_table *wait)
{
unsigned int mask;
// 監聽的文件描述符對應的inode
struct inode *inode = filp->f_dentry->d_inode;
struct pipe_inode_info *info = inode->i_pipe;
int nrbufs;
/*
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && wait_address)
p->qproc(filp, wait_address, p);
}
*/
poll_wait(filp, PIPE_WAIT(*inode), wait);
// 判斷哪些事件觸發了
nrbufs = info->nrbufs;
mask = 0;
if (filp->f_mode & FMODE_READ) {
mask = (nrbufs > 0) ? POLLIN | POLLRDNORM : 0;
if (!PIPE_WRITERS(*inode) && filp->f_version != PIPE_WCOUNTER(*inode))
mask |= POLLHUP;
}
if (filp->f_mode & FMODE_WRITE) {
mask |= (nrbufs < PIPE_BUFFERS) ? POLLOUT | POLLWRNORM : 0;
if (!PIPE_READERS(*inode))
mask |= POLLERR;
}
return mask;
}
我們看到具體的poll函數裏會首先執行poll_wait函數。這個函數只是簡單執行struct ep_pqueue epq結構體中的函數,即剛纔設置的ep_ptable_queue_proc。
// 監聽的文件描述符對應的file結構體,whead是等待監聽的文件描述符對應的inode可用的隊列
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
poll_table *pt)
{
struct epitem *epi = EP_ITEM_FROM_EPQUEUE(pt);
struct eppoll_entry *pwq;
if (epi->nwait >= 0 && (pwq = PWQ_MEM_ALLOC())) {
pwq->wait->flags = 0;
pwq->wait->task = NULL;
// 設置回調
pwq->wait->func = ep_poll_callback;
pwq->whead = whead;
pwq->base = epi;
// 插入等待監聽的文件描述符的inode可用的隊列,回調函數是ep_poll_callback
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
} else {
/* We have to signal that an error occurred */
epi->nwait = -1;
}
}
主要的邏輯是把當前進程插入監聽的文件的等待隊列中,等待喚醒。
三 epoll_wait
asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events,
int maxevents, int timeout)
{
int error;
struct file *file;
struct eventpoll *ep;
// 通過epoll的fd拿到對應的file結構體
file = fget(epfd);
// 通過file結構體拿到eventpoll結構體
ep = file->private_data;
error = ep_poll(ep, events, maxevents, timeout);
return error;
}
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout)
{
int res, eavail;
unsigned long flags;
long jtimeout;
wait_queue_t wait;
// 計算超時時間
jtimeout = timeout == -1 || timeout > (MAX_SCHEDULE_TIMEOUT - 1000) / HZ ?
MAX_SCHEDULE_TIMEOUT: (timeout * HZ + 999) / 1000;
retry:
res = 0;
// 就緒隊列爲空
if (list_empty(&ep->rdllist)) {
// 加入阻塞隊列
init_waitqueue_entry(&wait, current);
add_wait_queue(&ep->wq, &wait);
for (;;) {
// 掛起
set_current_state(TASK_INTERRUPTIBLE);
// 超時或者有就緒事件了,則跳出返回
if (!list_empty(&ep->rdllist) || !jtimeout)
break;
// 被信號喚醒返回EINTR
if (signal_pending(current)) {
res = -EINTR;
break;
}
// 設置定時器,然後進程掛起,等待超時喚醒(超時或者信號喚醒)
jtimeout = schedule_timeout(jtimeout);
}
// 移出阻塞隊列
remove_wait_queue(&ep->wq, &wait);
// 設置就緒
set_current_state(TASK_RUNNING);
}
// 是否有事件就緒,喚醒的原因有幾個,被喚醒不代表就有就緒事件
eavail = !list_empty(&ep->rdllist);
write_unlock_irqrestore(&ep->lock, flags);
// 處理就緒事件返回
if (!res && eavail &&
!(res = ep_events_transfer(ep, events, maxevents)) && jtimeout)
goto retry;
return res;
}
總的來說epoll_wait的邏輯主要是處理就緒隊列的節點。
1 如果就緒隊列爲空,則根據timeout做下一步處理,可能定時阻塞。
2 如果就緒隊列非空則處理就緒隊列,返回給用戶。處理就緒隊列的函數是ep_events_transfer。
static int ep_events_transfer(struct eventpoll *ep,
struct epoll_event __user *events, int maxevents)
{
int eventcnt = 0;
struct list_head txlist;
INIT_LIST_HEAD(&txlist);
if (ep_collect_ready_items(ep, &txlist, maxevents) > 0) {
eventcnt = ep_send_events(ep, &txlist, events);
ep_reinject_items(ep, &txlist);
}
return eventcnt;
}
主要是三個函數,我們一個個看。
1 ep_collect_ready_items收集就緒事件
static int ep_collect_ready_items(struct eventpoll *ep, struct list_head *txlist, int maxevents)
{
int nepi;
unsigned long flags;
// 就緒事件的隊列
struct list_head *lsthead = &ep->rdllist, *lnk;
struct epitem *epi;
for (nepi = 0, lnk = lsthead->next; lnk != lsthead && nepi < maxevents;) {
// 通過結構體字段的地址拿到結構體首地址
epi = list_entry(lnk, struct epitem, rdllink);
lnk = lnk->next;
/* If this file is already in the ready list we exit soon */
if (!EP_IS_LINKED(&epi->txlink)) {
epi->revents = epi->event.events;
// 插入txlist隊列,然後處理完再返回給用戶
list_add(&epi->txlink, txlist);
nepi++;
// 從就緒隊列中刪除
EP_LIST_DEL(&epi->rdllink);
}
}
return nepi;
}
2 ep_send_events判斷哪些事件觸發了
static int ep_send_events(struct eventpoll *ep, struct list_head *txlist,
struct epoll_event __user *events)
{
int eventcnt = 0;
unsigned int revents;
struct list_head *lnk;
struct epitem *epi;
// 遍歷就緒隊列,記錄觸發的事件
list_for_each(lnk, txlist) {
epi = list_entry(lnk, struct epitem, txlink);
// 判斷哪些事件觸發了
revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL);
epi->revents = revents & epi->event.events;
// 複製到用戶空間
if (epi->revents) {
if (__put_user(epi->revents,
&events[eventcnt].events) ||
__put_user(epi->event.data,
&events[eventcnt].data))
return -EFAULT;
// 只監聽一次,觸發完設置成對任何事件都不感興趣
if (epi->event.events & EPOLLONESHOT)
epi->event.events &= EP_PRIVATE_BITS;
eventcnt++;
}
}
return eventcnt;
}
3 ep_reinject_items重新插入就緒隊列
static void ep_reinject_items(struct eventpoll *ep, struct list_head *txlist)
{
int ricnt = 0, pwake = 0;
unsigned long flags;
struct epitem *epi;
while (!list_empty(txlist)) {
epi = list_entry(txlist->next, struct epitem, txlink);
EP_LIST_DEL(&epi->txlink);
// 水平觸發模式則一直通知,即重新加入就緒隊列
if (EP_RB_LINKED(&epi->rbn) && !(epi->event.events & EPOLLET) &&
(epi->revents & epi->event.events) && !EP_IS_LINKED(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
ricnt++;
}
}
}
我們發現,並有沒有在epoll_wait的時候去收集就緒事件,那麼就緒隊列是誰處理的呢?我們回顧一下插入紅黑樹的時候,做了一個事情,就是在文件對應的inode上註冊一個回調。當文件滿足條件的時候,就會喚醒因爲epoll_wait而阻塞的進程。epoll_wait會收集事件返回給用戶。
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
unsigned long flags;
struct epitem *epi = EP_ITEM_FROM_WAIT(wait);
struct eventpoll *ep = epi->ep;
// 插入就緒隊列
list_add_tail(&epi->rdllink, &ep->rdllist);
// 喚醒因epoll_wait而阻塞的進程
if (waitqueue_active(&ep->wq))
wake_up(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
return 1;
}
epoll的實現涉及的內容比較多,先分析一下大致的原理。有機會再深入分析。