# Linux 核心專題: 並行程式設計相關測驗題
> 執行人: Max042004
> 解說影片 [上](https://youtu.be/q6oEuorWU9c?si=cvaidM58sWbrTlX5) [下](https://youtu.be/VwUq1dOMB5I)
### Reviewed by `Andrewtangtang`
你提到目前是 SPSC 的 ring buffer,那這份程式是怎麼確保 worker 讀到的 socket 是正確的?有用鎖或其他同步機制嗎?如果 producer 很快塞資料,會不會在 consumer 還沒讀之前就被覆寫?
> [name=Max042004]
> 此程式不會發生 ABA 問題,因為每一個 ring buffer 為 worker 所擁有,consumer 僅有單一 worker。producer 是唯一的 listener。
>
> ring buffer 的單位是 socket 的指標: `struct socket *slot[RING_SIZE]`,listener 會在每一次 `accept` 後試圖把建立的 socket 放入 ring buffer,而 worker 會試圖從 ring buffer 一次取一個 socket。因此 head 和 tail 的遞增單位皆為 1。所以只需要在每一次放入 socket 前,利用 `next` 比對是否已滿,若當下已滿就不再放入新的 socket。
> 要確保 worker(consumer) 不會錯誤讀取,producer 必須使用 release 確保 socket 已放入後才更新 head 指標,然後 consumer 要使用 acquire 讀取 head 指標。在這份 [Linux docs](https://www.kernel.org/doc/Documentation/memory-barriers.txt) 說明:
> >after an ACQUIRE on a given variable, all memory accesses preceding any prior RELEASE on that same variable are guaranteed to be visible.
>
> 所以當 consumer 用 acquire 讀取 head 時,可以保證 release 前一行的 `r->slot[head] = s` 已執行完畢,避免當 consumer 以為 ring buffer 非空但 producer 還沒把 socket 放入的錯誤。
>
> 要確保 producer 不會覆蓋掉資料,要避免的情況就是指令重排導致當 tail 先更新了,讓 producer 以為有空位,但 consumer 卻還沒將 socket 讀取出來,導致 producer 把未讀取的 socket 覆蓋掉,consumer 因此讀取到覆蓋原有 socket 的新 socket。
> 因此解決辦法是 tail 的讀取與寫入也使用 release/acquire,consumer 在`struct socket *s = r->slot[tail]` 之後用 `smp_store_release(tail, (tail + 1) & RING_MASK)`,然後 producer 讀取 tail 使用 `smp_load_acquire(tail)`,這樣可以確保 producer 讀取 tail 之前,consumer 已經把 socket 讀取出來。
> [name=Max042004]
### Reviewed by 'Ian-Yen'
> `TASK_INTERUPPTIBLE` 少了一個 R 應為 `TASK_INTERRUPPTIBLE`
> 已更正,謝謝
> [name=Max042004]
### Reviewed by `alexc-313`
> 值得注意的是,`thread_ctx` 並沒有區分 listener 執行緒 或 worker 執行緒,皆使用相同的封裝。
你認為為何不區分 listener 執行緒 或 worker 執行緒?
另外,注意排版,例如 review 不應該在 TODO 裡面。
> 我認為單純是了程式碼更精簡
> [name=Max042004]
## TODO: 第 11 週測驗題
> [測驗題](https://hackmd.io/@sysprog/linux2025-quiz11)和延伸問題
:::danger
不要列出參考題解,專注於程式碼和你的洞見
:::
在一般的 ring buffer 實作,會遞增 write 和 read 指標再取餘數使其達到環形的效果,而取餘數運算本身運算成本高,所以把 ring buffer 大小設定為 2 的 10 次方,這樣就能用遮罩達到取餘數的效果。
ring buffer 儲存的是 socket,所以每一個 ring buffer 可存放 1024 個 socket。
觀察 ascii 對照表:

發現小寫英文字母剛好比大寫字母多 32,於是大小寫的轉換能通過 0x00000020 遮罩完成。
> 對照 [你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise)
`kthread_run` 用來建立一個核心執行緒,執行 `worker` 函式。在 `kweb_init` 將會迴圈呼叫 `start_worker` 以建立指定數量的 `worker` 核心執行緒。
#### 核心空間伺服器的好處
在核心空間運行的伺服器可以減少系統呼叫權限切換,使用者空間的資料傳遞的成本。
#### thread_ctx
整份程式碼主要邏輯包辦在 `listener` 與 `worker`、分別作為 SPSC 的生產者和消費者,而這兩個操作對象皆為 `thread_ctx`。每一個執行緒皆有一個私有的 `thread_ctx`,包裝各個核心執行緒的資訊。
```c
struct thread_ctx {
struct socket *listen_sock; /* listener only (idx 0) */
struct file *listen_file; /* listener only */
struct accept_ring ring;
struct client_ctx cli[MAX_CLIENTS];
int nr_cli;
char *rbuf;
struct task_struct *task;
struct poll_wqueues pwq;
};
```
值得注意的是,`thread_ctx` 並沒有區分 listener 執行緒 或 worker 執行緒,皆使用相同的封裝。
#### 建立 listener
`create_listen` 函式用來建立 listener,首先其呼叫核心空間建立 socket 的函式 `sock_create_kern`,傳入的參數分別是 net namespace, protocol family(`PF_INET`), communication type (`SOCK_STREAM`), protocol, new socket,此處傳入 socket 指標的地址,使用間接指標,避免只操作到指標的副本。
若為使用者空間的 socket 可以使用 `setsocketopt(2)` 設定 socket 的功能,而在 Linux 核心則使用 `sock_setsockopt(s, SOL_SOCKET, SO_REUSEADDR, kopt, sizeof(opt));`,解讀幾個重要參數,第二個參數 `SOL_SOCKET` 定義該 `sock_setsockopt()` 函式的選項變更範圍屬於 socket 層次 (該參數也可設定為 TCP/IP 層次)。當伺服器關閉後兩分鐘內在同一個連接埠和 IP 位址重啟,會得到 "address already in use" 錯誤,因此第三個參數 `SO_REUSEADDR` 則可以解決此問題。
> [Linux 核心網路:第一章 Above protocol stack (socket layer)](/FxrqqncFQ7OlYfaPBrsWnQ)

每一個 TCP 連線,以這五個資料區隔每個連線:
* protocol
* source address
* source port
* destination address
* destination port
每個系統呼叫分別設定了: `socket()` 設定 protocol,`bind()` 設定 src addr, src port。`connect()` 設定 dest addr, dest port。
若要設定伺服器端的 socket,必須經過 `socket()`,`bind()`,`listen()`,然後才能使用 `accept()` 系統呼叫開始等待連線請求。
若要設定客戶端的 socket,則必須經過 `socket()`,`connect()`。
#### accept(2) v.s kernel_accept()
使用者空間的 `accept` 系統呼叫的作用與核心空間的 `kernel_accept` 函式有幾處不同:
* 參數:
觀察第二個參數
`accept` 系統呼叫為 `struct sockaddr *addr`,當連線成功後 `addr` 會被設定成客戶的 socket 地址。
`kernel_accept` 為 `struct socket **newsock`,當連線成功後,會建立一個新的 connected socket 並由 `newsock` 指向它。
* 返回值:
`accept` 系統呼叫的返回值為 `connfd`,`connfd` 描述的是 `accept` 系統呼叫建立的一個新連線的 socket。
`kernel_accept` 返回值為 0 或 error。
此外後續操作對象也不同,對於 `accept` 系統呼叫,使用者空間裡操作對象為其返回值 `connfd`,如果要讀取客戶的請求,便 `read` 這個 `connfd`。而 `kernel_accept` 並不返回 fd,在核心空間裡直接操作的對象是傳入的參數,連線中的 socket。
由於 `accept` 具阻塞的性質,因此在呼叫 `accept` 前會使用 `select()`, `poll`, `epoll()` 等 Non blocking I/O,確保有客戶試圖連線後再進行 `accept`,但是,此時要注意有可能在 `accept` 被呼叫前,發生意外導致網路連線取消,如果此時試圖 `accept` 會阻塞直到下一次客戶連線前,因此設定 `accept` 為 `O_NONBLOCK` 可解決此意外。
#### worker
`handle_cli` 的 `kernel_recvmsg` 用來接收請求內容,然後 `parse_http` 會解析內容,`kernel_sendmsg` 用來向客戶端發送回應
### 接受連線請求的流程
當有客戶端進行連線請求,`kernel_accept` 便會依上述提及的流程處理連線請求,將新建立的 connected socket 儲存在 `cs`:
```c
struct socket *cs = NULL;
kernel_accept(ctx0->listen_sock, &cs, O_NONBLOCK);
```
接著將 `cs` 放入對應的 worker 的 ring buffer,尋找對應的 worker 依循 round-robin 規則。因為 `thread_ctx` 彼此以陣列擺放,因此 round robin 的實作便是遞增 listener 的 `thread_ctx` 指標值並取餘數:
```c
static struct thread_ctx threads[MAX_THREADS];
struct thread_ctx *dst = &ctx0[(rr++ % (nthreads - 1)) + 1];
if (ring_enqueue(&dst->ring, cs))
sock_release(cs); /* queue full */
```
假如該 worker 的 ring buffer 已滿,就關閉剛剛建立的新 connected socket,等於此次客戶端的連線請求失敗。
```
CPU0 CPU1 CPU2 CPU3
[kweb/1] [kweb/2] [kweb/3] [kweb/accept] (listener) 總機
分機 +
ring ring ring |
socket : ring_enqueue (producer: socket)
|
+<--- HTTP client
+<--- HTTP client
+<--- HTTP client
+<--- HTTP client
+<--- HTTP client
+<--- HTTP client
```
#### recv/send v.s. read/write
當 `recv` 的旗標參數設定為 0 時,其行為基本上等同於 `read`,不過 `recv` 專門用來接收 socket 傳送過來的資料。同理 `send`。
> 見 [recv(2)](https://man7.org/linux/man-pages/man2/recv.2.html) 與 [send(2)](https://man7.org/linux/man-pages/man2/send.2.html)
這裡則將 `recv`/`send` 旗標設定為 `MSG_DONTWAIT`,因此作用與 `read` 不同,它使其不會阻塞程式進行,效果與 `fcntl(2)` 設定 `O_NONBLOCK` 相同。
除此之外,若 socket 為 [`NON_BLOCK`](https://man7.org/linux/man-pages/man2/socket.2.html),也會使 `recv` 不會阻塞,但差別是 socket 的 `NON_BLOCK` 屬於 socket 層面,`MSG_DONTWAIT` 屬於每次 `recv`/`send` 呼叫的層面。
順帶一題,因為 socket 結構體中也屬於 `struct file`,雖專門用 `recv`, `send`,但 read/write 也能適用。
#### 適度的休息
接受連線請求採用事件驅動。在 listener 中,以迴圈包覆 `vfs_poll` 這個非阻塞函式,如果未接收到連線時就會把 listener 執行緒設定為可中斷睡眠,讓出 CPU 資源。
```c
while (!kthread_should_stop()) {
if (!(vfs_poll(ctx0->listen_file, NULL) & POLLIN)) {
schedule_timeout_interruptible(msecs_to_jiffies(10));
continue;
}
```
在 worker 中也有相似做法,如果 worker 輪詢每個監控的 socket,但都沒有請求,則 `active` 維持否,然後設定為可中斷睡眠,那假如有客戶端發送請求讓 `active` 為是,處理完之後同樣自願排程讓出 CPU 資源,避免 Softlockup:
```c
bool active = false;
for (int i = ctx->nr_cli - 1; i >= 0; i--) {
int mask = vfs_poll(ctx->cli[i].file, &ctx->pwq.pt);
if (mask & (POLLERR | POLLHUP | POLLNVAL)) {
drop_cli(ctx, i);
continue;
}
if (mask & POLLIN) {
if (handle_cli(&ctx->cli[i], ctx->rbuf))
drop_cli(ctx, i);
active = true;
}
}
if (!active) {
set_current_state(TASK_INTERRUPTIBLE);
if (!kthread_should_stop())
schedule_timeout(msecs_to_jiffies(IDLE_TIMEOUT_MS));
__set_current_state(TASK_RUNNING);
}
cond_resched();
```
### 短時間處理大量請求
對於伺服器而言,能否及時回應外界的連線請求,是衡量伺服器服務品質的指標。在此伺服器接受請求的方法為事件驅動處理
當 `accept` 後建立一個與客戶端連線的 socket,若要接收來自客戶端的封包,必須呼叫 `recv` 系列函式,但問題是 `recv` 和 `accept` 一樣都是 blocking 函式,因此在客戶端封包抵達前,`recv` 會一直阻塞程式進行。
因此依靠 `vfs_poll` 非阻塞式 I/O 監控是否有請求。
#### 效能瓶頸
第一個瓶頸是 listener [poll](https://man7.org/linux/man-pages/man2/poll.2.html) 的效率,就是總機轉接電話的速度,影響回應客戶端連線請求的速度。
在這裡使用的是 `vfs_poll` 其一次只能查詢一個檔案的讀寫狀態,`vfs_poll` 是一個封裝過後的 helper 函式,它檢查該 file 是否支援 poll 操作,若有則將 poll 操作轉由 file 各自定義的實作執行。
使用者空間的 `select()` 和 `poll()` 等的 I/O multiplexing 底層實作就是藉由 `vfs_poll` 一次監控一個檔案,然後不斷的迴圈走訪。
:::danger
務必依循課程規範的術語,見: https://hackmd.io/@sysprog/it-vocabulary
:::
```c
static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt)
{
if (unlikely(!file->f_op->poll))
return DEFAULT_POLLMASK;
return file->f_op->poll(file, pt);
}
```
第二個瓶頸是 worker 監控多個客戶端封包請求,因為 poll 需要線性搜索每一個客戶端是否發送封包,因此當 worker 監控的客戶端數量高時,線性搜索的成本成為瓶頸。
而 `vfs_poll` 呼叫底層的 socket poll `tcp_poll`。
第三個瓶頸,對於 worker 而言,處理客戶端封包會呼叫 `handle_cli`。假如連線數量非常多,ring buffer 一下就被塞滿,但 worker 在執行 `handle_cli` 的時候,造成無法繼續處理連線請求,多餘的就被捨棄變成連線失敗。參考 [事件驅動伺服器:原理和實例](/SjHcc4IbS6iKQ2Lbceyy3A)
可實驗驗證上述猜測,若建立一個 thread pool 給 `handle_cli` 能否解決。
### vfs_poll
在 worker 與 listener 的 vfs_poll 在參數傳遞有所不同:
```c
// listener
vfs_poll(ctx0->listen_file, NULL)
// worker
vfs_poll(ctx->cli[i].file, &ctx->pwq.pt)
```
### 生產者與消費者問題
在這個程式中,涉及生產者與消費者問題的是傳輸連線的 socket,當 listener `accept` 後建立連線的 socket,這個 socket 需要由 worker 來監控客戶端傳輸過來的封包,因此 listener 需要把 socket 的位址交給 worker。
```c
static struct thread_ctx threads[MAX_THREADS];
```
每一個 thread 皆有一個 `thread_ctx` 結構體,其中包含一個 ring buffer,對於每一個 worker thread 而言,這個各自擁有的 ring buffer 存放 listener 建立的連線 socket:
```c
struct socket *cs = NULL;
int err = kernel_accept(ctx0->listen_sock, &cs, O_NONBLOCK);
struct thread_ctx *dst = &ctx0[(rr++ % (nthreads - 1)) + 1];
if (ring_enqueue(&dst->ring, cs))
sock_release(cs); /* queue full */
```
因此在這過程中, listener 扮演單一生產者的角色,而每一個 worker 為單一消費者。為 SPSC 模式。
那考慮另一種實作,若所有的 worker 共用同一個 ring buffer,那就是 SPMC(單一生產者,多個消費者)模式。此情況下需要考慮多個 worker 對共享的 ring buffer 的存取,要做同步處理,比如使用 mutex lock、或是 lock-free 的 atomic CAS。
https://github.com/sysprog21/concurrent-programs?tab=readme-ov-file
`struct file` 在核心空間與使用者空間的檔案描述子不同,使用者空間的檔案描述子會將檔案封裝,並且幫助行程管理檔案。但核心空間沒有這些機制。
socket 可否做 lseek
VFS 如何更新 open file table?
## TODO: 第 8 週測驗題
> [測驗題](https://hackmd.io/@sysprog/linux2025-quiz8)和延伸問題
與第 11 周測驗題的 kweb 有多處實作上的差異不同。
1. 可用 sysfs 讀寫來控制 server 的啟動與關閉。
2. 使用 workqueue 機制,每一個 worker 代表著正處理一個客戶端連線或封包
3. 連線的生命週期,每一次回傳封包給客戶端後,該連線 socket 就釋放
因此相比第 11 周測驗題的 kweb,並行性較差
## 核心伺服器模組的運作流程
首先是核心模組的初始化 `kweb_init`,其中一步為建立 network namespace subsystem,network namespace 用來隔離網路資源,讓 network namespace 中的行程以為自己獨享系統資源與網路設施如 network devices, IPv4 and IPv6 protocol stacks, IP routing tables, firewall rules:
```c
ret = register_pernet_subsys(&netns_subsys_ops);
```
`register_pernet_subsys` 的原始碼為:
```c
/**
* register_pernet_subsys - register a network namespace subsystem
* @ops: pernet operations structure for the subsystem
*
* Register a subsystem which has init and exit functions
* that are called when network namespaces are created and
* destroyed respectively.
*
* When registered all network namespace init functions are
* called for every existing network namespace. Allowing kernel
* modules to have a race free view of the set of network namespaces.
*
* When a new network namespace is created all of the init
* methods are called in the order in which they were registered.
*
* When a network namespace is destroyed all of the exit methods
* are called in the reverse of the order with which they were
* registered.
*/
int register_pernet_subsys(struct pernet_operations *ops)
{
int error;
down_write(&pernet_ops_rwsem);
error = register_pernet_operations(first_device, ops);
up_write(&pernet_ops_rwsem);
return error;
}
```
其中 `down_write` 與 `up_write` 為 semaphore 讀寫鎖,因為涉及一個全域的 pernet operations 鏈結串列操作,`register_pernet_operations` 用處是加入定義的 `pernet_operations`。
`kweb` 所定義的 `pernet_operations` 如下:
```c
static int netns_subsys_setup(struct net *net)
{
struct pernet_server_net *pernet = net_generic(net, netns_subsys_id);
struct server_data *data;
data = pernet_data_alloc(net);
...
pernet->data = data;
return 0;
}
static void netns_subsys_destroy(struct net *net)
{
struct pernet_server_net *pernet = net_generic(net, netns_subsys_id);
struct server_data *data = pernet->data;
/* Stop the server if it's running. */
server_stop(pernet);
/* Remove sysfs kobject. */
kobject_del(&data->kobject);
kobject_put(&data->kobject);
}
static struct pernet_operations netns_subsys_ops = {
.init = netns_subsys_setup,
.exit = netns_subsys_destroy,
.id = &netns_subsys_id,
.size = sizeof(struct pernet_server_net),
};
```
socket 建立經過的流程:
```
static struct kobj_attribute status_attribute
↓
status_store
↓
server_init
↓
__sock_create(net, AF_INET, SOCK_STREAM, IPPROTO_TCP,
&pernet->server_sock, 1);
↓
bind
↓
listen
```
### kobject
kobject 一系列 api 函式定義於 [lib/kobject.c](https://elixir.bootlin.com/linux/v6.15.6/source/lib/kobject.c)
https://docs.kernel.org/filesystems/sysfs.html
https://docs.kernel.org/core-api/kobject.html
sysfs 介面使得使用者層級可以操作核心的數據
https://www.kernel.org/doc/ols/2005/ols2005v1-pages-321-334.pdf?utm_source=chatgpt.com
sysfs 的前身是 ddfs (Device Driver Filesystem),用來 debug 新的 driver model,這 debug 的功能更之前使用 procfs 輸出 device tree,但後來 Linus Torvalds 認為這樣的結構使得 procfs 過於雜亂,於是新建立一個基於記憶體的檔案系統,才有 sysfs 誕生。
### workqueue
workqueue 內的單位是 work item,可被排程
為什麼需要 workqueue,要解決的問題就是 top half, bottom half 議題。
## TODO: 第 6 週測驗題
> [測驗題 `1`](https://hackmd.io/@sysprog/linux2025-quiz6) 和延伸問題
閱讀 [Beej's Guide to Network Programming](https://www.beej.us/guide/bgnet/html/split/system-calls-or-bust.html#system-calls-or-bust) 第五章提及 `getaddrinfo` 函式:
```c
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *restrict node,
const char *restrict service,
const struct addrinfo *restrict hints,
struct addrinfo **restrict res);
```
其中 node 是網路域名 (或 IP 地址),service 是 port 號 (或從 `/etc/services` 裡註冊的服務如 telnet, http, ftp),hints 是要符合的條件。
`getaddrinfo` 的作用是返回一系列網路服務的 IP 位址,其域名、埠號、`addrinfo` 皆符合給定的參數條件,使用目的是增加使用者空間的伺服器更具維護性、可搬移性。使用 `getaddrinfo` 避免手動初始化 `struct sockaddr_in` (若要支援 IPv6 則又要初始化 `struct sockaddr_in6`)。`getaddrinfo` 會自行把給定的 IP 位址、埠號填入最後一個參數指向的 `struct addrinfo`,其後建立 socket 時就能直接使用此 `struct addrinfo` 的資料,因此 `getaddrinfo` 的用處是讓網路程式獨立於特定的 IP 協定。
以下範例是建立一個伺服器 socket 時使用 `getaddrinfo`。
```c
struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, "3490", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// bind it to the port we passed in to getaddrinfo():
bind(sockfd, res->ai_addr, res->ai_addrlen);
```
`hints` 僅允許設定 `ai_family`,`ai_socktype`,`ai_protocol`,`ai_flags`,其中設定 `ai_flags` 為 `AI_PASSIVE`,`getaddrinfo` 第一個參數設定為 `NULL`,這樣 `res` 的 `ai_addr` 就會是 wildcard 位址,告訴核心這個伺服器會接受這個宿主所有 IP 位址的請求,作用跟手動設定 `INADDR_ANY` 一樣。
假如不使用 `getaddrinfo` 要達到同樣目的,程式碼會是:
```c
// !!! THIS IS THE OLD WAY !!!
int sockfd;
struct sockaddr_in my_addr;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT); // short, network byte order
my_addr.sin_addr.s_addr = INADDR_ANY;
memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr);
```
以下另一個範例是客戶端要建立一個連線到伺服器的 socket,使用 `getaddrinfo`,其作用是返回一系列適合與伺服器連線的 `struct addrinfo`,因此接下來就是一一走訪每一個 `struct addrinfo` 直到可以成功建立連線。可以發現程式碼過程沒有涉及任何 IP 協定的 hardcode (比如 IPv4 和 IPv6),讓這份程式會更有乾淨可攜:
```c
int open_clientfd(char *hostname, char *port) {
int clientfd, rc;
struct addrinfo hints, *listp, *p;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Open a connection */
hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. */
hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections */
if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
return -2;
}
/* Walk the list for one that we can successfully connect to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Connect to the server */
if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break; /* Success */
if (close(clientfd) < 0) { /* Connect failed, try another */ //line:netp:openclientfd:closefd
fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
return -1;
}
}
/* Clean up */
freeaddrinfo(listp);
if (!p) /* All connects failed */
return -1;
else /* The last connect succeeded */
return clientfd;
}
```
以下則是使用 `getaddrinfo` 建立 listening socket:
```c
int open_listenfd(char *port)
{
struct addrinfo hints, *listp, *p;
int listenfd, rc, optval=1;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Accept connections */
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
return -2;
}
/* Walk the list for one that we can bind to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Eliminates "Address already in use" error from bind */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, //line:netp:csapp:setsockopt
(const void *)&optval , sizeof(int));
/* Bind the descriptor to the address */
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break; /* Success */
if (close(listenfd) < 0) { /* Bind failed, try the next */
fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
return -1;
}
}
/* Clean up */
freeaddrinfo(listp);
if (!p) /* No address worked */
return -1;
/* Make it a listening socket ready to accept connection requests */
if (listen(listenfd, LISTENQ) < 0) {
close(listenfd);
return -1;
}
return listenfd;
}
```
[showip.c](https://gist.github.com/Max042004/f416f2d55e4a122e85ec9d7eac651482) 為使用 `getaddrinfo` 的簡單示範程式碼:
```bash
$./showip wiki.csie.ncku.edu.tw
IP addresses for wiki.csie.ncku.edu.tw:
IPv4: 172.237.11.151
$./showip www.youtube.com
IP addresses for www.youtube.com:
IPv6: 2404:6800:4012:6::200e
IPv6: 2404:6800:4012:7::200e
IPv6: 2404:6800:4012:8::200e
IPv6: 2404:6800:4012:5::200e
IPv4: 142.250.198.78
IPv4: 142.250.66.78
IPv4: 142.250.196.206
IPv4: 142.250.77.14
IPv4: 142.250.204.46
```
`getaddrinfo` 最後一個參數返回會指向一個儲存 `addrinfo` 的鏈結串列,假如一個域名可能有多個不同位址的宿主,如 `www.youtube.com` ,那麼可以走訪這個鏈結串列得到每一個宿主的 IP 位址。
若使用 `tracerounte`,碰到如 `www.youtube.com` 時,只會追溯其中一個 IP 位址途經的連線位址。
```bash
traceroute www.youtube.com
traceroute to www.youtube.com (2404:6800:4012:5::200e), 30 hops max, 80 byte packets
1 * * *
2 2001-b000-00e3-0005-0023-2252-0002-0002.hinet-ip6.hinet.net (2001:b000:e3:5:23:2252:2:2) 5.109 ms 5.209 ms 5.331 ms
3 * * *
4 2001-b000-0088-0003-0088-00e3-0018-000a.hinet-ip6.hinet.net (2001:b000:88:3:88:e3:18:a) 8.535 ms * *
5 2001-b000-0088-0004-3032-3335-0001-000b.hinet-ip6.hinet.net (2001:b000:88:4:3032:3335:1:b) 9.125 ms 2001-b000-0088-0004-3032-3336-0003-000b.hinet-ip6.hinet.net (2001:b000:88:4:3032:3336:3:b) 9.220 ms 2001-b000-0088-0004-3031-3335-0001-000b.hinet-ip6.hinet.net (2001:b000:88:4:3031:3335:1:b) 9.137 ms
6 2001:4860:1:1::7b8 (2001:4860:1:1::7b8) 10.595 ms 9.367 ms 2001:4860:1:1::402 (2001:4860:1:1::402) 9.317 ms
7 2404:6800:8361:80::1 (2404:6800:8361:80::1) 8.844 ms * *
8 * * *
9 nctsaa-ab-in-x0e.1e100.net (2404:6800:4012:5::200e) 8.578 ms 2001:4860:0:1::83d8 (2001:4860:0:1::83d8) 9.010 ms *
```
## TODO: 第 10 週測驗題
> [測驗題 `1`](https://hackmd.io/@sysprog/linux2025-quiz10) 和延伸問題