iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
自我挑戰組

30 天 hypervisor 入門系列 第 10

Day 10 實作 Host I/O

  • 分享至 

  • xImage
  •  

Guest 想要與外界互動,最終都得透過 Host 以捕捉事件並轉換成可理解的中斷。為此我們需要實作 Host I/O 模組,用以負責「事件監控」與「事件分派」。
昨天我們先確立了結構體的樣子:
host_io_handle 描述一個被監控的 fd,包含它關心的事件、對應的回調函式、以及設備的資料。
host_io 則是模組的核心,保存一個 epoll fd,並用 linked list 串起所有的 handle。

Host I/O 模組

今天我們把 Host I/O 模組的土炮版本實作出來。這個模組的功能很單純,就是在 Host 端用 epoll 集中管理外部事件,然後把它們轉交給對應的回調函式處理。

模組的初始化從 host_io_init 開始。這個函式會建立一個 epoll fd,並且清空的 watcher。當我們需要監控新的事件來源時,就調用 host_io_register,把對應的 FD、監控事件類型以及回呼函式註冊進去。相對地,當設備不再需要時,可以透過 host_io_unregister 把它移除。這些操作底層都會同步更新到 epoll。

如果有些 FD 在運行過程中需要切換事件型態,例如從單純監聽 EPOLLIN 改成同時關心 EPOLLOUT,則可以調用 host_io_modify。這個函式會經過一次 epoll_ctl(EPOLL_CTL_MOD),把監控條件更新過來。整個過程對外都是透明的。

核心在於 host_io_poll。這個函式會調用 epoll_wait() 進入阻塞,直到有事件發生。當事件觸發時,將會依序把這些事件交給各自的回呼處理。對 I/O thread 來說,他只處理這些事情,在迴圈裡不斷調用 host_io_poll(),然後讓 callback 去處理細節。

最後是清理階段,host_io_shutdown 負責釋放所有 handle,並關閉 epoll fd。

Host I/O 初版實作

目前的設計中 Host I/O 的所有函式只會被一個獨立的 I/O thread 調用,以此避免 race condition。

host_io_init

int host_io_init(struct host_io *io)
{
    if (!io) {
        errno = EINVAL;
        return -1;
    }

    io->epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    if (io->epoll_fd < 0)
        return -1;

    io->watchers = NULL;
    return 0;
}

初始化只做兩件事,建立 epoll 物件並把內部狀態清零。
透過 epoll_create1(EPOLL_CLOEXEC) 建出 epoll 的 fd,帶 CLOEXEC 可避免之後 exec() 洩漏 fd。
這裡隱含一個規則:epoll 與 watchers 的寫入權,將只屬於 I/O thread,否則會有麻煩地同步問題。

註:我預計只會開一個 thread 處理 i/o 事件,因此 EPOLL_CLOEXEC 到是沒啥意義。

host_io_register

static int host_io_ctl(struct host_io *io, int op, struct host_io_handle *handle, uint32_t events)
{
    struct epoll_event ev;
    memset(&ev, 0, sizeof(ev));
    ev.events = events;
    ev.data.ptr = handle;
    return epoll_ctl(io->epoll_fd, op, handle->fd, &ev);
}

struct host_io_handle *host_io_register(struct host_io *io, int fd, uint32_t events,
                                        host_io_callback cb, void *opaque)
{
    if (!io || io->epoll_fd < 0 || fd < 0 || !cb) {
        errno = EINVAL;
        return NULL;
    }

    struct host_io_handle *handle = calloc(1, sizeof(*handle));
    if (!handle)
        return NULL;

    handle->fd = fd;
    handle->events = events;
    handle->callback = cb;
    handle->opaque = opaque;

    if (host_io_ctl(io, EPOLL_CTL_ADD, handle, events) < 0) {
        free(handle);
        return NULL;
    }

    handle->next = io->watchers;
    io->watchers = handle;
    return handle;
}

host_io_ctl 是對 epoll_ctl 的簡單封裝,負責組裝 struct epoll_event 並綁上 ev.data.ptr = handle。之後在 epoll_wait() 取回事件,就能拿回原本的 handle 指標。
host_io_register 是對 host_io_ctl 的再封裝,他負責調用 host_io_ctl 把 fd 註冊給 epoll,同時註冊 struct host_io_handle 到 link list 中統一管理。

host_io_unregister

static int host_io_detach(struct host_io *io, struct host_io_handle *handle)
{
    struct host_io_handle **pp = &io->watchers;
    while (*pp) {
        if (*pp == handle) {
            *pp = handle->next;
            handle->next = NULL;
            return 0;
        }
        pp = &(*pp)->next;
    }
    errno = ENOENT;
    return -1;
}

int host_io_unregister(struct host_io *io, struct host_io_handle *handle)
{
    if (!io || io->epoll_fd < 0 || !handle) {
        errno = EINVAL;
        return -1;
    }

    if (epoll_ctl(io->epoll_fd, EPOLL_CTL_DEL, handle->fd, NULL) < 0)
        return -1;

    if (host_io_detach(io, handle) < 0)
        return -1;

    free(handle);
    return 0;
}

這個函式只負責 epoll 的關聯乾淨拆除,並清理 struct host_io_handle。再設計上不主動 close(fd),fd 的建立與銷毀須交由各設備模組負責。

host_io_modify

int host_io_modify(struct host_io *io, struct host_io_handle *handle, uint32_t events)
{
    if (!io || io->epoll_fd < 0 || !handle) {
        errno = EINVAL;
        return -1;
    }

    if (host_io_ctl(io, EPOLL_CTL_MOD, handle, events) < 0)
        return -1;

    handle->events = events;
    return 0;
}

此函式負責在運行中變更某個 FD 的監聽條件。不過對目前要實現的設備如鍵盤,應該是用不到他。另外假如有一天這個函式被使用,他必須由 I/O thread 來調用,避免在 linked list 上發生 race condition。

host_io_poll

int host_io_poll(struct host_io *io, int timeout_ms)
{
    if (!io || io->epoll_fd < 0) {
        errno = EINVAL;
        return -1;
    }
    struct epoll_event events[8];
    int ready = epoll_wait(io->epoll_fd, events, (int)(sizeof(events) / sizeof(events[0])), timeout_ms);
    if (ready <= 0)
        return ready;

    for (int i = 0; i < ready; ++i) {
        struct host_io_handle *handle = events[i].data.ptr;
        if (!handle || !handle->callback)
            continue;
        handle->callback(handle->fd, events[i].events, handle->opaque);
    }
    return ready;
}

此函式為 epoll_wait 的封裝, epoll_wait 會把當前被觸發的 fd 依序填進 events[0..ready-1],目前暫定每輪最多觸發 8 個事件。 events[i].data.ptr 是在 host_io_ctl 設定的 handle。 events[i].events 代表事件位元,用以表達甚麼類型的事件被觸發。我們只需再 callback 中填入這個值,剩下交由設備實作的 callback 處理即可。

host_io_shutdown

void host_io_shutdown(struct host_io *io)
{
    if (!io)
        return;

    struct host_io_handle *handle = io->watchers;
    while (handle) {
        struct host_io_handle *next = handle->next;
        epoll_ctl(io->epoll_fd, EPOLL_CTL_DEL, handle->fd, NULL);
        free(handle);
        handle = next;
    }

    io->watchers = NULL;

    if (io->epoll_fd >= 0) {
        close(io->epoll_fd);
        io->epoll_fd = -1;
    }
}

這裡就單純的回收資源而已。


上一篇
Day 09 模擬 io 設備
下一篇
Day 11 實作 I/O thread
系列文
30 天 hypervisor 入門11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言