epoll多路實現模型相比selecte、poll避免了出現文件描述符越多性能越差的情況,並且省去了大量內核/用戶空間的拷貝和輪詢所有文件描述符的系統消耗。爲了理解epoll實現高併發,本文從源碼瞭解epoll的實現機制。
epoll的用法:
int epoll_create(int size);//創建一個epoll fd
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);//添加事件
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);//監控文件描述符
epoll_create()
創建一系列數據結構並建立的聯繫。
int fd;//文件描述符
struct inode* inode;//內核用該結構在內部表示一個文件
struct file* file;//代表一個打開的文件
struct eventpoll;//epoll的描述符
源碼摘錄
sys_epoll_create(int size);
>> int error, fd;
struct inode *inode;
struct file *file;
error = ep_getfd(&fd, &inode, &file);
>> file = get_empty_filp();//分配一個文件索引節點並初始化
inode = ep_eventpoll_inode();//從eventpoll文件系統分配一個inode
dentry = d_alloc(eventpoll_mnt->mnt_sb->s_root, &this);
d_add(dentry, inode);
>> d_instantiate(entry, inode);
>> list_add(&entry->d_alias, &inode->i_dentry);//file.inode指向inode
fd_install(fd, file);
>> files->fd[fd] = file;//將文件索引點添加到進程中
error = ep_file_init(file);//設置文件內部數據結構(“struct eventpoll”)
>> memset(ep, 0, sizeof(*ep));//初始化epoll描述符
rwlock_init(&ep->lock);
init_rwsem(&ep->sem);
init_waitqueue_head(&ep->wq);
init_waitqueue_head(&ep->poll_wait);
INIT_LIST_HEAD(&ep->rdllist);
ep->rbr = RB_ROOT;
file->private_data = ep;//file.private_data指向epoll 描述符
return fd;//epfd
創建struct file、strcut inode 、struct eventpoll等結構體並初始化,建立file與inode的聯繫,task_struct與file之間的聯繫,file與eventpolld之間的聯繫。
epoll_ctl()
插入、刪除、更改epoll描述符中的內核文件描述符,下面以插入爲例:
源碼摘錄:
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;//當前節點
file = fget(epfd);//根據進程文件描述符獲得文件對象的地址。並增加其引用計數。
tfile = fget(fd);
ep = file->private_data;
epi = ep_find(ep, tfile, fd);//在eventpoll.rbr中查找fd
ep_insert(ep, &epds, tfile, fd);//給fd註冊事件
>> struct epitem *epi;
struct ep_pqueue epq;
/*struct ep_pqueue {
poll_table pt;//poll_queue_proc qproc;
struct epitem *epi;
};
*/
epi->ep = ep;
epi->event = *event;
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
>> pt->qproc = qproc;
/*
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt) //監聽的fd生改變時,指定的調用函數會被調用
unsigned int (*poll) (struct file *, struct poll_table_struct *);
*/
revents = tfile->f_op->poll(tfile, &epq.pt);//回調函數
ep_rbtree_insert(ep, epi);//添加當前項到eventpoll.rbr
/* 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(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
將用戶空間的epoll_event拷貝到內核,只拷貝一次;
在eventpoll->rbr中查找fd是否存在;
設置回調函數並且插入節點。
epoll_wait()
sys_epoll_wait()
struct file *file;
struct eventpoll *ep;
file = fget(epfd);
ep = file->private_data;
ep_epoll(ep,events,maxevents,timeout);
>> 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;
if (signal_pending(current)) {//如果有信號產生
res = -EINTR;
break;
}
write_unlock_irqrestore(&ep->lock, flags);
jtimeout = schedule_timeout(jtimeout);//如果此時回調函數被調用,直接被喚醒
write_lock_irqsave(&ep->lock, flags);
}
>> if (ep_collect_ready_items(ep, &txlist, maxevents) > 0) {//收集就緒的描述符
eventcnt = ep_send_events(ep, &txlist, events);
revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL);
epi->revents = revents & epi->event.events;//只返回相關的fd
if (!res && eavail &&!(res = ep_events_transfer(ep, events, maxevents)) && jtimeout)//向用戶空間拷貝
如果是LT,無論是否狀態發生改變都會重新插入就緒隊列,下次調用返回值爲0。
參考資料: