Guest 想要與外界互動,最終都得透過 Host 以捕捉事件並轉換成可理解的中斷。為此我們需要實作 Host I/O 模組,用以負責「事件監控」與「事件分派」。
昨天我們先確立了結構體的樣子:
host_io_handle 描述一個被監控的 fd,包含它關心的事件、對應的回調函式、以及設備的資料。
host_io 則是模組的核心,保存一個 epoll fd,並用 linked list 串起所有的 handle。
今天我們把 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 的所有函式只會被一個獨立的 I/O thread 調用,以此避免 race condition。
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 到是沒啥意義。
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 中統一管理。
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 的建立與銷毀須交由各設備模組負責。
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。
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 處理即可。
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;
}
}
這裡就單純的回收資源而已。