天天看點

對于資料發送,網絡發送緩沖區與視窗關系的探究與思考

本作品采用​​知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協定​​進行許可。

本作品 (李兆龍​ 博文, 由 李兆龍​ 創作),由 李兆龍 确認,轉載請注明版權。

文章目錄

  • ​​引言​​
  • ​​套接字​​
  • ​​send過程​​
  • ​​sendto​​
  • ​​tcp_sendmsg​​
  • ​​一個關于視窗與緩沖區的誤區​​
  • ​​tcp_push​​
  • ​​tcp_write_xmit​​
  • ​​總結​​

核心版本:3.15.2

引言

對于這個問題的想法起源于阿裡一面面試官提出的問題,在使用者态send成功以後我們可以保證對端收到所有的資料嗎?

首先簡單的考慮就是成功的定義,man手冊中這樣定義;

對于資料發送,網絡發送緩沖區與視窗關系的探究與思考

是以其實隻有傳回的資料為不是-1的話都算成功,此時就分為兩種情況,完全寫入和部分寫入,顯然後者當然無法保證所有的資料都被接收,因為寫入的資料已然不足,當然無法收到全部的資料,這一般是由于擁塞視窗或者滑動視窗小于發送的資料包導緻的。

另一種情況,其實也就是面試官想考的點,send完全發送資料以後是否可以保證對端收到資料[2],如果get到這個點的話其實可以看出其實考的就是發送緩沖區的問題,問題到了這裡我不禁又對發送緩沖區和視窗的關系産生了疑問,因為在以前學習網絡的過程中雖然很清楚滑動視窗和擁塞視窗的關系,但是對它們如何以代碼的形式展現出來其實還是不清楚,好了,我們現在就解決這個疑問吧。

套接字

首先是套接字的格式:

struct socket {
  socket_state    state;  // 套接字的狀态
  short        type;  // 套接字類型
  unsigned long    flags;  // 

  struct file      *file;  // socket對應的file結構體
  struct sock      *sk;  // 套接字維護所有的所有狀态
  const struct proto_ops  *ops;  // 裡面存着協定相關的一些狀态和鈎子函數

  struct socket_wq  wq;
};      

其中套接字的​

​state​

​其實就是去表示這個套接字處于哪個狀态,比如說維護一個TCP連接配接的狀态,具體可參考[3]:

enum {
  TCP_ESTABLISHED = 1,
  TCP_SYN_SENT,
  TCP_SYN_RECV,
  TCP_FIN_WAIT1,
  TCP_FIN_WAIT2,
  TCP_TIME_WAIT,
  TCP_CLOSE,
  TCP_CLOSE_WAIT,
  TCP_LAST_ACK,
  TCP_LISTEN,
  TCP_CLOSING,       /* now a valid state */
  TCP_MAX_STATES /* Leave at the end! */
};      

而​

​type​

​則是表示這是一個應用于什麼協定的套接字,是以這樣看來不同的協定是可以綁定同一個IP:port的,因為它們對應的套接字以及底層維護的狀态根本就不是一套的。

在所有成員中​

​struct sock​

​​其實是最重要的一個,其中維護了這個套接字上幾乎所有的狀态,其實在文檔中也把這個字段描述為​

​internal networking protocol agnostic socket representation​

​[5],其中包括緩沖區,雙方位址資訊等等,當然這篇文章旨在搞清楚發送緩沖區與視窗關系,其中很多字段我其實沒有深入了解,後面有興趣研究網絡協定棧的時候可以了解一下。

對于使用者态來說我們得到一個套接字的方法一般有兩種,一個是使用​

​socket​

​系統調用進行建立,其參數如下:

int socket(int domain, int type, int protocol);

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);      

我們可以看到​

​socket​

​​其中指定了網絡層和傳輸層的協定,​

​type​

​​字段可以讓我們指定對使用者态來說套接字的行為,目前有兩個選項​

​SOCK_NONBLOCK​

​​和​

​SOCK_CLOEXEC​

​​[6]。然後使用​

​bind​

​和某個IP:poet綁定在一起。

還有一種方法是​

​accept​

​傳回一個套接字。

當​

​accept​

​傳回一個fd時,此時其實我們已經持有了一個完成三次握手的可運作的套接字 了,此時就是如何操作它了。

整體套接字的結構體之間可以簡單的用下圖來描述[7]:

對于資料發送,網絡發送緩沖區與視窗關系的探究與思考

套接字當然是一個檔案,我們使用fd可以在​

​task_struct​

​​中的​

​files_struct​

​​中找到​

​file​

​​,​

​struct socket​

​​存儲在file的​

​private_data​

​字段,這樣我們就可以通過fd拿到套接字了。

send過程

其實​

​send​

​​本身就是​

​sendto​

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len, unsigned, flags) 
{ 
    return sys_sendto(fd, buff, len, flags, NULL, 0); 
}      

下面這張圖的一部分将會貫穿這篇文章:

對于資料發送,網絡發送緩沖區與視窗關系的探究與思考

我們從​

​sys_sendto​

​開始分析。

從參數可以看出其實就是正常的那些使用者态傳入的玩意:

  1. fd用于查找​

    ​struct socket​

    ​。
  2. ​buff​

    ​​和​

    ​len​

    ​代表了使用者要發送的資料。
  3. ​addr​

    ​​和​

    ​addr_len​

    ​代表了對端位址,用于udp的傳輸。

sendto

// 這個函數做的事情其實就是封裝一個msg資料塊
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len, unsigned, flags, 
    struct sockaddr __user *, addr, int, addr_len) 
{ 
    struct socket *sock; 
    struct sockaddr_storage address; 
    int err; 
    struct msghdr msg; 
    struct iovec iov; 
    int fput_needed; 
  
    if (len > INT_MAX) 
       len = INT_MAX; 
  
    /* 通過檔案描述符fd,找到對應的socket執行個體。 
     * 以fd為索引從目前程序的檔案描述符表files_struct執行個體中找到對應的file執行個體, 
     * 然後從file執行個體的private_data成員中擷取socket執行個體。 
     */ 
    sock = sockfd_lookup_light(fd, &err, &fput_needed); 
    if (! sock) 
        goto out; 
  
    /* 初始化消息頭 */ 
    iov.iov_base = buff; 
    iov.iov_len = len; 
    msg.msg_name = NULL; 
    msg.msg_iov = &iov; 
    msg.msg_iovlen = 1; /* 隻有一個資料塊 */ 
    msg.msg_control = NULL; 
    msg.msg_controllen = 0; 
    msg.msg_namelen = 0; 
  
    if (addr) { 
        /* 把套接字位址從使用者空間拷貝到核心空間 */ 
        err = move_addr_to_kernel(addr, addr_len, &address); 
        if (err < 0) 
            goto out_put; 
  
        msg.msg_name = (struct sockaddr *)&address; 
        msg.msg_namelen = addr_len; 
    } 
  
    /* 如果設定了非阻塞标志 */ 
    if (sock->file->f_flags & O_NONBLOCK) 
        flags |= MSG_DONTWAIT; 
    msg.msg_flags = flags; 
    /* 調用統一的發送入口函數sock_sendmsg(),我們下面看看這個函數*/ 
    err = sock_sendmsg(sock , &msg, len); 
  
out_put: 
    fput_light(sock->file, fput_needed); 
out: 
    return err; 
}      
// 通過fd拿到socket
static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)
{
  struct fd f = fdget(fd);
  struct socket *sock;

  *err = -EBADF;
  if (f.file) {
    sock = sock_from_file(f.file);
    if (likely(sock)) {
      *fput_needed = f.flags & FDPUT_FPUT;
      return sock;
    }
    *err = -ENOTSOCK;
    fdput(f);
  }
  return NULL;
}

struct socket *sock_from_file(struct file *file)
{
  if (file->f_op == &socket_file_ops)
    return file->private_data;  /* set in sock_map_fd */

  return NULL;
}      
// 初始化一個異步IO控制塊
int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) 
{ 
    struct kiocb iocb; 
    struct sock_iocb siocb; 
    int ret; 
  
    init_sync_kiocb(&iocb, NULL); 
    iocb.private = &siocb; 
    // 這裡的size其實還是使用者态傳入的資料塊長度,但是msg裡面其實也是有的,不清楚為什麼要加這麼一個參數
    ret = __sock_sendmsg(&iocb, sock, msg, size); 
  
    /* iocb queued, will get completion event */ 
    if (-EIOCBQUEUED == ret) 
        ret = wait_on_sync_kiocb(&iocb); 
  
    return ret; 
} 
  
/* AIO控制塊 */ 
struct kiocb { 
    struct file *ki_filp; 
    struct kioctx *ki_ctx; /* NULL for sync ops,如果是同步的則為NULL */ 
    kiocb_cancel_fn *ki_cancel; 
    void *private; /* 指向sock_iocb */ 
    union { 
        void __user *user; 
        struct task_struct *tsk; /* 執行io的程序 */ 
    } ki_obj; 
    __u64 ki_user_data; /* user's data for completion */ 
    loff_t ki_pos; 
    size_t ki_nbytes; /* copy of iocb->aio_nbytes */ 
  
    struct list_head ki_list; /* the aio core uses this for cancellation */ 
    /* If the aio_resfd field of the userspace iocb is not zero, 
     * this is the underlying eventfd context to deliver events to. 
     */ 
    struct eventfd_ctx *ki_eventfd; 
};      

接下來看看​

​__sock_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 security_socket_sendmsg(struct socket *sock,
                      struct msghdr *msg, int size){
    return 0;
}



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(),最後調用tcp_sendmsg。 當然如果是SOCK_DGRAM調用的就是udp_sendmsg了。
     */ 
    return sock->ops->sendmsg(iocb, sock, msg, size); 
}      

我們再來看看​

​proto_ops​

​​的初始化過程,其實就是​

​struct socket​

​​的​

​ops​

​字段:

const struct proto_ops inet_stream_ops = {
  .family       = PF_INET,
  .flags       = PROTO_CMSG_DATA_ONLY,
  .owner       = THIS_MODULE,
  .release     = inet_release,
  .bind       = inet_bind,
  .connect     = inet_stream_connect,
  .socketpair     = sock_no_socketpair,
  .accept       = inet_accept,
  .getname     = inet_getname,
  .poll       = tcp_poll,
  .ioctl       = inet_ioctl,
  .gettstamp     = sock_gettstamp,
  .listen       = inet_listen,
  .shutdown     = inet_shutdown,
  .setsockopt     = sock_common_setsockopt,
  .getsockopt     = sock_common_getsockopt,
  .sendmsg     = inet_sendmsg,
  .recvmsg     = inet_recvmsg,
#ifdef
  .mmap       = tcp_mmap,
#endif
  .sendpage     = inet_sendpage,
  .splice_read     = tcp_splice_read,
  .read_sock     = tcp_read_sock,
  .sendmsg_locked    = tcp_sendmsg_locked,
  .sendpage_locked   = tcp_sendpage_locked,
  .peek_len     = tcp_peek_len,
#ifdef
  .compat_ioctl     = inet_compat_ioctl,
#endif
  .set_rcvlowat     = tcp_set_rcvlowat,
};

const struct proto_ops inet_dgram_ops = {
  .family       = PF_INET,
  .release     = inet_release,
  .bind       = inet_bind,
  .connect     = inet_dgram_connect,
  .socketpair     = sock_no_socketpair,
  .accept       = sock_no_accept,
  .getname     = inet_getname,
  .poll       = udp_poll,
  .ioctl       = inet_ioctl,
  .gettstamp     = sock_gettstamp,
  .listen       = sock_no_listen,
  .shutdown     = inet_shutdown,
  .setsockopt     = sock_common_setsockopt,
  .getsockopt     = sock_common_getsockopt,
  .sendmsg     = inet_sendmsg,    
  .recvmsg     = inet_recvmsg,
  .mmap       = sock_no_mmap,
  .sendpage     = inet_sendpage,
  .set_peek_off     = sk_set_peek_off,
#ifdef
  .compat_ioctl     = inet_compat_ioctl,
#endif
};      

可以看到​

​sendmsg​

​​字段中的鈎子函數都一樣,都是​

​inet_sendmsg​

​函數。

int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size)
{ 
    struct sock *sk = sock->sk; 
    sock_rps_record_flow(sk); 
  
    /* We may need to bnd the socket. 
     * 如果連接配接還沒有配置設定本地端口,且允許自動綁定,那麼給連接配接綁定一個本地端口。 
     * tcp_prot的no_autobaind為true,是以TCP是不允許自動綁定端口的。 
     */ 
    if (! inet_sk(sk)->inet_num && ! sk->sk_prot->no_autobind && inet_autobind(s)) 
        return -EAGAIN; 
  
    /* 如果傳輸層使用的是TCP,則sk_prot為tcp_prot,sendmsg指向tcp_sendmsg() */ 
    return sk->sk_prot->sendmsg(iocb, sk, msg, size); 
} 
   
/* Automatically bind an unbound socket. */ 
static int inet_autobind(struct sock *sk) 
{ 
    // 這個結構中其實存着對端ip:port 和本端的ip:port
    struct inet_sock *inet; 
  
    /* We may need to bind the socket. */ 
    lock_sock(sk); 
  
    /* 如果還沒有配置設定本地端口 */ 
    if (! inet->inet_num) { 
  
        /* SOCK_STREAM套接口的TCP操作函數集為tcp_prot,其中端口綁定函數為 
         * inet_csk_get_port()。 
         */ 
        if (sk->sk_prot->get_port(sk, 0)) { 
            release_sock(sk); 
            return -EAGAIN; 
        } 
        inet->inet_sport = htons(inet->inet_num); 
    } 
  
    release_sock(sk); 
    return 0; 
}

struct inet_sock {
    ........
    inet_daddr - Foreign IPv4 addr
    inet_rcv_saddr - Bound local IPv4 addr
    inet_dport - Destination port
    inet_num - Local port
    inet_saddr - Sending source
    ........
}      

​sk->sk_prot->sendmsg​

​​調用的就是不同的協定的鈎子函數了,TCP調用​

​tcp_sendmsg​

​​,UDP調用​

​udp_sendmsg​

​。

tcp_sendmsg

我們來看看​

​tcp_sendmsg​

​,此時我們持有四個資源:

  1. 異步IO控制塊
  2. ​struct sock​

  3. 經過封裝的消息,其中資料塊長度為1
  4. 這一個資料塊的長度

可以看到整體的發送流程其實到這才剛剛開始。

借[9]中的話來說就是​

​tcp_sendmsg​

​​的主要工作是把使用者層的資料,填充到skb中,然後加入到sock的發送隊列。之後調用​

​tcp_write_xmit​

​來把sock發送隊列中的skb盡量地發送出去。下面的代碼是我從[9]中中直接拷來的,[9]中有更詳細的總結:

int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t size)
{
    struct iovec *iov;
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;    // 一個插入發送緩沖區的資料塊
    int iovlen, flags, err, copied = 0;
    int mss_now = 0, size_goal, copied_syn = 0, offset = 0;
    bool sg;
    long timeo;
 
    lock_sock(sk);
 
    flags = msg->msg_flags;    // 資料塊的狀态,如果套接字為非阻塞,flag設定為MSG_DONTWAIT,sendto中有設定過
 
    /* Send data in TCP SYN.
     * 使用了TCP Fast Open時,會在發送SYN時攜帶上資料。
     * 回想一下,fastopen會在第一次連接配接的時候設定cookie,後面連接配接隻需要一個RTT,且可以攜帶資料。
     */
    if (flags & MSG_FASTOPEN) {
        err = tcp_sendmsg_fastopen(sk, msg, &copied_syn, size);
        if (err == -EINPROGRESS && copied_syn > 0)
            goto out;
        else if (err)
            goto out_err;
 
        offset = copied_syn;
    }
 
    /* 發送的逾時時間,如果是非阻塞的則為0 */
    /*
    static inline long sock_sndtimeo(const struct sock *sk, bool noblock){
        return noblock ? 0 : sk->sk_sndtimeo;
    }
    */
    timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT); 
 
    /* Wait for a connection to finish.
     * One exception is TCP Fast Open (passive side) where data is allowed to
     * be sent before a connection is fully established.
     * 等待連接配接完成。 TCP快速打開(被動端)是一個例外,其中允許在完全建立連接配接之前發送資料。
     */
 
    /* 如果連接配接尚未完成三次握手,是不允許發送資料的,除非是Fast Open的被動打開方 */
    if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&
        ! (tcp_passive_fastopen(sk)) {
 
        /* 等待連接配接的建立,成功時傳回值為0 */
        if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
            goto do_error;
    }
 
    /* 使用TCP_REPAIR選項時 */
    if (unlikely(tp->repair)) {
 
        /* 發送到接收隊列中 */
        if (tp->repair_queue == TCP_RECV_QUEUE) {
            copied = tcp_send_rcvq(sk, msg, size);
            goto out;
        }
 
        err = -EINVAL;
        if (tp->repair_queue == TCP_NO_QUEUE)
            goto out_err;
 
        /* common sending to sendq */
    }
 
    /* This should be in poll.
     * 清除使用異步情況下,發送隊列滿了的标志。
     */
    clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
 
    /* 擷取目前的發送MSS.
     * 擷取可發送到網卡的最大資料長度,如果使用GSO,會是MSS的整數倍。
     * 擷取一個skb可以容納的資料量。
    int tcp_send_mss(struct sock *sk, int *size_goal, int flags)
    {
        int mss_now;
    
        mss_now = tcp_current_mss(sk);
        // 計算size_goal的過程還是挺複雜的,有興趣的朋友可以看看,這個函數傳回值是 max(size_goal, mss_now);
        *size_goal = tcp_xmit_size_goal(sk, mss_now, !(flags & MSG_OOB));
    
        return mss_now;
    }     
    */
    mss_now = tcp_send_mss(sk, &size_goal, flags);
 
    /* Ok commence sending. */
    iovlen = msg->msg_iovlen; /* 應用層資料塊的個數*/
    iov = msg->msg_iov; /* 應用層資料塊數組的位址 */
    copied = 0; /* 已拷貝到發送隊列的位元組數 */
 
    err = -EPIPE; /* Broken pipe */
    /* 檢查之前TCP連接配接是否發生過異常,如果連接配接有錯誤,或者不允許發送資料了,那麼傳回-EPIPE */
    if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
        goto out_err;
 
    sg = !! (sk->sk_route_caps & NETIF_F_SG); /* 網卡是否支援分散聚合 */
 
    /* 周遊使用者層的資料塊數組 */
    while (--iovlen >= 0) {
 
        size_t seglen = iov->iov_len; /* 資料塊的長度 */
        unsigned char __user *from = iov->iov_base; /* 資料塊的位址 */
 
        iov++; /* 指向下一個資料塊 */
 
        /* Skip bytes copied in SYN.
         * 如果使用了TCP Fast Open,需要跳過SYN包發送過的資料。
         */
        if (unlikely(offset > 0)) {
            if (offset >= seglen) {
                offset -= seglen;
                continue;
            }
 
            seglen -= offset;
            from += offset;
            offset = 0; 
        }
        
        // 使用者态資料塊中還沒發送的資料
        while (seglen > 0) {
            //copy儲存本輪循環要拷貝的資料量
            int copy = 0;
            int max = size_goal; /* 單個skb的最大資料長度,如果使用了GSO,長度為MSS的整數倍 */
            
            /*拿到發送隊列的最後一個skb,因為該資料塊目前已儲存資料可能還沒有超過size_goal,是以可以繼續往該資料塊中填充資料*/
            /* sk_write_queue其實就是緩沖區了
            static inline struct sk_buff *tcp_write_queue_tail(const struct sock *sk){
                return skb_peek_tail(&sk->sk_write_queue);
            }
            static inline struct sk_buff *skb_peek_tail(const struct sk_buff_head *list_){
                struct sk_buff *skb = READ_ONCE(list_->prev);
                if (skb == (struct sk_buff *)list_)
                    skb = NULL;
                return skb;
            
            }
            */
            skb = tcp_write_queue_tail(sk);
 
            if (tcp_send_head(sk)) { /* 還有未發送的資料,說明該skb還未發送 */
                /* 如果網卡不支援檢驗和計算,那麼skb的最大長度為MSS,即不能使用GSO */
                if (skb->ip_summed == CHECKSUM_NONE)
                    max = mss_now;
 
                copy = max - skb->len; /* 此skb可追加的最大資料長度 */
            }
 
            if (copy <= 0) { /* 需要使用新的skb來裝資料 */
new_segment:
                /* Allocate new segment. If the interface is SG,
                 * allocate skb fitting to single page.
                 */
 
                /* 如果發送隊列的總大小sk_wmem_queued大于等于發送緩存的上限sk_sndbuf,
                 * 或者發送緩存中尚未發送的資料量超過了使用者的設定值,就進入等待。
                  */
                if (! sk_stream_memory_free(sk))
                    goto wait_for_sndbuf;
 
                /* 申請一個skb,其線性資料區的大小為:
                 * 通過select_size()得到的線性資料區中TCP負荷的大小 + 最大的協定頭長度。
                 * 如果申請skb失敗了,或者雖然申請skb成功,但是從系統層面判斷此次申請不合法,
                 * 那麼就進入睡眠,等待記憶體。
                 */
                skb = sk_stream_alloc_skb(sk, select_size(sk, sg), sk->sk_allocation);
                if (! skb)
                    goto wait_for_memory;            
 
                /* All packets are restored as if they have already been sent.
                 * 如果使用了TCP REPAIR選項,那麼為skb設定“發送時間”。
                 */
                if (tp->repair)
                    TCP_SKB_CB(skb)->when = tcp_time_stamp;
 
               /* Check whether we can use HW checksum.
                * 如果網卡支援校驗和的計算,那麼由硬體計算報頭和首部的校驗和。
                */
               if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
                    skb->ip_summed = CHECKSUM_PARTIAL;
                
                /* 更新skb的TCP控制塊字段,把skb加入到sock發送隊列的尾部,
                 * 增加發送隊列的大小,減小預配置設定緩存的大小。
                 */
                skb_entail(sk, skb);
 
                copy = size_goal;
                max = size_goal;
            }
 
            /* Try to append data to the end of skb.
             * 本次可拷貝的資料量不能超過資料塊的長度。
             */
            if (copy > seglen)    // 小于的話當然還是copy本身了
                copy = seglen;
            
            /* Where to copy to ?
             * 如果skb的線性資料區還有剩餘空間,就先複制到線性資料區。
             */
            if (skb_availroom(skb) > 0) {
                copy = min_t(int, copy, skb_availroom(skb));
 
                /* 拷貝使用者空間的資料到核心空間,同時計算校驗和 */
                err = skb_add_data_nocache(sk, skb, from, copy);
                if (err)
                    goto do_fault;
 
            } else { /* 如果skb的線性資料區已經用完了,那麼就使用分頁區 */
                bool merge = true;
                int i = skb_shinfo(skb)->nr_frags; /* 分頁數 */
                struct page_frag *pfrag = sk_page_frag(sk); /* 上次緩存的分頁 */
 
                /* 檢查分頁是否有可用空間,如果沒有就申請新的page。
                 * 如果申請失敗,說明系統記憶體不足。
                 * 之後會設定TCP記憶體壓力标志,減小發送緩沖區的上限,睡眠等待記憶體。
                 */
                if (! sk_page_frag_refill(sk, pfrag))
                    goto wait_for_memory;
 
                /* 判斷能否往最後一個分頁追加資料 */
                if (! skb_can_coalesce(skb, i, pfrag->page, pfrag->offset)) {
 
                    /* 不能追加時,檢查分頁數是否達到了上限,或者網卡不支援分散聚合。
                     * 如果是的話,就為此skb設定PSH标志,盡快地發送出去。
                     * 然後跳轉到new_segment處申請新的skb,來繼續填裝資料。
                     */
                    if (i == MAX_SKB_FRAGS || ! sg) {
                        tcp_mark_push(tp, skb);
                        goto new_segment;
                    }
                    merge = false;
                }
 
                copy = min_t(int ,copy, pfrag->size - pfrag->offset);
 
                /* 從系統層面判斷發送緩存的申請是否合法 */
                if (! sk_wmem_schedule(sk, copy))
                    goto wait_for_memory;
 
                /* 拷貝使用者空間的資料到核心空間,同時計算校驗和。
                 * 更新skb的長度字段,更新sock的發送隊列大小和預配置設定緩存。
                 */
                err = skb_copy_to_page_nocache(sk, from, skb, pfrag->page, pfrag->offset, copy);
                if (err)
                    goto do_error;
 
                /* Update the skb. */
                if (merge) { /* 如果把資料追加到最後一個分頁了,更新最後一個分頁的資料大小 */
                    skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], copy);
                } else {
                    /* 初始化新增加的頁 */
                    skb_fill_page_desc(skb, i, pfrag->page, pfrag->offset, copy);
                    get_page(pfrag->page);
                }
 
                pfrag->offset += copy;
            }
 
            /* 如果這是第一次拷貝,取消PSH标志,*/
            if (! copied)
                TCP_SKB_CB(skb)->tcp_flags &= ~TCPHDR_PSH;
 
            tp->write_seq += copy; /* 更新發送隊列的最後一個序号 */
            TCP_SKB_CB(skb)->send_seq += copy; /* 更新skb的結束序号 */
            skb_shinfo(skb)->gso_segs = 0;
 
            from += copy; /* 下次拷貝的位址 */
            copied += copy; /* 已經拷貝到發送隊列的資料量 */
 
            /* 如果所有資料都拷貝好了,準備發送資料 */
            if ((seglen -= copy) == 0 && iovlen == 0)
                goto out;
 
            /* 如果skb還可以繼續填充資料,或者發送的是帶外資料,或者使用TCP REPAIR選項,
             * 那麼繼續拷貝資料,先不發送。
             */
            if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
                continue;
 
            /* 如果需要設定PSH标志 */
            if (forced_push(tp)) {
                tcp_mark_push(tp, skb);
 
                /* 盡可能的将發送隊列中的skb發送出去,禁用nalge */
                __tcp_push_pending_frames(sk, mss_now,TCP_NAGLE_PUSH);
 
            } else if (skb == tcp_send_head(sk))
                tcp_push_one(sk, mss_now); /* 隻發送一個skb */
 
            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, size_goal);
 
                /* 分兩種情況:
                 * 1. sock的發送緩存不足。等待sock有發送緩存可寫事件,或者逾時。
                 * 2. TCP層記憶體不足,等待2~202ms之間的一個随機時間。
                 */
                if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
                    goto do_err;  
 
                /* 睡眠後MSS和TSO段長可能會發生變化,重新計算 */
                mss_now = tcp_send_mss(sk, &size_goal, flags);
 
            } // end while seglen > 0
        } // end while --iovlen >= 0
 
out:
    /* 如果已經有資料複制到發送隊列了,就嘗試立即發送 */
    if (copied)
        tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
 
    release_sock(sk);
    return copied + copied_syn;
 
do_fault:
    if (! skb->len) { /* 如果skb沒有負荷 */
        tcp_unlink_write_queue(skb, sk); /* 把skb從發送隊列中删除 */
 
        /* It is the one place in all of TCP, except connection reset,
         * where we can be unlinking the send_head.
         */
        tcp_check_send_head(sk, skb); /* 是否要撤銷sk->sk_send_head */
        sk_wmem_free_skb(sk, skb); /* 更新發送隊列的大小和預配置設定緩存,釋放skb */
    }
 
do_error:
    if (copied + copied_syn)
        goto out;
 
out_err:
    err = sk_stream_error(sk, flags, err);
    release_sock(sk);
    return err;
}      
對于資料發送,網絡發送緩沖區與視窗關系的探究與思考

一個關于視窗與緩沖區的誤區

我們可以看到​

​tcp_sendmsg​

​其實就是把使用者态的資料拷貝到發送緩沖區中,注意這個拷貝的過程可以看出和視窗沒啥關系,隻和發送緩沖區大小以及核心的記憶體資源有關,是以我們不能簡單的把視窗和發送緩沖區簡單的了解為一個東西。課本上的滑動視窗的圖很容易讓人了解為當資料超過視窗大小時就會寫入失敗,其實這個想法是完全錯誤的。

tcp_push

在​

​sock​

​​發送緩存不足、系統記憶體不足或應用層的資料都拷貝完畢等情況下,都會調用​

​tcp_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;
 
    /* 如果沒有未發送過的資料 */
    if (! tcp_send_head(sk))
        return;
 
    /* 發送隊列的最後一個skb */
    skb = tcp_write_queue_tail(sk);
 
    /* 如果接下來沒有更多的資料需要發送,或者距離上次PUSH後又有比較多的資料,
     * 那麼就需要設定PSH标志,讓接收端馬上把接收緩存中的資料送出給應用程式。
     */
    if (! (flags & MSG_MORE) || forced_push(tp))
        tcp_mark_push(tp, skb);    
 
    /* 如果設定了MSG_OOB标志,就記錄緊急指針 */
    tcp_mark_urg(tp, flags);
 
    /* 如果需要自動阻塞小包 */
    if (tcp_should_autocork(sk, skb, size_goal)) {
        /* avoid atomic op if TSQ_THROTTED bit is already set, 設定阻塞标志位 */
        if (! test_bit(TSQ_THROTTLED, &tp->tsq_flags)) {
            NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
            set_bit(TSQ_THROTTLED, &tp->tsq_flags);
        }
       
        /* It is possible TX completion already happened before we set TSQ_THROTTED.
         * 我的了解是,當送出給IP層的資料包都發送出去後,sk_wmem_alloc的值就會變小,
         * 此時這個條件就為假,之後可以發送被阻塞的資料包了。
         */
        if (atomic_read(&sk->sk_wmem_alloc) > skb->truesize)
            return;
    }
 
    /* 如果之後還有更多的資料,那麼使用TCP CORK,顯式地阻塞發送 */
    if (flags & MSG_MORE)
        nonagle = TCP_NAGLE_CORK;// nagle和cork都可以阻止小包的發送,後者更極端一點
 
    /* 盡可能地把發送隊列中的skb發送出去。
     * 如果發送失敗,檢查是否需要啟動零視窗探測定時器。
     */
    __tcp_push_pending_frames(sk, mss_now, nonagle);
}      
/* 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.
 * push由于TCP_CORK而被阻止的所有挂起的幀,或嘗試合并微小的資料包。
 */
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle)
{
    /* If we are closed, the bytes will have to remain here.
     * In time closedown will finish, we empty the write queue and
     * all will be happy.
     */
    if (unlikely(sk->sk_state == TCP_CLOSE))
        return;
 
    /* 如果發送失敗 */
    if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_atomic(sk, GFP_ATOMIC)))
        tcp_check_probe_timer(sk); /* 檢查是否需要啟用0視窗探測定時器*/
}      

可以看到​

​tcp_write_xmit​

​才是最後的大boss,我們來看一看:

tcp_write_xmit

/* This routine writes packets to the network.  It advances the
 * send_head.  This happens as incoming acks open up the remote
 * window for us.
 * 此例程将資料包寫入網絡。 它使send_head移動。 這發生在收到的ack中視窗大小變大的時候,注意,這裡指流量視窗
 *
 * LARGESEND note: !tcp_urg_mode is overkill, only frames between
 * snd_up-64k-mss .. snd_up cannot be large. However, taking into
 * account rare use of URG, this is not a big flaw.
 *
 * Returns 1, if no segments are in flight and we have queued segments, but
 * cannot send anything now because of SWS or another problem.
 * 如果沒有分段正在運作并且我們已将分段排隊,則傳回1,但是由于SWS或其他問題現在無法發送任何内容。
 */
static int 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;
 
  /* sent_pkts用來統計函數中已發送封包總數。*/
  sent_pkts = 0;
  
  /* 檢查是不是隻發送一個skb buffer,即push one */
  if (!push_one) {
  /* 如果要發送多個skb,則需要檢測MTU。
         * 這時會檢測MTU,希望MTU可以比之前的大,提高發送效率。
         */
    /* Do MTU probing. */
    result = tcp_mtu_probe(sk);
    if (!result) {
      return 0;
    } else if (result > 0) {
      sent_pkts = 1;
    }
  }
 
  while ((skb = tcp_send_head(sk))) {
    unsigned int limit;
    
    /* 設定有關TSO的資訊,包括GSO類型,GSO分段的大小等等。
     * 這些資訊是準備給軟體TSO分段使用的。
     * 如果網絡裝置不支援TSO,但又使用了TSO功能,
     * 則封包在送出給網絡裝置之前,需進行軟分段,即由代碼實作TSO分段。
     */
    tso_segs = tcp_init_tso_segs(sk, skb, mss_now);
    BUG_ON(!tso_segs);
    
    /* 檢查擁塞視窗, 可以發送幾個segment */
    /* 檢測擁塞視窗的大小,如果為0,則說明擁塞視窗已滿,目前不能發送。
         * 拿擁塞視窗和正在網絡上傳輸的包數目相比,如果擁塞視窗更大,則傳回擁塞視窗減掉正在網絡上傳輸的包數目剩下的大小。
         * 該函數目的是判斷正在網絡上傳輸的包數目是否超過擁塞視窗,如果超過了,則不發送。
     */
    cwnd_quota = tcp_cwnd_test(tp, skb);
    if (!cwnd_quota)
      break;
    
    /* 檢測目前封包是否完全處于發送視窗内,如果是則可以發送,否則不能發送 */
    /*
    Does at least the first segment of SKB fit into the send window?
    static bool tcp_snd_wnd_test(const struct tcp_sock *tp,
               const struct sk_buff *skb,
               unsigned int cur_mss)
    {
      u32 end_seq = TCP_SKB_CB(skb)->end_seq;
    
      if (skb->len > cur_mss) // 發送的包以cur_mss為機關,把skb的偏移量後移cur_mss
        end_seq = TCP_SKB_CB(skb)->seq + cur_mss;
      // 可以看出發送的機關是一個skb,如果小于滑動視窗的話可以發送;
      // 至少在這看起來當資料大小大于視窗大小的時候不發送,這和我學的網絡說的不太一樣,這樣就天然避免小包了,疑惑
      // 當然換個角度,既然資料不到一個mss都小于視窗了,那麼對端狀态肯定不太好,不發也是對的
      return !after(end_seq, tcp_wnd_end(tp));
    }
    static inline bool after(u32 seq1, u32 seq2)
    {
            return (s32)(seq1 - seq2) > 0;
    }
    // Returns end sequence number of the receiver's advertised window 
    static inline u32 tcp_wnd_end(const struct tcp_sock *tp)
    {
      return tp->snd_una + tp->snd_wnd;
    }
    */
    // 
    if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
      break;
 
    /* tso_segs=1表示無需tso分段 */
    if (tso_segs == 1) {
      // 更據上面的代碼,nagle隻有在發送資料小于視窗的時候才有用
      /* 根據nagle算法,計算是否需要發送資料 */
      if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
                 (tcp_skb_is_last(sk, skb) ?
                  nonagle : TCP_NAGLE_PUSH))))
        break;
    } else {
            /* 當不止一個skb時,通過TSO計算是否需要延時發送 */
      /* 如果需要TSO分段,則檢測該封包是否應該延時發送。
       * tcp_tso_should_defer()用來檢測GSO段是否需要延時發送。
             * 在段中有FIN标志,或者不處于open擁塞狀态,或者TSO段延時超過2個時鐘滴答,
             * 或者擁塞視窗和發送視窗的最小值大于64K或三倍的目前有效MSS,在這些情況下會立即發送,
             * 而其他情況下會延時發送,這樣主要是為了減少軟GSO分段的次數,以提高性能。
             */
      if (!push_one && tcp_tso_should_defer(sk, skb))
        break;
    }
 
    limit = mss_now;
    /* 在TSO分片大于1的情況下,且TCP不是URG模式。通過MSS計算發送資料的limit
     * 以發送視窗和擁塞視窗的最小值作為分段段長*/
     */
    if (tso_segs > 1 && !tcp_urg_mode(tp))
      // limit就是我們可以立即發送的資料
      limit = tcp_mss_split_point(sk, skb, mss_now, cwnd_quota);
    /* 當skb的長度大于限制時,需要調用tso_fragment分片,如果分段失敗則暫不發送 */
    if (skb->len > limit &&
        unlikely(tso_fragment(sk, skb, limit, mss_now)))
      break;
    
    /* 以上6行:根據條件,可能需要對SKB中的封包進行分段處理,分段的封包包括兩種:
     * 一種是普通的用MSS分段的封包,另一種則是TSO分段的封包。
         * 能否發送封包主要取決于兩個條件:一是封包需完全在發送視窗中,而是擁塞視窗未滿。
         * 第一種封包,應該不會再分段了,因為在tcp_sendmsg()中建立封包的SKB時已經根據MSS處理了,
         * 而第二種封包,則一般情況下都會大于MSS,因為通過TSO分段的段有可能大于擁塞視窗的剩餘空間,
         * 如果是這樣,就需要以發送視窗和擁塞視窗的最小值作為段長對封包再次分段。
         */
    
    /* 更新tcp的時間戳,記錄此封包發送的時間 */
    TCP_SKB_CB(skb)->when = tcp_time_stamp;
    
    // 發送資料
    /* tcp_transmit_skb的注釋
     * 該例程實際上傳輸由tcp_do_sendmsg()排隊的TCP資料包。 
     * 初始傳輸和可能的後續重傳都使用此功能。 在這裡看到的所有SKB都 是完全無頭的。 
     * 我們的工作是建構TCP标頭,并将資料包向下傳遞到IP,這樣它就可以執行相同的操作,并将資料包傳遞給裝置。
     * 我們在這裡使用的是原始SKB的克隆,或者是由重新傳輸引擎制作的新的唯一副本。
     * 這裡讓我想到sendfile,可以參考這篇文章
     * 發送資料的時候從socket到協定層還需要一次拷貝,看來就在這裡了。
     */
    if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
      break;
 
    /* Advance the send_head.  This one is sent out.
     * This call will increment packets_out.
     */
    /* 更新統計,并啟動重傳計時器 */
    /* 調用tcp_event_new_data_sent()-->tcp_advance_send_head()更新sk_send_head,
     * 即取發送隊列中的下一個SKB。同時更新snd_nxt,即等待發送的下一個TCP段的序号,
     * 然後統計發出但未得到确認的資料報個數。最後如果發送該封包前沒有需要确認的封包,
     * 則複位重傳定時器,對本次發送的封包做重傳逾時計時。
     */
    tcp_event_new_data_sent(sk, skb);
    
    /* 更新struct tcp_sock中的snd_sml字段。snd_sml表示最近發送的小包(小于MSS的段)的最後一個位元組序号,
     * 在發送成功後,如果封包小于MSS,即更新該字段,主要用來判斷是否啟動nagle算法
     */
    tcp_minshall_update(tp, mss_now, skb);
    sent_pkts++;
 
    if (push_one)
      break;
  }
        /* 如果本次有資料發送,則對TCP擁塞視窗進行檢查确認。*/
  if (likely(sent_pkts)) {
    tcp_cwnd_validate(sk);
    return 0;
  }
  /* 
         * 如果本次沒有資料發送,則根據已發送但未确認的封包數packets_out和sk_send_head傳回,
   * packets_out不為零或sk_send_head為空都視為有資料發出,是以傳回成功。
   */
  return !tp->packets_out && tcp_send_head(sk);
}      

我們從​

​tcp_transmit_skb​

​​看到資料的一次拷貝,是以我們要從使用者态發一個網絡資料包的話至少兩次拷貝,一次使用者态到核心态,一次從​

​socket​

​緩沖區(skb)copy到相關協定層。

總結

我們再來回顧一下一個資料包的發送過程以及一些需要注意的點:

  1. ​sendto​

    ​對使用者态的資料進行封裝,伴随這一次從使用者态到核心态的拷貝。
  2. ​sendmsg​

    ​​通過套接字協定的不同調用不同的發送處理函數,TCP調用​

    ​tcp_sendmsg​

    ​​,UDP調用​

    ​udp_sendmsg​

    ​。
  3. 以​

    ​tcp_sendmsg​

    ​為例,主要把資料封裝後插入資料緩沖區,可能插入失敗,但是隻于發送緩沖區大小以及核心的記憶體資源有關,這裡暫時與視窗無關。
  4. ​tcp_push​

    ​​負責把已經拷貝到發送隊列中的資料給發送出去,最終調用​

    ​tcp_write_xmit​

    ​。
  5. ​tcp_write_xmit​

    ​先檢查擁塞視窗,再檢查流量視窗,當小于兩者的時候才進行發送,這裡有一點很迷惑,就這個版本的源碼來看,當發送的資料小于mss且大于視窗時一個位元組也不發送。這樣看來nagle,cork隻有在資料包小于流量視窗時才有效。
  6. 最後調用​

    ​tcp_transmit_skb​

    ​發送資料包,這裡藏着又一次拷貝,即從核心态到協定引擎的拷貝。
  7. 資料拷貝到了協定層以後就傳回了,至于發送成功不成功與send就沒有關系了。

回到這篇文章的主旨,一個是面試官的問題,一個是題目,我想答案已經很清楚了。

首先是面試官的問題,顯然不可以,因為資料拷貝到協定引擎中的時候send已經傳回了,發沒發到對端機器根本不知道,就算發送到了也可能存在接收緩沖區沒被應用接收,是以唯一一個确定對端收到資料的方法就是使用者态的ACK。

接下來是網絡發送緩沖區與視窗的關系,其實如果真的看完了上面的代碼這個問題已經很明顯了,或者隻看​

​tcp_write_xmit​

​​也可以,其實就是資料在沒超過緩沖區上限(​

​/proc/sys/net/ipv4/tcp_wmemd​

​ 描述了最小預設和最大的發送緩沖區大小)和記憶體上限的時候資料都存在發送緩沖區中,每次send都會嘗試發送,當超過擁塞視窗和流量視窗的時候不會發送,

這裡有一個不确定的小細節,就是當發送的資料小于mss且大于視窗時一個位元組也不發送,這篇文章我已經第三次提起這個問題了。

最後糾正一個一直以來我觀念上的一個錯誤,也是一個寫過服務端代碼的人可能都有的謬誤,即​

​write​

​​傳回值小于資料塊的時候與什麼有關?我一直堅定不移的認為隻與擁塞視窗和流量視窗有關,其實這是一個大錯特錯的想法,這也再次高速我們想當然的都不是真理。問題的答案是與發送緩沖區上限,核心記憶體上限,擁塞視窗和流量視窗都有關,且與前兩者的關系更緊密些。這也告訴我們​

​write​

​成功這個資料可能根本就在緩沖區,連協定引擎都沒進過去,更别說網卡,甚至對方機器或者對端應用層了。

這隻是TCP發送部分,東西就已經很多了,其中很多細節都不是很了解,隻能說大概明白了發送的過程。

接收部分就留到後面有時間或者需求的時候吧。

祝春招一切順利。

  1. ​​send manual​​
  2. ​​當應用程式調用Send之後怎麼判斷對方是否成功接收?​​
  3. ​​Linux TCP/IP協定棧之Socket的實作分析(socket listen)​​
  4. ​​為什麼可以同時在TCP和UDP上使用相同的端口?​​
  5. ​​kernel struct socket​​
  6. ​​socket(2) — Linux manual page​​
  7. linux TCP發送過程源碼分析
  8. 對Linux服務端程式設計的一點淺薄了解
  9. TCP的發送系列 — tcp_sendmsg()的實作(一)
  10. TCP的發送系列 — tcp_sendmsg()的實作(二)
  11. TCP源碼分析–tcp_write_xmit