網絡程序設計調研報告html
TCP/IP協議棧在Linux內核中的運行時序分析linux
姓名:柴浩宇算法
學號:SA20225105api
班級:軟設1班數組
2021年1月緩存
調研要求服務器
目錄網絡
1 調研要求數據結構
2 目錄架構
3 Linux概述
3.1 Linux操做系統架構簡介
3.2 協議棧簡介
3.3 Linux內核協議棧的實現
4 本次調研採起的代碼簡介
5 應用層流程
5.1 發送端
5.2 接收端
6 傳輸層流程
6.1 發送端
6.2 接收端
7 IP層流程
7.1 發送端
7.2 接收端
8 數據鏈路層流程
8.1 發送端
8.2 接收端
9 物理層流程
9.1 發送端
9.2 接收端
10 時序圖展現和總結
11 參考資料
正文
3 Linux概述
3.1 Linux操做系統架構簡介
Linux操做系統整體上由Linux內核和GNU系統構成,具體來說由4個主要部分構成,即Linux內核、Shell、文件系統和應用程序。內核、Shell和文件系統構成了操做系統的基本結構,使得用戶能夠運行程序、管理文件並使用系統。
內核是操做系統的核心,具備不少最基本功能,如虛擬內存、多任務、共享庫、需求加載、可執行程序和TCP/IP網絡功能。咱們所調研的工做,就是在Linux內核層面進行分析。
3.2 協議棧簡介
3.3 Linux內核協議棧
Linux的協議棧實際上是源於BSD的協議棧,它向上以及向下的接口以及協議棧自己的軟件分層組織的很是好。
Linux的協議棧基於分層的設計思想,總共分爲四層,從下往上依次是:物理層,鏈路層,網絡層,應用層。
物理層主要提供各類鏈接的物理設備,如各類網卡,串口卡等;鏈路層主要指的是提供對物理層進行訪問的各類接口卡的驅動程序,如網卡驅動等;網路層的做用是負責將網絡數據包傳輸到正確的位置,最重要的網絡層協議固然就是IP協議了,其實網絡層還有其餘的協議如ICMP,ARP,RARP等,只不過不像IP那樣被多數人所熟悉;傳輸層的做用主要是提供端到端,說白一點就是提供應用程序之間的通訊,傳輸層最著名的協議非TCP與UDP協議末屬了;應用層,顧名思義,固然就是由應用程序提供的,用來對傳輸數據進行語義解釋的「人機界面」層了,好比HTTP,SMTP,FTP等等,其實應用層還不是人們最終所看到的那一層,最上面的一層應該是「解釋層」,負責將數據以各類不一樣的表項形式最終呈獻到人們眼前。
Linux網絡核心架構Linux的網絡架構從上往下能夠分爲三層,分別是:
用戶空間的應用層。
內核空間的網絡協議棧層。
物理硬件層。
其中最重要最核心的固然是內核空間的協議棧層了。
Linux網絡協議棧結構Linux的整個網絡協議棧都構建與Linux Kernel中,整個棧也是嚴格按照分層的思想來設計的,整個棧共分爲五層,分別是 :
1,系統調用接口層,實質是一個面向用戶空間應用程序的接口調用庫,向用戶空間應用程序提供使用網絡服務的接口。
2,協議無關的接口層,就是SOCKET層,這一層的目的是屏蔽底層的不一樣協議(更準確的來講主要是TCP與UDP,固然還包括RAW IP, SCTP等),以便與系統調用層之間的接口能夠簡單,統一。簡單的說,無論咱們應用層使用什麼協議,都要經過系統調用接口來創建一個SOCKET,這個SOCKET實際上是一個巨大的sock結構,它和下面一層的網絡協議層聯繫起來,屏蔽了不一樣的網絡協議的不一樣,只吧數據部分呈獻給應用層(經過系統調用接口來呈獻)。
3,網絡協議實現層,毫無疑問,這是整個協議棧的核心。這一層主要實現各類網絡協議,最主要的固然是IP,ICMP,ARP,RARP,TCP,UDP等。這一層包含了不少設計的技巧與算法,至關的不錯。
4,與具體設備無關的驅動接口層,這一層的目的主要是爲了統一不一樣的接口卡的驅動程序與網絡協議層的接口,它將各類不一樣的驅動程序的功能統一抽象爲幾個特殊的動做,如open,close,init等,這一層能夠屏蔽底層不一樣的驅動程序。
5,驅動程序層,這一層的目的就很簡單了,就是創建與硬件的接口層。
能夠看到,Linux網絡協議棧是一個嚴格分層的結構,其中的每一層都執行相對獨立的功能,結構很是清晰。
其中的兩個「無關」層的設計很是棒,經過這兩個「無關」層,其協議棧能夠很是輕鬆的進行擴展。在咱們本身的軟件設計中,能夠吸取這種設計方法。
4 本次調研採起的代碼簡介
本文采用的測試代碼是一個很是簡單的基於socket的客戶端服務器程序,打開服務端並運行,再開一終端運行客戶端,二者創建鏈接並能夠發送hello\hi的信息,server端代碼以下:
#include <stdio.h> /* perror */ #include <stdlib.h> /* exit */ #include <sys/types.h> /* WNOHANG */ #include <sys/wait.h> /* waitpid */ #include <string.h> /* memset */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/socket.h> #include <errno.h> #include <arpa/inet.h> #include <netdb.h> /* gethostbyname */ #define true 1 #define false 0 #define MYPORT 3490 /* 監聽的端口 */ #define BACKLOG 10 /* listen的請求接收隊列長度 */ #define BUF_SIZE 1024 int main() { int sockfd; if ((sockfd = socket(PF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } struct sockaddr_in sa; /* 自身的地址信息 */ sa.sin_family = AF_INET; sa.sin_port = htons(MYPORT); /* 網絡字節順序 */ sa.sin_addr.s_addr = INADDR_ANY; /* 自動填本機IP */ memset(&(sa.sin_zero), 0, 8); /* 其他部分置0 */ if (bind(sockfd, (struct sockaddr *)&sa, sizeof(sa)) == -1) { perror("bind"); exit(1); } struct sockaddr_in their_addr; /* 鏈接對方的地址信息 */ unsigned int sin_size = 0; char buf[BUF_SIZE]; int ret_size = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&their_addr, &sin_size); if(ret_size == -1) { perror("recvfrom"); exit(1); } buf[ret_size] = '\0'; printf("recvfrom:%s", buf); }
client端代碼以下:
#include <stdio.h> /* perror */ #include <stdlib.h> /* exit */ #include <sys/types.h> /* WNOHANG */ #include <sys/wait.h> /* waitpid */ #include <string.h> /* memset */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/socket.h> #include <errno.h> #include <arpa/inet.h> #include <netdb.h> /* gethostbyname */ #define true 1 #define false 0 #define PORT 3490 /* Server的端口 */ #define MAXDATASIZE 100 /* 一次能夠讀的最大字節數 */ int main(int argc, char *argv[]) { int sockfd, numbytes; char buf[MAXDATASIZE]; struct hostent *he; /* 主機信息 */ struct sockaddr_in server_addr; /* 對方地址信息 */ if (argc != 2) { fprintf(stderr, "usage: client hostname\n"); exit(1); } /* get the host info */ if ((he = gethostbyname(argv[1])) == NULL) { /* 注意:獲取DNS信息時,顯示出錯須要用herror而不是perror */ /* herror 在新的版本中會出現警告,已經建議不要使用了 */ perror("gethostbyname"); exit(1); } if ((sockfd = socket(PF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); /* short, NBO */ server_addr.sin_addr = *((struct in_addr *)he->h_addr_list[0]); memset(&(server_addr.sin_zero), 0, 8); /* 其他部分設成0 */ if ((numbytes = sendto(sockfd, "Hello, world!\n", 14, 0, (struct sockaddr *)&server_addr, sizeof(server_addr))) == -1) { perror("sendto"); exit(1); } close(sockfd); return true; }
簡單來講,主要流程以下圖所示:
5 應用層流程
5.1 發送端
下面咱們具體結合Linux內核源碼進行一步步仔細分析:
根據上述分析可知,發送端首先建立socket,建立以後會經過send發送數據。具體到源碼級別,會經過send,sendto,sendmsg這些系統調用來發送數據,而上述三個函數底層都調用了sock_sendmsg。見下圖:
咱們再跳轉到__sys_sendto看看這個函數幹了什麼:
咱們能夠發現,它建立了兩個結構體,分別是:struct msghdr msg和struct iovec iov,這兩個結構體根據命名咱們能夠大體猜出是發送數據和io操做的一些信息,以下圖:
咱們再來看看__sys_sendto調用的sock_sendmsg函數執行了什麼內容:
發現調用了sock_sendmsg_nosec函數:
發現調用了inet_sendmsg函數:
至此,發送端調用完畢。咱們能夠經過gdb進行調試驗證:
恰好符合咱們的分析。
5.2 接收端
咱們結合源碼進行仔細分析:
接收端調用的是__sys_recvfrom函數:
__sys_recvfrom函數具體以下:
發現它調用了sock_recvmsg函數:
發現它調用了sock_recvmsg_nosec函數:
發現它調用了inet_recvmsg函數:
最後調用的是tcp_recvmsg這個系統調用。至此接收端調用分析完畢。
下面用gdb打斷點進行驗證:
驗證結果恰好符合咱們的調研。
6 傳輸層流程
6.1 發送端
傳輸層的最終目的是向它的用戶提供高效的、可靠的和成本有效的數據傳輸服務,主要功能包括 (1)構造 TCP segment (2)計算 checksum (3)發送回覆(ACK)包 (4)滑動窗口(sliding windown)等保證可靠性的操做。TCP 協議棧的大體處理過程以下圖所示:
TCP 棧簡要過程:
UDP 棧簡要過程:
下面咱們結合代碼依次分析:
根據咱們對應用層的追查能夠發現,傳輸層也是先調用send()->sendto()->sys_sento->sock_sendmsg->sock_sendmsg_nosec,咱們看下sock_sendmsg_nosec這個函數:
在應用層調用的是inet_sendmsg函數,在傳輸層根據後面的斷點能夠知道,調用的是sock->ops-sendmsg這個函數。而sendmsg爲一個宏,調用的是tcp_sendmsg,以下;
struct proto tcp_prot = { .name = "TCP", .owner = THIS_MODULE, .close = tcp_close, .pre_connect = tcp_v4_pre_connect, .connect = tcp_v4_connect, .disconnect = tcp_disconnect, .accept = inet_csk_accept, .ioctl = tcp_ioctl, .init = tcp_v4_init_sock, .destroy = tcp_v4_destroy_sock, .shutdown = tcp_shutdown, .setsockopt = tcp_setsockopt, .getsockopt = tcp_getsockopt, .keepalive = tcp_set_keepalive, .recvmsg = tcp_recvmsg, .sendmsg = tcp_sendmsg, ......
而tcp_sendmsg實際上調用的是
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
這個函數以下:
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size) { struct tcp_sock *tp = tcp_sk(sk);/*進行了強制類型轉換*/ struct sk_buff *skb; flags = msg->msg_flags; ...... if (copied) tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH, size_goal); }
在tcp_sendmsg_locked中,完成的是將全部的數據組織成發送隊列,這個發送隊列是struct sock結構中的一個域sk_write_queue,這個隊列的每個元素是一個skb,裏面存放的就是待發送的數據。而後調用了tcp_push()函數。結構體struct sock以下:
struct sock{ ... struct sk_buff_head sk_write_queue;/*指向skb隊列的第一個元素*/ ... struct sk_buff *sk_send_head;/*指向隊列第一個尚未發送的元素*/ }
在tcp協議的頭部有幾個標誌字段:URG、ACK、RSH、RST、SYN、FIN,tcp_push中會判斷這個skb的元素是否須要push,若是須要就將tcp頭部字段的push置一,置一的過程以下:
static void tcp_push(struct sock *sk, int flags, int mss_now, int nonagle, int size_goal) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; skb = tcp_write_queue_tail(sk); if (!skb) return; if (!(flags & MSG_MORE) || forced_push(tp)) tcp_mark_push(tp, skb); tcp_mark_urg(tp, flags); if (tcp_should_autocork(sk, skb, size_goal)) { /* avoid atomic op if TSQ_THROTTLED bit is already set */ if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) { NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING); set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags); } /* It is possible TX completion already happened * before we set TSQ_THROTTLED. */ if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize) return; } if (flags & MSG_MORE) nonagle = TCP_NAGLE_CORK; __tcp_push_pending_frames(sk, mss_now, nonagle); }
首先struct tcp_skb_cb
結構體存放的就是tcp的頭部,頭部的控制位爲tcp_flags
,經過tcp_mark_push
會將skb中的cb,也就是48個字節的數組,類型轉換爲struct tcp_skb_cb
,這樣位於skb的cb就成了tcp的頭部。tcp_mark_push以下:
static inline void tcp_mark_push(struct tcp_sock *tp, struct sk_buff *skb) { TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; tp->pushed_seq = tp->write_seq; } ... #define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0])) ... struct sk_buff { ... char cb[48] __aligned(8); ...
struct tcp_skb_cb { __u32 seq; /* Starting sequence number */ __u32 end_seq; /* SEQ + FIN + SYN + datalen */ __u8 tcp_flags; /* tcp頭部標誌,位於第13個字節tcp[13]) */ ...... };
而後,tcp_push
調用了__tcp_push_pending_frames(sk, mss_now, nonagle);
函數發送數據:
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle) { if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_mask(sk, GFP_ATOMIC))) tcp_check_probe_timer(sk); }
發現它調用了tcp_write_xmit函數來發送數據:
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; unsigned int tso_segs, sent_pkts; int cwnd_quota; int result; bool is_cwnd_limited = false, is_rwnd_limited = false; u32 max_segs; /*統計已發送的報文總數*/ sent_pkts = 0; ...... /*若發送隊列未滿,則準備發送報文*/ while ((skb = tcp_send_head(sk))) { unsigned int limit; if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) { /* "skb_mstamp_ns" is used as a start point for the retransmit timer */ skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache; list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue); tcp_init_tso_segs(skb, mss_now); goto repair; /* Skip network transmission */ } if (tcp_pacing_check(sk)) break; tso_segs = tcp_init_tso_segs(skb, mss_now); BUG_ON(!tso_segs); /*檢查發送窗口的大小*/ cwnd_quota = tcp_cwnd_test(tp, skb); if (!cwnd_quota) { if (push_one == 2) /* Force out a loss probe pkt. */ cwnd_quota = 1; else break; } if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) { is_rwnd_limited = true; break; ...... limit = mss_now; if (tso_segs > 1 && !tcp_urg_mode(tp)) limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle); if (skb->len > limit && unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE, skb, limit, mss_now, gfp))) break; if (tcp_small_queue_check(sk, skb, 0)) break; if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) break; ......
tcp_write_xmit
位於tcpoutput.c中,它實現了tcp的擁塞控制,而後調用了tcp_transmit_skb(sk, skb, 1, gfp)
傳輸數據,實際上調用的是__tcp_transmit_skb:
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt) { skb_push(skb, tcp_header_size); skb_reset_transport_header(skb); ...... /* 構建TCP頭部和校驗和 */ th = (struct tcphdr *)skb->data; th->source = inet->inet_sport; th->dest = inet->inet_dport; th->seq = htonl(tcb->seq); th->ack_seq = htonl(rcv_nxt); tcp_options_write((__be32 *)(th + 1), tp, &opts); skb_shinfo(skb)->gso_type = sk->sk_gso_type; if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) { th->window = htons(tcp_select_window(sk)); tcp_ecn_send(sk, skb, th, tcp_header_size); } else { /* RFC1323: The window in SYN & SYN/ACK segments * is never scaled. */ th->window = htons(min(tp->rcv_wnd, 65535U)); } ...... icsk->icsk_af_ops->send_check(sk, skb); if (likely(tcb->tcp_flags & TCPHDR_ACK)) tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt); if (skb->len != tcp_header_size) { tcp_event_data_sent(tp, sk); tp->data_segs_out += tcp_skb_pcount(skb); tp->bytes_sent += skb->len - tcp_header_size; } if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq) TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS, tcp_skb_pcount(skb)); tp->segs_out += tcp_skb_pcount(skb); /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */ skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb); skb_shinfo(skb)->gso_size = tcp_skb_mss(skb); /* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */ /* Cleanup our debris for IP stacks */ memset(skb->cb, 0, max(sizeof(struct inet_skb_parm), sizeof(struct inet6_skb_parm))); err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); ...... }
tcp_transmit_skb
是tcp發送數據位於傳輸層的最後一步,這裏首先對TCP數據段的頭部進行了處理,而後調用了網絡層提供的發送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
實現了數據的發送,自此,數據離開了傳輸層,傳輸層的任務也就結束了。
gdb調試驗證以下:
6.2 接收端
對於傳輸層的代碼階段,咱們須要分析recv函數,這個與send相似,調用的是__sys_recvfrom,整個函數的調用路徑與send很是相似:
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags, struct sockaddr __user *addr, int __user *addr_len) { ...... err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter); if (unlikely(err)) return err; sock = sockfd_lookup_light(fd, &err, &fput_needed); ..... msg.msg_control = NULL; msg.msg_controllen = 0; /* Save some cycles and don't copy the address if not needed */ msg.msg_name = addr ? (struct sockaddr *)&address : NULL; /* We assume all kernel code knows the size of sockaddr_storage */ msg.msg_namelen = 0; msg.msg_iocb = NULL; msg.msg_flags = 0; if (sock->file->f_flags & O_NONBLOCK) flags |= MSG_DONTWAIT; err = sock_recvmsg(sock, &msg, flags); if (err >= 0 && addr != NULL) { err2 = move_addr_to_user(&address, msg.msg_namelen, addr, addr_len); ..... }
__sys_recvfrom
調用了sock_recvmsg
來接收數據,整個函數實際調用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);
,一樣,根據tcp_prot
結構的初始化,調用的實際上是tcp_rcvmsg
接受函數比發送函數要複雜得多,由於數據接收不只僅只是接收,tcp的三次握手也是在接收函數實現的,因此收到數據後要判斷當前的狀態,是否正在創建鏈接等,根據發來的信息考慮狀態是否要改變,在這裏,咱們僅僅考慮在鏈接創建後數據的接收。
tcp_rcvmsg函數以下:
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len) { ...... if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) && (sk->sk_state == TCP_ESTABLISHED)) sk_busy_loop(sk, nonblock); lock_sock(sk); ..... if (unlikely(tp->repair)) { err = -EPERM; if (!(flags & MSG_PEEK)) goto out; if (tp->repair_queue == TCP_SEND_QUEUE) goto recv_sndq; err = -EINVAL; if (tp->repair_queue == TCP_NO_QUEUE) goto out; ...... last = skb_peek_tail(&sk->sk_receive_queue); skb_queue_walk(&sk->sk_receive_queue, skb) { last = skb; ...... if (!(flags & MSG_TRUNC)) { err = skb_copy_datagram_msg(skb, offset, msg, used); if (err) { /* Exception. Bailout! */ if (!copied) copied = -EFAULT; break; } } *seq += used; copied += used; len -= used; tcp_rcv_space_adjust(sk);
這裏共維護了三個隊列:prequeue
、backlog
、receive_queue
,分別爲預處理隊列,後備隊列和接收隊列,在鏈接創建後,若沒有數據到來,接收隊列爲空,進程會在sk_busy_loop
函數內循環等待,知道接收隊列不爲空,並調用函數數skb_copy_datagram_msg
將接收到的數據拷貝到用戶態,實際調用的是__skb_datagram_iter
,這裏一樣用了struct msghdr *msg
來實現。__skb_datagram_iter函數以下:
int __skb_datagram_iter(const struct sk_buff *skb, int offset, struct iov_iter *to, int len, bool fault_short, size_t (*cb)(const void *, size_t, void *, struct iov_iter *), void *data) { int start = skb_headlen(skb); int i, copy = start - offset, start_off = offset, n; struct sk_buff *frag_iter; /* 拷貝tcp頭部 */ if (copy > 0) { if (copy > len) copy = len; n = cb(skb->data + offset, copy, data, to); offset += n; if (n != copy) goto short_copy; if ((len -= copy) == 0) return 0; } /* 拷貝數據部分 */ for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) { int end; const skb_frag_t *frag = &skb_shinfo(skb)->frags[i]; WARN_ON(start > offset + len); end = start + skb_frag_size(frag); if ((copy = end - offset) > 0) { struct page *page = skb_frag_page(frag); u8 *vaddr = kmap(page); if (copy > len) copy = len; n = cb(vaddr + frag->page_offset + offset - start, copy, data, to); kunmap(page); offset += n; if (n != copy) goto short_copy; if (!(len -= copy)) return 0; } start = end; }
拷貝完成後,函數返回,整個接收的過程也就完成了。
用一張函數間的相互調用圖能夠表示:
經過gdb調試驗證以下:
Breakpoint 1, __sys_recvfrom (fd=5, ubuf=0x7ffd9428d960, size=1024, flags=0, addr=0x0 <fixed_percpu_data>, addr_len=0x0 <fixed_percpu_data>) at net/socket.c:1990 1990 { (gdb) c Continuing. Breakpoint 2, sock_recvmsg (sock=0xffff888006df1900, msg=0xffffc900001f7e28, flags=0) at net/socket.c:891 891 { (gdb) c Continuing. Breakpoint 3, tcp_recvmsg (sk=0xffff888006479100, msg=0xffffc900001f7e28, len=1024, nonblock=0, flags=0, addr_len=0xffffc900001f7df4) at net/ipv4/tcp.c:1933 1933 { (gdb) c
Breakpoint 1, __sys_recvfrom (fd=5, ubuf=0x7ffd9428d960, size=1024, flags=0, addr=0x0 <fixed_percpu_data>, addr_len=0x0 <fixed_percpu_data>) at net/socket.c:1990 1990 { (gdb) c Continuing. Breakpoint 2, sock_recvmsg (sock=0xffff888006df1900, msg=0xffffc900001f7e28, flags=0) at net/socket.c:891 891 { (gdb) c Continuing. Breakpoint 3, tcp_recvmsg (sk=0xffff888006479100, msg=0xffffc900001f7e28, len=1024, nonblock=0, flags=0, addr_len=0xffffc900001f7df4) at net/ipv4/tcp.c:1933 1933 { (gdb) c Continuing. Breakpoint 4, __skb_datagram_iter (skb=0xffff8880068714e0, offset=0, to=0xffffc900001efe38, len=2, fault_short=false, cb=0xffffffff817ff860 <simple_copy_to_iter>, data=0x0 <fixed_percpu_data>) at net/core/datagram.c:414 414 {
符合咱們以前的分析。
7 IP層流程
7.1 發送端
網絡層的任務就是選擇合適的網間路由和交換結點, 確保數據及時傳送。網絡層將數據鏈路層提供的幀組成數據包,包中封裝有網絡層包頭,其中含有邏輯地址信息- -源站點和目的站點地址的網絡地址。其主要任務包括 (1)路由處理,即選擇下一跳 (2)添加 IP header(3)計算 IP header checksum,用於檢測 IP 報文頭部在傳播過程當中是否出錯 (4)可能的話,進行 IP 分片(5)處理完畢,獲取下一跳的 MAC 地址,設置鏈路層報文頭,而後轉入鏈路層處理。
IP 頭:
IP 棧基本處理過程以下圖所示:
具體代碼分析以下:
入口函數是ip_queue_xmit,函數以下:
發現調用了__ip_queue_xmit函數:
發現調用了skb_rtable函數,其實是開始找路由緩存,繼續看:
發現調用ip_local_out進行數據發送:
發現調用__ip_local_out函數:
發現返回一個nf_hook函數,裏面調用了dst_output,這個函數實質上是調用ip_finish__output函數:
發現調用__ip_finish_output函數:
若是分片就調用ip_fragment,不然就調用IP_finish_output2函數:
在構造好 ip 頭,檢查完分片以後,會調用鄰居子系統的輸出函數 neigh_output 進行輸 出。neigh_output函數以下:
輸出分爲有二層頭緩存和沒有兩種狀況,有緩存時調用 neigh_hh_output 進行快速輸 出,沒有緩存時,則調用鄰居子系統的輸出回調函數進行慢速輸出。這個函數以下:
最後調用dev_queue_xmit函數進行向鏈路層發送包,到此結束。gdb驗證以下:
7.2 接收端
接收相對簡單,入口在ip_rcv,這個函數以下:
裏面調用ip_rcv_finish函數:
發現調用dst_input函數,其實是調用ip_local_deliver函數:
若是分片,就調用ip_defrag函數,沒有則調用ip_local_deliver_finish函數:
發現調用ip_protocol_deliver_rcu函數:
調用完畢以後進入tcp棧,調用完畢,經過gdb驗證以下:
8 數據鏈路層流程
8.1 發送端
功能上,在物理層提供比特流服務的基礎上,創建相鄰結點之間的數據鏈路,經過差錯控制提供數據幀(Frame)在信道上無差錯的傳輸,並進行各電路上的動做系列。數據鏈路層在不可靠的物理介質上提供可靠的傳輸。該層的做用包括:物理地址尋址、數據的成幀、流量控制、數據的檢錯、重發等。在這一層,數據的單位稱爲幀(frame)。數據鏈路層協議的表明包括:SDLC、HDLC、PPP、STP、幀中繼等。
實現上,Linux 提供了一個 Network device 的抽象層,其實如今 linux/net/core/dev.c。具體的物理網絡設備在設備驅動中(driver.c)須要實現其中的虛函數。Network Device 抽象層調用具體網絡設備的函數。
發送端調用dev_queue_xmit,這個函數實際上調用__dev_queue_xmit:
發現它調用了dev_hard_start_xmit函數:
調用xmit_one:
調用trace_net_dev_start_xmit,實際上調用__net_dev_start_xmit函數:
到此,調用鏈結束。gdb調試以下:
8.2 接收端
簡要過程:
入口函數是net_rx_action:
發現調用napi_poll,實質上調用napi_gro_receive函數:
napi_gro_receive 會直接調用 netif_receive_skb_core。而它會調用__netif_receive_skb_one_core,將數據包交給上層 ip_rcv 進行處理。
調用結束以後,經過軟中斷通知CPU,至此,調用鏈結束。gdb驗證以下:
9 物理層流程
9.1 發送端
9.2 接收端
10 時序圖展現和總結
時序圖以下:
本次實驗主要是經過分析Linux內核源代碼,一步步的經過gdb進行調試函數調用鏈,最終清楚了tcp/ip協議棧的調用過程。由於時間有限,部分細節可能會有錯誤,但願讀者多加指正。
11 參考資料
1 《庖丁解牛Linux內核分析》
2 https://www.cnblogs.com/myguaiguai/p/12069485.html
3 https://www.cnblogs.com/jmilkfan-fanguiju/p/12789808.html#Linux__23