套接字socket 的地址族和型別、工作原理、建立過程

s1mba發表於2013-09-17

注:本分類下文章大多整理自《深入分析linux核心原始碼》一書,另有參考其他一些資料如《linux核心完全剖析》、《linux c 程式設計一站式學習》等,只是為了更好地理清系統程式設計和網路程式設計中的一些概念性問題,並沒有深入地閱讀分析原始碼,我也是草草翻過這本書,請有興趣的朋友自己參考相關資料。此書出版較早,分析的版本為2.4.16,故出現的一些概念可能跟最新版本核心不同。

此書已經開源,閱讀地址 http://www.kerneltravel.net


一、套接字socket

(一)、套接字在網路中的地位和作用


socket 在網路系統中的作用如下。
(1)socket 位於網路協議之上,遮蔽了不同網路協議之間的差異。
(2)socket 是網路程式設計的入口,它提供了大量的系統呼叫,構成了網路程式的主體。
(3)在Linux 系統中,socket 屬於檔案系統的一部分,網路通訊可以被看作是對檔案的讀取,使得我們對網路的控制和對檔案的控制一樣方便。

(二)、套接字介面的種類

Linux 支援多種套接字種類,不同的套接字種類稱為“地址族”,這是因為每種套接字種類擁有自己的通訊定址方法。Linux 所支援的套接字地址族見表12.3。Linux 將上述套接字地址族抽象為統一的 BSD 套接字介面,應用程式關心的只是 BSD 套接字介面,而 BSD 套接字由各地址族專有的軟體支援。一般而言,BSD 套接字可支援多種套接字型別,不同的套接字型別提供的服務不同,Linux 所支援的部分 BSD 套接字型別見表12.4,但表12.3 中的套接字地址族並不一定全部支援表12.4 中的這些套接字型別。



(三)、套接字的工作原理

INET 套接字就是支援 Internet 地址族的套接字,它位於TCP 之上,BSD 套接字之下,如圖12.8 所示,這裡也體現了Linux 網路模組分層的設計思想。

INET 和 BSD 套接字之間的介面通過 Internet 地址族套接字操作集實現,這些操作集實際是一組協議的操作例程,在include/linux/net.h 中定義為struct proto_ops:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 
struct proto_ops
{
    int family;
    int (*release) (struct socket *sock);
    int (*bind) (struct socket *sock, struct sockaddr *umyaddr,
                 int sockaddr_len);
    int (*connect) (struct socket *sock, struct sockaddr *uservaddr,
                    int sockaddr_len, int flags);
    int (*socketpair) (struct socket *sock1, struct socket *sock2);
    int (*accept) (struct socket *sock, struct socket *newsock,
                   int flags);
    int (*getname) (struct socket *sock, struct sockaddr *uaddr,
                    int *usockaddr_len, int peer);
    unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct
                          *wait);
    int (*ioctl) (struct socket *sock, unsigned int cmd,
                  unsigned long arg);
    int (*listen) (struct socket *sock, int len);
    int (*shutdown) (struct socket *sock, int flags);
    int (*setsockopt) (struct socket *sock, int level, int optname,
                       char *optval, int optlen);
    int (*getsockopt) (struct socket *sock, int level, int optname,
                       char *optval, int *optlen);
    int (*sendmsg) (struct socket *sock, struct msghdr *m, int total_len, struct
                    scm_cookie *scm);
    int (*recvmsg) (struct socket *sock, struct msghdr *m, int total_len, int flags,
                    struct scm_cookie *scm);
    int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct *vma);
    ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags);
};

這個操作集類似於檔案系統中的file_operations 結構。BSD 套接字層通過呼叫proto_ops 結構中的相應函式執行任務。BSD 套接字層向 INET 套接字層傳遞 socket 資料結構來代表一個 BSD 套接字,socket 結構在include/linux/net.h 中定義如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
 
struct socket
{
    socket_state state;
    unsigned long flags;
    struct proto_ops *ops;
    struct inode *inode;
    struct fasync_struct *fasync_list; /* Asynchronous wake up list */
    struct file *file; /* File back pointer for gc */
    struct sock *sk;
    wait_queue_head_t wait;
    short type;
    unsigned char passcred;
};

但在 INET 套接字層中,它利用自己的 sock 資料結構來代表該套接字,因此,這兩個結構之間存在著連結關係,sock 結構定義於include/net/sock.h。在 BSD 的 socket 資料結構中存在一個指向sock 的指標sk,而在sock 中又有一個指向socket 的指標,這兩個指標將 BSD socket 資料結構和sock 資料結構連結了起來。通過這種連結關係,套接字呼叫就可以方便地檢索到 sock 資料結構。實際上,sock 數據結構可適用於不同的協議,它也定義有自己的協議操作集proto_ops。在建立套接字時,sock資料結構的協議操作集指標指向所請求的協議操作集。如果請求 TCP,則 sock 資料結構的協議操作集指標將指向 TCP 的協議操作集。

BSD 套接字上的詳細操作與具體的底層地址族有關,底層地址族的不同實際意味著定址方式、採用的協議等的不同。Linux 利用 BSD 套接字層抽象了不同的套接字介面。在核心的初始化階段,內建於核心的不同地址族分別以 BSD 套接字介面在核心中註冊。然後,隨著應用程式建立並使用 BSD 套接字。核心負責在 BSD 套接字和底層的地址族之間建立聯絡。這種聯絡通過交叉連結資料結構以及地址族專有的支援例程表建立。在核心中, 地址族和協議資訊儲存在inet_protos 向量中, 其定義於include/net/protocol.h:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
 
struct inet_protocol *inet_protos[MAX_INET_PROTOS];
/* This is used to register protocols. */
struct inet_protocol
{
    int (*handler)(struct sk_buff *skb);//The Linux kernel uses an sk_buff data structure to describe each packet.
    void (*err_handler)(struct sk_buff *skb, u32 info);
    struct inet_protocol *next;
    unsigned char protocol;
    unsigned char copy: 1;
    void *data;
    const char *name;
};

每個地址族由其名稱以及相應的初始化例程地址代表。在引導階段初始化套接字介面時,核心呼叫每個地址族的初始化例程,這時,每個地址族註冊自己的協議操作集。協議操作集實際是一個例程集合,其中每個例程執行一個特定的操作。

(四)、套接字的建立過程

Linux 在利用socket()系統呼叫建立新的套接字時,需要傳遞套接字的地址族識別符號、套接字型別以及協議,其函式定義於net/socket.c 中:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
asmlinkage long sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        goto out;
    retval = sock_map_fd(sock);
    if (retval < 0)
        goto out_release;
out:
    /* It may be already another descriptor 8) Not kernel problem. */
    return retval;
out_release:
    sock_release(sock);
    return retval;
}

實際上,套接字對於使用者程式而言就是特殊的已開啟的檔案。核心中為套接字定義了一種特殊的檔案型別,形成一種特殊的檔案系統sockfs,其定義於net/socket.c:

 C++ Code 
1
2
3
 
static struct vfsmount *sock_mnt;
static DECLARE_FSTYPE(sock_fs_type, "sockfs", sockfs_read_super, FS_NOMOUNT);

在系統初始化時,要通過kern_mount()安裝這個檔案系統。安裝時有個作為連線件的vfsmount 資料結構,這個結構的地址就儲存在一個全域性的指標sock_mnt 中。所謂建立一個套接字,就是在sockfs 檔案系統中建立一個特殊檔案,或者說一個節點,並建立起為實現套接字功能所需的一整套資料結構。所以,函式sock_create()首先是建立一個socket 資料結構,然後將其“對映”到一個已開啟的檔案中,進行socket 結構和sock 結構的分配和初始化。

新建立的 BSD socket 資料結構包含有指向地址族專有的套接字例程的指標,這一指標實際就是 proto_ops 資料結構的地址。

BSD 套接字的套接字型別設定為所請求的 SOCK_STREAM 或 SOCK_DGRAM 等。然後,內核利用 proto_ops 資料結構中的資訊呼叫地址族專有的建立例程。之後,核心從當前程式的 fd 向量中分配空閒的檔案描述符,該描述符指向的 file 數據結構被初始化。初始化過程包括將檔案操作集指標指向由 BSD 套接字介面支援的 BSD 文件操作集。所有隨後的套接字(檔案)操作都將定向到該套接字介面,而套接字介面則會進一步呼叫地址族的操作例程,從而將操作傳遞到底層地址族,如圖12.10 所示。

實際上,socket 結構與sock 結構是同一事物的兩個方面。如果說socket 結構是面向進程和系統呼叫介面的,那麼sock 結構就是面向底層驅動程式的。可是,為什麼不把這兩個數據結構合併成一個呢?我們說套接字是一種特殊的檔案系統,因此,inode 結構內部的union 的一個成分就用作socket 結構,其定義如下:

 C++ Code 
1
2
3
4
5
6
7
8
 
struct inode {
        .......
        union
    {
        ........
        struct socket socket_i;
    }
}



由於套接字操作的特殊性,這個結構中需要大量的結構成分。可是,如果把這些結構成分全都放在socket 結構中,則inode 結構中的這個union 就會變得很大,從而inode 結構也會變得很大,而對於其他檔案系統,這個union 成分並不需要那麼龐大。因此,就把套接字所需的這些結構成分拆成兩部分,把與檔案系統關係比較密切的那一部分放在socket 結構中,把與通訊關係比較密切的那一部分則單獨組成一個資料結構,即sock 結構。由於這兩部分資料在邏輯上本來就是一體的,所以要通過指標互相指向對方,形成一對一的關係。


相關文章