33 Socket通信
無論是用socket操作TCP,還是UDP,首先都要調用socket函數,socket函數用於創建一個socket的文件描述符,唯一標識一個socket;把它叫作文件描述符,是因爲在內核中會創建類似文件系統的數據結構,並且後續的操作都有用到它:
int socket(int domain, int type, int protocol);
三個參數的含義:
domain:表示使用什麼IP層協議。AF_INET表示IPv4,AF_INET6表示IPv6。
type:表示socket類型。SOCK_STREAM是TCP面向流的,SOCK_DGRAM是UDP面向數據報的,SOCK_RAW可以直接操作IP層,或者非TCP和UDP的協議。例如ICMP。
protocol表示的協議,包括IPPROTO_TCP、IPPTOTO_UDP。
TCP編程模式:
其總體狀態:用戶態加內核態如圖
UDP編程模式:
34 Socket內核數據結構
首先,Socket系統調用會有三級參數family、type、protocal,通過這三級參數,分別在net_proto_family表
中找到type鏈表,在type鏈表中找到protocal對應的操作。
這個操作分爲兩層,對於TCP協議來講,第一層是inet_stream_ops層,第二層是tcp_prot層。
於是,接下來的系統調用規律就都一樣了:
bind第一層調用inet_stream_ops的inet_bind函數,第二層調用tcp_prot的inet_csk_get_port函數;
listen第一層調用inet_stream_ops的inet_listen函數,第二層調用tcp_prot的inet_csk_get_port函數;
accept第一層調用inet_stream_ops的inet_accept函數,第二層調用tcp_prot的inet_csk_accept函數;
connect第一層調用inet_stream_ops的inet_stream_connect函數,第二層調用tcp_prot的tcp_v4_connect函數。
在內核中,爲每個Socket維護兩個隊列:
一個是已經建立了連接的隊列,這時候連接三次握手已經完畢,處於established狀態;
一個是還沒有完全建立連接的隊列,這個時候三次握手還沒完成,處於syn_rcvd的狀態。
三次連接體現在connect函數中:
35 發送網絡包
35.1 (上)ip層以上是如何發送數據的
socket對於用戶來講,是一個文件一樣的存在,擁有一個文件描述符。
因而對於網絡包的發送,可以使用對於socket文件的寫入系統調用,也就是write系統調用。
根據tcp_prot的定義,tcp的write調用的是tcp_sendmsg。
爲了減少內存拷貝的代價,有的網絡設備支持分散聚合(Scatter/Gather)I/O:
IP層沒必要通過內存拷貝進行聚合,直接讓散的數據零散的放在原處,在設備層進行聚合。
如果使用這種模式,網絡包的數據就不會放在連續的數據區域,而是放在struct skb_shared_info結構裏面指向的離散數據,skb_shared_info的成員變量skb_frag_t frags[MAX_SKB_FRAGS],會指向一個數組的頁面,就不能保證連續了:
- 總結
到ip層,這個過程分成幾個層次:
VFS層:write系統調用找到struct file,根據裏面的file_operations的定義,調用sock_write_iter函數。
sock_write_iter函數調用sock_sendmsg函數。Socket層:從struct file裏面的private_data得到struct socket,根據裏面ops的定義,調用inet_sendmsg函數。
Sock層:從struct socket裏面的sk得到struct sock,根據裏面sk_prot的定義,調用tcp_sendmsg函數。
TCP層:tcp_sendmsg函數會調用tcp_write_xmit函數,tcp_write_xmit函數會調用tcp_transmit_skb,在這裏實現了TCP層面向連接的邏輯。
IP層:擴展struct sock,得到struct inet_connection_sock,根據裏面icsk_af_ops的定義,調用ip_queue_xmit函數。
35.2 (下)總過程
總過程:
VFS層:write系統調用找到struct file,根據裏面的file_operations的定義,調用sock_write_iter函數。
sock_write_iter函數調用sock_sendmsg函數。
Socket層:從struct file裏面的private_data得到struct socket,根據裏面ops的定義,調用inet_sendmsg
函數。
Sock層:從struct socket裏面的sk得到struct sock,根據裏面sk_prot的定義,調用tcp_sendmsg函數。
TCP層:tcp_sendmsg函數會調用tcp_write_xmit函數,tcp_write_xmit函數會調用tcp_transmit_skb,在這
裏實現了TCP層面向連接的邏輯。
IP層:擴展struct sock,得到struct inet_connection_sock,根據裏面icsk_af_ops的定義,調用
ip_queue_xmit函數。
IP層:ip_route_output_ports函數裏面會調用fib_lookup查找路由表。FIB全稱是Forwarding Information
Base,轉發信息表,也就是路由表。
還要填寫IP層的頭、通過iptables規則。
MAC層:IP層調用ip_finish_output進行MAC層。
MAC層需要ARP獲得MAC地址,因而要調用___neigh_lookup_noref
查找屬於同一個網段的鄰居,他會調用
neigh_probe發送ARP。
有了MAC地址,就可以調用dev_queue_xmit發送二層網絡包了,它會調用__dev_xmit_skb
會將請求放入
隊列。
設備層:網絡包的發送回觸發一個軟中斷NET_TX_SOFTIRQ來處理隊列中的數據。這個軟中斷的處理函數
是net_tx_action。
在軟中斷處理函數中,會將網絡包從隊列上拿下來,調用網絡設備的傳輸函數ixgb_xmit_frame,將網絡
包發的設備的隊列上去。
36 接收網絡包
網卡作爲一個硬件,接收到網絡包,應該怎麼通知操作系統,這個網絡包到達了呢?
可以觸發一箇中斷。
但是這裏有個問題,就是網絡包的到來,往往是很難預期的。網絡吞吐量比較大的時候,網絡包的到達會十分頻繁。這個時候,如果非常頻繁地去觸發中斷,是個很大的災難。
比如說,CPU正在做某個事情,一些網絡包來了,觸發了中斷,CPU停下手裏的事情,去處理這些網絡包,處理完畢按照中斷處理的邏輯,應該回去繼續處理其他事情。
這個時候,另一些網絡包又來了,又觸發了中斷,CPU手裏的事情又要停下來,去處理網絡包。
能不能要來的一起來,把網絡包好好處理一把,然後再回去集中處理其他事情呢?
“網絡包能不能一起來”,這個沒法控制,但是可以有一種集中時間段的機制:
就是當一些網絡包到來觸發了中斷,內核處理完這些網絡包之後,我們可以先進入主動輪詢poll網卡的方式,主動去接收到來的網絡包,如果一直有,就一直處理,等處理告一段落,就返回幹其他的事情。
當再有下一批網絡包到來的時候,再中斷,再輪詢poll。這樣就會大大減少中斷的數量,提升網絡處理的效率,這種處理方式稱爲NAPI。
- 內核接收網絡包的過程
硬件網卡接收到網絡包之後,通過DMA技術,將網絡包放入Ring Buffer;
硬件網卡通過中斷通知CPU新的網絡包的到來;
網卡驅動程序會註冊中斷處理函數ixgb_intr;
中斷處理函數處理完需要暫時屏蔽中斷的核心流程之後,通過軟中斷NET_RX_SOFTIRQ觸發接下來的處理
過程;
NET_RX_SOFTIRQ軟中斷處理函數net_rx_action,net_rx_action會調用napi_poll,進而調用
ixgb_clean_rx_irq,從Ring Buffer中讀取數據到內核struct sk_buff
;
調用netif_receive_skb進入內核網絡協議棧,進行一些關於VLAN的二層邏輯處理後,調用ip_rcv進入三層
IP層;
在IP層,會處理iptables規則,然後調用ip_local_deliver交給更上層TCP層;
在TCP層調用tcp_v4_rcv,這裏面有三個隊列
需要處理:
如果當前的Socket不是正在被用戶態進程讀取,則放入backlog隊列;
如果正在被讀取,不需要很實時的話,則放入prequeue隊列;
其他情況調用tcp_v4_do_rcv;
在tcp_v4_do_rcv中:
如果是處於TCP_ESTABLISHED狀態,調用tcp_rcv_established;
其他的狀態,調用tcp_rcv_state_process;
在tcp_rcv_established中,調用tcp_data_queue,如果序列號能夠接的上,則放入sk_receive_queue隊列;
如果序列號接不上,則暫時放入out_of_order_queue隊列,等序列號能夠接上的時候,再放入sk_receive_queue隊列。
- 用戶態讀取網絡包的過程
VFS層:read系統調用找到struct file,根據裏面的file_operations的定義,調用sock_read_iter函數。
sock_read_iter函數調用sock_recvmsg函數。
Socket層:從struct file裏面的private_data得到struct socket,根據裏面ops的定義,調用inet_recvmsg函
數。
Sock層:從struct socket裏面的sk得到struct sock,根據裏面sk_prot的定義,調用tcp_recvmsg函數。
TCP層:tcp_recvmsg函數會依次讀取receive_queue隊列、prequeue隊列和backlog隊列。
用戶態和內核態核心就是通過三個隊列來交互數據