sysprog
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Help
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # 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 對照表: ![image](https://hackmd.io/_uploads/r1vNUmNNxg.png) 發現小寫英文字母剛好比大寫字母多 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) ![image](https://hackmd.io/_uploads/rksf7MZrgl.png =50%x) 每一個 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) 和延伸問題

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully