天天看點

tcp/ip協定棧--tcp進行中的幾個隊列0x01 緣由0x02 單步跟蹤分析0x03 隊列分析0x04 總結

0x01 緣由

     昨天簡單過了下tcp層的syn的過程,其中對幾個隊列處理有點疑問,對資料如何到使用者态的過程也存在的疑問?帶着這幾個問題網上查找了相關資料,發現一位大神講解得比較清楚。傳送:http://blog.csdn.net/russell_tao/article/details/9950615      講解得比較到位,博文編寫也是我後面要改進的方向。原理已經講解得比較清楚,下面主要做一下單步跟蹤分析。

0x02 單步跟蹤分析

     1.recv調用

tcp/ip協定棧--tcp進行中的幾個隊列0x01 緣由0x02 單步跟蹤分析0x03 隊列分析0x04 總結

 1.1 recv調用過程

SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
        unsigned, flags, struct sockaddr __user *, addr,
        int __user *, addr_len)
{
    .......
    //根據fd找對象的socket結構
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;

    //相關結構填充,io位址等
    msg.msg_control = NULL;
    msg.msg_controllen = 0;
    msg.msg_iovlen = 1;
    msg.msg_iov = &iov;
    iov.iov_len = size;
    iov.iov_base = ubuf;
    msg.msg_name = (struct sockaddr *)&address;
    msg.msg_namelen = sizeof(address);
    //阻塞與非阻塞調用
    if (sock->file->f_flags & O_NONBLOCK)
        flags |= MSG_DONTWAIT;
    //sock接收消息
    err = sock_recvmsg(sock, &msg, size, flags);
    .......
}
           

1.2 sock_recvmsg調用過程

int sock_recvmsg(struct socket *sock, struct msghdr *msg,
         size_t size, int flags)
{
    //相關io操作,這裡還不太明白
    struct kiocb iocb;
    struct sock_iocb siocb;
    int ret;

    init_sync_kiocb(&iocb, NULL);
    iocb.private = &siocb;
    //繼續調用
    ret = __sock_recvmsg(&iocb, sock, msg, size, flags);
    if (-EIOCBQUEUED == ret)
        ret = wait_on_sync_kiocb(&iocb);
    return ret;
}
           

1.3 sock_recvmsg調用過程

static inline int __sock_recvmsg(struct kiocb *iocb, struct socket *sock,
                 struct msghdr *msg, size_t size, int flags)
{
    int err;
    struct sock_iocb *si = kiocb_to_siocb(iocb);

    si->sock = sock;
    si->scm = NULL;
    si->msg = msg;
    si->size = size;
    si->flags = flags;

    err = security_socket_recvmsg(sock, msg, size, flags);
    if (err)
        return err;
    //根據sock回調指針情況執行,有inet\unix\netlink。tcp則調用tcp.c中tcp_recvmsg函數。這裡就是我傳送的那篇文章内容;
    return sock->ops->recvmsg(iocb, sock, msg, size, flags);
}
           

0x03 隊列分析

     上層應用調用和底層tcp收包通過隊列進行異步緩沖等解耦。是以通過多種隊列來進行各種場景下的資料緩沖。下面根據對tcp_recvmsg的分析來處理。  

/*
 *    這個函數是将sock結構拷貝到使用者的緩沖區。
 * 參數:len對應read、recv方法裡的記憶體長度,flags對應方法的flags參數,nonblock則是阻塞、非阻塞标志位
 */
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
        size_t len, int nonblock, int flags, int *addr_len)
{
    struct tcp_sock *tp = tcp_sk(sk);//對應結構
    ......
    //鎖住socket,防止多程序并發通路TCP連接配接,告知軟中斷目前socket在程序上下文中。
    lock_sock(sk);

    TCP_CHECK_TIMER(sk);
    //初始化errno這個錯誤碼
    err = -ENOTCONN;
    //sock狀态監聽狀态,還未建立連接配接,是以釋放目前處理sock。
    if (sk->sk_state == TCP_LISTEN)
        goto out;
    //如果socket是阻塞套接字,則取出SO_RCVTIMEO作為讀逾時時間;若為非阻塞,則timeo為0。下面會看到timeo是如何生效的
    timeo = sock_rcvtimeo(sk, nonblock);

    /* 帶有URG的緊急資料特殊處理*/
    if (flags & MSG_OOB)
        goto recv_urg;

    //擷取下一個要拷貝的位元組序号 
    //注意:seq的定義為u32 *seq;,它是32位指針。為何?因為下面每向使用者态記憶體拷貝後,會更新seq的值,這時就會直接更改套接字上的copied_seq 
    seq = &tp->copied_seq;
    //當flags參數有MSG_PEEK标志位時,意味着這次拷貝的内容,當再次讀取socket時(比如另一個程序)還能再次讀到
    if (flags & MSG_PEEK) {
        //是以不會更新copied_seq,當然,下面會看到也不會删除封包,不會從receive隊列中移除封包
        peek_seq = tp->copied_seq;
        seq = &peek_seq;
    }
    //擷取SO_RCVLOWAT最低接收閥值,當然,target實際上是使用者态記憶體大小len和SO_RCVLOWAT的最小值
    //注意:flags參數中若攜帶MSG_WAITALL标志位,則意味着必須等到讀取到len長度的消息才能傳回,此時target隻能是len
    target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);

#ifdef CONFIG_NET_DMA //配置網絡DMA
    tp->ucopy.dma_chan = NULL;
    preempt_disable();
    skb = skb_peek_tail(&sk->sk_receive_queue);
    {
        int available = 0;

        if (skb)
            available = TCP_SKB_CB(skb)->seq + skb->len - (*seq);
        if ((available < target) &&
            (len > sysctl_tcp_dma_copybreak) && !(flags & MSG_PEEK) &&
            !sysctl_tcp_low_latency &&
            dma_find_channel(DMA_MEMCPY)) {
            preempt_enable_no_resched();
            tp->ucopy.pinned_list =
                    dma_pin_iovec_pages(msg->msg_iov, len);
        } else {
            preempt_enable_no_resched();
        }
    }
#endif
    //以下開始讀取消息
    do {
        u32 offset;

        /* urgent data 處理*/
        if (tp->urg_data && tp->urg_seq == *seq) {
            if (copied)
                break;
            if (signal_pending(current)) {
                copied = timeo ? sock_intr_errno(timeo) : -EAGAIN;
                break;
            }
        }

        //從sk_receive_queue隊列中擷取skb
        skb_queue_walk(&sk->sk_receive_queue, skb) {
            //不應該有兩個recv隊列
            if (WARN(before(*seq, TCP_SKB_CB(skb)->seq),
                 KERN_INFO "recvmsg bug: copied %X "
                       "seq %X rcvnxt %X fl %X\n", *seq,
                       TCP_SKB_CB(skb)->seq, tp->rcv_nxt,
                       flags))
                break;
            //offset是待拷貝序号在目前這個封包中的偏移量
            offset = *seq - TCP_SKB_CB(skb)->seq;
            //syn偏移-1
            if (tcp_hdr(skb)->syn)
                offset--;
            //偏移小于skb長度
            if (offset < skb->len)
                goto found_ok_skb;
            //fin辨別
            if (tcp_hdr(skb)->fin)
                goto found_fin_ok; //處理skb
            WARN(!(flags & MSG_PEEK), KERN_INFO "recvmsg bug 2: "
                    "copied %X seq %X rcvnxt %X fl %X\n",
                    *seq, TCP_SKB_CB(skb)->seq,
                    tp->rcv_nxt, flags);
        }

        /*backLog隊列的處理*/
        if (copied >= target && !sk->sk_backlog.tail)
            break;

     ......

            /* 如果prequeue不是空的,我們必須在釋放套接字之前處理它,否則在第二次疊代時将會被打破。
             * 需要更優雅的解決方案!
             * 看:我們有以下(僞)隊列:
             *
             * 1. packets in flight  --在處理的隊列
             * 2. backlog   -- 後備隊列
             * 3. prequeue  --  處理隊列
             * 4. receive_queue --接收隊列
             * 每個隊列僅僅在下一個隊列為空時才能處理。
             */
            if (!skb_queue_empty(&tp->ucopy.prequeue))
                goto do_prequeue;

            /* __ Set realtime policy in scheduler __ */
        }

        if (copied >= target) {
            /* Do not sleep, just process backlog. */
            release_sock(sk);
            lock_sock(sk);
        } else
            sk_wait_data(sk, &timeo);

#ifdef CONFIG_NET_DMA
        tp->ucopy.wakeup = 0;
#endif

        if (user_recv) {
            int chunk;

            /* __ Restore normal policy in scheduler __ */

            if ((chunk = len - tp->ucopy.len) != 0) {
                NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk);
                len -= chunk;
                copied += chunk;
            }

            if (tp->rcv_nxt == tp->copied_seq &&
                !skb_queue_empty(&tp->ucopy.prequeue)) {
do_prequeue:
                tcp_prequeue_process(sk);

                if ((chunk = len - tp->ucopy.len) != 0) {
                    NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
                    len -= chunk;
                    copied += chunk;
                }
            }
        }
        if ((flags & MSG_PEEK) &&
            (peek_seq - copied - urg_hole != tp->copied_seq)) {
            if (net_ratelimit())
                printk(KERN_DEBUG "TCP(%s:%d): Application bug, race in MSG_PEEK.\n",
                       current->comm, task_pid_nr(current));
            peek_seq = tp->copied_seq;
        }
        continue;

    found_ok_skb:
        /* receive隊列的這個封包從其可以使用的偏移量offset,到總長度len之間,可以拷貝的長度為used  */
        used = skb->len - offset;
        //len是使用者态空閑記憶體,len更小時,當然隻能拷貝len長度消息,總不能導緻記憶體溢出吧
        if (len < used)
            used = len;

        /* 有緊急辨別資料嗎? */
        if (tp->urg_data) {
            u32 urg_offset = tp->urg_seq - *seq;
            if (urg_offset < used) {
                if (!urg_offset) {
                    if (!sock_flag(sk, SOCK_URGINLINE)) {
                        ++*seq;
                        urg_hole++;
                        offset++;
                        used--;
                        if (!used)
                            goto skip_copy;
                    }
                } else
                    used = urg_offset;
            }
        }
         //MSG_TRUNC标志位表示不要管len這個使用者态記憶體有多大,隻管拷貝資料吧 
        if (!(flags & MSG_TRUNC)) {
        ......
            {
                向使用者态拷貝資料,開始複制資料到iovec
                err = skb_copy_datagram_iovec(skb, offset,
                        msg->msg_iov, used);
                if (err) {
                    /* Exception. Bailout! */
                    if (!copied)
                        copied = -EFAULT;
                    break;
                }
            }
        }

        *seq += used;
        copied += used;
        len -= used;
        /*每次将資料複制到使用者空間時,都應該調用此函數。它計算适當的TCP接收緩沖區空間。*/
        tcp_rcv_space_adjust(sk);

skip_copy:
        if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {
            tp->urg_data = 0;
            tcp_fast_path_check(sk);
        }
        if (used + offset < skb->len)
            continue;

        if (tcp_hdr(skb)->fin)
            goto found_fin_ok;
        if (!(flags & MSG_PEEK)) {
            sk_eat_skb(sk, skb, copied_early);
            copied_early = 0;
        }
        continue;

    found_fin_ok:
        /* Process the FIN. */
        ++*seq;
        if (!(flags & MSG_PEEK)) {
            sk_eat_skb(sk, skb, copied_early); //釋放相關隊列
            copied_early = 0;
        }
        break;
    } while (len > 0);
     //已經裝載了接收器 
    if (user_recv) {
        //prequeue隊列不為空則處理之
        if (!skb_queue_empty(&tp->ucopy.prequeue)) {
            int chunk;

            tp->ucopy.len = copied > 0 ? len : 0;

            tcp_prequeue_process(sk);

            if (copied > 0 && (chunk = len - tp->ucopy.len) != 0) {
                NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
                len -= chunk;
                copied += chunk;
            }
        }
    /準備傳回使用者态,socket上不再裝載接收任務
        tp->ucopy.task = NULL;
        tp->ucopy.len = 0;
    }
    ......

    TCP_CHECK_TIMER(sk);
    //釋放socket時,還會檢查、處理backlog隊列中的封包 
    release_sock(sk);
    return copied;

out:
    TCP_CHECK_TIMER(sk);
    //釋放socket時,還會檢查、處理backlog隊列中的封包 
    release_sock(sk);
    return err;

recv_urg:
    err = tcp_recv_urg(sk, msg, len, flags);
    goto out;
}
           

0x04 總結

     此次結合大牛的分析,重新學習了一次核心态資料如何到使用者态的,且了解了幾個關鍵隊列。(學習過程,大牛勿噴,多指教)