TCP发送源码学习(1)--tcp_sendmsg

一、tcp_sendmsg()函数分析:

int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
        size_t size)
{
    struct iovec *iov;
    /*从通用的struct sock *sk得到struct tcp_sock *tp,其实只是一个强制类型转换,因为strcut sock是所有其它socket类型的第一个成员,所有可以直接对指针进行强制类型转换*/
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;
    int iovlen, flags;
    int mss_now, size_goal;
    int sg, err, copied;
    long timeo;

    lock_sock(sk);
    TCP_CHECK_TIMER(sk);

    flags = msg->msg_flags;
  /*设置发送等待时间,如果设置了DONTWAIT,则timeo为0. 如果没有该标志,则timeo就为sock->sk_sndtimeo。
    发送超时时间保存在sock结构的sk_sndtimeo成员中。*/
    timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);

    /* Wait for a connection to finish. */
  /*TCP只在ESTABLISHED和CLOSE_WAIT这两种状态下,接收窗口是打开的,才能接收数据。
    因此如果不处于这两种状态,则调用sk_stream_wait_connect()等待建立起连接,一旦超时则跳转到out_err处做出错处理*/
    if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))
        if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
            goto out_err;

    /* This should be in poll */
  /*清除套接口发送缓冲队列已满的标志。
    struct socket ->flags一组标志位,如下:
    SOCK_ASYNC_NOSPACE:标识该套接口的发送队列是否已满。
    SOCK_ASYNC_WAITDATA:标识应用程序通过recv调用时,是否在等待数据的接收。
    SOCK_NOSPACE:标识非异步的情况下该套接口的发送队列是否已满。
    SOCK_PASSCRED:用于标识是否设置了SO_PASSCRE套接口选项。
    SOCK_PASSSEC:用于标识是否设置了SO_PASSSEC选项。*/
    clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
    
  /*调用tcp_send_mss获取当前有效mss即mss_now和数据段的最大长度即size_goal。
    在此传入是否标识MSG_OOB位,这是因为MSG_OOB是判断是否支持GSO的条件之一,而紧急数据不支持GSO。
    mss_now:当前的最大报文分段长度(Maxitum Segment Size)。
    size_goal:发送数据报到达网络设备时数据段的最大长度,该长度用来分割数据。TCP发送报文时,每个SKB的大小不能超过该值。
    在不支持GSO的情况下,size_goal就等于mss_now,而如果支持GSO,则size_goal会是MSS的整数倍。数据报发送到网络设备后再由网络设备根据MSS进行分割。*/
    mss_now = tcp_send_mss(sk, &size_goal, flags);

    /* Ok commence sending. */
    /*获取待发送数据块块数和数据指针,同时清零copied,copied是已经从用户数据块复制到SKB的字节数。*/
    iovlen = msg->msg_iovlen;
    iov = msg->msg_iov;
    copied = 0;
    
    /*在开始分段前,初始化错误码为EPIPE,然后判断此时套接口是否存在错误,以及该套接口是否允许发送数据,如果有错误或不允许发送数据,则跳转到do_err处做出错处理。*/
    err = -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++;

        /*分段过程的内层循环控制每个数据块是否复制完成。*/
        while (seglen > 0) {
            int copy = 0;
            int max = size_goal;
            /*max=size_goal=tp->xmit_size_goal,表示发送数据报到达网络设备时数据段的最大长度,该长度用来分割数据,TCP发送报文时,每个SKB的大小不能超过该值。
            在不支持GSO情况下,xmit_size_goal就等于MSS;而如果支持GSO,则xmit_size_goal会是MSS的整数倍。数据报发送到网络设备后再由网络设备根据MSS进行分割。*/

            /*获取传输控制块发送队列的尾部的那个SKB,因为只有队尾的那个SKB才有可能存在剩余空间的。*/
            skb = tcp_write_queue_tail(sk);
            if (tcp_send_head(sk)) {
                if (skb->ip_summed == CHECKSUM_NONE)//校验和的相关知识详见段三。
                    max = mss_now;
                /*本次循环copy的数据长度。max是当前SKB的最大数据长度,skb->len是当前skb的数据长度,相减得到当前skb的剩余数据空间。*/
                copy = max - skb->len;
            }

            /*copy小于等于0,说明当前SKB已使用空间大于等于size_goal,则需要分配新的SKB*/
            if (copy <= 0) {
new_segment:
                /* Allocate new segment. If the interface is SG,
                 * allocate skb fitting to single page.
                 */
              /*判断发送队列中报文总长度是否已达到发送缓冲区的上限,如果超过,则只能跳到wait_for_sndbuf处理。
                sk_stream_memory_free中两个参数的说明:
                sk->sk_wmem_queued:表示发送缓冲队列中已分配的字节数,一般来说,分配一个struct sk_buff是用于存放一个tcp数据报,其分配字节数应该是MSS+协议首部长度。
                    在我的实验环境中,MSS值是1448,协议首部取最大长度 MAX_TCP_HEADER,在我的实验环境中为224。经数据对齐处理后,最后struct sk_buff的truesize为1956。
                    也就是队列中每分配一个struct sk_buff,sk->sk_wmem_queued的值就增加1956。
                sk->sk_rcvbuf、sk->sk_sndbuf,这两个值分别代表每个sock的接收发送队列的最大限制。*/
                if (!sk_stream_memory_free(sk))
                    goto wait_for_sndbuf;

                /*开始alloc一个新的skb,alloc的大小一般都等于mss的大小,这里是通过select_size得到的。*/
                skb = sk_stream_alloc_skb(sk,
                             select_size(sk, sg),
                             sk->sk_allocation);
                if (!skb)
                    goto wait_for_memory;

                /*
                 * Check whether we can use HW checksum.
                 */
                /*根据目的路由网络设备的特性,确定是否设置由硬件执行校验和的标志。*/
                if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
                    skb->ip_summed = CHECKSUM_PARTIAL;
            
                /*将该skb插入到发送队列的尾部。*/
                skb_entail(sk, skb);
                
                /*最后初始化copy变量为发送数据报到网络设备时的最大数据段的长度,copy表示每次复制到skb的数据长度。*/
                copy = size_goal;
                max = size_goal;
            }

            /* Try to append data to the end of skb. */
            /*copy不能大于当前数据块剩余待复制的数据长度,如果大于,则需要调整copy的值。*/
            if (copy > seglen)
                copy = seglen;

            /* Where to copy to? */
            /*判断skb的线性存储区底部是否还有空间。*/
            if (skb_tailroom(skb) > 0) {
                /* We have some space in skb head. Superb! */
              /*判断skb的线性存储区底部是否还有空间。如果还有,则进一步测试底部剩余空间是否小于copy,如果是则再次调整待复制数据长度copy。
                到此为止已经计算除了本次需要复制数据的长度,接下来调用skb_add_data从用户空间复制长度为copy的数据到skb中。如果复制失败,则跳转到do_fault处。*/
                if (copy > skb_tailroom(skb))
                    copy = skb_tailroom(skb);
                if ((err = skb_add_data(skb, from, copy)) != 0)
                    goto do_fault;
            } else {
                /*如果SKB线性存储区底部已经没有空间了,那就需要把数据复制到支持分散聚合的分页中*/
                /*merge标识是否在最后一个分页中添加数据,初始化为0*/
                int merge = 0;
                /*获取当前SKB的分片段数,在skb_shared_info中用nr_frags表示。*/
                int i = skb_shinfo(skb)->nr_frags;
              /*通过宏TCP_PAGE获取最后一个分片的页面page。
                通过宏TCP_OFF获取已复制数据尾端在最后一个分片的页面的页内偏移。
                #define TCP_PAGE(sk)	(sk->sk_sndmsg_page)
                #define TCP_OFF(sk)	(sk->sk_sndmsg_off)
                sk_sndmsg_page:指向为本传输控制块最近一次分配的页面,通常是当前套接口发送队列中最后一个SKB的分片数据的最后一页。
                sk_sndmsg_off:表示最后一页分片的页内偏移,新的数据可以直接从这个位置复制到该分片中。*/
                struct page *page = TCP_PAGE(sk);
                int off = TCP_OFF(sk);

                /*调用skb_can_coalesce(),判断SKB上最后一个分散聚合页面是否有效,即能否将数据添加到该分页上,如果可以则设置merge标志。*/
                if (skb_can_coalesce(skb, i, page, off) &&
                 off != PAGE_SIZE) {
                    /* We can extend the last page
                     * fragment. */
                    merge = 1;
                } else if (i == MAX_SKB_FRAGS || !sg) {
                  /*如果不能往最后一个分片内追加数据,则需要判断分片数量是否已达到上限,如果达到上限,则说明不能再往此SKB复制数据了,需要分配新的SKB。
                    或者网络设备不支持分散聚合I/O,则也说明不能往分片复制数据。
                    在这种情况下,对当前的TCP报文设置TCPHDR_PSH标志,并更新pushed_seq成员,表示到pushed_seq为止都是希望能尽快发送出去的。
                    最后跳转到new_segment处,又开始分配新的SKB,因为数据还没复制完。*/
                    /* Need to add new fragment and cannot
                     * do this because interface is non-SG,
                     * or because all the page slots are
                     * busy. */
                    tcp_mark_push(tp, skb);
                    goto new_segment;
                } else if (page) {
                    /*最后一个分页中数据已经填满,且分页数量未达到上限。*/
                    if (off == PAGE_SIZE) {
                        put_page(page);
                        TCP_PAGE(sk) = page = NULL;
                        off = 0;
                    }
                } else
                    /*到此处只剩下一种情况了,既不能在最后一个分页追加数据,又不能分配新的SKB,那么不管这个SKB是否存在分页,数据必定复制到分页起始位置。*/
                    off = 0;

                /*待复制的数据长度如果大于页面内剩余空间的长度,则调整待复制的数据长度copy。*/
                if (copy > PAGE_SIZE - off)
                    copy = PAGE_SIZE - off;

                /*判断用于输出使用的缓存是否达到上限,一旦达到上限就只能等待,只有有可用输出缓存或超时为止。*/
                if (!sk_wmem_schedule(sk, copy))
                    goto wait_for_memory;
    
              /*如果最后一个页面为空,一般是新分配的SKB,或者是前一次复制数据时一个页面分段刚好使用完,那么就需要调用sk_stream_alloc()分配一个新的页面来存储数据。
                如果分配失败则跳转到wait_for_memory处*/
                if (!page) {
                    /* Allocate new cache page. */
                    if (!(page = sk_stream_alloc_page(sk)))
                        goto wait_for_memory;
                }

                /* Time to copy data. We are close to
                 * the end! */
              /*此刻,SKB分页已准备好,调用skb_copy_to_page()将用户态的数据复制到分页中。
                如果复制失败,则需要更新sk_sndmsg_page和sk_snd_off。因为虽然复制失败了,但是这个页面有可能是刚刚分配的,
                因此需要记录以备下次复制时或者释放时使用。*/
                err = skb_copy_to_page(sk, from, skb, page,
                         off, copy);
                if (err) {
                    /* If this page was new, give it to the
                     * socket so it does not get leaked.
                     */
                    if (!TCP_PAGE(sk)) {
                        TCP_PAGE(sk) = page;
                        TCP_OFF(sk) = 0;
                    }
                    goto do_error;
                }

                /* Update the skb. */
                if (merge) {
                    /*merge=1表示是在一个分页里复制数据,则更新有关分段的信息,即更新该页面内有效数据的长度。*/
                    skb_shinfo(skb)->frags[i - 1].size +=
                                    copy;
                } else {
                  /*如果是复制到一个全新的页面分段中,则需要更新的有关分段信息就会多一些,如分段数据的长度、页内偏移、分段数量等。
                    调用skb_fill_page_desc()来完成。如果标识最近一次分配页面的sk_sndmsg_page不为空,则增加对该页面的引用。
                    否则说明复制了数据的页面是新分配的,且没有使用完,在增加对该页面的引用的同时,还需更新sk_sndmsg_page的值。
                    如果新分配的页面已使用完,就无须更新sk_sndmsg_page的值了,因为如果SKB未超过段上限,那么下次必定还会分配新的页面,
                    因此在此处就省去了对off+copy=PAGE_SIZE这条分支的处理。*/
                    skb_fill_page_desc(skb, i, page, off, copy);
                    if (TCP_PAGE(sk)) {
                        get_page(page);
                    } else if (off + copy < PAGE_SIZE) {
                        get_page(page);
                        TCP_PAGE(sk) = page;
                    }
                }
                
                /*由于复制了新的数据,需要更新数据尾端在最后一页分片的页内偏移。*/
                TCP_OFF(sk) = off + copy;
            }
            
            /*如果复制的数据长度为零,则取消TCPHDR_PSH*/
            if (!copied)
                TCP_SKB_CB(skb)->flags &= ~TCPHDR_PSH;
            
            /*更新发送队列中的最后一个序号write_seq,以及数据包的最后一个序列end_seq,初始化gso分段数gso_segs。*/
            tp->write_seq += copy;
            TCP_SKB_CB(skb)->end_seq += copy;
            skb_shinfo(skb)->gso_segs = 0;
            /*更新指向数据源的指针和已复制字节数。*/
            from += copy;
            copied += copy;
            /*如果所有数据已全部复制到SKB中,则跳转到out处理。*/
            if ((seglen -= copy) == 0 && iovlen == 0)
                goto out;

            /*如果当前SKB中的数据小于max,说明还可以往里填充数据,或者发送的是带外数据(MSG_OOB),则跳过以下发送过程,继续复制数据到SKB*/
            if (skb->len < max || (flags & MSG_OOB))
                continue;

          /*检查是否必须立即发送,即检查自上次发送后产生的数据是否已超过对方曾经通告过的最大窗口值的一半。如果必须立即发送,
            则设置TCPHDR_PSH标志后调用__tcp_push_pending_frames(),在发送队列上从sk_send_head开始把SKB发送出去。*/
            if (forced_push(tp)) {
                tcp_mark_push(tp, skb);
                __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
            } else if (skb == tcp_send_head(sk))
                /*如果没有必要立即发送,且发送队列上只存在这个段,则调用tcp_push_one()只发送当前段。*/
                tcp_push_one(sk, mss_now);
            continue;

/*套接口的发送缓存是有大小限制的,当发送队列中的数据段总长度超过发送缓冲区的长度上限时,就不能再分配SKB了,只能等待。
设置SOCK_NOSPACE标志,表示套接口发送缓冲区已满。*/
wait_for_sndbuf:
            set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
/*跳到这里,表示整个系统内存不够*/
wait_for_memory:
          /*虽然分配SKB失败,但是如果之前有数据从用户空间复制过来,则调用tcp_push()将其发送出去。
            其中第三个参数中去掉MSG_MORE标志,表示本次发送没有更多的数据了。
            因为分配SKB失败,因此可以加上TCPHDR_PSH标志,第五个参数使用nagle算法,可能会推迟发送。*/
            if (copied)
                tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
        
            /*调用sk_stream_wait_memory()进入睡眠,等待内存空闲的信号,如果在超时时间内没有得到该信号,则跳转到do_error处执行。*/
            if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
                goto do_error;
    
            /*等待内存未超时,有空闲内存可用。睡眠后,MSS有可能发生了变化,所以重新获取当前的MSS和TSO分段段长,然后继续循环复制数据。*/
            mss_now = tcp_send_mss(sk, &size_goal, flags);
        }
    }
/*发送过程中正常的退出。*/
out:
    /*如果已有复制的数据,则调用tcp_push()将其发送出去,是否立即发送取决于nagle算法。*/
    if (copied)
        tcp_push(sk, flags, mss_now, tp->nonagle);
    TCP_CHECK_TIMER(sk);
    /*解锁传输控制块。*/
    release_sock(sk);
    /*返回已复制的字节数。*/
    return copied;

/*在复制数据异常时进入到这里。*/
do_fault:
    if (!skb->len) {
        tcp_unlink_write_queue(skb, sk);
        /* 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_wmem_free_skb(sk, skb);
    }

do_error:
    /*如果已复制了部分数据,那么即使发生了错误,也可以发送数据包,因此跳转到out处*/
    if (copied)
        goto out;
out_err:
    /*如果没有复制数据,则调用sk_stream_error()来获取错误码。然后对传输层控制块解锁后返回错误码。*/
    err = sk_stream_error(sk, flags, err);
    TCP_CHECK_TIMER(sk);
    release_sock(sk);
    return err;
}
二、tcp_send_mss()函数分析

static int tcp_send_mss(struct sock *sk, int *size_goal, int flags)
{
    int mss_now;

    mss_now = tcp_current_mss(sk);
    *size_goal = tcp_xmit_size_goal(sk, mss_now, !(flags & MSG_OOB));

    return mss_now;
}
调用tcp_current_mss获取当前有效mss,即mss_now。
调用tcp_xmit_size_goal获取发送数据报到达网络设备时数据段的最大长度,该长度用来分割数据,TCP发送报文时,每个SKB的大小不能超过该值。
在此传入是否标识MSG_OOB位,这是因为MSG_OOB是判断是否支持GSO的条件之一,而紧急数据不支持GSO。
在不支持GSO的情况下,size_goal就等于mss_now,而如果支持GSO,则size_goal会是MSS的整数倍。数据报发送到网络设备后再由网络设备根据MSS进行分割。
MSS:Maxitum Segment Size最大报文分段长度
MTU:Maxitum Transmission Unit 最大传输单元
函数中相关变量解释:
metrics[RTAX_MTU]:与路径相关的MTU,通过路径MTU发送的下一跳MTU会保存在此,初始化值为576或网络设备的MTU。
                  该变量保存在struct dst_entry中。
icsk_pmtu_cookie:最近一次更新的路径MTU,由metrics[RTAX_MTU]初始化和更新。
                  该变量保存在struct tcp_sock ==>> struct inet_connection_sock中。
mss_cache:发送方当前有效MSS,该变量保存在struct tcp_sock中。
mss_clamp:当前连接对端接收的MSS上限,在更新mss_cache时必须保证mss_cache不大于mss_clamp。
          该变量保存在struct tcp_sock ==>> struct tcp_options_received中。
user_mss:用户设置的MSS上限,与建立连接时SYN段中MSS两者取最小值作为该连接对端接收MSS的上限,存储在mss_clamp中。
         该变量保存在struct tcp_sock ==>> struct tcp_options_received中。

三、校验和的几个宏定义
/* Don't change this without changing skb_csum_unnecessary! */
#define CHECKSUM_NONE 0
#define CHECKSUM_UNNECESSARY 1
#define CHECKSUM_COMPLETE 2
#define CHECKSUM_PARTIAL 3

CHECKSUM_NONE:表示硬件不支持校验和,完全由软件来执行校验和。
CHECKSUM_UNNECESSARY:表示没有必要执行校验和。
CHECKSUM_COMPLETE:表示已经完成执行校验和。
CHECKSUM_PARTIAL:表示由硬件来执行校验和。

扩展阅读:
skb->csum和skb->ip_summed这两个域也是与校验相关的,这两个域的含义依赖于skb表示的是一个输入包还是一个输出帧。 

当数据包是一个输入包时,skb->csum表示的是当前数据包的4层的checksum值,skb->ip_summed表示的是四层校验的状态,下面的几个宏定义表示了设备驱动传递给4层的一些信息(通过ip_sumed),这里要注意,一旦当四层接受了这个包,他可能会改变ip_summed的值。 
 
/* Don't change this without changing skb_csum_unnecessary! */
#define CHECKSUM_NONE 0
#define CHECKSUM_UNNECESSARY 1
#define CHECKSUM_COMPLETE 2

CHECKSUM_NONE:表示csum域中的校验值是错误的,也就是校验失败。这里要注意的是,一般来说当2层的校验失败后,驱动会直接丢掉这个包,可是如果输入帧是要被forward的,那么路由器不应该由于一个四层的校验失败而丢掉这个包(路由器不建议查看四层的校验值),它将会将这位置为CHECKSUM_NONE,然后将包发向目的地址,交由目的地址的主机来进行处理。 

CHECKSUM_UNNECESSARY:表示网卡已经计算和验证了四层的头和校验值。也就是计算了tcp udp的伪头。还有一种情况就是回环,因为在回环中错误发生的概率太低了,因此就不需要计算校验来节省cpu时间。 

CHECKSUM_COMPLETE:表示nic已经计算了4层头的校验,并且csum已经被赋值,此时4层的接收者只需要加伪头并验证校验结果。 

接下来我们来看当数据包是输出包时的情况,此时csum表示为一个指针,它表示硬件网卡存放将要计算的校验值的地址。这个域在输出包时使用,只在校验值在硬件计算的情况下。比如NAT,它会修改ip头,此时就需要重新计算4层的校验值,也就是从4层传递下来的4层校验值需要在底层进行修改。当修改后,我们在底层就可以通过csum来存取这个校验值。 

而此时ip_summed可以被设置的值有下面两种: 
#define CHECKSUM_NONE 0
#define CHECKSUM_COMPLETE 2

这时含义就完全不一样了。第一个表示已经计算好了校验值,设备不需要做任何事。 
第二个表示4层的伪头的校验已经完毕,并且已经加入到ip头中,此时只需要设备计算整个头4层头的校验值。


from :http://sunjiangang.blog.chinaunix.net/uid-9543173-id-3546189.html

posted on 2014-12-28 00:07  胡永光  阅读(508)  评论(0编辑  收藏  举报

导航