qemu中的eventfd——用法與原理

eventfd可以用於線程或者父子進程間通信,內核通過eventfd也可以向用戶空間進程發消息。其核心實現是在內核空間維護一個計數器,向用戶空間暴露一個與之關聯的匿名fd。不同線程通過讀寫該fd通知或等待對方,內核通過寫該fd通知用戶程序

eventfd用法

  • eventfd機制接口簡單,核心只有4個,分別是創建eventfd(eventfd),寫eventfd(write),讀eventfd(read),監聽eventfd(poll/select)。
  1. int eventfd(unsigned int initval, int flags):創建一個eventfd,它的返回值是一個文件fd,可以讀寫。該接口傳入一個初始值initval用於內核初始化計數器,flags用於控制返回的eventfd的read行爲。flags如果包含EFD_NONBLOCK,read eventfd將不會阻塞,如果包含EFD_SEMAPHORE,read eventfd每次讀之後內核計數器都減1。
  2. ssize_t write(int fd, const void *buf, size_t count):寫eventfd,傳入一個8字節的buffer,buffer的值增加到內核維護的計數器中。
  3. ssize_t read(int fd, void *buf, size_t count):讀eventfd,如果計數器非0,信號量方式返回1,否則返回計數器的值。如果計數器爲0,讀失敗,阻塞模式下會阻塞直到計數器非0,非阻塞模式下返回EAGAIN錯誤。
  4. 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使用了三種方式,分別如下:
  1. 阻塞非信號量:以非信號量方式創建的eventfd,在讀eventfd之後,內核的計數器歸零,下一次再讀就會阻塞,除非有進程再次寫eventfd。
    在這裏插入圖片描述
    內核計數器初始值爲4,主線程第1次寫入2,計數器增至6
    讀線程返回6,之後計數器清0,讀線程阻塞
    下一次主線程寫入2,計數器增至2,讀線程返回2
  2. 阻塞信號量:以信號量方式創建的eventfd,在讀eventfd之後,內核的計數器減1
    在這裏插入圖片描述
    內核計數器初始值爲4,主線程第一次寫入2,計數器增至6
    讀線程返回1,計數器減1變成5,讀線程循環讀返回1,計數器再減1變成4
    主線程寫入2計數器增至6
  3. 非阻塞非信號量:讀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;
}; 
  1. wqh:等待隊列頭,所有阻塞在eventfd上的讀進程掛在該等待隊列上
  2. count:eventfd計數器,當用戶程序write eventfd時內核會將值加在計數器上,用戶程序read eventfd之後,內核會將值減1或者清0(由EFD_SEMAPHORE標誌決定),當計數器爲0時,內核會將read進程掛載等待隊列頭wqh指向的隊列上。
    兩種方式可以喚醒等待在eventfd上的進程,一個是用戶態write,另一個是內核態的eventfd_signal。從這裏可以看出eventfd不僅可以用於用戶進程相互通信,也可以用作內核通知用戶進程的手段。
  3. 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之後會喚醒阻塞在其上的讀進程。但是寫進程阻塞的場景比較少,它只在計數器到達最大值或者溢出的情況纔出現。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章