# Linux 核心網路:第一章 Above protocol stack (socket layer) > 作者:吳恩緯 (enweiwu@FreeBSD.org) :::warning :information_source: 聲明 1. 這文是探討 Linux 核心網路子系統的系列文章之一,之後陸續會有多個章節探討 socket layer 以下的協定層。 2. 勘誤與討論,請在 Facebook「[系統軟體系列課程](https://www.facebook.com/groups/system.software2023/)」討論。 3. 本文探討 Linux 核心網路子系統,而非應用程式的網路程式設計。 ::: ## BSD socket [Berkeley socket](https://en.wikipedia.org/wiki/Berkeley_sockets) 源於 4.2BSD,是核心開放給使用者層級的程式開發介面 (API),允許後者藉由 socket 間接存取到核心的網路及行程間通訊的協定。後來 POSIX 將 Berkeley socket 予以標準化,部分程式開發介面已變更,Linux 核心和應用程式主要採納 POSIX 規範的 socket 介面,但也有 BSD 實作的身影。 依據 Dictionary.com,[socket](https://www.dictionary.com/browse/socket) 一詞的解釋如下: > a hollow part or piece for receiving and holding some part or thing. 漢語的「插座」是 [socket](https://www.dictionary.com/browse/socket) 一詞在電氣領域的特化用語,但不代表 socket 就只翻譯為「插座」 —— socket 原本在英語就有多個意思,例如 [eye socket](https://en.wikipedia.org/wiki/Orbit_(anatomy)) 指眼眶,後者是顱骨的一個體腔,眼球就位於眼眶中。無論是解剖學還是在電器領域,socket 都有連接後,得以存取某種資源和支撐某個部分的寓意。由於「插座」在漢語已是特化用語,我們就不以「插座」來稱呼電腦網路領域的 socket。 要注意的是, socket API 支援的底層網路及行程間通訊協定不僅限於 TCP/IP, 還有諸多如 Unix domain socket, netlink 等等。詳細的協定總類請參閱 [socket(2)](https://man7.org/linux/man-pages/man2/socket.2.html)。 我們常常聽到開發者會說「產生一個 socket」或「一個 TCP socket」。相信很多讀者會有困惑,socket 不是程式開發介面 (API) 嗎, 怎麼會用量詞「一個」呢?是的,socket 其實還隱含另一個意思,也就是使用者呼叫 socket API 的 `socket()` 後得到的 file descriptor,我們把它稱之為 endpoint。`socket()`的函式宣告如下: ```c #include <sys/socket.h> int socket(int domain, int type, int protocol); ``` 此函式的參數有: 1. `domain`: 使用的核心網路層,與 address family 以及 protocol family 為同義詞。在標頭檔中定義為 `AF_*` 或 `PF_*`。常見的有: - `AF_INET`: TCP/IP,網路層為 IPv4。 - `AF_INET6`: TCP/IP,網路層為 IPv6。 - `AF_UNIX`: Unix domain socket。 - `AF_NETLINK`: netlink socket, 用於 user space 以及 kernel space 之間的通訊 2. `type`: socket 的形式,其根據指定的 `domain` 而有不同的語意。常見的有: - `SOCK_STREAM`: 提供 connection-oriented, reliable 的 byte streams。 - `SOCK_DGRAM`: connectionless, unreliable 的通道。 - `SOCK_RAW`: bypass 核心網路層,於用戶空間實作網路層。 3. `protocol`: 指定網路協定。大多數的情況`domain` 及 `type` 給定後核心的 socket layer 便能決定特定的網路協定, 故通常填 `0`。 通常使用 socket API 的第一步就是呼叫 `socket()`, 回傳值為核心分配的 file descriptor (以下稱 `fd`), 在呼叫其他 socket API 或相關函式時將 `fd` 作為參數傳入。又因為一個 socket 在使用者的視角即是個 file descriptor, 因此可以作為其他帶有 file descriptor 參數的系統呼叫的參數,如 `read()`, `write()`, `ioctl()`, `fcntl()`, `select()` 等等。 Socket 介面提供了一系列的程式開發介面 (API) 給使用者, 常見的有:`socket()`, `bind()`, `listen()`, `accept()`, `send()`, `sendto()`, `recv()`, `recvfrom()`, `getsockopt()`, `setsockopt()` 等等。 這些 socket API 大多數為系統呼叫,核心的系統呼叫層有對應的函示負責處理,如 `socket()` 在系統呼叫層的名稱為 `sys_socket()` (由 `SYSCALL_DEFINE3` 巨集展開): ```c SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { return __sys_socket(family, type, protocol); } ``` 相關函式將在後續做詳細探討。 以下章節主要探討的是 Linux 核心層級網路子系統的實作, 而非使用者層級的 socket 程式設計。若對 socket 程式設計不熟悉, 建議先去閱讀 [Beej's Guide to Network Programming. Using Internet Sockets](https://www.beej.us/guide/bgnet/) 或者相關材料。《[UNIX Network Programming](https://en.wikipedia.org/wiki/UNIX_Network_Programming)》是這個領域的經典書籍。 ## Layering 下圖展示 socket layer 作為 socket API 以及核心網路層之間的橋樑: ![](https://hackmd.io/_uploads/HJbgBiWr3.png) ### Socket address 為了與達成與目標行程或主機之間的通訊,一個 socket endpoint 需要指定一個 socket address。不同的 address family 的 socket address 所隱含的意義及成員不同,例如 `AF_INET` 的 socket address `struct sockaddr_in` 包含了 IP address 以及 port number,而 `AF_NETLINK` 的 socket address `struct sockaddr_nl` 則包含 pid 以及 multicast groups mask (本章節只討論 TCP/IP 核心網路層,若對 netlink 有興趣可以參閱 [Linux kernel document: Introduction to Netlink](https://www.kernel.org/doc/html/latest/userspace-api/netlink/intro.html))。 既然存在好幾種 socket address,則為了統一傳入 socket API 函式的參數,需要一個結構體來表示所有的 socket address,這個結構體為 `struct sockaddr`: ```c struct sockaddr { unsigned short sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ }; ``` 並觀察 `struct sockaddr_in` 的定義: ```c struct sockaddr_in { unsigned short sin_family; /* Address family */ __be16 sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ unsigned char __pad[8]; /* Pad to size of `struct sockaddr'. */ }; ``` ```c struct in_addr { __be32 s_addr; }; ``` 應用程式通常在操作完特定的 socket address 後會將其強制轉型為 `sockaddr *`,作為 socket API 的參數傳入: ```c struct sockaddr_in serv; /* Operate on struct sockaddr_in * ... */ sendto(fd, buf, BUFSIZE, 0, (struct sockaddr *) &serv, sizeof(serv)); ``` 我們可以以物件導向的角度思考轉型這件事,則 `struct sockaddr` 即為父類別,`struct sockaddr_in` 以及其他種 socket address 即為子類別。物件導向的概念在大量出現在網路程式以及 Linux 核心,並且是以「C 語言」實現,因為這邊說的物件導向是「概念」而不是「語法」,可參照〈[你所不知道的 C 語言:物件導向程式設計篇](https://hackmd.io/@sysprog/c-oop)〉。 回到 `struct sockaddr_in`, 我們可以發現兩件事: 1. `sin_port` 以及 `s_addr` 的型別分別為 `__be16` 以及 `__be32`,這是因為 network order 為 big endian。`__be16` 及 `__be32` 的定義在 [include/uapi/linux/types.h](https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/types.h)。 2. `struct sockaddr` 的大小 (16 bytes) 足以容納 `struct sockaddr_in`。 讓我們再看看 IPv6 (`AF_INET6`) 的 socket address `struct sockaddr_in6`: ```c struct sockaddr_in6 { unsigned short int sin6_family; /* AF_INET6 */ __be16 sin6_port; /* Transport layer port # */ __be32 sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ __u32 sin6_scope_id; /* scope id (new in RFC2553) */ }; ``` ```c struct in6_addr { union { uint8_t __u6_addr8[16]; uint16_t __u6_addr16[8]; uint32_t __u6_addr32[4]; } __in6_u; }; ``` 我們可以發現 `struct sockaddr_in6` 的大小是 28 bytes,遠大於 `struct sockaddr` 的 16 bytes。 實際上 `struct sockaddr` 是個錯誤,因為該結構體的空間不足以容納多數的 socket address。於是更大的結構體被提出: ```c /* 128 bytes */ struct sockaddr_storage { u16 ss_family; char __data[126]; } ``` 但可惜的是,由於歷史因素,許多舊的程式以及 socket API 仍然使用 `struct sockaddr`。 ## Linux 核心中的 socket socket 的表示在 Linux 核心中分為以下二個結構體: - `struct socket`: BSD socket,銜接使用者層面的抽象結構,較為靠近使用者空間。 - `struct sock`: INET socket,保存網路連線的資訊 INET 與 TCP/IP 為同義詞,可見 `struct sock` 與核心網路層更為靠近。 讀到這邊讀者一定會分不清楚這兩個結構體到底差在哪裡, 讓我們細部解釋他們: ## struct socket 結構體 `struct socket` 代表一個 BSD socket,其提供的操作通常與系統呼叫有關 (通常一個系統呼叫完會緊接著此結構體的操作)。以下會做更詳細的說明。 ```c /** * struct socket - general BSD socket * @state: socket state (%SS_CONNECTED, etc) * @type: socket type (%SOCK_STREAM, etc) * @flags: socket flags (%SOCK_NOSPACE, etc) * @ops: protocol specific socket operations * @file: File back pointer for gc * @sk: internal networking protocol agnostic socket representation * @wq: wait queue for several uses */ struct socket { socket_state state; short type; unsigned long flags; struct socket_wq __rcu *wq; struct file *file; struct sock *sk; const struct proto_ops *ops; }; ``` 可以看到裡面內嵌指向 `struct sock` 的指標,事實上 `struct sock` 裡頭一樣內嵌指向 `struct socket` 的指標。並且,我們可以看到型別為 `struct file` 的成員,其為 `socket()` 回傳的 file descriptor 指向的 open file structure。 觀察成員 `ops`,其儲存了很多 protocol-specific (INET, netlink 等) 的操作: ```c struct proto_ops { int family; struct module *owner; int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int (*listen) (struct socket *sock, int len); struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags, bool kern); int (*sendmsg) (struct socket *sock, struct msghdr *m, size_t total_len); int (*recvmsg) (struct socket *sock, struct msghdr *m, size_t total_len, int flags); #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0) int (*setsockopt)(struct socket *sock, int level, int optname, sockptr_t optval, unsigned int optlen); int (*getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen); #endif /* ... */ } ``` `struct proto_ops` 中的操作與核心提供的 socket API 名稱非常相似。以下是 INET socket 對 `struct proto_ops` 的定義 ([net/ipv4/af_inet.c](https://elixir.bootlin.com/linux/latest/source/net/ipv4/af_inet.c)): ```c const struct proto_ops inet_stream_ops = { .family = PF_INET, .owner = THIS_MODULE, .release = inet_release, .bind = inet_bind, .connect = inet_stream_connect, .accept = inet_accept, .poll = tcp_poll, .ioctl = inet_ioctl, .listen = inet_listen, .setsockopt = sock_common_setsockopt, .getsockopt = sock_common_getsockopt, .sendmsg = inet_sendmsg, .recvmsg = inet_recvmsg, /* ... */ }; ``` 事實上,這些操作會緊跟隨與其同名的 socket API 後被呼叫,如 `bind()`: 1. 系統呼叫層 ```c SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen) { return __sys_bind(fd, umyaddr, addrlen); } ``` 2. socket layer ```c int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen) { struct socket *sock; struct sockaddr_storage address; int err, fput_needed; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { /* ... */ err = sock->ops->bind(sock, (struct sockaddr *) &address, addrlen); } return err; } ``` 3. TCP/IP stack ```c int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len) { /* ... */ } ``` ## struct socket 相關操作 `struct socket` 的相關操作通常都是 `sock_` 開頭。 ### 建立 socket 建立 `struct socket` 通常會緊跟隨使用者層級呼叫 `socket()`。事實上,並非只有應用程式可建立 socket endpoint,核心層級也可以建立,[khttp](https://github.com/sysprog21/khttpd) 是個很好的範例,其在核心層級實作一個 HTTP 伺服器。 根據 `struct socket` 被建立於用戶層級或核心層級,socket layer 提供不同的函式: - `int sock_create(int family, int type, int protocol, struct socket **res)`: 在使用者層級呼叫 `socket()` 後建立 `struct socket`。 - `int sock_create_kern(struct net *net, int family, int type, int protocol, struct socket **res)`: 產生一個核心 socket。 - `int sock_create_lite(int family, int type, int protocol, struct socket **res)`: 無邊界檢查版本的 `sock_create_kern()`。 以上函式建立的 `struct socket` 會指派到 `res`。`sock_create_kern` 的參數 `net` 指向一個 network namespace,通常設定為全域變數 `init_net`。 `sock_create()` 會進一步呼叫與 address family 關聯的 `create()` 函式: ```c err = pf->create(net, sock, protocol, kern); /* pf is specific to protocol family */ if (err < 0) goto out_module_put; ``` 以 `AF_INET` 為例,即 `inet_create()`。 ### 傳送/接收 messages 以下函式負責傳送或接收 messages: - `int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags);` - `int kernel_recvmsg(struct socket *sock, struct msghdr *msg, struct kvec *vec, size_t num, size_t size, int flags);` - `int sock_sendmsg(struct socket *sock, struct msghdr *msg);` - `int kernel_sendmsg(struct socket *sock, struct msghdr *msg, struct kvec *vec, size_t num, size_t size);` 上述函式中的以 `kernel_` 開頭的用於核心 socket。這些函式會進一步呼叫 `sock->ops->sendmsg()` 或 `sock->ops->recvmsg()`。 以上函式傳送/接收的資料是以 `struct msghdr` 的形式存在。在探討他之前,我們可以先用以下圖理解資料在不同層級時的存在形式: ![](https://hackmd.io/_uploads/BkaC1QQr2.png) 如上圖所示,每個層級對資料的封裝不同。圖中的 `struct sk_buff` 是核心網路子系統中極其重要的結構,於下一個章節會詳細探討。 回到 `struct msghdr`,其重要的欄位有兩個: - `void *msg_name;`: socket address, UDP socket 的必要欄位。 - `struct iov_iter msg_iter`: 包含 `struct iovec` 的陣列。以 `send()` 或 `read()` 為例,陣列長度為 1; 而在呼叫 scatter-gether IO 系統呼叫時 (諸如 `writev()` 或 `readv()`),陣列長度為參數 `iovcnt`。 `struct kvec` 的定義如下: ```c struct kvec { void *iov_base; /* and that should *never* hold a userland pointer */ size_t iov_len; }; ``` 與 `struct iovec` 的差別僅在於 `iov_base` 是核心空間的地址,而不是用戶空間的地址。 ## struct sock `struct sock` 代表一個 INET socket (`AF_INET`, `AF_INET6`)。其相較於 `struct socket` 更接近網路協定層。其他的協定如 `AF_UNIX` 有 `struct unix_sock`, `AF_NETLINK` 有 `struct netlink_sock`。 以下是 `struct sock` 的主要成員: ```c struct sock { unsigned int sk_padding : 1, sk_no_check_tx : 1, sk_no_check_rx : 1, sk_userlocks : 4, sk_protocol : 8, sk_type : 16; struct socket *sk_socket; struct sk_buff *sk_send_head; // ... void (*sk_state_change)(struct sock *sk); void (*sk_data_ready)(struct sock *sk); void (*sk_write_space)(struct sock *sk); void (*sk_error_report)(struct sock *sk); int (*sk_backlog_rcv)(struct sock *sk, struct sk_buff *skb); void (*sk_destruct)(struct sock *sk); }; ``` `sk_protocol` 為 socket 使用的協定,`sk_type` 為 socket type (`SOCK_STREAM`, `SOCK_DGRAM`, etc.)。 `sk_socket` 為指向 `struct socket` 的 back pointer。在 Linux 核心中,我們常常可以看到兩個結構體互相內嵌在彼此的結構體。 `sk_send_head` 是用以傳送封包的 linked list 的 head,其節點的型別為 `struct sk_buff`,表示網路封包以及內部資訊,在之後的章節會詳細探討。 `struct sock` 的初始化以及 attach 到 BSD socket 發生於 `inet_create()`: ```c static int inet_create(struct net *net, struct socket *sock, int protocol, int kern) { sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern); // ... } ``` `struct sock` 相關操作的函式名稱以 `sk_` 開頭 (不同於 `struct socket` 的 `sock_`)。 ## 傳送/接收之系統呼叫 workflow 以下是使用者層級呼叫 `send()`/`sendto()`, `recv()`/`recvfrom()` 後進入核心, 呼叫的函式順序。這邊只列出使用者層級到協定層上端的的函式,協定層之下則待後續章節解說。 ![](https://i.imgur.com/N6CBWCF.png) ## 參考文獻 - TCP IP Illustrated Volume 2 The Implementation (Addison-Wesley 出版) - [Understanding Linux Network Internals](https://www.oreilly.com/library/view/understanding-linux-network/0596002556/) - [linux-kernel-labs-networking](https://linux-kernel-labs.github.io/refs/heads/master/labs/networking.html#sending-receiving-messages) - [linux-kernel-source](https://elixir.bootlin.com/linux/latest/source) - [linux-network-performance-parameters](https://github.com/leandromoreira/linux-network-performance-parameters)