--- tags: LINUX KERNEL, LKI --- # [2021 年暑期「Linux 核心」](https://hackmd.io/@sysprog/linux2021-summer)課程先決測驗題 :::info :information_source: [2021 年暑期「Linux 核心」](https://hackmd.io/@sysprog/linux2021-summer)課程大部分教材 (含解說錄影) 可公開存取 (因此就算報名失敗,也可免費學習),但之所以要求報名者先通過測驗 (依據作答表現排序) 才能完成報名,是因為授課教師會**逐一批改作業**和針對學員的學習狀況**進行一對一討論**,若不限制名額,實在難以進行,還請包涵。 -- [jserv](http://wiki.csie.ncku.edu.tw/User/jserv) ::: :::warning :warning: 注意事項 1. 測驗時間: 2021 年 7 月 6 日到 2021 年 7 月 18 日 23:00 * 可優先作答下方測驗題 $\alpha$ 到 $\zeta$ (並及早填寫[作答表單](https://docs.google.com/forms/d/e/1FAIpQLSfAdDd0p7vgDFUjAM_aZ-xQy1olnaV6mlRcLu0fMA43-5-Uaw/viewform)),至於測驗題 $\eta$ 可在指定的 [HackMD](https://hackmd.io/) 建立後,再逐步改進和補充 * ==[作答表單](https://docs.google.com/forms/d/e/1FAIpQLSfAdDd0p7vgDFUjAM_aZ-xQy1olnaV6mlRcLu0fMA43-5-Uaw/viewform)== :notebook: 2. 測驗的目的是為了控制學員的數量,並非評價學員的能力,請斟酌進行 3. 可使用 Google 搜尋,但請不要借助技術論壇提問來進行這些題目,儘量「==誠實面對自己==」 4. 建議用 25 分鐘 (或更長時間) 來進行測驗題 5. 所有測驗題的程式碼都該符合 C99 或 C11 規範 6. 作答儘量用簡短的程式碼,且二元運算子 (binary operator,如 `+` [加法], `-` [減法], `*` [乘法], `^`, `&` [逐位元 AND] 等等) 和運算元 (operand) 之間用一個空白字元 (不多不少!) 區隔,如 `(v >> c)`、逗點 (`,`) 後應該有空白或換行符號,並避免非必要的小括號 (即 `(` 和 `)`),也該刪去 (trim) 起始/結尾的空白字元 * 程式碼排版風格可見 [.clang-format](https://github.com/sysprog21/concurrent-programs/blob/master/.clang-format) 7. 顧及批改的便利,本測驗採用 Google 表單自動比對參考答案,難免會有疏漏的答案組合 (特別是程式碼風格的落差),請發訊息到[授課教師的 Facebook 粉絲專頁](https://www.facebook.com/JservFans)告知和討論 8. 測驗結束後,凡符合報名條件者,會由授課教師以電子郵件聯繫 (來自 `<embedded.master2015@gmail.com>`),通知課程事宜和提供電子書 ::: --- ### 測驗 $\alpha$ 考慮到位元旋轉 (bitwise rotation 或 bit rotation) 的操作: ![](https://i.imgur.com/NNFZBXg.png) 以下程式碼可建立針對不同位元的向左/向右位元旋轉的實作: ```cpp #include <stdint.h> #define __DECLARE_ROTATE(bits, type) \ static inline type rotl##bits(const type v, int c) \ { \ const int mask = (bits) - (1); \ c &= mask; \ \ return (v << c) | (LLL); \ } \ \ static inline type rotr##bits(const type v, int c) \ { \ const int mask = (bits) - (1); \ c &= mask; \ \ return (v >> c) | (RRR); \ } #define DECLARE_ROTATE(bits) __DECLARE_ROTATE(bits, uint##bits##_t) ``` 使用案例: ```cpp DECLARE_ROTATE(64); DECLARE_ROTATE(32); DECLARE_ROTATE(16); DECLARE_ROTATE(8); ``` 請填補上述 `LLL` 和 `RRR` 區域的程式碼,限制只能用 `+`, `-`, `|`, `&`, `^`, `<<`, `>>` 等運算子,不得出現 [ternary conditional operator](https://en.wikipedia.org/wiki/%3F:) (即 `?` 搭配 `:`),且不該出現 `bits`。 ==作答區== LLL = ? RRR = ? :::success 延伸問題: 1. 舉出 Linux 核心原始程式碼裡頭 bit rotation 的案例並說明 2. [x86_64 指令集](https://en.wikipedia.org/wiki/X86_instruction_listings)具備 `rotr` 和 `rotl` 指令,上述 C 程式碼經過編譯器最佳化 (例如使用 `gcc`) 後,能否運用到這二個指令呢? ::: --- ### 測驗 $\beta$ 以下程式碼可針對給定的 alignment 數值,輸出大於等於 alignment 的記憶體對齊地址: ```cpp #include <stdint.h> static inline uintptr_t align_up(uintptr_t sz, size_t alignment) { uintptr_t mask = alignment - 1; if ((alignment & mask) == 0) { /* power of two? */ return MMM; } return (((sz + mask) / alignment) * alignment); } ``` 已知 alignment 不為 `0`,參考執行輸出: ```cpp align_up(120, 4) = 120 align_up(121, 4) = 124 align_up(122, 4) = 124 align_up(123, 4) = 124 ``` 請填補上述 `MMM` 標注的程式碼,使得執行結果符合預期。限制只能用 `+`, `-`, `|`, `&`, `^`, `~`, `<<`, `>>` 等運算子,不得出現 [ternary conditional operator](https://en.wikipedia.org/wiki/%3F:) (即 `?` 搭配 `:`),且不該出現 `alignment` ==作答區== MMM = ? :::success 延伸問題: 1. 說明上述程式碼的運作原理 2. 在 Linux 核心原始程式碼找出類似 `align_up` 的程式碼,並舉例說明其用法 ::: --- ### 測驗 $\gamma$ 在 GNU/Linux 環境,假設我們編譯的程式都會動態連結到 [glibc](https://www.gnu.org/software/libc/) 提供的 `libc.so.6`,考慮以下程式碼: (檔名: `fork.c`) ```cpp #include <stdio.h> #include <unistd.h> int main(void) { for (int i = 0; i < NNN; i++) { fork(); printf("-"); } fflush(stdout); return 0; } ``` 編譯程式碼: ```shell $ gcc -o fork fork.c ``` 執行並統計有多少 `-` 字元輸出: ```shell $ ./fork | wc -c ``` 若要讓 `./fork | wc -c` 的輸出為 `49152`,那上方程式碼的 `NNN` 應為何值?注意:應該考慮 buffered I/O 的影響,可參考 [CS:APP 第 10 章](https://hackmd.io/@sysprog/H1TtmVTTz)。 ==作答區== NNN = ? :::success 延伸問題: 1. 解釋上述程式碼輸出 `-` 字元數量的原理 ::: --- ### 測驗 $\delta$ 考慮以下 C11 程式碼,實作[並行 (concurrent)](https://hackmd.io/@sysprog/concurrency) 的多個生產者、多個消費者的佇列 (multi-producer, multi-consumer queue),列表如下: (檔名: `queue.c`) ```cpp #include <threads.h> enum { Q_OK, Q_ERROR }; typedef struct { /* Queue node */ void *value; void *next; } node_t; typedef struct { /* Two lock queue */ node_t *first, *last; mtx_t *first_mutex, *last_mutex; } con_queue_t; /* Free the queue struct. It assumes that the queue is depleted, and it will * not manage allocated elements inside of it. */ void con_free(con_queue_t *); #include <stdlib.h> #include <string.h> inline static node_t *_con_node_init(void *value) { node_t *node = malloc(sizeof(node_t)); if (!node) return NULL; node->value = value; node->next = NULL; return node; } /* Allocates and initializes queue. * Returns a pointer to an allocated struct for the synchronized queue or NULL * on failure. */ con_queue_t *con_init() { /* Allocate queue */ con_queue_t *queue = malloc(sizeof(con_queue_t)); if (!queue) return NULL; if ((queue->first_mutex = malloc(sizeof(mtx_t))) == NULL) { free(queue); return NULL; } if ((queue->last_mutex = malloc(sizeof(mtx_t))) == NULL) { free(queue->first_mutex); free(queue); return NULL; } if (mtx_init(queue->first_mutex, mtx_plain) != thrd_success || mtx_init(queue->last_mutex, mtx_plain) != thrd_success) { con_free(queue); return NULL; } node_t *dummy = _con_node_init(NULL); if (!dummy) { con_free(queue); return NULL; } queue->first = queue->last = dummy; return queue; } void con_free(con_queue_t *queue) { if (!queue) return; if (!queue->first) free(queue->first); if (queue->first_mutex) { mtx_destroy(queue->first_mutex); free(queue->first_mutex); } if (queue->last_mutex) { mtx_destroy(queue->last_mutex); free(queue->last_mutex); } free(queue); } /* Add element to queue. The client is responsible for freeing elementsput into * the queue afterwards. Returns Q_OK on success or Q_ERROR on failure. */ int con_push(con_queue_t *restrict queue, void *restrict new_element) { /* Prepare new node */ node_t *node = _con_node_init(new_element); if (!node) return Q_ERROR; /* Add to queue with lock */ mtx_lock(queue->last_mutex); AAA; queue->last = node; mtx_unlock(queue->last_mutex); return Q_OK; } /* Retrieve element and remove it from the queue. * Returns a pointer to the element previously pushed in or NULL of the queue is * emtpy. */ void *con_pop(con_queue_t *queue) { mtx_lock(queue->first_mutex); node_t *node = queue->first; /* Node to be removed */ node_t *new_header = queue->first->next; /* become the first in the queue */ /* Queue is empty */ if (!new_header) { mtx_unlock(queue->first_mutex); return NULL; } /* Queue not empty: retrieve data and rewire */ void *return_value = BBB; CCC; mtx_unlock(queue->first_mutex); /* Free removed node and return */ free(node); return return_value; } #include <assert.h> #include <stdio.h> #define N_PUSH_THREADS 4 #define N_POP_THREADS 4 #define NUM 1000000 /* This thread writes integers into the queue */ int push_thread(void *queue_ptr) { con_queue_t *queue = (con_queue_t *) queue_ptr; /* Push ints into queue */ for (int i = 0; i < NUM; ++i) { int *pushed_value = malloc(sizeof(int)); *pushed_value = i; if (con_push(queue, pushed_value) != Q_OK) printf("Error pushing element %i\n", i); } thrd_exit(0); } /* This thread reads ints from the queue and frees them */ int pop_thread(void *queue_ptr) { con_queue_t *queue = (con_queue_t *) queue_ptr; /* Read values from queue. Break loop on -1 */ while (1) { int *popped_value = con_pop(queue); if (popped_value) { if (*popped_value == -1) { free(popped_value); break; } free(popped_value); } } thrd_exit(0); } int main() { thrd_t push_threads[N_PUSH_THREADS], pop_threads[N_POP_THREADS]; con_queue_t *queue = con_init(); for (int i = 0; i < N_PUSH_THREADS; ++i) { if (thrd_create(&push_threads[i], push_thread, queue) != thrd_success) printf("Error creating push thread %i\n", i); } for (int i = 0; i < N_POP_THREADS; ++i) { if (thrd_create(&pop_threads[i], pop_thread, queue) != thrd_success) printf("Error creating pop thread %i\n", i); } for (int i = 0; i < N_PUSH_THREADS; ++i) { if (thrd_join(push_threads[i], NULL) != thrd_success) continue; } /* Push kill signals */ for (int i = 0; i < N_POP_THREADS; ++i) { int *kill_signal = malloc(sizeof(int)); /* signal pop threads to exit */ *kill_signal = -1; con_push(queue, kill_signal); } for (int i = 0; i < N_POP_THREADS; ++i) { if (thrd_join(pop_threads[i], NULL) != thrd_success) continue; } con_free(queue); return 0; } ``` 程式碼編譯方式: ```shell $ gcc -Wall -std=c11 -o queue queue.c -lpthread ``` 假設 `malloc` 和 `free` 執行過程不會遇到任何錯誤,請補完程式碼,讓這樣的並行佇列得以正確運作,且不會輸出任何錯誤訊息。 ==作答區== AAA = ? BBB = ? CCC = ? :::success 延伸問題: 1. 解釋上述程式碼運作原理並指出實作缺失 2. 以 [lock-free 程式設計](https://hackmd.io/@sysprog/concurrency-lockfree) 改寫上述程式碼 ::: --- ### 測驗 $\epsilon$ 考慮以下程式碼藉由 [mmap](https://man7.org/linux/man-pages/man2/mmap.2.html) 系統呼叫來實作 [memory pool](https://en.wikipedia.org/wiki/Memory_pool): ```cpp #include <assert.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <unistd.h> #define PAGE_SIZE 4096 /* FIXME: set at runtime */ typedef struct { int cnt; /* actual pool count */ int pal; /* pool array length (2^x ceil of cnt) */ int min_pool, max_pool; /* minimum/maximum pool size */ void **ps; /* pools */ int *sizes; /* chunk size for each pool */ void *hs[1]; /* heads for pools' free lists */ } mpool; static void *get_mmap(long sz) { void *p = mmap(0, sz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (p == MAP_FAILED) return NULL; return p; } /* base-2 integer ceiling */ static unsigned int iceil2(unsigned int x) { x = x - 1; x = x | (x >> 1); x = x | (x >> 2); x = x | (x >> 4); x = x | (x >> 8); x = x | (x >> 16); return XXX; } /* mmap a new memory pool of TOTAL_SZ bytes, then build an internal * freelist of SZ-byte cells, with the head at (result)[0]. * Returns NULL on error. */ void **mpool_new_pool(unsigned int sz, unsigned int total_sz) { int o = 0; /* o=offset */ void *p = get_mmap(sz > total_sz ? sz : total_sz); if (!p) return NULL; int **pool = (int **) p; assert(pool); assert(sz > sizeof(void *)); void *last = NULL; int lim = (total_sz / sz); for (int i = 0; i < lim; i++) { if (last) assert(last == pool[o]); o = (i * sz) / sizeof(void *); pool[o] = (int *) &pool[o + (sz / sizeof(void *))]; last = pool[o]; } pool[o] = NULL; return p; } /* Add a new pool, resizing the pool array if necessary. */ static int add_pool(mpool *mp, void *p, int sz) { assert(p); assert(sz > 0); if (mp->cnt == mp->pal) { mp->pal *= 2; /* RAM will exhaust before overflow */ void **nps = realloc(mp->ps, mp->pal * sizeof(void **)); void *nsizes = realloc(mp->sizes, mp->pal * sizeof(int *)); if (!nps || !nsizes) return -1; mp->sizes = nsizes; mp->ps = nps; } mp->ps[mp->cnt] = p; mp->sizes[mp->cnt] = sz; mp->cnt++; return 0; } /* Initialize a memory pool for allocations between 2^min2 and 2^max2, * inclusive. Larger allocations will be directly allocated and freed * via mmap / munmap. * Returns NULL on error. */ mpool *mpool_init(int min2, int max2) { int cnt = max2 - min2 + 1; /* pool array count */ int palen = iceil2(cnt); assert(cnt > 0); mpool *mp = malloc(sizeof(mpool) + (cnt - 1) * sizeof(void *)); void *pools = malloc(palen * sizeof(void **)); int *sizes = malloc(palen * sizeof(int)); if (!mp || !pools || !sizes) return NULL; mp->cnt = cnt; mp->ps = pools; mp->pal = palen; mp->sizes = sizes; mp->min_pool = (1 << min2), mp->max_pool = (1 << max2); memset(sizes, 0, palen * sizeof(int)); memset(pools, 0, palen * sizeof(void *)); memset(mp->hs, 0, cnt * sizeof(void *)); return mp; } /* Free a memory pool set. */ void mpool_free(mpool *mp) { assert(mp); for (long i = 0; i < mp->cnt; i++) { void *p = mp->ps[i]; if (!p) continue; long sz = mp->sizes[i]; assert(sz > 0); if (sz < PAGE_SIZE) sz = PAGE_SIZE; if (munmap(mp->ps[i], sz) == -1) fprintf(stderr, "Failed to unmap %ld bytes at %p\n", sz, mp->ps[i]); } free(mp->ps); free(mp); } /* Allocate memory out of the relevant memory pool. * If larger than max_pool, just mmap it. If pool is full, mmap a new one and * link it to the end of the current one. * Returns NULL on error. */ void *mpool_alloc(mpool *mp, int sz) { void **cur; int i, p; assert(mp); if (sz >= mp->max_pool) { cur = get_mmap(sz); /* just mmap it */ if (!cur) return NULL; return cur; } long szceil = 0; for (i = 0, p = mp->min_pool;; i++, p *= 2) { if (p > sz) { szceil = p; break; } } assert(szceil > 0); cur = mp->hs[i]; /* get current head */ if (!cur) { /* lazily allocate and initialize pool */ void **pool = mpool_new_pool(szceil, PAGE_SIZE); if (!pool) return NULL; mp->ps[i] = mp->hs[i] = pool; mp->sizes[i] = szceil; cur = mp->hs[i]; } assert(cur); if (!(*cur)) { /* if at end, attach to a new page */ void **np = mpool_new_pool(szceil, PAGE_SIZE); if (!np) return NULL; *cur = &np[0]; assert(*cur); if (add_pool(mp, np, szceil) < 0) return NULL; } assert(*cur > (void *) PAGE_SIZE); mp->hs[i] = *cur; /* set head to next head */ return cur; } /* Push an individual pointer P back on the freelist for the pool with size * SZ cells. if SZ is > the max pool size, just munmap it. * Return pointer P (SZ bytes in size) to the appropriate pool. */ void mpool_repool(mpool *mp, void *p, int sz) { int i = 0; if (sz > mp->max_pool) { if (munmap(p, sz) == -1) fprintf(stderr, "Failed to unmap %d bytes at %p\n", sz, p); return; } void **ip = (void **) p; YYY; assert(ip); mp->hs[i] = ip; } ``` 對應的測試程式如下: ```cpp #define PMAX 11 int main(void) { /* Initialize a new mpool for values 2^4 to 2^PMAX */ mpool *mp = mpool_init(4, PMAX); srandom(getpid() ^ (intptr_t) &main); /* Assume ASLR */ for (int i = 0; i < 5000000; i++) { int sz = random() % 64; /* also alloc some larger chunks */ if (random() % 100 == 0) sz = random() % 10000; if (!sz) sz = 1; /* avoid zero allocations */ int *ip = (int *) mpool_alloc(mp, sz); *ip = 7; /* randomly repool some of them */ if (random() % 10 == 0) /* repool, known size */ mpool_repool(mp, ip, sz); if (i % 10000 == 0 && i > 0) { putchar('.'); if (i % 700000 == 0) putchar('\n'); } } mpool_free(mp); putchar('\n'); return 0; } ``` 該 [memory pool](https://en.wikipedia.org/wiki/Memory_pool) 初始化時,指定 $2^4$ 到 $2^{11}$ 範圍的物件預先配置,大於 $2^{11}$ 的物件配置會藉由 [mmap](https://man7.org/linux/man-pages/man2/mmap.2.html) 系統呼叫達成。 假設上方程式碼的 `mmap`, `munmap`, `malloc`, `free`, `realloc` 均可正確執行,請補完程式碼,使得進行壓力測試的過程中,不會印出錯誤訊息和觸發 assertion。 ==作答區== XXX = ? YYY = ? :::success 延伸問題: 1. 解釋上述程式碼運作原理 2. 提出效能和功能的改進策略,並予以實作 ::: --- ### 測驗 $\zeta$ 以下程式碼嘗試透過「[高效 Web 伺服器開發](https://hackmd.io/@sysprog/fast-web-server)」提到的 [poll](https://man7.org/linux/man-pages/man2/poll.2.html) 系統呼叫和「[以 sendfile 和 splice 系統呼叫達到 Zero-Copy](https://hackmd.io/@sysprog/linux2020-zerocopy)」提到 Linux 特有的 splice 系統呼叫,實作出具體而微的 [Port forwarding](https://en.wikipedia.org/wiki/Port_forwarding)。考慮一個情境:我們對外有一台防火牆,在 DNS 設定方面,我們設定 `ftp.mydomain.com` 及 `www.mydomain.com` 都指向這台防火牆。但我們希望所有 HTTP 連線都重新導向到內部的 `192.168.0.2` 這台機器上,而所有 FTP 連線都交由 `192.168.0.3` 來處理。這時候我們就可以使用 port forwarding 的方式來達成。對應的 NAT (Network Address Translation) 的設定如下: ```= redirect_port tcp 192.168.0.2:80 80 redirect_port tcp 192.168.0.3:20 20 redirect_port tcp 192.168.0.3:21 21 ``` 第 1 行的目的就是將 port 80 的 tcp 連線重新導向到 `192.168.0.2` 的 port 80,而第 2 和第 3 行是將 port 20 及 port 21 的連線交由 `192.168.0.3` 來處理。在 `192.168.0.2` 及 `192.168.0.3` 這二台機器上,我們只要設定它們的 gateway 為防火牆的 IP,例如 `192.168.0.1` 即可。 使用 splice 系統呼叫,我們有機會在網路介面控制器的支援下,達到 Zero-copy 資料傳輸。 原始程式碼可見 `proxy.c`。 假設本地機器系統 port 8081 已有網頁伺服器在等待連線。`proxy` 的測試方式為 ```shell $ ./proxy localhost 8081 ``` 等程式執行後,在另一個終端機畫面中輸入下列命令: ```shell $ telnet localhost 1922 ``` 接著你就可以輸入 HTTP 請求字串,如 `GET /index.html`。 此外,你還可以把 port 1922 轉向到 Google 首頁: 先找出 `www.google.com` 的 IP 地址: ```shell $ nslookup www.google.com ``` 得到以下輸出: ``` Name: www.google.com Address: 216.58.200.36 ``` 修改上述命令: ```shell ./proxy 216.58.200.36 80 ``` 重複上述 `telnet` 命令,這時候就會看到 Google 首頁的字串。 以下是 `proxy.c` 程式碼列表: ```cpp #define _GNU_SOURCE #include <arpa/inet.h> #include <errno.h> #include <fcntl.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/poll.h> #include <sys/socket.h> #include <unistd.h> static int connect_to(char *ip, int port) { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { printf("Failed to create socket.\n"); return -1; } struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); if (inet_pton(AF_INET, ip, &addr.sin_addr) <= 0) { perror("inet_pton"); return -1; } if (connect(sockfd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { printf("Failed to connect.\n"); return -1; } return sockfd; } static void move(int in_fd, int out_fd, int pip[2]) { int ret = splice(in_fd, NULL, pip[1], NULL, 8, SPLICE_F_MOVE); if (ret == -1) { perror("splice(1)"); return; } ret = splice(pip[0], NULL, out_fd, NULL, 8, SPLICE_F_MOVE); if (ret == -1) { perror("splice(1)"); return; } } static void proxy(int cl_fd, int target_fd) { if (target_fd == -1) return; int fds[2]; if (pipe(fds) == -1) { perror("pipe"); return; } struct pollfd polls[2] = { [1] = {III}, [0] = {JJJ}, }; int ret; while ((ret = poll(polls, 2, 1000)) != -1) { if (ret == 0) continue; int from_client = polls[0].revents & POLLIN; if (from_client) move(cl_fd, target_fd, fds); else move(target_fd, cl_fd, fds); } perror("poll"); } #define PORT 1922 int main(int argc, char *argv[]) { if (argc < 3) { fprintf(stderr, "Usage: %s <target IP address> <target port>\n", argv[0]); return -1; } int listenfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(PORT); int optval = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)); bind(listenfd, (struct sockaddr *) &addr, sizeof(addr)); listen(listenfd, 1); while (1) { int connfd = accept(listenfd, (struct sockaddr *) NULL, NULL); int target_fd = connect_to(argv[1], atoi(argv[2])); if (target_fd >= 0) proxy(connfd, target_fd); close(connfd); } return 0; /* should not reach here */ } ``` 請補完程式碼,使其執行符合上述,並且儘量用字元數較少的答覆 (但仍要符合 [.clang-format](https://github.com/sysprog21/concurrent-programs/blob/master/.clang-format) 排版規範)。 ==作答區== III = ? JJJ = ? :::success 延伸問題: 1. 解釋上述程式碼運作原理 2. 以 [epoll](https://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫改寫程式碼,並設計實驗來驗證 proxy 程式碼的效率 ::: --- ### 測驗 $\eta$ 自上述測驗題 $\alpha$ 到 $\zeta$ 選出至少二項延伸問題,建立 [HackMD](https://hackmd.io/) 頁面、設定權限為「==已登入者可編輯==」(這樣授課教師才能給予眉批)、用固定網址發布筆記 (建議命名為 `linux2021-quiz`),並依據下方格式來答覆延伸問題: * 標題: linux2021: **你的 GitHub 帳號名稱** * 內文: 用 `## ` 開頭並標注題目,如 `## 測驗` $\alpha-1$ 表示「測驗 $\alpha$」的第一個延伸問題 延伸閱讀: * [HackMD: 筆記權限設定](https://hackmd.io/s/how-to-set-permissions-tw) * [HackMD: 用固定網址發布筆記](https://hackmd.io/s/how-to-share-note-tw) --- :::danger :face_with_cowboy_hat: 感謝您的耐心,瀏覽以上先決測驗題,就算您無緣參加課程,但我相信,練習這些測驗題仍可給您一絲啟發。 -- [jserv](http://wiki.csie.ncku.edu.tw/User/jserv) :::