eventfd可以用於線程或者父子進程間通信,內核通過eventfd也可以向用戶空間進程發消息。其核心實現是在內核空間維護一個計數器,向用戶空間暴露一個與之關聯的匿名fd。不同線程通過讀寫該fd通知或等待對方,內核通過寫該fd通知用戶程序
eventfd用法
- eventfd機制接口簡單,核心只有4個,分別是創建eventfd(eventfd),寫eventfd(write),讀eventfd(read),監聽eventfd(poll/select)。
int eventfd(unsigned int initval, int flags)
:創建一個eventfd,它的返回值是一個文件fd,可以讀寫。該接口傳入一個初始值initval用於內核初始化計數器,flags用於控制返回的eventfd的read行爲。flags如果包含EFD_NONBLOCK
,read eventfd將不會阻塞,如果包含EFD_SEMAPHORE
,read eventfd每次讀之後內核計數器都減1。ssize_t write(int fd, const void *buf, size_t count)
:寫eventfd,傳入一個8字節的buffer,buffer的值增加到內核維護的計數器中。ssize_t read(int fd, void *buf, size_t count)
:讀eventfd,如果計數器非0,信號量方式返回1,否則返回計數器的值。如果計數器爲0,讀失敗,阻塞模式下會阻塞直到計數器非0,非阻塞模式下返回EAGAIN錯誤。int poll(struct pollfd *fds, nfds_t nfds, int timeout)
:監聽eventfd是否可讀。
demo
- 代碼
#include <stdio.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <pthread.h>
#include <unistd.h>
int efd;
void *threadFunc()
{
uint64_t buffer;
int rc;
int i = 0;
while(i++ < 2){
/* 如果計數器非0,read成功,buffer返回計數器值。成功後有兩種行爲:信號量方式計數器每次減,其它每次清0。
* 如果計數器0,read失敗,由兩種返回方式:EFD_NONBLOCK方式會阻塞,反之返回EAGAIN
*/
rc = read(efd, &buffer, sizeof(buffer));
if (rc == 8) {
printf("notify success, eventfd counter = %lu\n", buffer);
} else {
perror("read");
}
}
}
static void
open_eventfd(unsigned int initval, int flags)
{
efd = eventfd(initval, flags);
if (efd == -1) {
perror("eventfd");
}
}
static void
close_eventfd(int fd)
{
close(fd);
}
/* counter表示寫eventfd的次數,每次寫入值爲2 */
static void test(int counter)
{
int rc;
pthread_t tid;
void *status;
int i = 0;
uint64_t buf = 2;
/* create thread */
if(pthread_create(&tid, NULL, threadFunc, NULL) < 0){
perror("pthread_create");
}
while(i++ < counter){
rc = write(efd, &buf, sizeof(buf));
printf("signal to subscriber success, value = %lu\n", buf);
if(rc != 8){
perror("write");
}
sleep(2);
}
pthread_join(tid, &status);
}
int main()
{
unsigned int initval;
printf("NON-SEMAPHORE BLOCK way\n");
/* 初始值爲4, flags爲0,默認blocking方式讀取eventfd */
initval = 4;
open_eventfd(initval, 0);
printf("init counter = %lu\n", initval);
test(2);
close_eventfd(efd);
printf("change to SEMAPHORE way\n");
/* 初始值爲4, 信號量方式維護counter */
initval = 4;
open_eventfd(initval, EFD_SEMAPHORE);
printf("init counter = %lu\n", initval);
test(2);
close_eventfd(efd);
printf("change to NONBLOCK way\n");
/* 初始值爲4, NONBLOCK方式讀eventfd */
initval = 4;
open_eventfd(initval, EFD_NONBLOCK);
printf("init counter = %lu\n", initval);
test(2);
close_eventfd(efd);
return 0;
}
分析
- demo中創建eventfd使用了三種方式,分別如下:
- 阻塞非信號量:以非信號量方式創建的eventfd,在讀eventfd之後,內核的計數器歸零,下一次再讀就會阻塞,除非有進程再次寫eventfd。
內核計數器初始值爲4,主線程第1次寫入2,計數器增至6
讀線程返回6,之後計數器清0,讀線程阻塞
下一次主線程寫入2,計數器增至2,讀線程返回2 - 阻塞信號量:以信號量方式創建的eventfd,在讀eventfd之後,內核的計數器減1
內核計數器初始值爲4,主線程第一次寫入2,計數器增至6
讀線程返回1,計數器減1變成5,讀線程循環讀返回1,計數器再減1變成4
主線程寫入2計數器增至6 - 非阻塞非信號量:讀eventfd之後,計數器清0,再次讀eventfd返回EAGAIN
內核計數器初始值爲4,主線程第一次寫入2,計數器增至6
讀線程返回6,計數器清0,讀線程循環非阻塞讀返回錯誤碼EAGAIN
主線程寫入2計數器增至2
- demo阻塞讀模式下,信號量和非信號量方式如下圖所示:
eventfd內核實現
創建eventfd
系統調用
- eventfd系統調用有三個步驟:
SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags)
{
......
get_unused_fd_flags(flags & EFD_SHARED_FCNTL_FLAGS); /* 1 */
file = eventfd_file_create(count, flags); /* 2 */
fd_install(fd, file); /* 3 */
......
}
1. 從進程的文件描述符表(fdt)中查詢可用的fd
2. 在匿名節點文件系統(anon_inodefs)中創建一個文件結構體
3. 安裝文件描述符,將fd和file關聯起來
eventfd_ctx
- eventfd_ctx結構體是eventfd實現的核心,如下:
struct eventfd_ctx {
struct kref kref;
wait_queue_head_t wqh;
/*
* Every time that a write(2) is performed on an eventfd, the
* value of the __u64 being written is added to "count" and a
* wakeup is performed on "wqh". A read(2) will return the "count"
* value to userspace, and will reset "count" to zero. The kernel
* side eventfd_signal() also, adds to the "count" counter and
* issue a wakeup.
*/
__u64 count;
unsigned int flags;
};
- wqh:等待隊列頭,所有阻塞在eventfd上的讀進程掛在該等待隊列上
- count:eventfd計數器,當用戶程序write eventfd時內核會將值加在計數器上,用戶程序read eventfd之後,內核會將值減1或者清0(由EFD_SEMAPHORE標誌決定),當計數器爲0時,內核會將read進程掛載等待隊列頭wqh指向的隊列上。
兩種方式可以喚醒等待在eventfd上的進程,一個是用戶態write,另一個是內核態的eventfd_signal。從這裏可以看出eventfd不僅可以用於用戶進程相互通信,也可以用作內核通知用戶進程的手段。 - flags:決定用戶read後內核的處理方式,
EFD_SEMAPHORE,EFD_CLOEXEC,EFD_NONBLOCK
三個取值
- eventfd_ctx的初始化在eventfd_file_create中實現,如下:
struct file *eventfd_file_create(unsigned int count, int flags)
{
struct file *file;
struct eventfd_ctx *ctx;
......
ctx = kmalloc(sizeof(*ctx), GFP_KERNEL);
kref_init(&ctx->kref);
init_waitqueue_head(&ctx->wqh); /* 1 */
ctx->count = count;
ctx->flags = flags;
file = anon_inode_getfile("[eventfd]", &eventfd_fops, ctx, /* 2 */
O_RDWR | (flags & EFD_SHARED_FCNTL_FLAGS));
......
return file;
}
1. 初始化等待隊列
2. 把eventfd_ctx作爲file的private_data存放在file結構體中,這樣通過fd可以在進程的fd表中找到相應file結構體,最終找到eventfd_ctx
- eventfd的file操作由eventfd_fops實現,如下:
static const struct file_operations eventfd_fops = {
......
.read = eventfd_read,
.write = eventfd_write,
......
};
讀eventfd
- 用戶進程讀eventfd時,最終會進入 eventfd_read,如下:
static ssize_t eventfd_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
struct eventfd_ctx *ctx = file->private_data;
......
res = eventfd_ctx_read(ctx, file->f_flags & O_NONBLOCK, &cnt); /* 如果count非0,將其放在cnt中返回給用戶進程 */
......
}
- eventfd_read的核心是eventfd_ctx_read,看一下它的說明和實現:
/**
* eventfd_ctx_read - Reads the eventfd counter or wait if it is zero.
* @ctx: [in] Pointer to eventfd context.
* @no_wait: [in] Different from zero if the operation should not block.
* @cnt: [out] Pointer to the 64-bit counter value.
*
* Returns %0 if successful, or the following error codes:
*
* - -EAGAIN : The operation would have blocked but @no_wait was non-zero.
* - -ERESTARTSYS : A signal interrupted the wait operation.
*
* If @no_wait is zero, the function might sleep until the eventfd internal
* counter becomes greater than zero.
*/
ssize_t eventfd_ctx_read(struct eventfd_ctx *ctx, int no_wait, __u64 *cnt)
{
ssize_t res;
/* 聲明一個等待隊列項並關聯本進程的task
* 等待隊列關聯的函數爲default_wake_function,作爲喚醒函數存在
*/
DECLARE_WAITQUEUE(wait, current);
spin_lock_irq(&ctx->wqh.lock);
*cnt = 0;
res = -EAGAIN;
/* 如果count大於0,讀進程不阻塞 */
if (ctx->count > 0)
res = 0;
else if (!no_wait) {
__add_wait_queue(&ctx->wqh, &wait); /* 將等待隊列項添加到eventfd的等待隊列頭中 */
for (;;) {
/* 設置阻塞狀態 */
set_current_state(TASK_INTERRUPTIBLE);
/* 如果count大於0,break,這種情況在第一次循環之後可能發生
* eventfd的寫進程在寫完計數器後,會喚醒阻塞在eventfd上的讀進程
* 這時讀進程重新被調度器調度,進入新一輪的循環,來到這裏,重新檢查內核計數器
*/
if (ctx->count > 0) {
res = 0;
break;
}
/* 如果有未處理的信號,也break,進行處理 */
if (signal_pending(current)) {
res = -ERESTARTSYS;
break;
}
spin_unlock_irq(&ctx->wqh.lock);
/* 主動請求調度,當前進程被掛起 */
schedule();
/* 掛起的進程重新運行 */
spin_lock_irq(&ctx->wqh.lock);
}
/* 從循環中跳出,將當前進程從eventfd的等待隊列中刪除 */
__remove_wait_queue(&ctx->wqh, &wait);
/* 設置運行狀態 */
__set_current_state(TASK_RUNNING);
}
if (likely(res == 0)) {
/* 讀取counter */
eventfd_ctx_do_read(ctx, cnt);
/* 如果eventfd上有阻塞的寫進程,將其喚醒 */
if (waitqueue_active(&ctx->wqh))
wake_up_locked_poll(&ctx->wqh, POLLOUT);
}
spin_unlock_irq(&ctx->wqh.lock);
return res;
}
- eventfd_ctx_read函數中有一個地方要注意,當執行eventfd_ctx_do_read函數之後,表示真正獲取到了計數器的值,這之後會檢查阻塞在eventfd上的寫進程,因爲寫進程可能會因爲計數器超過範圍而阻塞,讀進程完成之後計數器值會變小,這個時候可以喚醒寫進程重新檢查寫入的值是否超過範圍。這個在寫eventfd中會介紹。
- 看一下真正獲取內核計數器的函數:
static void eventfd_ctx_do_read(struct eventfd_ctx *ctx, __u64 *cnt)
{
/* 如果是信號量方式,返回給用戶進程的counter始終是1,之後內核counter減1
* 如果是非信號量方式,返回給用戶進程的counter就是內核維護的counter
* 之後內核counter清0
*/
*cnt = (ctx->flags & EFD_SEMAPHORE) ? 1 : ctx->count;
ctx->count -= *cnt;
}
寫eventfd
- 從eventfd_ctx 的數據結構解釋中我們瞭解到,寫eventfd有兩種場景,一是用戶態寫,二是內核態寫。這兩種場景分別用於實現用戶態通知和內核態的通知。這節主要介紹用戶態發起的eventfd寫操作。
- 寫eventfd的主要動作是往計數器加用戶態傳入的值,有一種情況會阻塞寫進程,就是計數器值到達或者超出範圍。這時寫進程阻塞直到計數器在正常範圍內,注意,用戶態寫eventfd也不允許到達計數器的最大值,因爲這個最大值要爲內核態的寫保留,後面的內核態寫會介紹
static ssize_t eventfd_write(struct file *file, const char __user *buf, size_t count,
loff_t *ppos)
{
struct eventfd_ctx *ctx = file->private_data;
ssize_t res;
__u64 ucnt;
/* 聲明等待隊列項,default_wake_function爲喚醒函數 */
DECLARE_WAITQUEUE(wait, current);
/* 用戶態傳入的buffer長度必須>=8字節,否則返回錯誤 */
if (count < sizeof(ucnt))
return -EINVAL;
if (copy_from_user(&ucnt, buf, sizeof(ucnt)))
return -EFAULT;
/* 如果寫入的值等於64bit所能表示的最大值,返回錯誤 */
if (ucnt == ULLONG_MAX)
return -EINVAL;
spin_lock_irq(&ctx->wqh.lock);
res = -EAGAIN;
/* 寫入的ucnt後內核計數器不會溢出 */
if (ULLONG_MAX - ctx->count > ucnt)
res = sizeof(ucnt);
/* 寫入後內核計數器溢出,並且是阻塞方式打開的eventfd */
else if (!(file->f_flags & O_NONBLOCK)) {
__add_wait_queue(&ctx->wqh, &wait); // 寫進程加入等待隊列
for (res = 0;;) {
/* 設置阻塞狀態 */
set_current_state(TASK_INTERRUPTIBLE);
/* 當有讀進程read eventfd之後,內核計數器會減少
* 讀進程讀取計數器成功後,會喚醒阻塞在evenfd上的寫進程
* 這時寫進程重新被調度進入新一輪的循環檢查,走到這裏
* */
if (ULLONG_MAX - ctx->count > ucnt) {
res = sizeof(ucnt);
break;
}
if (signal_pending(current)) {
res = -ERESTARTSYS;
break;
}
spin_unlock_irq(&ctx->wqh.lock);
/* 主動讓出CPU,申請調度器調度 */
schedule();
spin_lock_irq(&ctx->wqh.lock);
}
/* 跳出循環,從等待隊列中退出 */
__remove_wait_queue(&ctx->wqh, &wait);
/* 設置運行狀態 */
__set_current_state(TASK_RUNNING);
}
if (likely(res > 0)) {
/* 增加內核計數器 */
ctx->count += ucnt;
/* 如果eventfd上有阻塞的讀進程,將其喚醒 */
if (waitqueue_active(&ctx->wqh))
wake_up_locked_poll(&ctx->wqh, POLLIN);
}
spin_unlock_irq(&ctx->wqh.lock);
return res;
}
- 在讀eventfd的流程中,我們提到讀完eventfd之後,會喚醒阻塞在其上的寫進程,同樣,在寫eventfd的流程中,寫完eventfd之後會喚醒阻塞在其上的讀進程。但是寫進程阻塞的場景比較少,它只在計數器到達最大值或者溢出的情況纔出現。