# [2021q1](http://wiki.csie.ncku.edu.tw/linux/schedule) 第 12 週測驗題
###### tags: `linux2021`
:::info
目的: 檢驗學員對 ==[高效 Web 伺服器開發](https://hackmd.io/@sysprog/fast-web-server)==, ==[透過 timerfd 處理週期性任務](https://hackmd.io/@sysprog/linux-timerfd)== 的認知
:::
==[作答表單](https://docs.google.com/forms/d/e/1FAIpQLSdg3gwVB2GQhB1_5bjBwGXFDMvWwqhxT0L4nZUlMmXd-zGmoQ/viewform)==
### 測驗 `1`
UNIX 的 [pipeline](https://en.wikipedia.org/wiki/Pipeline_(Unix)) (管線) 允許我們我們任意串接各種程式的輸入和輸出、將資料導入檔案或從檔案中導出資料,示意圖:
![](https://i.imgur.com/a4DjIc8.png)
> 詳見 [你所不知道的 C 語言: Stream I/O, EOF 和例外處理](https://hackmd.io/@sysprog/c-stream-io) 和 [The UNIX Command Language](https://github.com/susam/tucl)
[pipeline](https://en.wikipedia.org/wiki/Pipeline_(Unix)) 使用案例: 將 `ls` 命令輸出後的內容,轉交給 `less` 讀取, 並利用後者的功能進行全螢幕瀏覽和檢索:
```shell
$ ls -al /proc | less
```
我們也能對 [/proc/uptime](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/s2-proc-uptime) 進行文字處理,例如:
```shell
$ cat /proc/uptime | wc -c
```
依據處理器的數量不同,會得到略有落差的數值。倘若我們在上述命令安插 `sleep` 命令,即:
```shell
$ (cat /proc/uptime ; sleep 2; cat /proc/uptime) | wc -c
```
會等待 2 秒才會得到結果,而且僅有一筆數值。現在我們想延伸[第 11 週測驗題](https://hackmd.io/@sysprog/linux2021-quiz11)的方式,透過 [timerfd](https://man7.org/linux/man-pages/man2/timerfd_create.2.html) 和 [epoll](https://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫,即時監控 [pipeline](https://en.wikipedia.org/wiki/Pipeline_(Unix)) 輸入端的資料變化,即時統計讀取的數值並處理。假設執行檔名為 `filter`,預期執行方式如下:
```shell
$ (cat /proc/uptime ; sleep 2; cat /proc/uptime) | ./filter
```
參考執行輸出:
```
Read 24 bytes
2989775.15 182320010.92
Read 24 bytes
2989777.15 182320135.18
```
`filter` 程式會監控 [pipeline](https://en.wikipedia.org/wiki/Pipeline_(Unix)) 的資料變化,倘若沒有輸入時,就會離開執行。注意 2 次的 `Read 24 bytes` 輸出之間,程式會等待 2 秒,而非等待 2 秒才輸出字元統計數值。
以下是 `filter.c` 程式碼列表:
```cpp=
#include <stdbool.h>
#include <stdio.h>
#include <sys/epoll.h>
#define EV_FOREACH(node, list) \
for (typeof(node) nxt, node = list; node && (nxt = node->next); node = nxt)
#define EV_INSERT(node, list) \
do { \
typeof(node) next = list; \
list = node; \
if (next) next->prev = node; \
node->next = next, node->prev = NULL; \
} while (0)
#define EV_REMOVE(node, list) \
do { \
typeof(node) prev = node->prev, next = node->next; \
if (prev) prev->next = next; \
if (next) next->prev = prev; \
node->prev = node->next = NULL; \
if (list == node) list = next; \
} while (0)
/* I/O or timer watcher */
typedef enum { EV_IO_TYPE = 1, EV_TIMER_TYPE } ev_type_t;
/* Event mask, used internally only. */
#define EV_EVENT_MASK \
(EV_ERROR | EV_READ | EV_WRITE | EV_PRI | EV_RDHUP | EV_HUP | EV_EDGE | \
EV_ONESHOT)
/* Main EV context */
typedef struct {
bool running;
int fd; /* For epoll() */
struct ev *watchers;
bool workaround; /* For workarounds, e.g. redirected stdin */
} ev_ctx_t;
/* hide all private data members in ev_t */
#define ev_private_t \
struct ev *next, *prev; \
\
int active; \
int events; \
\
/* Watcher callback with optional argument */ \
void (*cb)(struct ev *, void *, int); \
void *arg; \
\
/* Arguments for different watchers */ \
union { \
struct { /* Timer watchers, time in milliseconds */ \
int timeout, period; \
} t; \
} u; \
\
/* Watcher type */ \
ev_type_t
static int _ev_watcher_init(ev_ctx_t *ctx,
struct ev *w,
ev_type_t type,
void (*cb)(struct ev *, void *, int),
void *arg,
int fd,
int events);
static int _ev_watcher_start(struct ev *w);
static int _ev_watcher_stop(struct ev *w);
static bool _ev_watcher_active(struct ev *w);
static int _ev_watcher_rearm(struct ev *w);
/* Max number of simulateneous events */
#define EV_MAX_EVENTS 10
/* I/O events and timer revents are always EV_READ */
#define EV_ERROR EPOLLERR
#define EV_READ EPOLLIN
#define EV_WRITE EPOLLOUT
#define EV_PRI EPOLLPRI
#define EV_HUP EPOLLHUP
#define EV_RDHUP EPOLLRDHUP
#define EV_EDGE EPOLLET
#define EV_ONESHOT EPOLLONESHOT
/* Run flags */
enum { EV_ONCE = 1, EV_NONBLOCK = 2 };
/* Event watcher */
typedef struct ev {
ev_private_t type; /* private data */
int fd;
ev_ctx_t *ctx;
} ev_t;
/* Generic callback for watchers, @events holds %EV_READ and/or %EV_WRITE with
* optional %EV_PRI (priority data available to read) and any of the %EV_HUP
* and/or %EV_RDHUP, which may be used to signal hang-up events.
*
* %EV_ERROR conditions must be handled by all callbacks.
* I/O watchers may also need to check EV_HUP. Appropriate action, e.g.
* restart the watcher, is up to the application and is thus delegated to the
* callback.
*/
typedef void(ev_cb_t)(ev_t *w, void *arg, int events);
/* Public interface */
int ev_init(ev_ctx_t *ctx);
int ev_exit(ev_ctx_t *ctx);
int ev_run(ev_ctx_t *ctx, int flags);
int ev_io_start(ev_t *w);
int ev_io_stop(ev_t *w);
#include <errno.h>
/* Create an I/O watcher
* @param ctx A valid EV context
* @param w Pointer to an ev_t watcher
* @param cb I/O callback
* @param arg Optional callback argument
* @param fd File descriptor to watch, or -1 to register an empty watcher
* @param events Events to watch for: %EV_READ, %EV_WRITE, %EV_EDGE,
* %EV_ONESHOW
*
* @return POSIX OK(0) or non-zero with @param errno set on error.
*/
int ev_io_init(ev_ctx_t *ctx,
ev_t *w,
ev_cb_t *cb,
void *arg,
int fd,
int events)
{
if (fd < 0) {
errno = EINVAL;
return -1;
}
if (_ev_watcher_init(ctx, w, EV_IO_TYPE, cb, arg, fd, events)) return -1;
return _ev_watcher_start(w);
}
/* Reset an I/O watcher
* @param w Pointer to an ev_t watcher
* @param fd New file descriptor to monitor
* @param events Requested events to watch for, a mask of %EV_READ and
* %EV_WRITE
*
* @return POSIX OK(0) or non-zero with @param errno set on error.
*/
static int ev_io_set(ev_t *w, int fd, int events)
{
if ((events & EV_ONESHOT) && _ev_watcher_active(w))
return _ev_watcher_rearm(w);
/* Ignore any errors, only to clean up anything lingering */
ev_io_stop(w);
return ev_io_init(w->ctx, w, (ev_cb_t *) w->cb, w->arg, fd, events);
}
/* Start an I/O watcher
* @param w Watcher to start (again)
*
* @return POSIX OK(0) or non-zero with @param errno set on error.
*/
int ev_io_start(ev_t *w)
{
return ev_io_set(w, w->fd, w->events);
}
/* Stop an I/O watcher
* @param w Watcher to stop
*
* @return POSIX OK(0) or non-zero with @param errno set on error.
*/
int ev_io_stop(ev_t *w)
{
return _ev_watcher_stop(w);
}
#include <sys/timerfd.h>
#include <unistd.h>
static inline void ms2ts(int ms, struct timespec *ts)
{
if (ms) /* millisecond */
ts->tv_sec = ms / 1000, ts->tv_nsec = (ms % 1000) * 1000000;
else
ts->tv_sec = 0, ts->tv_nsec = 0;
}
static int ev_timer_set(ev_t *w, int timeout, int period);
/* Create and start a timer watcher
* @param ctx A valid EV context
* @param w Pointer to an ev_t watcher
* @param cb Callback function
* @param arg Optional callback argument
* @param timeout Timeout in milliseconds before @param cb is called
* @param period For periodic timers this is the period time that @param
* timeout is reset to
*
* This function creates, and optionally starts, a timer watcher. There are
* two types of timers: one-shot and periodic.
*
* One-shot timers only use @param timeout, @param period is zero.
*
* Periodic timers can either start their life disabled, with @param timeout
* set to zero, or with the same value as @param period.
*
* When the timeout expires, for either of the two types, @param cb is called,
* with the optional @param arg argument. A one-shot timer ends its life there,
* while a periodic task's @param timeout is reset to the @param period and
* restarted.
*
* A timer is automatically started if the event loop is already running,
* otherwise it is kept on hold until triggered by calling ev_run().
*
* @return POSIX OK(0) or non-zero with @param errno set on error.
*/
static int ev_timer_init(ev_ctx_t *ctx,
ev_t *w,
ev_cb_t *cb,
void *arg,
int timeout,
int period)
{
if (timeout < 0 || period < 0) {
errno = ERANGE;
return -1;
}
int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (fd < 0) return -1;
if (_ev_watcher_init(ctx, w, EV_TIMER_TYPE, cb, arg, fd, EV_READ)) goto out;
if (ev_timer_set(w, timeout, period)) {
_ev_watcher_stop(w);
out:
close(fd);
w->fd = -1;
return -1;
}
return 0;
}
/* Reset a timer
* @param w Watcher to reset
* @param timeout Timeout in milliseconds before @param cb is called, zero
* disarms timer
* @param period For periodic timers this is the period time that @param
* timeout is reset to
*
* Note, the @param timeout value must be non-zero. Setting it to zero will
* disarm the timer. This is the underlying Linux function @func
* timerfd_settimer() which has this behavior.
*
* @return POSIX OK(0) or non-zero with @param errno set on error.
*/
static int ev_timer_set(ev_t *w, int timeout, int period)
{
/* Every watcher must be registered to a context */
if (!w || !w->ctx) {
errno = EINVAL;
return -1;
}
if (timeout < 0 || period < 0) {
errno = ERANGE;
return -1;
}
if (w->fd < 0) { /* Handle stopped timers */
if (!timeout && !period) return 0; /* Timer already stopped */
if (ev_timer_init(w->ctx, w, w->cb, w->arg, timeout, period)) return -1;
}
w->u.t.timeout = timeout, w->u.t.period = period;
if (w->ctx->running) {
struct itimerspec time;
ms2ts(timeout, &time.it_value), ms2ts(period, &time.it_interval);
if (timerfd_settime(w->fd, 0, &time, NULL) < 0) return 1;
}
return _ev_watcher_start(w);
}
/* Stop and unregister a timer watcher
* @param w Watcher to stop
*
* @return POSIX OK(0) or non-zero with @param errno set on error.
*/
static int ev_timer_stop(ev_t *w)
{
if (!_ev_watcher_active(w)) return 0;
if (_ev_watcher_stop(w)) return -1;
close(w->fd);
w->fd = -1;
return 0;
}
#include <string.h> /* memset() */
#include <fcntl.h> /* O_CLOEXEC */
#include <sys/ioctl.h>
#include <sys/select.h> /* for select() workaround */
static int _init(ev_ctx_t *ctx, bool close_old)
{
int fd = epoll_create1(EPOLL_CLOEXEC);
if (fd < 0) return -1;
if (close_old) close(ctx->fd);
ctx->fd = fd;
return 0;
}
/* Used by file I/O workaround when epoll => EPERM */
static bool has_data(int fd)
{
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);
struct timeval timeout = {0, 0};
if (select(1, &fds, NULL, NULL, &timeout) > 0) {
int n = 0;
return (ioctl(0, FIONREAD, &n) == 0) && (n > 0);
}
return false;
}
static int _ev_watcher_init(ev_ctx_t *ctx,
ev_t *w,
ev_type_t type,
ev_cb_t *cb,
void *arg,
int fd,
int events)
{
if (!ctx || !w) {
errno = EINVAL;
return -1;
}
w->ctx = ctx, w->type = type, w->active = 0, w->fd = fd;
w->cb = cb, w->arg = arg;
w->events = events;
return 0;
}
static int _ev_watcher_start(ev_t *w)
{
if (!w || w->fd < 0 || !w->ctx) {
errno = EINVAL;
return -1;
}
if (_ev_watcher_active(w)) return 0;
struct epoll_event ev = {.events = w->events | EPOLLRDHUP, .data.ptr = w};
if (epoll_ctl(w->ctx->fd, EPOLL_CTL_ADD, w->fd, &ev) < 0) {
if (errno != EPERM) return -1;
/* Handle special case: "prog < file.txt" */
if (w->type != EV_IO_TYPE || w->events != EV_READ) return -1;
if (w->fd != STDIN_FILENO) return -1; /* special handling for stdin */
w->ctx->workaround = true;
w->active = -1;
} else
w->active = 1;
/* Add to internal list for bookkeeping */
EV_INSERT(w, w->ctx->watchers);
return 0;
}
static int _ev_watcher_stop(ev_t *w)
{
if (!w) {
errno = EINVAL;
return -1;
}
if (!_ev_watcher_active(w)) return 0;
w->active = 0;
/* Remove from internal list */
YYY;
/* Remove from kernel */
if (epoll_ctl(w->ctx->fd, EPOLL_CTL_DEL, w->fd, NULL) < 0) return -1;
return 0;
}
static bool _ev_watcher_active(ev_t *w)
{
return w ? (ZZZ) : false;
}
static int _ev_watcher_rearm(ev_t *w)
{
if (!w || w->fd < 0) {
errno = EINVAL;
return -1;
}
struct epoll_event ev = {.events = w->events | EPOLLRDHUP, .data.ptr = w};
if (epoll_ctl(w->ctx->fd, EPOLL_CTL_MOD, w->fd, &ev) < 0) return -1;
return 0;
}
/* Create an event loop context
* @param ctx Pointer to an ev_ctx_t context to be initialized
*
* In cases where you have multiple events pending in the cache and some event
* may cause later ones, already sent by the kernel to userspace, to be deleted
* the pointer returned to the event loop for this later event may be deleted.
*
* @return POSIX OK(0) on success, or non-zero on error.
*/
int ev_init(ev_ctx_t *ctx)
{
if (!ctx) {
errno = EINVAL;
return -1;
}
memset(ctx, 0, sizeof(*ctx));
return _init(ctx, false);
}
/* Terminate the event loop
* @param ctx A valid EV context
*
* @return POSIX OK(0) or non-zero with @param errno set on error.
*/
int ev_exit(ev_ctx_t *ctx)
{
if (!ctx) {
errno = EINVAL;
return -1;
}
ev_t *w;
EV_FOREACH (w, ctx->watchers) {
/* Remove from internal list */
EV_REMOVE(w, ctx->watchers);
if (!_ev_watcher_active(w)) continue;
switch (w->type) {
case EV_IO_TYPE: ev_io_stop(w); break;
case EV_TIMER_TYPE: ev_timer_stop(w); break;
}
}
ctx->watchers = NULL, ctx->running = false;
if (ctx->fd > -1) close(ctx->fd);
ctx->fd = -1;
return 0;
}
/* Start the event loop
* @param ctx A valid EV context
* @param flags A mask of %EV_ONCE and %EV_NONBLOCK, or zero
*
* With @flags set to %EV_ONCE the event loop returns after the first event has
* been served, useful for instance to set a timeout on a file descriptor. If
* @flags also has %EV_NONBLOCK flag set the event loop will return immediately
* if no event is pending, useful when run inside another event loop.
*
* @return POSIX OK(0) upon successful termination of the event loop, or
* non-zero on error.
*/
int ev_run(ev_ctx_t *ctx, int flags)
{
if (!ctx || ctx->fd < 0) {
errno = EINVAL;
return -1;
}
int timeout = -1;
if (flags & EV_NONBLOCK) timeout = 0;
ctx->running = true; /* Start the event loop */
/* Start all dormant timers */
ev_t *w;
EV_FOREACH (w, ctx->watchers) {
if (w->type == EV_TIMER_TYPE)
ev_timer_set(w, w->u.t.timeout, w->u.t.period);
}
while (ctx->running && ctx->watchers) {
struct epoll_event ee[EV_MAX_EVENTS];
int nfds, rerun = 0;
/* Handle special case: "prog < file.txt" */
if (ctx->workaround) {
EV_FOREACH (w, ctx->watchers) {
if (w->active != -1 || !w->cb) continue;
if (!has_data(w->fd)) {
w->active = 0;
EV_REMOVE(w, ctx->watchers);
}
rerun++;
w->cb(w, w->arg, EV_READ);
}
}
if (rerun) continue;
ctx->workaround = false;
while ((nfds = WWW(ctx->fd, ee, EV_MAX_EVENTS, timeout)) < 0) {
if (!ctx->running) break;
if (EINTR == errno) continue; /* Signalled, try again */
/* Unrecoverable error, cleanup and exit with error. */
ev_exit(ctx);
return -2;
}
for (int i = 0; ctx->running && i < nfds; i++) {
uint64_t exp;
w = (ev_t *) ee[i].data.ptr;
uint32_t events = ee[i].events;
switch (w->type) {
case EV_IO_TYPE:
if (events & (XXX | EPOLLERR)) ev_io_stop(w);
break;
case EV_TIMER_TYPE:
if (read(w->fd, &exp, sizeof(exp)) != sizeof(exp)) {
ev_timer_stop(w);
events = EV_ERROR;
}
if (!w->u.t.period) w->u.t.timeout = 0;
if (!w->u.t.timeout) ev_timer_stop(w);
break;
}
/* Must be last action for watcher, callback may delete itself */
if (w->cb) w->cb(w, w->arg, events & EV_EVENT_MASK);
}
if (flags & EV_ONCE) break;
}
return 0;
}
#include <err.h>
#include <stdlib.h>
static void process_stdin(ev_t *w, void *arg /* unused */, int events)
{
if (events == EV_ERROR) {
warnx("Spurious problem with the stdin watcher, restarting.");
ev_io_start(w);
}
char buf[256];
int len = read(w->fd, buf, sizeof(buf));
if (len == -1) {
warn("Error reading from stdin");
return;
}
if (len == 0 || EV_HUP == events) /* Ignore */
return;
printf("Read %d bytes\n", len);
if (write(STDOUT_FILENO, buf, len) != len) warn("Failed writing to stdout");
}
int main(void)
{
ev_ctx_t ctx;
ev_init(&ctx);
ev_t watcher;
if (ev_io_init(&ctx, &watcher, process_stdin, NULL, STDIN_FILENO, EV_READ))
err(errno, "Failed setting up STDIN watcher");
return ev_run(&ctx, 0);
}
```
請補完程式碼,使得行為符合預期。
==作答區==
WWW = ? (第 541 行)
XXX = ? (第 560 行)
YYY = ? (第 410 行)
ZZZ = ? (第 420 行)
:::success
延伸問題:
1. 解釋上述程式碼運作原理,要涵蓋 epoll 和 select 的使用
2. 學習[第 11 週測驗題](https://hackmd.io/@sysprog/linux2021-quiz11)和本週測驗題的程式碼,嘗試實作類似 [libevent](https://libevent.org/) 或 [libuv](https://en.wikipedia.org/wiki/Libuv) 的通用事件驅動框架 (framework)
:::