linux下socket通信時的sockfd是怎么來的


2020-04-22

關鍵字:socket通信時的底層調用流程


 

這篇文章簡單記錄一下在Linux環境下使用C語言做 socket 通信時的一些流程。

 

1、sockfd的由來

 

典型的C語言建立socket通信的第一行代碼基本都如下所示:

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);  //UDP通信
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //TCP通信

 

sockfd 就代表本次socket連接的文件句柄,后續的通信我們只需要像對待普通文件一樣往這個文件句柄中讀寫數據即可實現socket通信的過程。

 

但這簡簡單單的一行語句,它的底層邏輯是怎樣的呢?sockfd 到底是怎樣分配出來的呢?

 

通過查詢Linux中的編程手冊

man socket

可以發現,socket 函數是一個系統調用函數。它的原型如下:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

 

既然是系統調用,就意味着它的源碼實現在 kernel 層。

 

在Linux系統源碼目錄,或Android系統源碼目錄下,./kernel/net 目錄的代碼就是 socket 相關的源碼實現。

 

其中,socket() 函數的源碼實現位於:

./kernel/net/socket.c

 

因為 socket 源碼在目前來說是一個應用相當廣泛且成熟的代碼體系,因此,可以認為所有基於Linux平台下的socket的實現都是一致的。

 

在 socket.c 文件的第 1361 行可以發現對 socket 函數聲明為系統調用的代碼實現,如下圖所示:

 

上圖所示代碼中的 SYSCALL_DEFINE3 是一個系統定義的宏。在這里我們沒有必要深究它的實現原理,只需要知道經過這樣的聲明以后,系統中就多了一個函數名稱為 socket 的系統調用函數了,且我們在用戶態調用 socket() 函數時它最終會執行到這里來就可以了。

 

SYSCALL_DEFINE3 的定義位於以下文件中:

kernel/include/linux/syscalls.h

 

言歸正傳。

 

在上圖所示代碼中,我們可以發現在第 1385 行有一句 retval = sock_map_fd() 的代碼。這句代碼就是用來分配我們的 sockfd 以及創建一個對應文件並綁定標准的文件IO操作函數實現的地方了。我們跟進去看看。

 

sock_map_fd() 函數的實現也在 socket.c 代碼文件中。它的源碼較為簡單,如下圖所示:

 

 

上圖中第390行根據函數的名稱我們就能猜到它就是用於向系統申請一個可用的文件描述符fd的,也就是我們在應用程序中拿到的 sockfd 了。由於Linux系統會為每個進程都開辟一個文件描述符池專門用於保存管理在該進程中打開的所有文件的fd。因此,這個 get_unused_fd_flags() 函數的內部實現我們猜也能猜得到了,就是取出調用這個函數的進程的文件描述符池里最近的(或者說最小的)可用的文件描述符。若當前沒有可用的fd,則返回一個負的值。因此,這個函數我們就不再細跟了。

 

這就是 sockfd 的由來。

 

但 sockfd 的本質僅僅是一個數字值而已。想要能響應標准的文件操作,如 open()、 read()、  write()、  close() 等,還得再綁定上一個標准的 file_operations 結構體實現才行。

 

這個操作就位於上圖所示代碼的第 394 行的 sock_alloc_file() 函數中。

 

sock_alloc_file() 函數的實現仍然在 socket.c 中,如下圖所示:

 

 

老實說,這個函數的目的也很明顯,猜也猜得到它就是創建一個文件,然后再綁定上對應的文件操作函數就是了。

 

這個函數前面幾行我們不必理會,它的目的是要為這個進程或者說這個socket確定一個保存即將要創建的文件的路徑的。筆者沒去研究過這塊的詳細實現,但看樣子這個路徑可能是一個虛擬路徑,不會實際落地到設備磁盤上去的。

 

我們重點關注一下上圖代碼的第 371 行。它直接為 struct file 申請了一段內存。在Linux內核中用 struct file 來描述一個文件。第三個參數 socket_file_ops 就是一個標准的 struct file_operations 結構體了。它在這里的實現如下圖所示:

 

 

這下終於明了了。open、close、read、write都有了。

 

不過需要強調一下,sockfd 對讀寫的響應函數注冊的是 .aio_read 和 .aio_write。它們是“異步讀寫”的意思。我們在用戶態的應用程序中對 sockfd 寫數據的時候,在socket底層是直接將用戶態的數據轉移到socket層的緩沖區中的,然后立即給應用程序返回結果。至於這些數據什么時候才通過網口發送出去就看下面的程序的調度了。不過其實socket的數據緩沖區也是有限的,當這個緩沖區滿了的時候,應用層的write()調用還是會進入阻塞狀態。它的這個“異步”准確來說只是數據到網口的“異步”而已。

 

2、數據在socket中的傳遞流程

 

前面講到的 socket() 函數的調用,僅僅是在本地做一些通信前的准備而已。想要能真正實現通信,還得經過bind、connect等函數的調用。

 

不管是TCP還是UDP,在建立連接的時候都要先“探測路由”,TCP是發生在 connect() 被調用的時候,UDP是發生在每次要發送數據的時候。探測路由說白了就是發一些網絡數據出去,看我的數據能否被接收端接收到。

 

這個“探測路由”就位於以下代碼文件中:

./kernel/net/ipv4/route.c

這里我們只討論 ipv4 情況下的 socket 通信。所有 ipv4 下的 socket 通信的代碼實現都在 ./kernel/net/ipv4 目錄下。

 

TCP下的發送數據流程大致如下,僅供參考:

tcp.c
    int tcp_sendmsg(...)
ip_output.c
    int ip_output(...)
    static int ip_finish_output()
    static inline int ip_finish_output2()
./kernel/include/net/dst.h
    static inline int dst_neigh_output()
./kernel/include/net/beighbour.h
    static inline int neigh_hh_output()
./kernel/net/core/dev.c
    int dev_queue_xmit()
    static inline int __dev_xmit_skb()
./kernel/net/sched/sch_generic.c
    int sch_direct_xmit()
./kernel/net/core/dev.c
    int dev_hard_start_xmit()

最后在 dev.c 的 dev_hard_start_xmit() 函數中有一句代碼 rc = ops->ndo_start_xmit(skb, dev); 會調到網卡驅動程序中的函數注冊中去。

 

在寫網卡驅動的時候必須要注冊這個 ndo_start_xmit() 函數,如下圖所示:

 

 

這個函數已經是最底層了,它要做的事應該就是直接將數據往網線里送了。畢竟當數據到達這個函數時,所有該封的協議都已經封裝好了。ndo_start_xmit 函數已經屬於“數據鏈路層”到“物理層”之間的過渡了。

 

以上就是TCP下發送數據的大致流程。至於接收和UDP的流程就不貼了,大同小異的。

 


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM