Try   HackMD

Linux 核心網路:第一章 Above protocol stack (socket layer)

作者:吳恩緯 (enweiwu@FreeBSD.org)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
聲明

  1. 這文是探討 Linux 核心網路子系統的系列文章之一,之後陸續會有多個章節探討 socket layer 以下的協定層。
  2. 勘誤與討論,請在 Facebook「系統軟體系列課程」討論。
  3. 本文探討 Linux 核心網路子系統,而非應用程式的網路程式設計。

BSD socket

Berkeley socket 源於 4.2BSD,是核心開放給使用者層級的程式開發介面 (API),允許後者藉由 socket 間接存取到核心的網路及行程間通訊的協定。後來 POSIX 將 Berkeley socket 予以標準化,部分程式開發介面已變更,Linux 核心和應用程式主要採納 POSIX 規範的 socket 介面,但也有 BSD 實作的身影。

依據 Dictionary.comsocket 一詞的解釋如下:

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

此函式的參數有:

  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: 指定網路協定。大多數的情況domaintype 給定後核心的 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》是這個領域的經典書籍。

Layering

下圖展示 socket layer 作為 socket API 以及核心網路層之間的橋樑:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

既然存在好幾種 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, 我們可以發現兩件事:

  1. sin_port 以及 s_addr 的型別分別為 __be16 以及 __be32,這是因為 network order 為 big endian。__be16__be32 的定義在 include/uapi/linux/types.h
  2. 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

Linux 核心中的 socket

socket 的表示在 Linux 核心中分為以下二個結構體:

  • struct socket: BSD socket,銜接使用者層面的抽象結構,較為靠近使用者空間。
  • struct sock: INET socket,保存網路連線的資訊

INET 與 TCP/IP 為同義詞,可見 struct sock 與核心網路層更為靠近。

讀到這邊讀者一定會分不清楚這兩個結構體到底差在哪裡, 讓我們細部解釋他們:

struct socket 結構體

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

  1. 系統呼叫層
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
    return __sys_bind(fd, umyaddr, addrlen);
}
  1. socket layer
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;
}
  1. TCP/IP stack
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
    /* ... */
}

struct socket 相關操作

struct socket 的相關操作通常都是 sock_ 開頭。

建立 socket

建立 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 會指派到 ressock_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

以下函式負責傳送或接收 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

struct sock 代表一個 INET socket (AF_INET, AF_INET6)。其相較於 struct socket 更接近網路協定層。其他的協定如 AF_UNIXstruct unix_sock, AF_NETLINKstruct 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 socketsock_)。

傳送/接收之系統呼叫 workflow

以下是使用者層級呼叫 send()/sendto(), recv()/recvfrom() 後進入核心, 呼叫的函式順序。這邊只列出使用者層級到協定層上端的的函式,協定層之下則待後續章節解說。

參考文獻