作者:吳恩緯 (enweiwu@FreeBSD.org)
Berkeley socket 源於 4.2BSD,是核心開放給使用者層級的程式開發介面 (API),允許後者藉由 socket 間接存取到核心的網路及行程間通訊的協定。後來 POSIX 將 Berkeley socket 予以標準化,部分程式開發介面已變更,Linux 核心和應用程式主要採納 POSIX 規範的 socket 介面,但也有 BSD 實作的身影。
依據 Dictionary.com,socket 一詞的解釋如下:
a hollow part or piece for receiving and holding some part or thing.
漢語的「插座」是 socket 一詞在電氣領域的特化用語,但不代表 socket 就只翻譯為「插座」 —— socket 原本在英語就有多個意思,例如 eye socket 指眼眶,後者是顱骨的一個體腔,眼球就位於眼眶中。無論是解剖學還是在電器領域,socket 都有連接後,得以存取某種資源和支撐某個部分的寓意。由於「插座」在漢語已是特化用語,我們就不以「插座」來稱呼電腦網路領域的 socket。
要注意的是, socket API 支援的底層網路及行程間通訊協定不僅限於 TCP/IP, 還有諸多如 Unix domain socket, netlink 等等。詳細的協定總類請參閱 socket(2)。
我們常常聽到開發者會說「產生一個 socket」或「一個 TCP socket」。相信很多讀者會有困惑,socket 不是程式開發介面 (API) 嗎, 怎麼會用量詞「一個」呢?是的,socket 其實還隱含另一個意思,也就是使用者呼叫 socket API 的 socket()
後得到的 file descriptor,我們把它稱之為 endpoint。socket()
的函式宣告如下:
此函式的參數有:
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 之間的通訊type
: socket 的形式,其根據指定的 domain
而有不同的語意。常見的有:
SOCK_STREAM
: 提供 connection-oriented, reliable 的 byte streams。SOCK_DGRAM
: connectionless, unreliable 的通道。SOCK_RAW
: bypass 核心網路層,於用戶空間實作網路層。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
巨集展開):
相關函式將在後續做詳細探討。
以下章節主要探討的是 Linux 核心層級網路子系統的實作, 而非使用者層級的 socket 程式設計。若對 socket 程式設計不熟悉, 建議先去閱讀 Beej's Guide to Network Programming. Using Internet Sockets 或者相關材料。《UNIX Network Programming》是這個領域的經典書籍。
下圖展示 socket layer 作為 socket API 以及核心網路層之間的橋樑:
為了與達成與目標行程或主機之間的通訊,一個 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)。
既然存在好幾種 socket address,則為了統一傳入 socket API 函式的參數,需要一個結構體來表示所有的 socket address,這個結構體為 struct sockaddr
:
並觀察 struct sockaddr_in
的定義:
應用程式通常在操作完特定的 socket address 後會將其強制轉型為 sockaddr *
,作為 socket API 的參數傳入:
我們可以以物件導向的角度思考轉型這件事,則 struct sockaddr
即為父類別,struct sockaddr_in
以及其他種 socket address 即為子類別。物件導向的概念在大量出現在網路程式以及 Linux 核心,並且是以「C 語言」實現,因為這邊說的物件導向是「概念」而不是「語法」,可參照〈你所不知道的 C 語言:物件導向程式設計篇〉。
回到 struct sockaddr_in
, 我們可以發現兩件事:
sin_port
以及 s_addr
的型別分別為 __be16
以及 __be32
,這是因為 network order 為 big endian。__be16
及 __be32
的定義在 include/uapi/linux/types.h。struct sockaddr
的大小 (16 bytes) 足以容納 struct sockaddr_in
。讓我們再看看 IPv6 (AF_INET6
) 的 socket address struct sockaddr_in6
:
我們可以發現 struct sockaddr_in6
的大小是 28 bytes,遠大於 struct sockaddr
的 16 bytes。
實際上 struct sockaddr
是個錯誤,因為該結構體的空間不足以容納多數的 socket address。於是更大的結構體被提出:
但可惜的是,由於歷史因素,許多舊的程式以及 socket API 仍然使用 struct sockaddr
。
socket 的表示在 Linux 核心中分為以下二個結構體:
struct socket
: BSD socket,銜接使用者層面的抽象結構,較為靠近使用者空間。struct sock
: INET socket,保存網路連線的資訊INET 與 TCP/IP 為同義詞,可見 struct sock
與核心網路層更為靠近。
讀到這邊讀者一定會分不清楚這兩個結構體到底差在哪裡, 讓我們細部解釋他們:
struct socket
代表一個 BSD socket,其提供的操作通常與系統呼叫有關 (通常一個系統呼叫完會緊接著此結構體的操作)。以下會做更詳細的說明。
可以看到裡面內嵌指向 struct sock
的指標,事實上 struct sock
裡頭一樣內嵌指向 struct socket
的指標。並且,我們可以看到型別為 struct file
的成員,其為 socket()
回傳的 file descriptor 指向的 open file structure。
觀察成員 ops
,其儲存了很多 protocol-specific (INET, netlink 等) 的操作:
struct proto_ops
中的操作與核心提供的 socket API 名稱非常相似。以下是 INET socket 對 struct proto_ops
的定義 (net/ipv4/af_inet.c):
事實上,這些操作會緊跟隨與其同名的 socket API 後被呼叫,如 bind()
:
struct socket
的相關操作通常都是 sock_
開頭。
建立 struct socket
通常會緊跟隨使用者層級呼叫 socket()
。事實上,並非只有應用程式可建立 socket endpoint,核心層級也可以建立,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()
函式:
以 AF_INET
為例,即 inet_create()
。
以下函式負責傳送或接收 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
的形式存在。在探討他之前,我們可以先用以下圖理解資料在不同層級時的存在形式:
如上圖所示,每個層級對資料的封裝不同。圖中的 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
的定義如下:
與 struct iovec
的差別僅在於 iov_base
是核心空間的地址,而不是用戶空間的地址。
struct sock
代表一個 INET socket (AF_INET
, AF_INET6
)。其相較於 struct socket
更接近網路協定層。其他的協定如 AF_UNIX
有 struct unix_sock
, AF_NETLINK
有 struct netlink_sock
。
以下是 struct sock
的主要成員:
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()
:
struct sock
相關操作的函式名稱以 sk_
開頭 (不同於 struct socket
的 sock_
)。
以下是使用者層級呼叫 send()
/sendto()
, recv()
/recvfrom()
後進入核心, 呼叫的函式順序。這邊只列出使用者層級到協定層上端的的函式,協定層之下則待後續章節解說。