0.要求
-
在深入理解Linux內核任務調度(中斷處理、softirg、tasklet、wq、內核線程等)機制的基礎上,分析梳理send和recv過程中TCP/IP協議棧相關的運行任務實體及相互協作的時序分析。
-
編譯、部署、運行、測評、原理、源代碼分析、跟蹤調試等
-
應該包括時序圖
1.TCP/IP協議棧概述
各層的功能如下:
1、應用層的功能為對客戶發出的一個請求,服務器作出響應並提供相應的服務。
2、傳輸層的功能為通信雙方的主機提供端到端的服務,傳輸層對信息流具有調節作用,提供可靠性傳輸,確保數據到達無誤。
3、網絡層功能為進行網絡互連,根據網間報文IP地址,從一個網絡通過路由器傳到另一網絡。
4、網絡接口層負責接收IP數據報,並負責把這些數據報發送到指定網絡上。
TCP/IP協議的主要特點:
(1)TCP/IP協議不依賴於任何特定的計算機硬件或操作系統,提供開放的協議標准,即使不考慮Internet,TCP/IP協議也獲得了廣泛的支持。所以TCP/IP協議成為一種聯合各種硬件和軟件的實用系統。
(2)標准化的高層協議,可以提供多種可靠的用戶服務。
(3)統一的網絡地址分配方案,使得整個TCP/IP設備在網中都具有惟一的地址。
(4)TCP/IP協議並不依賴於特定的網絡傳輸硬件,所以TCP/IP協議能夠集成各種各樣的網絡。用戶能夠使用以太網(Ethernet)、令牌環網(Token Ring Network)、撥號線路(Dial-up line)、X.25網以及所有的網絡傳輸硬件。
2.Linux內核系統體系概述
Linux 內核主要由 5 個模塊構成,它們分別是:
進程調度模塊 用來負責控制進程對 CPU 資源的使用。所采取的調度策略是各進程能夠公平合理地訪問 CPU,同時保證內核能及時地執行硬件操作。
內存管理模塊 用於確保所有進程能夠安全地共享機器主內存區,同時,內存管理模塊還支持虛擬內存管理方式,使得 Linux 支持進程使用比實際內存空間更多的內存容量。並可以利用文件系統把暫時不用的內存數據塊會被交換到外部存儲設備上去,當需要時再交換回來。
文件系統模塊 用於支持對外部設備的驅動和存儲。虛擬文件系統模塊通過向所有的外部存儲設備提供一個通用的文件接口,隱藏了各種硬件設備的不同細節。從而提供並支持與其它操作系統兼容的多種文件系統格式。
進程間通信模塊 子系統用於支持多種進程間的信息交換方式。
網絡接口模塊 提供對多種網絡通信標准的訪問並支持許多網絡硬件。
3.Socket編程概述
3.1簡介
•獨立於具體協議的網絡編程接口
•在OSI模型中,主要位於會話層和傳輸層之間
•BSD Socket(伯克利套接字)是通過標准的UNIX文件描述符和其它程序通訊的一個方法,目前已經被廣泛移植到各個平台。
3.2 Socket API函數
•socket 創建套接字
•connect 建立連接
•bind 綁定本機端口
•listen 監聽端口
•accept 接受連接
•recv, recvfrom 數據接收
•send, sendto 數據發送
close, shutdown 關閉套接字
3.3UDP數據通信的過程
UDP數據通信的過程如下所示:
3.4TCP數據通信的過程
TCP數據通信的過程如下圖所示:
tcp首部格式如下:
//tcp首部格式 //http://blog.csdn.net/wenqian1991/article/details/44598537 struct tcphdr { __u16 source;//源端口號 __u16 dest;//目的端口號 __u32 seq;//32位序列號 __u32 ack_seq;//32位確認號 #if defined(LITTLE_ENDIAN_BITFIELD) __u16 res1:4,//4位首部長度 doff:4,//保留 //下面為各個控制位 fin:1,//最后控制位,表示數據已全部傳輸完成 syn:1,//同步控制位 rst:1,//重置控制位 psh:1,//推控制位 ack:1,//確認控制位 urg:1,//緊急控制位 res2:2;// #elif defined(BIG_ENDIAN_BITFIELD) __u16 doff:4, res1:4, res2:2, urg:1, ack:1, psh:1, rst:1, syn:1, fin:1; #else #error "Adjust your <asm/byteorder.h> defines" #endif __u16 window;//16位窗口大小 __u16 check;//16位校驗和 __u16 urg_ptr;//16位緊急指針 };
3.4Socket源碼分析
1 //該結構表示一個網絡套接字 2 3 struct socket { 4 5 short type; /* 套接字所用的流類型*/ 6 7 socket_state state;//套接字所處狀態 8 9 long flags;//標識字段,目前尚無明確作用 10 11 struct proto_ops *ops; /* 操作函數集指針 */ 12 13 /* data保存指向‘私有'數據結構指針,在不同的域指向不同的數據結構 */ 14 15 //在INET域,指向sock結構,UNIX域指向unix_proto_data結構 16 17 void *data; 18 19 //下面兩個字段只用於UNIX域 20 21 struct socket *conn; /* 指向連接的對端套接字 */ 22 23 struct socket *iconn; /* 指向正等待連接的客戶端(服務器端) */ 24 25 struct socket *next;//鏈表 26 27 struct wait_queue **wait; /* 等待隊列 */ 28 29 struct inode *inode;//inode結構指針 30 31 struct fasync_struct *fasync_list; /* 異步喚醒鏈表結構 */ 32 33 };
struct sock { struct options *opt;//IP選項緩沖於此處 volatile unsigned long wmem_alloc;//發送緩沖隊列中存放的數據的大小,這兩個與后面的rcvbuf和sndbuf一起使用 volatile unsigned long rmem_alloc;//接收緩沖隊列中存放的數據的大小 /* 下面三個seq用於TCP協議中為保證可靠數據傳輸而使用的序列號 */ unsigned long write_seq;// unsigned long sent_seq;// unsigned long acked_seq;// unsigned long copied_seq;//應用程序有待讀取(但尚未讀取)數據的第一個序列號 unsigned long rcv_ack_seq;//目前本地接收到的對本地發送數據的應答序列號 unsigned long window_seq;//窗口大小 unsigned long fin_seq;//應答序列號 //下面兩個字段用於緊急數據處理 unsigned long urg_seq;//緊急數據最大序列號 unsigned long urg_data;//標志位,1表示收到緊急數據 /* * Not all are volatile, but some are, so we * might as well say they all are. */ volatile char inuse,//表示其他進程正在使用該sock結構,本進程需等待 dead,//表示該sock結構已處於釋放狀態 urginline,//=1,表示緊急數據將被當做普通數據處理 intr,// blog, done, reuse, keepopen,//=1,使用保活定時器 linger,//=1,表示在關閉套接字時需要等待一段時間以確認其已關閉 delay_acks,//=1,表示延遲應答 destroy,//=1,表示該sock結構等待銷毀 ack_timed, no_check, zapped, /* In ax25 & ipx means not linked */ broadcast, nonagle;//=1,表示不使用NAGLE算法 //NAGLE算法:在前一個發送的數據包被應答之前,不可再繼續發送其它數據包 unsigned long lingertime;//等待關閉操作的時間 int proc;//該sock結構所屬的進程的進程號 struct sock *next; struct sock *prev; /* Doubly linked chain.. */ struct sock *pair; //下面兩個字段用於TCP協議重發隊列 struct sk_buff * volatile send_head;//這個隊列中的數據均已經發送出去,但尚未接收到應答 struct sk_buff * volatile send_tail; struct sk_buff_head back_log;//接收的數據包緩存隊列,當套接字正忙時,數據包暫存在這里 struct sk_buff *partial;//用於創建最大長度的待發送數據包 struct timer_list partial_timer;//定時器,用於按時發送partial指針指向的數據包 long retransmits;//重發次數 struct sk_buff_head write_queue,//指向待發送數據包 receive_queue;//讀隊列,表示數據報已被正式接收,該隊列中的數據可被應用程序讀取? struct proto *prot;//傳輸層處理函數集 struct wait_queue **sleep; unsigned long daddr;//sock結構所代表套接字的遠端地址 unsigned long saddr;//本地地址 unsigned short max_unacked;//最大未處理請求連接數 unsigned short window;//遠端窗口大小 unsigned short bytes_rcv;//已接收字節總數 /* mss is min(mtu, max_window) */ unsigned short mtu; //和鏈路層協議密切相關 /* 最大傳輸單元 */ volatile unsigned short mss; //最大報文長度 =mtu-ip首部長度-tcp首部長度,也就是tcp數據包每次能夠傳輸的最大數據分段 volatile unsigned short user_mss; /* mss requested by user in ioctl */ volatile unsigned short max_window;//最大窗口大小 unsigned long window_clamp;//窗口大小鉗制值 unsigned short num;//本地端口號 //下面三個字段用於擁塞算法 volatile unsigned short cong_window; volatile unsigned short cong_count; volatile unsigned short ssthresh; volatile unsigned short packets_out;//本地已發送出去但尚未得到應答的數據包數目 volatile unsigned short shutdown;//本地關閉標志位,用於半關閉操作 volatile unsigned long rtt;//往返時間估計值 volatile unsigned long mdev;//絕對偏差 volatile unsigned long rto;//用rtt和mdev 用算法計算出的延遲時間值 /* currently backoff isn't used, but I'm maintaining it in case * we want to go back to a backoff formula that needs it */ volatile unsigned short backoff;//退避算法度量值 volatile short err;//錯誤標志值 unsigned char protocol;//傳輸層協議值 volatile unsigned char state;//套接字狀態值 volatile unsigned char ack_backlog;//緩存的未應答數據包個數 unsigned char max_ack_backlog;//最大緩存的未應答數據包個數 unsigned char priority;//該套接字優先級 unsigned char debug; unsigned short rcvbuf;//最大接收緩沖區大小 unsigned short sndbuf;//最大發送緩沖區大小 unsigned short type;//類型值如 SOCK_STREAM unsigned char localroute;//=1,表示只使用本地路由 /* Route locally only */ #ifdef CONFIG_IPX ipx_address ipx_dest_addr; ipx_interface *ipx_intrfc; unsigned short ipx_port; unsigned short ipx_type; #endif #ifdef CONFIG_AX25 /* Really we want to add a per protocol private area */ ax25_address ax25_source_addr,ax25_dest_addr; struct sk_buff *volatile ax25_retxq[8]; char ax25_state,ax25_vs,ax25_vr,ax25_lastrxnr,ax25_lasttxnr; char ax25_condition; char ax25_retxcnt; char ax25_xx; char ax25_retxqi; char ax25_rrtimer; char ax25_timer; unsigned char ax25_n2; unsigned short ax25_t1,ax25_t2,ax25_t3; ax25_digi *ax25_digipeat; #endif #ifdef CONFIG_ATALK struct atalk_sock at; #endif /* IP 'private area' or will be eventually */ int ip_ttl;//ip首部ttl字段值,實際上表示路由器跳數 /* TTL setting */ int ip_tos;//ip首部tos字段值,服務類型值 /* TOS */ struct tcphdr dummy_th;//緩存的tcp首部,在tcp協議中創建一個發送數據包時可以利用此字段快速創建tcp首部 struct timer_list keepalive_timer;//保活定時器,用於探測對方窗口大小,防止對方通報窗口大小的數據包丟棄 /* TCP keepalive hack */ struct timer_list retransmit_timer;//重發定時器,用於數據包超時重發 /* TCP retransmit timer */ struct timer_list ack_timer;//延遲應答定時器 /* TCP delayed ack timer */ int ip_xmit_timeout;//表示定時器超時原因 /* Why the timeout is running */ //用於ip多播 #ifdef CONFIG_IP_MULTICAST int ip_mc_ttl; /* Multicasting TTL */ int ip_mc_loop; /* Loopback (not implemented yet) */ char ip_mc_name[MAX_ADDR_LEN]; /* Multicast device name */ struct ip_mc_socklist *ip_mc_list; /* Group array */ #endif /* This part is used for the timeout functions (timer.c). */ int timeout; /* What are we waiting for? */ struct timer_list timer; /* This is the TIME_WAIT/receive timer when we are doing IP */ struct timeval stamp; /* identd */ //一個套接在在不同的層次上分別由socket結構和sock結構表示 struct socket *socket; /* Callbacks *///回調函數 void (*state_change)(struct sock *sk); void (*data_ready)(struct sock *sk,int bytes); void (*write_space)(struct sock *sk); void (*error_report)(struct sock *sk); };
4.send/recv過程分析
4.1 應用層
4.1.1發送端
-
絡應用調用Socket API socket (int family, int type, int protocol) 創建一個 socket,該調用最終會調用 Linux system call socket() ,並最終調用 Linux Kernel 的 sock_create() 方法。該方法返回被創建好了的那個 socket 的 file descriptor。對於每一個 userspace 網絡應用創建的 socket,在內核中都有一個對應的 struct socket和 struct sock。其中,struct sock 有三個隊列(queue),分別是 rx , tx 和 err,在 sock 結構被初始化的時候,這些緩沖隊列也被初始化完成;在收據收發過程中,每個 queue 中保存要發送或者接受的每個 packet 對應的 Linux 網絡棧 sk_buffer 數據結構的實例 skb。
-
對於 TCP socket 來說,應用調用 connect()API ,使得客戶端和服務器端通過該 socket 建立一個虛擬連接。在此過程中,TCP 協議棧通過三次握手會建立 TCP 連接。默認地,該 API 會等到 TCP 握手完成連接建立后才返回。在建立連接的過程中的一個重要步驟是,確定雙方使用的 Maxium Segemet Size (MSS)。因為 UDP 是面向無連接的協議,因此它是不需要該步驟的。
-
應用調用 Linux Socket 的 send 或者 write API 來發出一個 message 給接收端
-
sock_sendmsg 被調用,它使用 socket descriptor 獲取 sock struct,創建 message header 和 socket control message
-
_sock_sendmsg 被調用,根據 socket 的協議類型,調用相應協議的發送函數。
-
對於 TCP ,調用 tcp_sendmsg 函數。
-
對於 UDP 來說,userspace 應用可以調用 send()/sendto()/sendmsg() 三個 system call 中的任意一個來發送 UDP message,它們最終都會調用內核中的 udp_sendmsg() 函數。
-
以TCP為例,編譯代碼可得如下所示:
4.1.2接收端
-
每當用戶應用調用 read 或者 recvfrom 時,該調用會被映射為/net/socket.c 中的 sys_recv 系統調用,並被轉化為 sys_recvfrom 調用,然后調用 sock_recgmsg 函數。
-
對於 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被調用,它會調用相關協議的數據接收方法。
-
對 TCP 來說,調用 tcp_recvmsg。該函數從 socket buffer 中拷貝數據到 user buffer。
-
對 UDP 來說,從 user space 中可以調用三個 system call recv()/recvfrom()/recvmsg() 中的任意一個來接收 UDP package,這些系統調用最終都會調用內核中的 udp_recvmsg 方法。
4.2傳輸層
4.2.1發送端
傳輸層的最終目的是向它的用戶提供高效的、可靠的和成本有效的數據傳輸服務,主要功能包括 (1)構造 TCP segment (2)計算 checksum (3)發送回復(ACK)包 (4)滑動窗口(sliding windown)等保證可靠性的操作。
TCP 棧簡要過程:
-
tcp_sendmsg 函數會首先檢查已經建立的 TCP connection 的狀態,然后獲取該連接的 MSS,開始 segement 發送流程。
-
構造 TCP 段的 playload:它在內核空間中創建該 packet 的 sk_buffer 數據結構的實例 skb,從 userspace buffer 中拷貝 packet 的數據到 skb 的 buffer。
-
構造 TCP header。
-
計算 TCP 校驗和(checksum)和 順序號 (sequence number)。
-
TCP 校驗和是一個端到端的校驗和,由發送端計算,然后由接收端驗證。其目的是為了發現TCP首部和數據在發送端到接收端之間發生的任何改動。如果接收方檢測到校驗和有差錯,則TCP段會被直接丟棄。TCP校驗和覆蓋 TCP 首部和 TCP 數據。
-
TCP的校驗和是必需的
-
-
發到 IP 層處理:調用 IP handler 句柄 ip_queue_xmit,將 skb 傳入 IP 處理流程。
UDP 棧簡要過程:
-
UDP 將 message 封裝成 UDP 數據報
-
調用 ip_append_data() 方法將 packet 送到 IP 層進行處理。
以TCP為例,可得如下圖所示:
相應部分函數代碼截圖下:
4.2.2接收端
-
傳輸層 TCP 處理入口在 tcp_v4_rcv 函數(位於 linux/net/ipv4/tcp ipv4.c 文件中),它會做 TCP header 檢查等處理。
-
調用 _tcp_v4_lookup,查找該 package 的 open socket。如果找不到,該 package 會被丟棄。接下來檢查 socket 和 connection 的狀態。
-
如果socket 和 connection 一切正常,調用 tcp_prequeue 使 package 從內核進入 user space,放進 socket 的 receive queue。然后 socket 會被喚醒,調用 system call,並最終調用 tcp_recvmsg 函數去從 socket recieve queue 中獲取 segment。
4.3網絡層
4.3.1發送端
網絡層的任務就是選擇合適的網間路由和交換結點, 確保數據及時傳送。網絡層將數據鏈路層提供的幀組成數據包,包中封裝有網絡層包頭,其中含有邏輯地址信息- -源站點和目的站點地址的網絡地址。其主要任務包括 (1)路由處理,即選擇下一跳 (2)添加 IP header(3)計算 IP header checksum,用於檢測 IP 報文頭部在傳播過程中是否出錯 (4)可能的話,進行 IP 分片(5)處理完畢,獲取下一跳的 MAC 地址,設置鏈路層報文頭,然后轉入鏈路層處理。
IP 棧基本處理過程如下:
-
首先,ip_queue_xmit(skb)會檢查skb->dst路由信息。如果沒有,比如套接字的第一個包,就使用ip_route_output()選擇一個路由。
-
接着,填充IP包的各個字段,比如版本、包頭長度、TOS等。
-
中間的一些分片等,可參閱相關文檔。基本思想是,當報文的長度大於mtu,gso的長度不為0就會調用 ip_fragment 進行分片,否則就會調用ip_finish_output2把數據發送出去。ip_fragment 函數中,會檢查 IP_DF 標志位,如果待分片IP數據包禁止分片,則調用 icmp_send()向發送方發送一個原因為需要分片而設置了不分片標志的目的不可達ICMP報文,並丟棄報文,即設置IP狀態為分片失敗,釋放skb,返回消息過長錯誤碼。
-
接下來就用 ip_finish_ouput2 設置鏈路層報文頭了。如果,鏈路層報頭緩存有(即hh不為空),那就拷貝到skb里。如果沒,那么就調用neigh_resolve_output,使用 ARP 獲取。
下圖為相關函數的運行流程:
4.3.2接收端
-
IP 層的入口函數在 ip_rcv 函數。該函數首先會做包括 package checksum 在內的各種檢查,如果需要的話會做 IP defragment(將多個分片合並),然后 packet 調用已經注冊的 Pre-routing netfilter hook ,完成后最終到達 ip_rcv_finish 函數。
-
ip_rcv_finish 函數會調用 ip_router_input 函數,進入路由處理環節。它首先會調用 ip_route_input 來更新路由,然后查找 route,決定該 package 將會被發到本機還是會被轉發還是丟棄:
-
-
如果是發到本機的話,調用 ip_local_deliver 函數,可能會做 de-fragment(合並多個 IP packet),然后調用 ip_local_deliver 函數。該函數根據 package 的下一個處理層的 protocal number,調用下一層接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。對於 TCP 來說,函數 tcp_v4_rcv 函數會被調用,從而處理流程進入 TCP 棧。
-
如果需要轉發 (forward),則進入轉發流程。該流程需要處理 TTL,再調用 dst_input 函數。該函數會 處理 Netfilter Hook;執行 IP fragmentation;調用 dev_queue_xmit,進入鏈路層處理流程。
-
4.4數據鏈路層和物理層
4.4.1發送端
功能上,在物理層提供比特流服務的基礎上,建立相鄰結點之間的數據鏈路,通過差錯控制提供數據幀(Frame)在信道上無差錯的傳輸,並進行各電路上的動作系列。數據鏈路層在不可靠的物理介質上提供可靠的傳輸。該層的作用包括:物理地址尋址、數據的成幀、流量控制、數據的檢錯、重發等。在這一層,數據的單位稱為幀(frame)。數據鏈路層協議的代表包括:SDLC、HDLC、PPP、STP、幀中繼等。實現上,Linux 提供了一個 Network device 的抽象層,其實現在 linux/net/core/dev.c。具體的物理網絡設備在設備驅動中(driver.c)需要實現其中的虛函數。Network Device 抽象層調用具體網絡設備的函數。
物理層在收到發送請求之后,通過 DMA 將該主存中的數據拷貝至內部RAM(buffer)之中。在數據拷貝中,同時加入符合以太網協議的相關header,IFG、前導符和CRC。對於以太網網絡,物理層發送采用CSMA/CD,即在發送過程中偵聽鏈路沖突。一旦網卡完成報文發送,將產生中斷通知CPU,然后驅動層中的中斷處理程序就可以刪除保存的 skb 了。
4.4.2接收端
-
一個 package 到達機器的物理網絡適配器,當它接收到數據幀時,就會觸發一個中斷,並將通過 DMA 傳送到位於 linux kernel 內存中的 rx_ring。
-
網卡發出中斷,通知 CPU 有個 package 需要它處理。中斷處理程序主要進行以下一些操作,包括分配 skb_buff 數據結構,並將接收到的數據幀從網絡適配器I/O端口拷貝到skb_buff 緩沖區中;從數據幀中提取出一些信息,並設置 skb_buff 相應的參數,這些參數將被上層的網絡協議使用,例如skb->protocol;
-
終端處理程序經過簡單處理后,發出一個軟中斷(NET_RX_SOFTIRQ),通知內核接收到新的數據幀。
-
內核 2.5 中引入一組新的 API 來處理接收的數據幀,即 NAPI。所以,驅動有兩種方式通知內核:(1) 通過以前的函數netif_rx;(2)通過NAPI機制。該中斷處理程序調用 Network device的 netif_rx_schedule 函數,進入軟中斷處理流程,再調用 net_rx_action 函數。
-
該函數關閉中斷,獲取每個 Network device 的 rx_ring 中的所有 package,最終 pacakage 從 rx_ring 中被刪除,進入 netif _receive_skb 處理流程。
-
netif_receive_skb 是鏈路層接收數據報的最后一站。它根據注冊在全局數組 ptype_all 和 ptype_base 里的網絡層數據報類型,把數據報遞交給不同的網絡層協議的接收函數(INET域中主要是ip_rcv和arp_rcv)。該函數主要就是調用第三層協議的接收函數處理該skb包,進入第三層網絡層處理。
5.時序圖