Linux的eventfd機制
eventfd初始化
Linux繼承了UNIX”everything is a file”的思想,所有打開的文件都有一個fd與之對應,與QEMU一樣,很多程序都是事件驅動的,也就是select/poll/epoll等系統調用在一組fd上進行監聽,當fd狀態發生變化時,應用程序調用對應的事件處理函數。事件來源可以有很多種,如普通文件、socket、pipe等。但是有的時候需要的僅僅是一個事件通知,沒有對應的具體實體,這個時候就可以直接使用eventfd了。
eventfd本質上是一個系統調用,創建一個事件通知fd,在內核內部創建一個eventfd對象,可以用來實現進程之間的等待/通知機制,內核也可以利用eventfd通知用戶態事件。
eventfd系統調用的定義
SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags)
{
return do_eventfd(count, flags);
}
SYSCALL_DEFINE1(eventfd, unsigned int, count)
{
return do_eventfd(count, 0);
}
內核定義了2種 eventfd 相關的系統調用,分別為eventfd和eventfd2,二者的區別在於,eventfd系統調用的flags的flags參數為0.
eventfd和eventfd2系統調用都調用了do_eventfd.
do_eventfd
=> ctx = kmalloc(sizeof(*ctx), GFP_KERNEL)
=> kref_init(&ctx->kref)
=> init_waitqueue_head(&ctx->wqh)
=> ctx->count = count;
ctx->flags = flags;
ctx->id = ida_simple_get(&eventfd_ida, 0, 0, GFP_KERNEL);
=> fd = anon_inode_getfd("[eventfd]", &eventfd_fops, ctx,
O_RDWR | (flags & EFD_SHARED_FCNTL_FLAGS));
=> return fd;
上面是do_eventfd的主要框架,接下來具體來看。
eventfd_ctx
ctx是一個eventfd_ctx結構,該結構的形式如下:
struct eventfd_ctx {
struct kref kref;
wait_queue_head_t wqh;
__u64 count;
unsigned int flags;
int id;
};
在一個eventfd上執行write系統調用,會向count加上被寫入的值,並喚醒等待隊列wqh中的元素。內核中的eventfd_signal函數也會增加count的值並喚醒wqh中的元素。
在eventfd上執行read系統調用,會向用戶空間返回count的值,並且該eventfd對應的eventfd_ctx結構中的count會被清0.
剩下的kref、flags、id這3個變量在后面介紹。
do_eventfd為eventfd_ctx分配了空間,即ctx = kmalloc(sizeof(*ctx), GFP_KERNEL)。
kref
eventfd_ctx中的kref是一個內核中的通用變量,一般插入到結構體中,用於記錄該結構體被內核各處引用的次數,當kref->refcount為0時,該結構體不再被引用,需要進行釋放。
kref_init(&ctx->kref)
將eventfd_ctx->kref.refcount值置為了1,表明eventfd_ctx正在一處代碼中使用。
count、flags、id
eventfd_ctx中count的值在前面介紹過,對eventfd寫則會增加count並喚醒等待隊列元素,對eventfd讀則向用戶空間返回count值並清count值為0,event_signal()也會增加count並喚醒等待隊列元素。
flags由調用eventfd2的調用者傳入(eventfd的flags恆為0),
flags的可能取值為EFD_CLOEXEC、EFD_NONBLOCK、EFD_SEMAPHORE三者的任意或組合。
- EFD_CLOEXEC
#define EFD_CLOEXEC O_CLOEXEC
EFD_CLOEXEC flag本質上為O_CLOEXEC。
O_CLOEXEC即執行時關閉標志。進程中每個打開的文件描述符都有一個執行時關閉標志,如果設置此標志,則在進程調用exec時關閉該文件描述符。
O_CLOEXEC可以方便我們關閉無用的文件描述符。
例如,當父進程fork出一個子進程時,子進程是父進程的副本,獲得父進程的數據空間、堆和棧的副本,當然也包括父進程打開的文件描述符。一般情況下,fork之后我們會調用exec執行另一個程序,此時會用全新的程序替換子進程的context(即堆、棧、數據空間等),此時之前運行父/子進程打開的文件描述符肯定也不存在了,我們丟失了這些文件描述符的reference,但之前被打開的文件依舊處於open狀態,成了系統的負擔。
通常在簡單系統中,我們可以在fork出一個子進程之后,在子進程中關閉這些已經打開,但不需要的文件描述符。但是,在復雜系統中,在我們fork出子進程的那一刻,我們並不知道已經有多少文件處於open狀態,一一在子進程中清理難度很大,如果能在fork出子進程前,父進程打開某個文件時就約定好,在我fork出一個子進程后,執行exec時,就關閉該打開的文件,因此close-on-exec,也就是O_CLOEXEC flag,是打開的文件描述符中的一個標志位。
返回到eventfd話題中,因為eventfd本質上是一個文件描述符,打開后也會占用系統資源,因此也擁有與O_CLOEXEC相同的EFD_CLOEXEC標志。
- EFD_NONBLOCK
#define EFD_NONBLOCK O_NONBLOCK
EFD_NONBLOCK的實質為O_NONBLOCK,對於設置該flag的文件描述符,任何打開文件並返回文件描述符的系統調用都不會阻塞進程,即如果無法獲取文件描述符則立即范返回。
在eventfd機制中,使用該flag的目的是能夠讓fcntl系統調用作用於文件文件描述符上時得到與相關系統調用的相同的結果。
- EFD_SEMAPHORE
提供一種類似於信號量的機制,用於當從eventfd讀取內容時的機制保護。
- id
id即eventfd的id,用於唯一標識一個eventfd。
do_eventfd(count,flags)
通過以上的知識鋪墊,eventfd和eventfd2系統調用的處理過程就很清晰了。
- 分配一個eventfd_ctx結構用於存儲eventfd相關信息
- 設置eventfd_ctx->kref中的值為1,表明內核正在引用該eventfd
- 初始化eventfd_ctx結構中的等待隊列
- 為eventfd_ctx結構中的count(讀寫eventfd時要操作的量)賦上系統調用傳入的count
- 為eventfd_ctx結構中的id通過Linux提供的ida機制申請一個id
- 最后通過anon_inode_getfd創建一個文件實例,該文件的操作方法為eventfd_fops,fd->private_data為eventfd_ctx,文件實例名為eventfd。
- 返回該文件實例的文件描述符
使用eventfd
eventfd操作方法
在eventfd初始化的過程中,為eventfd注冊了一組操作函數。
static const struct file_operations eventfd_fops = {
#ifdef CONFIG_PROC_FS
.show_fdinfo = eventfd_show_fdinfo,
#endif
.release = eventfd_release,
.poll = eventfd_poll,
.read = eventfd_read,
.write = eventfd_write,
.llseek = noop_llseek,
};
讀eventfd
讀eventfd動作由eventfd_read函數提供支持,只有在eventfd_ctx->count大於0的情況下,eventfd才是可讀的,此時調用eventfd_ctx_do_read對eventfd_ctx的count進行處理,如果eventfd_ctx->flags中的EFD_SEMAPHORE置位,就將eventfd->count減一(因為semaphore只有0和1兩個值,因此該操作即為置0操作);如果eventfd_ctx->flags中的EFD_SEMAPHORE為0,就將eventfd_ctx->count減去自身,即置eventfd_ctx->count為0,也是對count變量的置0操作。
如果eventfd_ctx->count等於0,即該eventfd當前不可讀,此時如果檢查eventfd_ctx->flags中的O_NONBLOCK沒有置位,那么將發起讀eventfd動作的進程放入屬於eventfd_ctx的等待隊列,並重新調度新的進程運行。
- 如果eventfd_ctx->count大於0,就將該count置0,激活正在等待隊列中等待的EPOLLOUT進程。
- 如果eventfd_ctx->count等於0且該eventfd提供阻塞標志,就將讀進程放入等待隊列中。
寫eventfd
寫eventfd動作由eventfd_write函數提供支持,該函數中,ucnt獲得了想要寫入eventfd的值,通過判斷ULLONG_MAX - eventfd_ctx->count 與ucnt的值大小,確認eventfd中還有足夠空間用於寫入,如果有足夠空間用於寫入,就在eventfd_ctx->count的基礎上加上ucnt變為新的eventfd_ctx->count,並激活在等待隊列中等待的讀/POLLIN進程。
如果沒有足夠空間用於寫入,則將寫進程放入屬於eventfd_ctx的等待隊列。
Poll eventfd
Poll(查詢)eventfd動作由eventfd_poll函數提供支持,該函數中定義了一個poll結構的events,如果eventfd的count大於0,則eventfd可讀,且events中的POLLIN置位。如果eventfd的count與ULLONG_MAX之間的差使eventfd至少能寫入1,則該eventfd可寫,且events中的POLLOUT置位。
eventfd的通知方案
從上面的eventfd操作方法可以看出有兩種通知方案:
- 進程poll eventfd的POLLIN事件,如果在某個時間點,其它進程或內核向eventfd寫入一個值,即可讓poll eventfd的進程返回。
- 進程poll eventfd的POLLOUT事件,如果在某個時間點,其它進程或內核讀取eventfd,即可讓poll eventfd的進程返回。
Linux內核使用第一種通知方案,即進程poll eventfd的POLLIN事件,Linux提供了功能與eventfd_write類似的eventfd_signal函數,用於觸發對poll eventfd的進程的通知。