# 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)