這種本機網(wǎng)絡(luò) IO 方法,性能可以翻倍!
轉(zhuǎn)載自張彥非allen
大家好,我是飛哥!
很多讀者在看完《127.0.0.1 之本機網(wǎng)絡(luò)通信過程知多少 ?》這一篇后,讓我講講 Unix Domain Socket。好了,今天就安排!
在本機網(wǎng)絡(luò) IO 中,我們講到過基于普通 socket 的本機網(wǎng)絡(luò)通信過程中,其實在內(nèi)核工作流上并沒有節(jié)約太多的開銷。該走的系統(tǒng)調(diào)用、協(xié)議棧、鄰居系統(tǒng)、設(shè)備驅(qū)動(雖然說對于本機網(wǎng)絡(luò) loopback 設(shè)備來說只是一個軟件虛擬的東東)全都走了一遍。其工作過程如下圖

本文中,我們將分析 Unix Domain Socket 的內(nèi)部工作原理。你將理解為什么這種方式的性能比 127.0.0.1 要好很多。最后我們還給出了實際的性能測試對比數(shù)據(jù)。
相信你已經(jīng)迫不及待了,別著急,讓我們一一展開細說!
一、使用方法
Unix Domain Socket(后面統(tǒng)一簡稱 UDS) 使用起來和傳統(tǒng)的 socket 非常的相似。區(qū)別點主要有兩個地方需要關(guān)注。
第一,在創(chuàng)建 socket 的時候,普通的 socket 第一個參數(shù) family 為 AF_INET, 而 UDS 指定為 AF_UNIX 即可。
第二,Server 的標識不再是 ip 和 端口,而是一個路徑,例如 /dev/shm/fpm-cgi.sock。
其實在平時我們使用 UDS 并不一定需要去寫一段代碼,很多應(yīng)用程序都支持在本機網(wǎng)絡(luò) IO 的時候配置。例如在 Nginx 中,如果要訪問的本機 fastcgi 服務(wù)是以 UDS 方式提供服務(wù)的話,只需要在配置文件中配置這么一行就搞定了。
fastcgi_pass?unix:/dev/shm/fpm-cgi.sock;
如果 對于一個 UDS 的 server 來說,它的代碼示例大概結(jié)構(gòu)如下,大家簡單了解一下。只是個示例不一定可運行。
int?main()
{
?//?創(chuàng)建?unix?domain?socket
?int?fd?=?socket(AF_UNIX,?SOCK_STREAM,?0);
?//?綁定監(jiān)聽
?char?*socket_path?=?"./server.sock";
?strcpy(serun.sun_path,?socket_path);?
?bind(fd,?serun,?...);
?listen(fd,?128);
?while(1){
??//接收新連接
??conn?=?accept(fd,?...);
??//收發(fā)數(shù)據(jù)
??read(conn,?...);
??write(conn,?...);
?}
}
基于 UDS 的 client 也是和普通 socket 使用方式差不太多,創(chuàng)建一個 socket,然后 connect 即可。
int?main(){
?sock?=?socket(AF_UNIX,?SOCK_STREAM,?0);
?connect(sockfd,?...)
}
二、連接過程
總的來說,基于 UDS 的連接過程比 inet 的 socket 連接過程要簡單多了。客戶端先創(chuàng)建一個自己用的 socket,然后調(diào)用 connect 來和服務(wù)器建立連接。
在 connect 的時候,會申請一個新 socket 給 server 端將來使用,和自己的 socket 建立好連接關(guān)系以后,就放到服務(wù)器正在監(jiān)聽的 socket 的接收隊列中。這個時候,服務(wù)器端通過 accept 就能獲取到和客戶端配好對的新 socket 了。
總的 UDS 的連接建立流程如下圖。

內(nèi)核源碼中最重要的邏輯在 connect 函數(shù)中,我們來簡單展開看一下。unix 協(xié)議族中定義了這類 socket 的所有方法,它位于 net/unix/af_unix.c 中。
//file:?net/unix/af_unix.c
static?const?struct?proto_ops?unix_stream_ops?=?{
?.family?=?PF_UNIX,
?.owner?=?THIS_MODULE,
?.bind?=??unix_bind,
?.connect?=?unix_stream_connect,
?.socketpair?=?unix_socketpair,
?.listen?=?unix_listen,
?...
};
我們找到 connect 函數(shù)的具體實現(xiàn),unix_stream_connect。
//file:?net/unix/af_unix.c
static?int?unix_stream_connect(struct?socket?*sock,?struct?sockaddr?*uaddr,
??????????int?addr_len,?int?flags)
{
?struct?sockaddr_un?*sunaddr?=?(struct?sockaddr_un?*)uaddr;
?...
?//?1.?為服務(wù)器側(cè)申請一個新的?socket?對象
?newsk?=?unix_create1(sock_net(sk),?NULL);
?//?2.?申請一個?skb,并關(guān)聯(lián)上?newsk
?skb?=?sock_wmalloc(newsk,?1,?0,?GFP_KERNEL);
?...
?//?3.?建立兩個?sock?對象之間的連接
?unix_peer(newsk)?=?sk;
?newsk->sk_state??=?TCP_ESTABLISHED;
?newsk->sk_type??=?sk->sk_type;
?...
?sk->sk_state?=?TCP_ESTABLISHED;
?unix_peer(sk)?=?newsk;
?//?4.?把連接中的一頭(新?socket)放到服務(wù)器接收隊列中
?__skb_queue_tail(&other->sk_receive_queue,?skb);
}
主要的連接操作都是在這個函數(shù)中完成的。和我們平常所見的 TCP 連接建立過程,這個連接過程簡直是太簡單了。沒有三次握手,也沒有全連接隊列、半連接隊列,更沒有啥超時重傳。
直接就是將兩個 socket 結(jié)構(gòu)體中的指針互相指向?qū)Ψ骄托辛?。就?unix_peer(newsk) = sk 和?unix_peer(sk)?= newsk?這兩句。
//file:?net/unix/af_unix.c
#define?unix_peer(sk)?(unix_sk(sk)->peer)
當關(guān)聯(lián)關(guān)系建立好之后,通過 __skb_queue_tail 將 skb 放到服務(wù)器的接收隊列中。注意這里的 skb 里保存著新 socket 的指針,因為服務(wù)進程通過 accept 取出這個 skb 的時候,就能獲取到和客戶進程中 socket 建立好連接關(guān)系的另一個 socket。
怎么樣,UDS 的連接建立過程是不是很簡單!?
三、發(fā)送過程
看完了連接建立過程,我們再來看看基于 UDS 的數(shù)據(jù)的收發(fā)。這個收發(fā)過程一樣也是非常的簡單。發(fā)送方是直接將數(shù)據(jù)寫到接收方的接收隊列里的。

我們從 send 函數(shù)來看起。send 系統(tǒng)調(diào)用的源碼位于文件 net/socket.c 中。在這個系統(tǒng)調(diào)用里,內(nèi)部其實真正使用的是 sendto 系統(tǒng)調(diào)用。它只干了兩件簡單的事情,
第一是在內(nèi)核中把真正的 socket 找出來,在這個對象里記錄著各種協(xié)議棧的函數(shù)地址。第二是構(gòu)造一個 struct msghdr 對象,把用戶傳入的數(shù)據(jù),比如 buffer地址、數(shù)據(jù)長度啥的,統(tǒng)統(tǒng)都裝進去. 剩下的事情就交給下一層,協(xié)議棧里的函數(shù) inet_sendmsg 了,其中 inet_sendmsg 函數(shù)的地址是通過 socket 內(nèi)核對象里的 ops 成員找到的。大致流程如圖。

//file:
static?int?unix_stream_sendmsg(struct?kiocb?*kiocb,?struct?socket?*sock,
??????????struct?msghdr?*msg,?size_t?len)
{
?//?1.申請一塊緩存區(qū)
?skb?=?sock_alloc_send_skb(sk,?size,?msg->msg_flags&MSG_DONTWAIT,
??????&err);
?//?2.拷貝用戶數(shù)據(jù)到內(nèi)核緩存區(qū)
?err?=?memcpy_fromiovec(skb_put(skb,?size),?msg->msg_iov,?size);
?//?3.?查找socket?peer
?struct?sock?*other?=?NULL;
?other?=?unix_peer(sk);
?//?4.直接把?skb放到對端的接收隊列中
?skb_queue_tail(&other->sk_receive_queue,?skb);
?//?5.發(fā)送完畢回調(diào)
?other->sk_data_ready(other,?size);
}
和復(fù)雜的 TCP 發(fā)送接收過程相比,這里的發(fā)送邏輯簡單簡單到令人發(fā)指。申請一塊內(nèi)存(skb),把數(shù)據(jù)拷貝進去。根據(jù) socket 對象找到另一端,直接把 skb 給放到對端的接收隊列里了
接收函數(shù)主題是 unix_stream_recvmsg,這個函數(shù)中只需要訪問它自己的接收隊列就行了,源碼就不展示了。所以在本機網(wǎng)絡(luò) IO 場景里,基于 Unix Domain Socket 的服務(wù)性能上肯定要好一些的。
四、性能對比
為了驗證 Unix Domain Socket 到底比基于 127.0.0.1 的性能好多少,我做了一個性能測試。在網(wǎng)絡(luò)性能對比測試,最重要的兩個指標是延遲和吞吐。我從 Github 上找了個好用的測試源碼:https://github.com/rigtorp/ipc-bench。我的測試環(huán)境是一臺 4 核 CPU,8G 內(nèi)存的 KVM 虛機。
在延遲指標上,對比結(jié)果如下圖。

在包體達到 100 KB 以后,UDS 方法延遲 24 微秒左右(1 微秒等于 1000 納秒),TCP 是 32 微秒,仍然高一截。這里低于 2 倍的關(guān)系了,是因為當包足夠大的時候,網(wǎng)絡(luò)協(xié)議棧上的開銷就顯得沒那么明顯了。
再來看看吞吐效果對比。

五、總結(jié)
本文分析了基于 Unix Domain Socket 的連接創(chuàng)建、以及數(shù)據(jù)收發(fā)過程。其中數(shù)據(jù)收發(fā)的工作過程如下圖。

相對比本機網(wǎng)絡(luò) IO 通信過程上,它的工作過程要清爽許多。其中 127.0.0.1 工作過程如下圖。

