注:本分類下文章大多整理自《深入分析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 結構。由於這兩部分數據在邏輯上本來就是一體的,所以要通過指針互相指向對方,形成一對一的關系。