Linux socket 数据发送类函数实现(三)

注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4

上回我们说到send四兄弟在sock_sendmsg()完成集合,write家族也在__sock_sendmsg()处等待。

其实sock_sendmsg()的下一步就是进入__sock_sendmsg(),所以我们就从__sock_sendmsg()开始,带着这些兵马再次出发吧。

int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
    struct kiocb iocb;
    struct sock_iocb siocb;
    int ret;
    //构造IO请求
    init_sync_kiocb(&iocb, NULL);
    iocb.private = &siocb;
    //对于write家族,并不经过sock_sendmsg,直接横刀立马,在此等候
    ret = __sock_sendmsg(&iocb, sock, msg, size);
    if (-EIOCBQUEUED == ret)
        ret = wait_on_sync_kiocb(&iocb);
    return ret;
}

进入大一统的__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);
}

还记得sendmmsg里,在发送多个消息是,为了提高性能会调用sock_sendmsg_nosec(),这是为了跳过上面的安全检查,提高性能。简单检查后就进入__sock_sendmsg_nosec()

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);
    //对sock_iocb结构进行赋值
    si->sock = sock;
    si->scm = NULL;
    si->msg = msg;//消息地址
    si->size = size;//消息长度
    //调用tcp层发送函数
    return sock->ops->sendmsg(iocb, sock, msg, size);
}

在TCP协议中,sock->ops指向的是inet_stream_ops,在socket系统调用时赋值,

const struct proto_ops inet_stream_ops = {
    ...
    .sendmsg       = inet_sendmsg,
    .recvmsg       = inet_recvmsg,
    ...
};

由此可知,调用的是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 bind the socket. */
    //如果没有绑定端口,那就先绑定
    if (!inet_sk(sk)->inet_num && !sk->sk_prot->no_autobind &&
        inet_autobind(sk))
        return -EAGAIN;
    //又是一层指针
    return sk->sk_prot->sendmsg(iocb, sk, msg, size);
}

简单处理后,又进了sk->sk_prot的sendmsg,对于TCP而言,sk->sk_prot指向tcp_prot

struct proto tcp_prot = {
    .name           = "TCP",
    ...
    .recvmsg        = tcp_recvmsg,
    .sendmsg        = tcp_sendmsg,
    ...
};

这两个结构体用到的还是很频繁的,可以多点关注。

所以层层深入,走向tcp_sendmsg()函数,也是够曲折的,

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;
    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;
    }
    //如果是阻塞调用,获取发送超时时间,超时时间可通过SO_SNDTIMEO选项设置
    timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);

    //等待连接建立,使用快速开启选项的被动连接端可不用等待连接完全建立即可发送数据
    if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&
        !tcp_passive_fastopen(sk)) {
        if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
            goto do_error;
    }
    ...
    /* This should be in poll */
    clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
    //size_goal表示GSO支持的大小,为mss的整数倍,不支持GSO时则和mss相等
    mss_now = tcp_send_mss(sk, &size_goal, flags);

    /* Ok commence sending. */
    iovlen = msg->msg_iovlen;
    iov = msg->msg_iov;
    copied = 0;

    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++;
        if (unlikely(offset > 0)) {  /* Skip bytes copied in SYN */
            //如果是快速开启的连接,减去在SYN报文中发送的数据
            if (offset >= seglen) {
                offset -= seglen;
                continue;
            }
            seglen -= offset;
            from += offset;
            offset = 0;
        }

        while (seglen > 0) {
            int copy = 0;
            int max = size_goal;
            //获取发送队列的最后一个skb
            skb = tcp_write_queue_tail(sk);
            if (tcp_send_head(sk)) {//还有未发送数据,即skb还未发送
                if (skb->ip_summed == CHECKSUM_NONE)
                    max = mss_now;
                copy = max - skb->len;//能追加数据的长度
            }

            if (copy <= 0) {//无法追加数据,需要重新分配一个skb来存放数据
new_segment:
                //如果发送队列的总大小sk_wmem_queued大于等于发送缓存的上限sk_sndbuf
                //等待发送缓存释放
                if (!sk_stream_memory_free(sk))
                    goto wait_for_sndbuf;
                //重新分配skb结构
                skb = sk_stream_alloc_skb(sk, select_size(sk, sg), sk->sk_allocation);
                if (!skb)
                    goto wait_for_memory;

                if (sk->sk_route_caps & NETIF_F_CSUM_MASK)
                    skb->ip_summed = CHECKSUM_PARTIAL;
                //把新分配的skb加入发送队列的尾部,
                //同时增加发送队列的大小,减小预分配缓存的大小
                skb_entail(sk, skb);
                copy = size_goal;
                max = size_goal;

                if (tp->repair)
                    TCP_SKB_CB(skb)->sacked |= TCPCB_REPAIRED;
            }

            //本次可追加数据的数据足以满足此次数据发送,因此拷贝数设置为数据长度
            if (copy > seglen)
                copy = seglen;

            if (skb_availroom(skb) > 0) {
                /* skb线性区还有空间,那就先使用这部分空间 */
                copy = min_t(int, copy, skb_availroom(skb));
                //拷贝用户数据到内核空间,同时计算校验和
                err = skb_add_data_nocache(sk, skb, from, copy);
                if (err)
                    goto do_fault;
            } else {
                //线性区无空间,使用分页区
                bool merge = true;
                int i = skb_shinfo(skb)->nr_frags;//获取skb分页数
                struct page_frag *pfrag = sk_page_frag(sk);
                //检查分页是否有空间,没有则申请新的分页
                if (!sk_page_frag_refill(sk, pfrag))
                    goto wait_for_memory;
                //判断是否能往最后一个分页追加数据,即数据是否能合并
                if (!skb_can_coalesce(skb, i, pfrag->page, pfrag->offset)) {
                    //如果分页数量已达上限,或者网卡不支持分散聚合
                    //将数据包设置PSH标志,以便尽快发送,然后重新申请新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的分页空间,同时计算校验和。
                //更新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上,并更新分页数统计
                    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)->end_seq += copy;//更新skb数据结束序号
            skb_shinfo(skb)->gso_segs = 0;//清零gso_segs,在tcp_write_xmit中重新计算

            from += copy;//更新用户数据起始位置偏移
            copied += copy;//已拷贝数据统计
            if ((seglen -= copy) == 0 && iovlen == 0)//数据拷贝完毕,跳出循环
                goto out;

            if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
                continue;
            //如果需要设置PSH标记
            if (forced_push(tp)) {
                //设置PSH标记
                tcp_mark_push(tp, skb);
                //将发送队列的数据报文尽可能的发送出去
                __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
            } else if (skb == tcp_send_head(sk))
                //否则只发送当前这一个skb
                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, size_goal);

            if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
                goto do_error;

            mss_now = tcp_send_mss(sk, &size_goal, flags);
        }
    }

out:
    //如果已经有数据拷贝至发送队列,那就发送数据
    if (copied)
        tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
out_nopush:
    release_sock(sk);
    return copied + copied_syn;

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:
    if (copied + copied_syn)
        goto out;
out_err:
    err = sk_stream_error(sk, flags, err);
    release_sock(sk);
    return err;
}

可见,tcp_sendmsg()只要是在处理用户数据的存放,优先考虑报文的线性区,然后是分页区,必要时需要使用新skb或者新分页来存放用户数据。

拷贝好用户数据后,接下来就是发送数据,主要涉及__tcp_push_pending_framestcp_push_onetcp_push这三个发送函数。实际上tcp_push_one是tcp_push的一种特殊形式,且tcp_push简单封装后也会调用__tcp_push_pending_frames,因此我们主要介绍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);
    //判断是否需要设置PSH标记
    if (!(flags & MSG_MORE) || forced_push(tp))
        tcp_mark_push(tp, skb);
    //如果设置了MSG_OOB选项,就记录紧急指针
    tcp_mark_urg(tp, flags);
    //判断是否需要阻塞小包,将多个小数据段合并到一个skb中一起发送
    if (tcp_should_autocork(sk, skb, size_goal)) {

        /* avoid atomic op if TSQ_THROTTLED bit is already set */
        //设置TSQ_THROTTLED标志
        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_THROTTLED.
         */
        //有可能在设置TSQ_THROTTLED前,网卡tx ring已经完成发送
        //因此再次检查条件,避免错误阻塞数据报文
        if (atomic_read(&sk->sk_wmem_alloc) > skb->truesize)
            return;//返回,阻塞小包发送
    }
    //应用程序用MSG_MORE标识告诉4层将会有更多的小数据包的传输
    //然后将这个标记再传递给3层,3层就会提前划分一个mtu大小的数据包,来组合这些数据帧
    if (flags & MSG_MORE)
        //标记cork,阻塞
        nonagle = TCP_NAGLE_CORK;
    //TCP层还没处理完,接着往下走
    __tcp_push_pending_frames(sk, mss_now, nonagle);
}

tcp_push()中涉及到小包阻塞的问题,使用了TSQ机制,即TCP Small Queue,通过tcp_should_autocork()判断是否开启。

static bool tcp_should_autocork(struct sock *sk, struct sk_buff *skb, int size_goal)
{
    return skb->len < size_goal && //数据报文长度小于最大报文值,也就是小包
           sysctl_tcp_autocorking && //默认开启,由/proc/sys/net/ipv4/tcp_autocorking控制
           skb != tcp_write_queue_head(sk) && //该报文不是第一个将要发送的报文,即还有其他数据等待发送
           //网卡队列中还有数据,sk_wmem_alloc的值在报文发送后会减小
           atomic_read(&sk->sk_wmem_alloc) > skb->truesize; 
}

可以看出,其基本思想就是利用数据报文发送的这段时间,将小包尽量组合成大包发送,既减小发送带宽,同时也不会降低传输速率(网卡和发送队列有报文发送时才阻塞,无报文等待发送时,就直接发送,不阻塞)。

如果没有阻塞,则调用__tcp_push_pending_frames()继续往下递交数据发送,

void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle)
{
    //如果连接处于关闭状态,返回不处理
    if (unlikely(sk->sk_state == TCP_CLOSE))
        return;
    //tcp_write_xmit会再次判断是否要推迟数据发送,使用nagle算法
    if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_atomic(sk, GFP_ATOMIC)))
        //发送失败,检测是否需要开启零窗口探测定时器
        tcp_check_probe_timer(sk);
}

上面有讲到使用TSQ机制判断是否要阻塞小包,进入tcp_write_xmit()后还会使用nagle算法判断是否 要阻塞发送。

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;
    u32 max_segs;

    sent_pkts = 0;
    //如果只发送一个数据报文,则不做MTU探测
    if (!push_one) {
        //MTU探测
        result = tcp_mtu_probe(sk);
        if (!result) {
            return false;
        } else if (result > 0) {
            sent_pkts = 1;
        }
    }
    //计算TSO支持的最大segs数量
    max_segs = tcp_tso_autosize(sk, mss_now);
    //遍历发送队列里的报文,将其发送出去
    while ((skb = tcp_send_head(sk))) {
        unsigned int limit;
        //重新计算gso_segs,因为在tcp_sendmsg中被清零了
        //在支持GSO的情况下值为skb->len/mss_now
        tso_segs = tcp_init_tso_segs(sk, skb, mss_now);
        BUG_ON(!tso_segs);

        if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
            /* "skb_mstamp" is used as a start point for the retransmit timer */
            skb_mstamp_get(&skb->skb_mstamp);
            goto repair; /* Skip network transmission */
        }
        //根据窗口大小计算此时能发多少个segment
        cwnd_quota = tcp_cwnd_test(tp, skb);
        if (!cwnd_quota) {
            is_cwnd_limited = true;
            if (push_one == 2)
                /* Force out a loss probe pkt. */
                cwnd_quota = 1;
            else
                break;
        }
        //检测当前窗口是否能发送skb的第一个分段
        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
            break;

        if (tso_segs == 1) { //tso_segs = 1表示无需TSO分段
            //根据nagle算法判断是否需要推迟发送报文
            //如果是发送队列里最后一个报文,无需推迟
            if (unlikely(!tcp_nagle_test(tp, skb, mss_now, (tcp_skb_is_last(sk, skb) ?
                              nonagle : TCP_NAGLE_PUSH))))
                break;
        } else { //有TSO分段
            //有多个skb需要发送,根据TSO规则判断是否需要推迟发送
            if (!push_one && tcp_tso_should_defer(sk, skb, &is_cwnd_limited, max_segs))
                break;
        }
        //走到这说明是要立即发送的流程
        limit = mss_now;
        if (tso_segs > 1 && !tcp_urg_mode(tp))
            //根据窗口大小以及mss计算此时可发送报文的大小限制
            limit = tcp_mss_split_point(sk, skb, mss_now,
                            min_t(unsigned int, cwnd_quota, max_segs), nonagle);
        //如果发送的数据报文长度大于此时可发送报文限制,那就要进行分段了
        //按照limit的大小进行分割skb
        if (skb->len > limit && unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))
            break;

        /* TCP Small Queues :
         * Control number of packets in qdisc/devices to two packets / or ~1 ms.
         * This allows for :
         *  - better RTT estimation and ACK scheduling
         *  - faster recovery
         *  - high rates
         * Alas, some drivers / subsystems require a fair amount
         * of queued bytes to ensure line rate.
         * One example is wifi aggregation (802.11 AMPDU)
         */
        //两倍当前包大小或者每毫秒速率
        limit = max(2 * skb->truesize, sk->sk_pacing_rate >> 10);
        //不能大于sysctl_tcp_limit_output_bytes
        //由/proc/sys/net/ipv4/tcp_limit_output_bytes设置,默认为256K
        limit = min_t(u32, limit, sysctl_tcp_limit_output_bytes);
        //未发送数据报文过多,先不往发送队列里放数据了,设置TSQ_THROTTLED标记
        //往TSQ队列里放数据
        if (atomic_read(&sk->sk_wmem_alloc) > limit) {
            set_bit(TSQ_THROTTLED, &tp->tsq_flags);
            /* It is possible TX completion already happened
             * before we set TSQ_THROTTLED, so we must
             * test again the condition.
             * We abuse smp_mb__after_clear_bit() because
             * there is no smp_mb__after_set_bit() yet
             */
            smp_mb__after_clear_bit();
            //设置标记后再检查一次,毕竟不是同步操作
            if (atomic_read(&sk->sk_wmem_alloc) > limit)
                break;
        }
        //发送数据报文,往后会填充TCP头部,然后交由IP层继续封装
        //这里发送的是数据的副本,由第三个参数控制,1即发送副本
        //数据只有在收到ACK确认后才会从发送队列里删除
        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
            break;

repair:
        /* Advance the send_head.  This one is sent out.
         * This call will increment packets_out.
         */
        tcp_event_new_data_sent(sk, skb);

        tcp_minshall_update(tp, mss_now, skb);
        sent_pkts += tcp_skb_pcount(skb);//统计发送的报文数量

        if (push_one)
            break;
    }

    if (likely(sent_pkts)) {
        if (tcp_in_cwnd_reduction(sk))
            tp->prr_out += sent_pkts;

        /* Send one loss probe per tail loss episode. */
        if (push_one != 2)
            tcp_schedule_loss_probe(sk);
        tcp_cwnd_validate(sk, is_cwnd_limited);
        return false;
    }
    return (push_one == 2) || (!tp->packets_out && tcp_send_head(sk));
}

可见在一切顺利的情况下,最后将发往tcp_transmit_skb()去填充TCP头部,然后交往IP层继续封装。这里我们就不再往下跟踪了,有时间再看看IP层的处理。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值