TCP/IP協議棧在Linux內核中的運行時序分析
1.網絡體系結構模型
1.1.OSI七層模型
OSI模型是由國際化標准組織ISO提出的網絡體系結構模型。被稱為開放系統互聯參考模型。OSI模型總共有7層。自上而下依次為應用層、表示層、會話層、傳輸層、網絡層、數據鏈路層和物理層。七層模型結構清晰。共同完成數據的傳輸和處理工作。
在OSI模型中,各層的功能大致如下:
應用層為特定類型的網絡應用提供訪問OSI環境的手段。
表示層主要處理在兩個通信系統中交換信息的表示方式。
會話層允許不同主機上的各個進程之間進行會話。
傳輸層負責不同主機之間兩個進程的通信。功能是為端到端連接提供可靠的傳輸服務。
網絡層的主要任務是把網絡層的協議數據單元從源端傳遞到目的端,為分組交換網絡上的不同主機提供通信服務。
數據鏈路層的主要任務是將網絡層傳來的IP數據報封裝成幀,透明傳輸和差錯控制等。
物理層的任務是透明的傳輸比特流。
1.2.TCP/IP分層模型
ARPA在研究ARPAnet時提出了TCP/IP模型,模型從低到高依次為網絡接口層(對應OSI參考模型中的物理層和數據鏈路層)、網際層、傳輸層和應用層(對應OSI參考模型中的會話層、表示層和應用層)。TCP/IP由於得到廣泛應用而成為事實上的國際標准。其中,TCP/IP模型和OSI模型的層次結構及相關的協議如圖所示
網絡接口層的功能類似於OSI的物理層和數據鏈路層。它表示與物理網絡的接口,但實際上TCP/IP本身並未真正描述這一部分,只是指出主機必須使用某種協議與網絡連接,以便在其上傳遞IP分組。
網際層(主機-主機)是TCP/IP體系結構的關鍵部分。它和OSI網絡層在功能上非常相似。網際層將分組發往任何網絡,並為之獨立地選擇合適的路由,但它不保證各個分組有序地到達,各個分組的有序交付由高層負責。網際層定義了標准的分組格式和協議,即IP。當前采用的IP協議是第4版,即IPv4,它的下一-版本是IPv6。
傳輸層(應用-應用或進程進程)的功能同樣和OSI中的傳輸層類似,即使得發送端和目的端主機上的對等實體進行會話。傳輸層主要使用以下兩種協議:
TCP:它是面向連接的,能夠提供可靠的交付。
UDP:它是無連接的,不保證可靠的交付,只能提供盡最大努力的交付
應用層包含所有的高層協議,如Telnet、FTP、DNS、SMTP、HTTP等協議。
由上圖可以看出,IP協議是因特網的核心協議;TCP/IP可以為各式各樣的應用提供服務,正因為如此,因特網才會發展到今天的規模。
2.Linux網絡子系統
Linux 網絡子系統的頂部是系統調用接口層。它為為應用程序提供訪問內核網絡子系統的方法,如Socket系統調用。位於其下面的是一個協議無關層,實現一組通用函數來訪問各種不同的協議:通過socket實現。然后是具體協議的實現,網絡協議層用於實現各種具體的網絡協議,如:TCP、UDP 等,當然還有IP。然后是設備無關層,設備無關接口將協議與各種網絡設備驅動連接在一起。這一層提供一組通用函數供底層網絡設備驅動程序使用,讓它們可以對高層協議棧進行操作。最下面是設備驅動程序, 負責管理物理網絡設備。
3.套接字Socket
3.1.Socket簡介
Socket 是指網絡上的兩個程序過一個雙向的通訊連接實現數據的交換,這個 雙向鏈路的一端稱之為一個 socket,socket 通常用來實現客戶方和服務方的連接。 socket 是 TCP/IP 協議的一個十分流行的編程界面,一個 Socket 由一個 IP 地址和 一個端口號唯一確定。 但是,Socket 所支持的協議種類也不光 TCP/IP 一種,因此兩者之間是沒有必 然聯系的。在 Java 中,socket 編程主要是基於 TCP/IP 協議的網絡編程。現在, TCP/IP 協議族中的主要 socket 類型為流套接字(使用 TCP 協議)和數據報套接字 (使用 UDP 協議)以及raw套接字。
3.2.Socket數據結構
用戶使用socket系統調用編寫應用程序時,通過一個數字來表示一個socket,所有的操作都在該數字上進行,這個數字稱為套接字描述符。在系統調用 的實現函數里,這個數字就會被映射成一個表示socket的結構體,該結構體保存了該socket的所有屬性和數據。
struct socket { socket_state state; //描述套接字的狀態 unsigned long flags; //套接字的一組標志位 const struct proto_ops *ops; //將套接字層調用於傳輸層協議對應 struct fasync_struct *fasync_list; //存儲異步通知隊列 struct file *file; //套接字相關聯的文件指針 struct sock *sk; //套接字相關聯的傳輸控制塊 wait_queue_head_t wait; //等待套接字的進程列表 short type; //套接字的類型 }; state用於表示socket所處的狀態,是一個枚舉變量,其類型定義如下: typedef enum { SS_FREE = 0, //該socket還未分配 SS_UNCONNECTED, //未連向任何socket SS_CONNECTING, //正在連接過程中 SS_CONNECTED, //已連向一個socket SS_DISCONNECTING //正在斷開連接的過程中 }socket_state;
3.3.Socketcall系統調用
socketcall()是Linux操作系統中所有的socket系統調用的總入口。sys_soketcall()函數的第一個參數即為具體的操作碼,其中,每個數字可以代表一個操作碼,一共17種,函數中正是通過操作碼來跳轉到真正的系統調用函數的。具體操作碼對應情況如下:
#define SYS_SOCKET 1 /* sys_socket(2) */ #define SYS_BIND 2 /* sys_bind(2) */ #define SYS_CONNECT 3 /* sys_connect(2) */ #define SYS_LISTEN 4 /* sys_listen(2) */ #define SYS_ACCEPT 5 /* sys_accept(2) */ #define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */ #define SYS_GETPEERNAME 7 /* sys_getpeername(2) */ #define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */ #define SYS_SEND 9 /* sys_send(2) */ #define SYS_RECV 10 /* sys_recv(2) */ #define SYS_SENDTO 11 /* sys_sendto(2) */ #define SYS_RECVFROM 12 /* sys_recvfrom(2) */ #define SYS_SHUTDOWN 13 /* sys_shutdown(2) */ #define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */ #define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */ #define SYS_SENDMSG 16 /* sys_sendmsg(2) */ #define SYS_RECVMSG 17 /* sys_recvmsg(2) */ sys_socketcall()函數的源代碼在net/socket.c中,具體如下: asmlinkage long sys_socketcall(int call, unsigned long __user *args) { unsigned long a[6]; unsigned long a0, a1; int err; .......................................... a0 = a[0]; a1 = a[1]; switch (call) { case SYS_SOCKET: err = sys_socket(a0, a1, a[2]); break; case SYS_BIND: err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_LISTEN: err = sys_listen(a0, a1); break; case SYS_ACCEPT: err = do_accept(a0, (struct sockaddr __user *)a1, (int __user *)a[2], 0); break; case SYS_GETSOCKNAME: err = sys_getsockname(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; ..................................... return err; }
可以看到sys_socketcall()函數就是通過傳遞進來的call類型,來調用相應的socket相關的函數。而這里並沒有出現我們所熟知得write、read、aio、poll等系統調用函數,這是因為socket上面其實還有一層VFS層,內核把socket當做一個文件系統來處理,並且實現了相應得VFS方法。
下圖顯示了使用上述系統調用函數的順序,圖中藍色大方框中的IO系統調用函數可以在任何時候調用。注意,給出的圖中不是一個完整的狀態流程圖,僅顯示了一些常見的系統調用函數:
4.應用層分析
4.1發送數據
應用層發送數據時,sys_send實際只是對sys_sendto的簡單封裝。sys_sendto函數的作用是將數據報發送至指定的目的地址。而sys_sendto是通過調用sys_sendmsg函數進行發送數據的。sys_sendmsg函數的作用是將用戶空間的信息復制到內核空間中,然后再逐級調用發包接口發送數據。 sys_sendmsg最終調用了sock_sendmsg()。
sys_sendmsg函數部分源代碼如下:
asmlinkage long sys_sendmsg(int fd, struct msghdr __user *msg, unsigned flags) {
.......
//64位系統向32位系統兼容時,需將控制緩存信息進行復制 if ((MSG_CMSG_COMPAT & flags) && ctl_len) { err = cmsghdr_from_user_compat_to_kern(&msg_sys, ctl, sizeof(ctl)); if (err) goto out_freeiov; ctl_buf = msg_sys.msg_control; } else if (ctl_len) { //在32位系統中復制控制緩存信息 if (ctl_len > sizeof(ctl)) { ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL); if (ctl_buf == NULL) goto out_freeiov; } err = -EFAULT; /* * Careful! Before this, msg_sys.msg_control contains a user pointer. * Afterwards, it will be a kernel pointer. Thus the compiler-assisted * checking falls down on this. */ if (copy_from_user(ctl_buf, (void __user *) msg_sys.msg_control, ctl_len)) goto out_freectl; msg_sys.msg_control = ctl_buf; } msg_sys.msg_flags = flags; //設置數據發送標識 if (sock->file->f_flags & O_NONBLOCK) msg_sys.msg_flags |= MSG_DONTWAIT; //調用sock_sendmsg函數發送報文 err = sock_sendmsg(sock, &msg_sys, total_len); out_freectl: //釋放臨時申請的緩沖區 if (ctl_buf != ctl) sock_kfree_s(sock->sk, ctl_buf, ctl_len); out_freeiov: if (iov != iovstack) sock_kfree_s(sock->sk, iov, iov_size); out_put: sockfd_put(sock); out: return err; }
......
sock_sendmsg()又調用了__sock_sendmsg.__sock_sendmsg實際上又調用了sock->ops->sendmsg函數
static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size) { int err = security_socket_sendmsg(sock, msg, size); return err ?: __sock_sendmsg_nosec(iocb, sock, msg, size); } static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size) { struct sock_iocb *si = kiocb_to_siocb(iocb); si->sock = sock; si->scm = NULL; si->msg = msg; si->size = size; /* 調用Socket層的操作函數,如果是SOCK_STREAM,則proto_ops為inet_stream_ops, * 函數指針指向inet_sendmsg()。 */ return sock->ops->sendmsg(iocb, sock, msg, size); }
sock-ops->sendmsg的意思就是根據不同的協議調用不同的sendmsg函數,對於 TCP ,調用tcp_sendmsg函數。
通過以上的分析,且由於應用層的系統調用過程比價清晰,給出sendmsg系統調用的過程如圖所示:
gdb跟蹤調試結果如下:
4.2接收數據
接收數據與發送數據的系統調用過程十分相似,它們都歸結到sock_sendmsg()和sock_recvmsg()兩個函數上。不同的只是數據的流向相反而已。下面是recvmsg的系統調用過程:
gdb跟蹤調試結果如下:
5.傳輸層分析
5.1發送數據
TCP協議對發送數據相關系統調用內核實現,雖然發送相關的系統調用接口由很多,但是到了TCP協議層,都統一由tcp_sendmsg()處理。tcp_sendmsg()函數要完成的工作就是將應用程序要發送的數據組織成skb,然后調用tcp_push函數。
發送數據部分的源代碼:
... //查看是否立即發送數據包 if (forced_push(tp)) { //設置立即發送數據包標志PSH tcp_mark_push(tp, skb); //發送數據包,實際調用的是ip_queue_xmit __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH); } else if (skb == tcp_send_head(sk)) tcp_push_one(sk, mss_now); continue; wait_for_sndbuf: //緩沖數據段,等到一定數量再發送 set_bit(SOCK_NOSPACE, &sk->sk_socket->flags); wait_for_memory: if (copied) tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH); if ((err = sk_stream_wait_memory(sk, &timeo)) != 0) goto do_error; mss_now = tcp_send_mss(sk, &size_goal, flags); } } ...
tcp_push()在判斷了是否需要設置PUSH標記位之后,會調用__tcp_push_pending_frames()
static inline void tcp_push(struct sock *sk, int flags, int mss_now, int nonagle) { struct tcp_sock *tp = tcp_sk(sk); if (tcp_send_head(sk)) { //判斷是否需要設置PUSH標記 struct sk_buff *skb = tcp_write_queue_tail(sk); if (!(flags & MSG_MORE) || forced_push(tp)) tcp_mark_push(tp, skb); //MSG_OOB相關,忽略 tcp_mark_urg(tp, flags, skb); //調用__tcp_push_pending_frames()嘗試發送 __tcp_push_pending_frames(sk, mss_now, (flags & MSG_MORE) ? TCP_NAGLE_CORK : nonagle); } } __tcp_push_pending_frames()調用調用tcp_write_xmit()完成發送。 /* Push out any pending frames which were held back due to * TCP_CORK or attempt at coalescing tiny packets. * The socket must be locked by the caller. */ void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle) { struct sk_buff *skb = tcp_send_head(sk); //如果有新數據可供發送,調用tcp_write_xmit()發送 if (skb) { if (tcp_write_xmit(sk, cur_mss, nonagle)) //和PMTU相關 tcp_check_probe_timer(sk); } }
tcp_write_xmit()該函數是TCP發送新數據的核心函數,包括發送窗口判斷、擁塞控制判斷等核心操作都是在該函數中完成。它又將調用發送函數tcp_transmit_skb函數。
tcp_transmit_skb的作用是復制或者拷貝skb,構造skb中的tcp首部,並將調用網絡層的發送函數發送skb;在發送前,首先需要克隆或者復制skb,因為在成功發送到網絡設備之后,skb會釋放,而tcp層不能真正的釋放,是需要等到對該數據段的ack才可以釋放;然后構造tcp首部和選項;最后調用網絡層提供的發送回調函數發送skb,ip層的回調函數為ip_queue_xmit;
gdb調試結果如下:
5.2接收數據
tcp_v4_rcv函數為TCP的總入口,數據包從IP層傳遞上來,進入該函數。tcp_v4_rcv函數只要做以下幾個工作:(1) 設置TCP_CB (2) 查找控制塊 (3)根據控制塊狀態做不同處理,包括TCP_TIME_WAIT狀態處理,TCP_NEW_SYN_RECV狀態處理,TCP_LISTEN狀態處理 (4) 接收TCP段;當狀態為TCP_LISTEN調用tcp_v4_do_rcv
/* LISTEN狀態處理 */ if (sk->sk_state == TCP_LISTEN) { ret = tcp_v4_do_rcv(sk, skb); goto put_and_return; }
對於tcp_v4_do_rcv()函數,如果狀態為ESTABLISHED,即已連接狀態,就會調用tcp_rcv_established()函數
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */ struct dst_entry *dst = sk->sk_rx_dst; sock_rps_save_rxhash(sk, skb); sk_mark_napi_id(sk, skb); if (dst) { if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif || !dst->ops->check(dst, 0)) { dst_release(dst); sk->sk_rx_dst = NULL; } } tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len); return 0; }
tcp_rcv_established用於處理已連接狀態下的輸入,處理過程根據首部預測字段分為快速路徑和慢速路徑;在快路中,對是有有數據負荷進行不同處理;在慢路中,會進行更詳細的校驗,然后處理ack,處理緊急數據,接收數據段,其中數據段可能包含亂序的情況,最后進行是否有數據和ack的發送檢查;當一切正常時,調用tcp_data_queue()方法將報文放入隊列中。
tcp_data_queue作用為數據段的接收處理,當存在預期接收的數據段,就會有以下幾種處理方式:
預期接收的數據段,a. 進行0窗口判斷;b. 進程上下文,復制數據到用戶空間;c. 不滿足b或者b未完整拷貝此skb的數據段,則加入到接收隊列;d. 更新下一個期望接收的序號;e. 若有fin標記,則處理fin;f. 亂序隊列不為空,則處理亂序;g. 快速路徑的檢查和設置;h. 喚醒用戶空間進程讀取數據;
部分代碼如下:
......
/* 預期接收的數據段 */ if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) { /* 窗口為0,不能接收數據 */ if (tcp_receive_window(tp) == 0) goto out_of_window; /* Ok. In sequence. In window. */ /* 進程上下文 */ /* 當前進程讀取數據 */ if (tp->ucopy.task == current && /* 用戶空間讀取序號與接收序號一致&& 需要讀取的數據不為0 */ tp->copied_seq == tp->rcv_nxt && tp->ucopy.len && /* 被用戶空間鎖定&& 無緊急數據 */ sock_owned_by_user(sk) && !tp->urg_data) { /* 帶讀取長度和數據段長度的較小值 */ int chunk = min_t(unsigned int, skb->len, tp->ucopy.len); /* 設置running狀態 */ __set_current_state(TASK_RUNNING); /* 拷貝數據 */ if (!skb_copy_datagram_msg(skb, 0, tp->ucopy.msg, chunk)) { tp->ucopy.len -= chunk; tp->copied_seq += chunk; /* 完整讀取了該數據段 */ eaten = (chunk == skb->len); /* 調整接收緩存和窗口 */ tcp_rcv_space_adjust(sk); } } ....... }
.......
gdb調試結果如下:
6.網絡層分析:
6.1.發送數據
ip_queue_xmit()是ip層提供給tcp層發送回調,大多數tcp發送都會使用這個回調,tcp層使用tcp_transmit_skb封裝了tcp頭之后,調用該函數,該函數提供了路由查找校驗、封裝ip頭和ip選項的功能。
ip_queue_xmit()完成面向連接套接字的包輸出,當套接字處於連接狀態時,所有從套接字發出的包都具有確定的路由, 無需為每一個輸出包查詢它的目的入口,可將套接字直接綁定到路由入口上, 這由套接字的目的緩沖指針(dst_cache)來完成.ip_queue_xmit()首先為輸入包建立IP包頭, 經過本地包過濾器后,再將IP包分片輸出(ip_fragment)。
ip_queue_xmit調用函數__ip_queue_xmit()。在__ip_queue中,會調用skb_rtable函數來檢查skb是否已被路由,即獲取其緩存信息,將緩存信息保存在變量rt中,如果rt不為空,就直接進行packet_routed函數,如果rt不為空,就會自行ip_route_output_ports查找路由緩存。
packet_routed代碼段首先先進行嚴格源路由選項的處理。如果存在嚴格源路由選項,並且數據包的下一跳地址和網關地址不一致,則丟棄該數據包(goto no_route)。如果沒問題,就進行ip頭部設置,設置完成后調用ip_local_out函數。ip_local_out
函數內部調用__ip_local_out
。發現返回一個nf_hook
函數,里面調用了dst_output。
packet_routed: if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)//處理嚴路由選項,下一跳地址必須為網管地址 goto no_route; iph = (struct iphdr *) skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));//移動skb->data向上,准備填寫ip報頭 *((__u16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));//設置報頭長度為20字節,選項的長度,在ip_options_build中增加 iph->tot_len = htons(skb->len);//總長度,skb中所有數據的長度,包括報頭,選項,有效載荷 if (ip_dont_fragment(sk, &rt->u.dst) && !ipfragok)//檢查禁止了分片 iph->frag_off = htons(IP_DF);//設置報頭指示禁止分片 else iph->frag_off = 0;//分片中的偏移量 iph->ttl = ip_select_ttl(inet, &rt->u.dst);//生存周期 iph->protocol = sk->sk_protocol;//指示ip上層的l4協議類型 iph->saddr = rt->rt_src;//源地址,目的地址 iph->daddr = rt->rt_dst; skb->nh.iph = iph; if (opt && opt->optlen) {//sock中設置了選項 iph->ihl += opt->optlen >> 2; ip_options_build(skb, opt, inet->daddr, rt, 0);//處理選項 } ip_select_ident_more(iph, &rt->u.dst, sk, skb_shinfo(skb)->tso_segs);//為ip選擇id ip_send_check(iph);//ip報頭校驗和 skb->priority = sk->sk_priority;//skb的優先級為sock的優先級,在dev_queue_xmit中用於在規則隊列中排隊 return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, dst_output);//調用dst->output,由ip_route_output_flow設置
ip_local_out函數調用了__ip_local_out函數,而__ip_local_out函數最終調用的是nf_hook函數並在里面調用了dst_output函數。
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb) { struct iphdr *iph = ip_hdr(skb); /* 設置總長度 */ iph->tot_len = htons(skb->len); /* 計算校驗和 */ ip_send_check(iph); /* if egress device is enslaved to an L3 master device pass the * skb to its handler for processing */ skb = l3mdev_ip_out(sk, skb); if (unlikely(!skb)) return 0; /* 設置ip協議 */ skb->protocol = htons(ETH_P_IP); /* 經過NF的LOCAL_OUT鈎子點 */ return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, net, sk, skb, NULL, skb_dst(skb)->dev, dst_output); }
dst_output函數調用了ip_output函數,而ip_output函數調用了ip_finish_output函數。這個函數實際上調用的是__ip_finish_output函數。緊接着, ip_finish_output調用了 ip_finish_output2(),ip_finish_output2函數會檢測skb的前部空間是否還能存儲鏈路層首部。如果不夠,就會申請更大的存儲空間,最終會調用鄰居子系統的輸出函數neigh_output進行輸出。輸出分為有二層頭緩存和沒有兩種情況,有緩存時調用neigh_hh_output進行快速輸出,沒有緩存時,則調用鄰居子系統的輸出回調函數進行慢速輸出。不管執行哪個函數,最終都會調用dev_queue_xmit將數據包傳入數據鏈路層。
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb) { const struct hh_cache *hh = &n->hh; /* 連接狀態 &&緩存的頭部存在,使用緩存輸出 */ if ((n->nud_state & NUD_CONNECTED) && hh->hh_len) return neigh_hh_output(hh, skb); /* 使用鄰居項的輸出回調函數輸出,在連接或者非連接狀態下有不同的輸出函數 */ else return n->output(n, skb); }
gdb調試結果如下
6.2.接收數據
網絡IP層的入口函數在ip_rcv函數。ip_rcv函數調用第三層協議的接收函數處理該skb包,進入第三層網絡層處理。ip_rcv數首先會檢查檢驗和等各種字段,如果數據包的長度超過最大傳送單元MTU的話,會進行分片,最終到達 ip_rcv_finish 函數。
部分代碼如下:
/* 取得傳輸層頭部 */ skb->transport_header = skb->network_header + iph->ihl*4; /* Remove any debris in the socket control block */ /* 重置cb */ memset(IPCB(skb), 0, sizeof(struct inet_skb_parm)); /* 保存輸入設備信息 */ IPCB(skb)->iif = skb->skb_iif; /* Must drop socket now because of tproxy. */ skb_orphan(skb); /* 經過PRE_ROUTING鈎子點 */ return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, net, NULL, skb, dev, NULL, ip_rcv_finish);
ip_rcv_finish最終會調用dst_input函數,這個函數調用的是ip_input函數,當緩存查找沒有匹配路由時將調用ip_route_input_slow(),決定該 package 將會被發到本機還是會被轉發還是丟棄。
如果是發到本機的將會執行ip_local_deliver函數。可能會做 de-fragment(合並多個包),並調用ip_local_deliver_finish。ip_local_deliver_finish會調用ip_protocol_deliver_rcu函數
/* 如果忽略掉原始套接字和IPSec,則該函數僅僅是根據IP頭部中的協議字段選擇上層L4協議,並交給它來處理 */ static int ip_local_deliver_finish(struct sk_buff *skb) { /* 跳過IP頭部 */ __skb_pull(skb, ip_hdrlen(skb)); /* Point into the IP datagram, just past the header. */ /* 設置傳輸層頭部位置 */ skb_reset_transport_header(skb); rcu_read_lock(); { /* Note: See raw.c and net/raw.h, RAWV4_HTABLE_SIZE==MAX_INET_PROTOS */ int protocol = ip_hdr(skb)->protocol; int hash; struct sock *raw_sk; struct net_protocol *ipprot; resubmit: /* 這個hash根本不是哈希值,僅僅只是inet_protos數組中的下表而已 */ hash = protocol & (MAX_INET_PROTOS - 1); raw_sk = sk_head(&raw_v4_htable[hash]); /* If there maybe a raw socket we must check - if not we * don't care less */ /* 原始套接字?? 忽略... */ if (raw_sk && !raw_v4_input(skb, ip_hdr(skb), hash)) raw_sk = NULL; /* 查找注冊的L4層協議處理結構。 */ if ((ipprot = rcu_dereference(inet_protos[hash])) != NULL) { int ret; /* 啟用了安全策略,則交給IPSec */ if (!ipprot->no_policy) { if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { kfree_skb(skb); goto out; } nf_reset(skb); } /* 調用L4層協議處理函數 */ /* 通常會是tcp_v4_rcv, udp_rcv, icmp_rcv和igmp_rcv */ /* 如果注冊了其他的L4層協議處理,則會進行相應的調用。 */ ret = ipprot->handler(skb); if (ret < 0) { protocol = -ret; goto resubmit; } IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS); } else { if (!raw_sk) { /* 無原始套接字,提交給IPSec */ if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { IP_INC_STATS_BH(IPSTATS_MIB_INUNKNOWNPROTOS); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0); } } else IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS); kfree_skb(skb); } } out: rcu_read_unlock(); return 0; }
ip_protocol_deliver_rcu將輸入數據包從網絡層傳遞到傳輸層。
void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol) { const struct net_protocol *ipprot; int raw, ret; resubmit: raw = raw_local_deliver(skb, protocol); ipprot = rcu_dereference(inet_protos[protocol]); if (ipprot) { if (!ipprot->no_policy) { if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { kfree_skb(skb); return; } nf_reset_ct(skb); } ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, skb); if (ret < 0) { protocol = -ret; goto resubmit; } __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS); } else { if (!raw) { if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { __IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0); } kfree_skb(skb); } else { __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS); consume_skb(skb); } } }
gdb調試結果如下:
7.數據鏈路層分析:
7.1.發送數據
數據鏈路層的入口是dev_queue_xmit函數。這個函數調用的是__dev_queue_xmit函數。__dev_queue_xmit函數對不同類型的數據包進行不同的處理。__dev_queue_xmit會調用dev_hard_start_xmit函數獲取skb。
__dev_queue_xmit的部分代碼如下
/*此處是設備沒有Qdisc的,實際上沒有enqueue/dequeue的規則,無法進行擁塞控制的操作, *對於一些loopback/tunnel interface比較常見,判斷下設備是否處於UP狀態*/ if (dev->flags & IFF_UP) { int cpu = smp_processor_id(); /* ok because BHs are off */ if (txq->xmit_lock_owner != cpu) { if (__this_cpu_read(xmit_recursion) > RECURSION_LIMIT) goto recursion_alert; skb = validate_xmit_skb(skb, dev); if (!skb) goto drop; HARD_TX_LOCK(dev, txq, cpu); /*這個地方判斷一下txq不是stop狀態,那么就直接調用dev_hard_start_xmit函數來發送數據*/ if (!netif_xmit_stopped(txq)) { __this_cpu_inc(xmit_recursion); skb = dev_hard_start_xmit(skb, dev, txq, &rc); __this_cpu_dec(xmit_recursion); if (dev_xmit_complete(rc)) { HARD_TX_UNLOCK(dev, txq); goto out; } } HARD_TX_UNLOCK(dev, txq); net_crit_ratelimited("Virtual device %s asks to queue packet!\n", dev->name); } else { /* Recursion is detected! It is possible, * unfortunately */ recursion_alert: net_crit_ratelimited("Dead loop on virtual device %s, fix it urgently!\n", dev->name); } }
dev_hard_start_xmit函數會循環調用xmit_one函數,直到將待輸出的數據包提交給網絡設備的輸出接口,完成數據包的輸出。
在xmit_one中調用__net_dev_start_xmit函數。一旦網卡完成報文發送,將產生中斷通知 CPU,然后驅動層中的中斷處理程序就可以刪除保存的 skb。
static int xmit_one(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq, bool more) { unsigned int len; int rc; /*如果有抓包的工具的話,這個地方會進行抓包,such as Tcpdump*/ if (!list_empty(&ptype_all)) dev_queue_xmit_nit(skb, dev); len = skb->len; trace_net_dev_start_xmit(skb, dev); /*調用netdev_start_xmit,快到driver的tx函數了*/ rc = netdev_start_xmit(skb, dev, txq, more); trace_net_dev_xmit(skb, rc, dev, len); return rc; }
7.2.接收數據
接受數據的入口函數是net_rx_action,在net_rx_action函數中會去調用設備的napi_poll函數, 它是設備自己注冊的.
static void net_rx_action(struct softirq_action *h) { struct list_head *list = &__get_cpu_var(softnet_data).poll_list; /*設置軟中斷處理程序一次允許的最大執行時間為2個jiffies*/ unsigned long time_limit = jiffies + 2; int budget = netdev_budget;//輪詢配額 當數據包數超過此值 進行輪詢接收 此值初始為300 void *have; //禁止本地cpu中斷 local_irq_disable(); while (!list_empty(list)) { struct napi_struct *n; int work, weight; if (unlikely(budget <= 0 || time_after(jiffies, time_limit))) goto softnet_break; local_irq_enable(); //取得等待輪詢設備的結構 n = list_entry(list->next, struct napi_struct, poll_list); have = netpoll_poll_lock(n); //獲得權重 處理此設備n的最大幀數 weight = n->weight; work = 0; if (test_bit(NAPI_STATE_SCHED, &n->state)) { //對於NAPI設備如e1000驅動此處為poll為e1000_clean() 進行輪詢處理數據 //對於非NAPI設備此處poll為process_backlog() 看net_dev_init()中的初始化 work = n->poll(n, weight); trace_napi_poll(n); } //返回值work需要小於等於weight work返回的是此設備處理的幀個數不能超過weight WARN_ON_ONCE(work > weight); budget -= work; //禁止本地中斷 local_irq_disable(); //消耗完了此設備的權重 if (unlikely(work == weight)) { //若此時設備被禁止了 則從鏈表刪除此設備 if (unlikely(napi_disable_pending(n))) { //開啟本地cpu中斷 local_irq_enable(); napi_complete(n); //關閉本地cpu中斷 local_irq_disable(); } else//否則將設備放到鏈表的最后面 list_move_tail(&n->poll_list, list); } netpoll_poll_unlock(have); } out: //開啟本地cpu中斷 local_irq_enable(); #ifdef CONFIG_NET_DMA dma_issue_pending_all(); #endif return; softnet_break: __get_cpu_var(netdev_rx_stat).time_squeeze++; //在這里觸發下一次軟中斷處理 __raise_softirq_irqoff(NET_RX_SOFTIRQ); goto out; }
在設備的napi_poll函數中, 它負責調用napi_gro_receive函數。
static int napi_poll(struct napi_struct *n, struct list_head *repoll) { void *have; int work, weight; list_del_init(&n->poll_list); have = netpoll_poll_lock(n); weight = n->weight; work = 0; if (test_bit(NAPI_STATE_SCHED, &n->state)) { work = n->poll(n, weight); //調用網卡注冊的poll函數 trace_napi_poll(n, work, weight); } WARN_ON_ONCE(work > weight); if (likely(work < weight)) goto out_unlock; if (unlikely(napi_disable_pending(n))) { napi_complete(n); goto out_unlock; } if (n->gro_list) { napi_gro_flush(n, HZ >= 1000); } if (unlikely(!list_empty(&n->poll_list))) { pr_warn_once("%s: Budget exhausted after napi rescheduled\n", n->dev ? n->dev->name : "backlog"); goto out_unlock; } list_add_tail(&n->poll_list, repoll); out_unlock: netpoll_poll_unlock(have); return work; }
napi_gro_receive用來將網卡上的數據包發給協議棧處理。它會調用 netif_receive_skb_core。而它會調用__netif_receive_skb_one_core,將數據包交給上層 ip_rcv 進行處理。
8.時序圖