TCP/IP協議棧在Linux內核中的運行時序分析


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相關的函數。而這里並沒有出現我們所熟知得writereadaiopoll等系統調用函數,這是因為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首部和選項;最后調用網絡層提供的發送回調函數發送skbip層的回調函數為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標記,則處理finf. 亂序隊列不為空,則處理亂序;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_finiship_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.時序圖

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM