1.epoll_create()
在核心建立一個事件表,事件表用檔案表示。所以epoll_create()返回的是一個檔案描述符。主要原始碼:
asmlinkage long sys_epoll_create(int size)
{
int error, fd;
struct inode *inode; //inode結構
struct file *file; //檔案file結構
if (size <= 0)
goto eexit_1;
error = ep_getfd(&fd, &inode, &file); //獲得fd以及一個file和inode結構
if (error)
goto eexit_1;
error = ep_file_init(file); //file的初始化
if (error)
goto eexit_2;
return fd;
eexit_2:
sys_close(fd);
eexit_1:
return error;
}
(1)ep_getfd()
為epoll獲得相應file、inode等資料結構,並與新的fd繫結起來。
首先來看兩個資料結構:
- a.file結構: 定義了Linux中檔案所需要的所有資訊,代表一個開啟的檔案
file的部分資料結構:
struct file {
/* 檔案對應的目錄項結構。除了用filp->f_dentry->d_inode的方式來訪問索引節點結構之外,裝置驅動程式的開發者們一般無需關心dentry結構。
*/
struct dentry *f_dentry;
/**
* 與檔案相關的操作。核心在執行open操作時,對這個指標賦值,以後需要處理這些操作時就讀取這個指標。
* 不能為了方便而儲存起來。也就是說,可以在任何需要的時候修改檔案的關聯操作。即"方法過載"。
*/
struct file_operations *f_op;
/**
* open系統呼叫在呼叫驅動程式的open方法前將這個指標置為NULL。驅動程式可以將這個欄位用於任何目的或者忽略這個欄位。
* 驅動程式可以用這個欄位指向已分配的資料,但是一定要在核心銷燬file結構前在release方法中釋放記憶體。
* 它是跨系統呼叫時儲存狀態的非常有用的資源。
*/
void *private_data;
};
- inode結構:
inode在內部表示一個檔案,與file不同,file表示的是檔案描述符。同一個檔案被開啟多次,會有多個file檔案描述符,但是隻會有一個inode。在後面的程式碼中你可以發現,Linux針對epoll的操作專門會有一個屬於epoll的檔案系統,這個可以在初始化inode的時候可以看到。
這是ep_getfd的程式碼:
static int ep_getfd(int *efd, struct inode **einode, struct file **efile)
{
struct qstr this;
char name[32];
struct dentry *dentry;
struct inode *inode;
struct file *file;
int error, fd;
error = -ENFILE;
file = get_empty_filp(); //獲得新的空的file檔案描述符
if (!file)
goto eexit_1;
inode = ep_eventpoll_inode();//獲得新的inode結構,代表一個屬於epoll檔案系統的檔案。
error = PTR_ERR(inode);
if (IS_ERR(inode))
goto eexit_2;
error = get_unused_fd(); //獲得一個未使用的fd
if (error < 0)
goto eexit_3;
fd = error;
error = -ENOMEM;
sprintf(name, "[%lu]", inode->i_ino);
this.name = name;
this.len = strlen(name);
this.hash = inode->i_ino;
dentry = d_alloc(eventpoll_mnt->mnt_sb->s_root, &this);//獲得一個新的dentry檔案目錄項結構並初始化
if (!dentry)
goto eexit_4;
dentry->d_op = &eventpollfs_dentry_operations;
d_add(dentry, inode);//將inode和目錄項結構繫結
file->f_vfsmnt = mntget(eventpoll_mnt); //把該檔案所屬的檔案系統置為epoll檔案系統
file->f_dentry = dentry;
file->f_mapping = inode->i_mapping;
file->f_pos = 0;
file->f_flags = O_RDONLY;
file->f_op = &eventpoll_fops; //這一步是很重要的一步,用f_op指標指向epoll的回撥數
file->f_mode = FMODE_READ;
file->f_version = 0;
file->private_data = NULL;
fd_install(fd, file);
*efd = fd;
*einode = inode;
*efile = file;
return 0;
eexit_4:
put_unused_fd(fd);
eexit_3:
iput(inode);
eexit_2:
put_filp(file);
eexit_1:
return error;
}
ep_getfd其實就做了三件事:獲取新的file檔案描述符,獲取新的inode結構,獲得新的fd,最後把三者連線繫結在一起,就表示了epoll在核心的事件表。
(2) ep_file_init(file)
前面講到epoll的核心事件表已經建立完畢了,但是我們可以發現epoll是事件一次寫入核心,多次監聽的,然而在之前都沒有發現可以存事件的資料結構紅黑樹。所以這一步ep_file_init就是來解決這個問題的。
下面根據程式碼來說它幹了些什麼
static int ep_file_init(struct file *file)
{
struct eventpoll *ep; //一個指向eventpoll的指標
if (!(ep = kmalloc(sizeof(struct eventpoll), GFP_KERNEL)))
return -ENOMEM;
/*下面是對eventpoll結構的一系列初始化*/
memset(ep, 0, sizeof(*ep));
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; //這裡可以看到紅黑樹的root的初始化
file->private_data = ep; //ep指標給file
DNPRINTK(3, (KERN_INFO "[%p] eventpoll: ep_file_init() ep=%p
",
current, ep));
return 0;
}
這一步就是把一個eventpoll的結構建立並初始化,然後讓file->private_data指向這個結構,這個結構中我們就可以找到rb_root,這一步之後epoll_creat()也就是epoll所有的準備工作就已經做完了。
2.epoll_ctl()
因為epoll對監聽事件來說是一次寫入多次監聽的,所以必須要有對事件表的增刪改操作介面,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;//把使用者的事件從使用者空間考到核心空間
error = -EBADF;
file = fget(epfd); //獲得epoll核心事件表得檔案描述符
if (!file)
goto eexit_1;
tfile = fget(fd); //獲得要操作的事件得檔案描述符
if (!tfile)
goto eexit_2;
error = -EPERM;
if (!tfile->f_op || !tfile->f_op->poll)
goto eexit_3; //如果fd檔案描述符中f_op(指向所有檔案操作指標得結構體)和這裡需要用到的poll操作為空,就退出。因為poll是所有Io複用:select 、poll、epoll得底層實現。
error = -EINVAL;
if (file == tfile || !IS_FILE_EPOLL(file))
goto eexit_3; //判斷需要操作的fd檔案描述符是不是epfd檔案描述符和 file是不是epoll檔案系統的檔案
ep = file->private_data; //拿到核心事件表的eventpoll結構體
down_write(&ep->sem); //為寫獲得讀寫核心事件表eventpoll的訊號量。
epi = ep_find(ep, tfile, fd);//判斷該事件在核心事件表中是否存在
error = -EINVAL;
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;
}
if (epi)
ep_release_epitem(epi);
up_write(&ep->sem); //釋放核心事件表eventpoll的讀寫訊號量
eexit_3:
fput(tfile);
eexit_2:
fput(file);
eexit_1:
DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p) = %d
",
current, epfd, op, fd, event, error));
return error;
}
在epoll_ctl中,主要分為以下幾個步驟:
- 判斷需要操作的事件在事件表中是否存在
- 判斷需要進行的操作
- 使用更底層的程式碼對事件表進行增刪改
epoll比poll和select高效的地方,其實在這裡就可以 看出來,poll的系統呼叫會把該程式“掛”到fd所對應的裝置的等待佇列上,select和poll他們每一次都需要把current “掛”到fd對應裝置的等待佇列,同一個fd來說,你要不停監聽的話,你就需要不停的進行poll,那麼fd太多的話,這樣的poll操作其實是很低效的,epoll則不同的是,每一個事件,它只在新增的時候呼叫poll,以後都不用去呼叫poll,避免了太多重複的poll操作,所以epoll對於poll和select來說是更高效的。
還有一個比較有趣的地方是,所有的事件的儲存都是在紅黑樹裡面,但是我們可以發現紅黑樹的節點其實是這樣的:
struct rb_node
{
/**
* 紅黑樹節點的雙親。
*/
struct rb_node *rb_parent;
/**
* 紅黑樹節點顏色。
*/
int rb_color;
#define RB_RED 0
#define RB_BLACK 1
/**
* 右孩子
*/
struct rb_node *rb_right;
/**
* 左孩子
*/
struct rb_node *rb_left;
};
struct rb_root
{
struct rb_node *rb_node;
};
不管是根節點還是節點,你都看不到資料,那核心是怎麼通過紅黑樹運算元據?
struct epitem {
struct rb_node rbn;
........
}
其實這才是真正的紅黑樹的節點,裡面的東西很多,但是這裡只列出來了相關的。一個fd對應一個這樣的結構,同一個fd只能插入一次,這也是要採用一個紅黑樹這樣的資料結構的其中一個原因,還有一個原因就是因為紅黑樹的查詢等操作效率高。那麼從rb_node到struct epitem就可以很容易的做到了,只需要做一個指標的強轉就可以從rb_node到epitem:
epic = rb_entry(parent, struct epitem, rbn);
#define rb_entry(ptr, type, member) container_of(ptr, type, member)
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );})
這也就跟C++的模板類似,只不過這是C語言實現的,很多方法還是需要再重新定義。但是對C語言來說,在有限的條件下做到這樣的程式碼複用還是非常厲害的。
3.epoll_wait()
之前的兩個函式已經把事件新增到核心事件表,而且已經把當前程式“掛”到fd的所有裝置上,這就相當於一個回撥函式,當對應fd有可處理事件時,就會喚醒等待佇列的程式,程式會把當前可處理的事件及有關資訊記錄到一個rdllist的連結串列中,接下來就是epoll_wait所要做的事了:
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:
/*獲得eventpoll的寫鎖*/
write_lock_irqsave(&ep->lock, flags);
/*rdllist如果是空就阻塞迴圈等待回撥函式往rdllist中寫資料,一旦不為空或者超過超時時間就會退出迴圈*/
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;
if (signal_pending(current)) {
res = -EINTR;
break;
}
write_unlock_irqrestore(&ep->lock, flags);
jtimeout = schedule_timeout(jtimeout);
write_lock_irqsave(&ep->lock, flags);
}
remove_wait_queue(&ep->wq, &wait);
set_current_state(TASK_RUNNING);
}
/*判斷rdllist是否為空*/
eavail = !list_empty(&ep->rdllist);
/*釋放eventpoll的寫鎖*/
write_unlock_irqrestore(&ep->lock, flags);
/*這一步是把核心的rdllist中的事件copy到使用者空間*/
if (!res && eavail &&
!(res = ep_events_transfer(ep, events, maxevents)) && jtimeout)
goto retry;
return res;
}
簡單總結一下epoll_wait:
- 檢查超時事件和用來存就緒事件的使用者空間的大小
- 迴圈等待就緒事件
- 把就緒事件從核心空間copy到使用者空間
這就是epoll的整個流程和主要的步驟的分析。但是我們沒能看出來它的ET和LT工作模式體現在哪裡?所以下面就來說說epoll的ET模式具體實現。
4.ET模式
ET模式是epoll特有的高效工作模式。主要體現就是一次就緒事件只給使用者提醒一次。ET模式的實現其實就是在epoll_wait的最後一步,從核心連結串列往使用者空間考資料的時候,下面來看程式碼:
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); //初始化
down_read(&ep->sem); //獲得ep的讀寫訊號量
/*從ep->rdllist中copy到txlist*/
if (ep_collect_ready_items(ep, &txlist, maxevents) > 0) {
/*從txlist到使用者空間*/
eventcnt = ep_send_events(ep, &txlist, events);
/*從txlist再反過來copy給ep->rdllist,這一步是具體的ET實現*/
ep_reinject_items(ep, &txlist);
}
up_read(&ep->sem); //釋放ep讀寫訊號量
return eventcnt;
}
下面的函式就是ET的具體實現,也是ET和LT的區別
static void ep_reinject_items(struct eventpoll *ep, struct list_head *txlist)
{
int ricnt = 0, pwake = 0;
unsigned long flags;
struct epitem *epi;
/*獲得ep的讀寫鎖*/
write_lock_irqsave(&ep->lock, flags);
while (!list_empty(txlist)) {
/*這一步跟之前講過的rb_node到epi的一步是一樣的*/
epi = list_entry(txlist->next, struct epitem, txlink);
/*初始化*/
EP_LIST_DEL(&epi->txlink);
/*
1.核心事件表(紅黑樹)不為空
2.事件沒有設定ET工作模式
3.就緒事件型別和監聽事件型別相同
4.該事件的rdllink不為空
*/
if (EP_RB_LINKED(&epi->rbn) && !(epi->event.events & EPOLLET) &&
(epi->revents & epi->event.events) && !EP_IS_LINKED(&epi->rdllink)) {
/*把剛剛臨時量txlist中的該事件繼續新增到rdllist中*/
list_add_tail(&epi->rdllink, &ep->rdllist);
ricnt++;
}
}
if (ricnt) {
if (waitqueue_active(&ep->wq))
wake_up(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
/*釋放讀寫鎖*/
write_unlock_irqrestore(&ep->lock, flags);
if (pwake)
ep_poll_safewake(&psw, &ep->poll_wait);
}
先說LT模式對於同一個就緒事件會重複提醒,從上面可以看出來是因為它又把依舊就緒且未設定ET標誌的事件重新copy到了rdllist中,所以下一次epoll_wait還是會把該事件返回給使用者。那麼ET這裡就很好解釋了,儘管該事件未處理完,但是你只要設定了ET標誌,我就不會再次把該事件返回給使用者。這就是ET的實現。