linux網絡編程API函數初步剖析
今天我們來分析一下前幾篇博文中提到的網絡編程中幾個核心的API,探究一下當我們調用每個API時,內核中具體做了哪些準備和初始化工作。
1、socket(family,type,protocol)
當我們在開發網絡應用程序時,使用該系統調用來創建一個套接字。該API所做的工作如下所示:
該系統調用主要完成兩個任務:“創建套接字”和“為套接字綁定文件句柄”。
socket{}<include/linux.NET.h>結構定義如下:
struct socket {
socket_state state; //socket狀態
unsigned long flags; //標識,如SOCK_ASYNC_NOSAPCE
const struct proto_ops *ops; //協議特定的socket操作集
struct fasync_struct *fasync_list; //異步喚醒隊列
struct file *file; //指向文件的指針
struct sock *sk; //指向下一層中的sock結構
wait_queue_head_t wait; //等待在這個socket上的任務列表
short type; //數據包的類型
};
在創建socket套接字時,就是要完成ops、file和sk等這些成員的初始化。
1). 創建套接字:sock_create()
根據family參數值在全局數組struct net_proto_family net_families[]里找到我們所指定的地址簇。不同類型的地址簇都有一個struct net_proto_family{}類型的對象,例如我們常見的IPv4的inet_family_ops,IPv6的inet6_family_ops,X25協議的ax25_family_ops等。在內核是初始化時,這些模塊會在自己的初始化函數內部調用sock_register()接口將各自的地址簇對象注冊到net_families[]數組里。
我們分析的焦點集中在IPv4協議簇,即inet_family_ops對象上。重點是inet_create函數,該函數的主要任務就是創建一個socket套接字,并對其中相關結構體成員進行必要的初始化。至于它創建套接字時的依據和原理等到我們講協議棧時大家就明白了,這里主要是讓大家對其流程執行流程有個感性的把握。
sock_alloc()函數中我們創建一個struct socket{}類型的對象,假如叫做A,將socket()系統調用的第二參數type字段賦值給A->type。
在inet_create()函數中,我們根據type的值,在全局數組 struct inet_protosw inetsw[]里找到我們對應的協議轉換開關。而inetsw[]數組是在inet_init()函數里被初始化的:
其中inetsw_array[]是一個比較重要的數據結構,定義在af_inet.c文件中:
根據type的值,就可以確定struct socket{}->ops,到底是inet_stream_ops、inet_dgram_ops或者inet_sockraw_ops。然后,對應地,就以tcp_prot、udp_prot或raw_prot為輸入參數,實例化一個struct sock{}對象sk=sk_alloc()。緊接著建立socket{}和sock{}的關聯,最后將socket()系統調用的第三個參數protocol付給sock{}對象中的屬性sk_protocol。
看不懂別著急,我說過,這里只是給大家梳理整體流程,等到我們講了協議棧章節,然后再回頭看本篇,就感覺這些東西就太小兒科了。
2). 為套接字綁定文件句柄:sock_map_fd()
我們都知道網絡套接字也是一種系統IO,所以不可避免的要與文件系統打交道。每個套接字都對應一個已打開的文件標識符,所以在套接字初始化完成后,就要將其和本地一個唯一的文件標識符關聯起來,即建立socket{}和file{}之間的關聯關系。
2、bind (sockfd, sockaddr, addrlen)
該系統調用在內核中的執行過程如下:
重點是socket->ops->bind()回調接口。我們現在已經知道了,針對IPv4而言,這里的ops無非就是inet_stream_ops、inet_dgram_ops或inet_sockraw_ops對象。碰巧的是,這三個對象中的bind函數指針均指向inet_bind()函數。只有原始套接字的情況,這里會去調用raw_prot對象的bind回調函數,即raw_bind()。
3、listen(sockfd, backlog)
這里我們可以看到面向無連接的套接字和原始套接字是不用listen的,只有流式套接字才有效。
4、connect(sockfd, sockaddr, addrlen)
從這幅圖中我們確實看到,connect()系統調用不但可以面向連接的套接字,也可用于無連接及原始套接字。
5、accept(sockfd, sockaddr, addrlen)
同樣地,我們看到只有面向連接的流式套接字調用accept()才有意義。最終調用的是tcp_prot對象的accept成員函數。
需要C/C++ Linux高級服務器架構師學習資料后臺私信“資料”(包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)
Linux網絡編程數據收發的API流程分析
只要把數據在協議棧中的流動線路和脈絡弄清楚了,關于協議棧的實現部分,理解起來就輕松多了。在網絡編程章節的數據接收過程中,我們主要介紹過read()、recv()、recvfrom()還有一個recvmsg()沒介紹到,今天我們就來看一下這幾個API函數到底有什么差別。
數據接收
在接收數據的過程,主要分兩個階段:BOTTOM-HALF和TOP-HALF。 BOTTOM-HALF:
當從網卡驅動收到數據包后即進入BOTTOM-HALF階段,在這里要根據以太幀頭部中的類型字段來確定上層承載的具體協議類型,如IP,或ARP、RARP等。IP報文的處理函數通常交付給ip_recv()函數來處理,然后數據進入網絡層,具體流程: 如果該數據包是發給本機的一般調用ip_local_deliver()函數,如果是需要本機轉發給出去的,并且本機也開啟了轉發功能,那么就會調用ip_forward()函數。 在這里我們看到了Netfilter的身影,好久沒看到它了,還是有些親切。大家可以結合這幅圖回頭再理解一下Netfilter和協議棧的關系。 BOTTOM-HALF最后將收到的skb填充到socket套接字的接收隊列里,參見下圖。
TOP-HALF:
緊承BOTTOM-HALF階段,該階段的主要任務就是從接收隊列里拿出一個skb然后將其傳遞到用戶空間去,如下:
可以看出,這幾個函數的內部最終都統一到了一起:__sock_recvmsg()。
數據發送
同樣的,數據發送也分兩個階段,對照接收的情況,發送數據時肯定也存在一個發送隊列,這樣想就對了。前面關于發送數據包時我們介紹過的API有write()、send()、sendto()還有一個sendmsg()沒介紹到。 TOP-HALF如下:
BOTTOM-HALF如下所示:
經過這么一份探索,我們對這幾個數據收發的API至少理解的要比別人深刻些了吧。