走進Linux核心網路 套接字的祕密—socket與sock
雙十一
今天是雙十一,記得還在念書的時候,每次都會參加京東圖書滿200-100的活動,然後腦子一熱屯一堆書。印象中人文類的書基本都會看,而電子資訊類的可能就只看了一半,甚至買過哪些都忘了。究其原因,主要是人文類的通常都是自己想看的,而技術類的就跟風看哪個銷量大買哪個,比如下面這本《UNIX 網路程式設計》
如果現在讓我給別人推薦學習網路程式設計的書,我一定不會推薦這本。為什麼呢? 倒不是因為書中內容說的不對,而是我認為這本書更適合當工具書查閱,而不適合逐章學習。就像《新華字典》裡漢字很多,但也沒見誰是用它來學認字的。不過,對於當年的我來說,《UNIX 網路程式設計》中提供的例子倒也能提供最基本的套接字程式設計體驗。比如從中我第一次接觸到了套接字
注
的概念,也接觸到了 socket(),bind() , listen(),accept(),send() , recv() 這些函式的使用方法。不過有些問題當時的我並不明白,比如建立套接字時為什麼要返回一個描述符,為什麼傳送接收時要填寫這個描述符,難道套接字就是這個描述符?
注
:儘管“套接字”就是英文socket的翻譯,但本文中,為了避免和之後的socket
混淆,所以在從網路程式設計的角度描述時,使用的是中文“套接字”。
套接字究竟是什麼
如果你知道Linux系統中程序間通訊的方式,就應該知道套接字也是其中一種。但套接字特別之處在於它不僅可以用來實現同一臺主機上程序間的通訊,還可以用來實現主機間的程序間
如果把兩個套接字之間的‘連線’比喻成‘水管’,那麼套接字就是‘水龍頭’
Unix有一句格言:everything is a file,即‘萬物皆檔案’,套接字也不例外。那麼如何把套接字和檔案聯絡起來呢? 答案就是通過下面這張圖。 其中task_struct表示一個程序,files_struct中的fd_array[]表示該程序開啟的所有描述符,對於套接字來說,與其他型別檔案的區別就是最終f_op指向的是socket_file_ops。不過,可以看到,這裡的socket_file_ops只有一些通用的操作,並沒有send和recv
socket 和 sock
終於到今天的主角了。實際上,對每一個新建立的套接字,核心協議棧都會建立struct socket
和struct sock
兩個資料結構。這兩個結構就像孿生兄弟,struct socket
面向使用者空間,struct sock
面向核心空間。
struct socket
struct socket
簡化版的結構如下:
<net.h>
struct socket {
unsigned long flags;
const struct proto_ops *ops;
struct file *file;
struct sock *sk;
short type;
};
其中type
表示協議,這是在建立套接字的時候的protocol
引數確定的
int socket(int domain, int type, int protocol);
file
指標指向上面那張圖中的struct file
結構,通過它,socket
便與檔案系統關聯了起來。
sk
指向孿生的兄弟sock
結構。
socket
結構中最重要的要數ops
指標了,根據協議型別,它指向一種特定協議的實現。比如TCP的就是inet_stream_ops
,ICMP、UDP協議對應inet_dgram_ops
,RAWIP對應的是inet_sockraw_ops
同樣地,這些也都在建立套接字的時候就決定了。
struct proto_ops
的簡化版本的結構如下
<net.h>
struct proto_ops{
int family;
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 (*accept)(struct socket *sock, struct socket *newsock, int flags);
int (*sendmsg)(struct socket *sock, struct msghdr *m, size_t total_len);
}
其中的介面名字是不是很熟悉?是的,它們和進行網路程式設計時呼叫的C庫中函式名字是一樣的。以sendmsg為例,真實的呼叫過程是這樣
即當用戶呼叫sendmsg時,核心會找到描述符fd
對應的struct socket
結構,然後呼叫sock->ops->sendmsg
執行特定協議的傳送。
那麼,ops
欄位什麼時候被賦值呢?答案是,在建立struct sock
結構前。
struct sock
struct sock
的簡化結構如下圖所示
struct sock_common {
struct proto *skc_prot;
};
struct sock {
struct sock_common __sk_common;
struct sk_buff_head sk_receive_queue;
struct sk_buff_head sk_write_queue;
};
其中最重要的欄位就是skc_prot
,它也是協議相關的。作為struct socket
結構的孿生兄弟,struct sock
結構也是在使用者建立套接字時就建立的。
sock_alloc建立了struct socket
結構,隨後,根據使用者傳入的family
,查詢陣列net_families
,找到對應的函式指標,呼叫create.
net_families
儲存著核心啟動時註冊(通過sock_register)的 socket protocol handler,比如以下幾種:
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
static const struct net_proto_family netlink_family_ops = {
.family = PF_NETLINK,
.create = netlink_create,
.owner = THIS_MODULE, /* for consistency 8) */
};
static const struct net_proto_family packet_family_ops = {
.family = PF_PACKET,
.create = packet_create,
.owner = THIS_MODULE,
};
static const struct net_proto_family unix_family_ops = {
.family = PF_UNIX,
.create = unix_create,
.owner = THIS_MODULE,,
};
如果我們建立套接字時指定的family
是PF_INET,那麼此時我們就會呼叫inet_create
static int inet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
struct inet_protosw *answer;
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
err = 0;
/* Check the non-wild match. */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
}
sock->ops = answer->ops;
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
}
}
首先是從inetsw
陣列搜尋匹配對應的協議,inetsw
稱為協議開關表,核心啟動時,通過inet_register_protosw進行註冊。比如下面幾項都會被註冊
inetsw
中註冊的每種協議都有ops
和prot
兩個欄位,前者與struct socket
結構關聯到一起,後者與struct sock
關聯到一起。在inet_create
中,struct socket
的ops
欄位和struct sock
的sk_prot
欄位被賦值。
以我們建立的套接字型別是TCP為例,此時struct socket
和struct sock的關係如下
:
還是繼續剛才sendmsg的過程,由於struct socket
的ops
指向inet_stream_ops
,所以實際呼叫的就是inet_sendmsg
結果,最終是呼叫其孿生兄弟struct sock
的sk_prot
的sendmsg,即tcp_sendmsg,這時,才是資料真正開始協議棧旅程的地方……