作者:吳恩緯 (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()
的函式宣告如下:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
此函式的參數有:
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
巨集展開):
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 或者相關材料。《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 {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
並觀察 struct sockaddr_in
的定義:
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'. */
};
struct in_addr {
__be32 s_addr;
};
應用程式通常在操作完特定的 socket address 後會將其強制轉型為 sockaddr *
,作為 socket API 的參數傳入:
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 語言:物件導向程式設計篇〉。
回到 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 {
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) */
};
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。於是更大的結構體被提出:
/* 128 bytes */
struct sockaddr_storage {
u16 ss_family;
char __data[126];
}
但可惜的是,由於歷史因素,許多舊的程式以及 socket API 仍然使用 struct sockaddr
。
socket 的表示在 Linux 核心中分為以下二個結構體:
struct socket
: BSD socket,銜接使用者層面的抽象結構,較為靠近使用者空間。struct sock
: INET socket,保存網路連線的資訊INET 與 TCP/IP 為同義詞,可見 struct sock
與核心網路層更為靠近。
讀到這邊讀者一定會分不清楚這兩個結構體到底差在哪裡, 讓我們細部解釋他們:
struct socket
代表一個 BSD socket,其提供的操作通常與系統呼叫有關 (通常一個系統呼叫完會緊接著此結構體的操作)。以下會做更詳細的說明。
/**
* 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 等) 的操作:
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):
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()
:
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
return __sys_bind(fd, umyaddr, addrlen);
}
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;
}
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
/* ... */
}
struct socket
的相關操作通常都是 sock_
開頭。
建立 struct socket
通常會緊跟隨使用者層級呼叫 socket()
。事實上,並非只有應用程式可建立 socket endpoint,核心層級也可以建立,khttp 是個很好的範例,其在核心層級實作一個 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()
函式:
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:
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 kvec {
void *iov_base; /* and that should *never* hold a userland pointer */
size_t iov_len;
};
與 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
的主要成員:
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()
:
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_
)。
以下是使用者層級呼叫 send()
/sendto()
, recv()
/recvfrom()
後進入核心, 呼叫的函式順序。這邊只列出使用者層級到協定層上端的的函式,協定層之下則待後續章節解說。