跳转至

45 发送网络包(上):如何表达我们想让合作伙伴做什么?

上一节,我们通过socket函数、bind函数、listen函数、accept函数以及connect函数,在内核建立好了数据结构,并完成了TCP连接建立的三次握手过程。

这一节,我们接着来分析,发送一个网络包的过程。

解析socket的Write操作

socket对于用户来讲,是一个文件一样的存在,拥有一个文件描述符。因而对于网络包的发送,我们可以使用对于socket文件的写入系统调用,也就是write系统调用。

write系统调用对于一个文件描述符的操作,大致过程都是类似的。在文件系统那一节,我们已经详细解析过,这里不再多说。对于每一个打开的文件都有一个struct file结构,write系统调用会最终调用stuct file结构指向的file_operations操作。

对于socket来讲,它的file_operations定义如下:

static const struct file_operations socket_file_ops = {
    .owner =    THIS_MODULE,
    .llseek =   no_llseek,
    .read_iter =    sock_read_iter,
    .write_iter =   sock_write_iter,
    .poll =     sock_poll,
    .unlocked_ioctl = sock_ioctl,
    .mmap =     sock_mmap,
    .release =  sock_close,
    .fasync =   sock_fasync,
    .sendpage = sock_sendpage,
    .splice_write = generic_splice_sendpage,
    .splice_read =  sock_splice_read,
};

按照文件系统的写入流程,调用的是sock_write_iter。

static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
    struct file *file = iocb->ki_filp;
    struct socket *sock = file->private_data;
    struct msghdr msg = {.msg_iter = *from,
                 .msg_iocb = iocb};
    ssize_t res;
......
    res = sock_sendmsg(sock, &msg);
    *from = msg.msg_iter;
    return res;
}

在sock_write_iter中,我们通过VFS中的struct file,将创建好的socket结构拿出来,然后调用sock_sendmsg。而sock_sendmsg会调用sock_sendmsg_nosec。

static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
    int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));
......
}

这里调用了socket的ops的sendmsg,我们在上一节已经遇到它好几次了。根据inet_stream_ops的定义,我们这里调用的是inet_sendmsg。

int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
    struct sock *sk = sock->sk;
......
    return sk->sk_prot->sendmsg(sk, msg, size);
}

这里面,从socket结构中,我们可以得到更底层的sock结构,然后调用sk_prot的sendmsg方法。这个我们同样在上一节遇到好几次了。

解析tcp_sendmsg函数

根据tcp_prot的定义,我们调用的是tcp_sendmsg。

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;
    int flags, err, copied = 0;
    int mss_now = 0, size_goal, copied_syn = 0;
    long timeo;
......
    /* Ok commence sending. */
    copied = 0;
restart:
    mss_now = tcp_send_mss(sk, &size_goal, flags);

    while (msg_data_left(msg)) {
        int copy = 0;
        int max = size_goal;

        skb = tcp_write_queue_tail(sk);
        if (tcp_send_head(sk)) {
            if (skb->ip_summed == CHECKSUM_NONE)
                max = mss_now;
            copy = max - skb->len;
        }

        if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
            bool first_skb;

new_segment:
            /* Allocate new segment. If the interface is SG,
             * allocate skb fitting to single page.
             */
            if (!sk_stream_memory_free(sk))
                goto wait_for_sndbuf;
......
            first_skb = skb_queue_empty(&sk->sk_write_queue);
            skb = sk_stream_alloc_skb(sk,
                          select_size(sk, sg, first_skb),
                          sk->sk_allocation,
                          first_skb);
......
            skb_entail(sk, skb);
            copy = size_goal;
            max = size_goal;
......
        }

        /* Try to append data to the end of skb. */
        if (copy > msg_data_left(msg))
            copy = msg_data_left(msg);

        /* Where to copy to? */
        if (skb_availroom(skb) > 0) {
            /* We have some space in skb head. Superb! */
            copy = min_t(int, copy, skb_availroom(skb));
            err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
......
        } else {
            bool merge = true;
            int i = skb_shinfo(skb)->nr_frags;
            struct page_frag *pfrag = sk_page_frag(sk);
......
            copy = min_t(int, copy, pfrag->size - pfrag->offset);
......
            err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
                               pfrag->page,
                               pfrag->offset,
                               copy);
......
            pfrag->offset += copy;
        }

......
        tp->write_seq += copy;
        TCP_SKB_CB(skb)->end_seq += copy;
        tcp_skb_pcount_set(skb, 0);

        copied += copy;
        if (!msg_data_left(msg)) {
            if (unlikely(flags & MSG_EOR))
                TCP_SKB_CB(skb)->eor = 1;
            goto out;
        }

        if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
            continue;

        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(sk, mss_now);
        continue;
......
    }
......
}

tcp_sendmsg的实现还是很复杂的,这里面做了这样几件事情。

msg是用户要写入的数据,这个数据要拷贝到内核协议栈里面去发送;在内核协议栈里面,网络包的数据都是由struct sk_buff维护的,因而第一件事情就是找到一个空闲的内存空间,将用户要写入的数据,拷贝到struct sk_buff的管辖范围内。而第二件事情就是发送struct sk_buff。

在tcp_sendmsg中,我们首先通过强制类型转换,将sock结构转换为struct tcp_sock,这个是维护TCP连接状态的重要数据结构。

接下来是tcp_sendmsg的第一件事情,把数据拷贝到struct sk_buff。

我们先声明一个变量copied,初始化为0,这表示拷贝了多少数据。紧接着是一个循环,while (msg_data_left(msg)),也即如果用户的数据没有发送完毕,就一直循环。循环里声明了一个copy变量,表示这次拷贝的数值,在循环的最后有copied += copy,将每次拷贝的数量都加起来。

我们这里只需要看一次循环做了哪些事情。

第一步,tcp_write_queue_tail从TCP写入队列sk_write_queue中拿出最后一个struct sk_buff,在这个写入队列中排满了要发送的struct sk_buff,为什么要拿最后一个呢?这里面只有最后一个,可能会因为上次用户给的数据太少,而没有填满。

第二步,tcp_send_mss会计算MSS,也即Max Segment Size。这是什么呢?这个意思是说,我们在网络上传输的网络包的大小是有限制的,而这个限制在最底层开始就有。

MTU(Maximum Transmission Unit,最大传输单元)是二层的一个定义。以以太网为例,MTU为1500个Byte,前面有6个Byte的目标MAC地址,6个Byte的源MAC地址,2个Byte的类型,后面有4个Byte的CRC校验,共1518个Byte。

在IP层,一个IP数据报在以太网中传输,如果它的长度大于该MTU值,就要进行分片传输。

在TCP层有个MSS(Maximum Segment Size,最大分段大小),等于MTU减去IP头,再减去TCP头。也就是,在不分片的情况下,TCP里面放的最大内容。

在这里,max是struct sk_buff的最大数据长度,skb->len是当前已经占用的skb的数据长度,相减得到当前skb的剩余数据空间。

第三步,如果copy小于0,说明最后一个struct sk_buff已经没地方存放了,需要调用sk_stream_alloc_skb,重新分配struct sk_buff,然后调用skb_entail,将新分配的sk_buff放到队列尾部。

struct sk_buff是存储网络包的重要的数据结构,在应用层数据包叫data,在TCP层我们称为segment,在IP层我们叫packet,在数据链路层称为frame。在struct sk_buff,首先是一个链表,将struct sk_buff结构串起来。

接下来,我们从headers_start开始,到headers_end结束,里面都是各层次的头的位置。这里面有二层的mac_header、三层的network_header和四层的transport_header。

struct sk_buff {
    union {
        struct {
            /* These two members must be first. */
            struct sk_buff      *next;
            struct sk_buff      *prev;
......
        };
        struct rb_node  rbnode; /* used in netem & tcp stack */
    };
......
    /* private: */
    __u32           headers_start[0];
    /* public: */
......
    __u32           priority;
    int         skb_iif;
    __u32           hash;
    __be16          vlan_proto;
    __u16           vlan_tci;
......
    union {
        __u32       mark;
        __u32       reserved_tailroom;
    };

    union {
        __be16      inner_protocol;
        __u8        inner_ipproto;
    };

    __u16           inner_transport_header;
    __u16           inner_network_header;
    __u16           inner_mac_header;

    __be16          protocol;
    __u16           transport_header;
    __u16           network_header;
    __u16           mac_header;

    /* private: */
    __u32           headers_end[0];
    /* public: */

    /* These elements must be at the end, see alloc_skb() for details.  */
    sk_buff_data_t      tail;
    sk_buff_data_t      end;
    unsigned char       *head,
                *data;
    unsigned int        truesize;
    refcount_t      users;
};

最后几项, head指向分配的内存块起始地址。data这个指针指向的位置是可变的。它有可能随着报文所处的层次而变动。当接收报文时,从网卡驱动开始,通过协议栈层层往上传送数据报,通过增加 skb->data 的值,来逐步剥离协议首部。而要发送报文时,各协议会创建 sk_buff{},在经过各下层协议时,通过减少 skb->data的值来增加协议首部。tail指向数据的结尾,end指向分配的内存块的结束地址。

要分配这样一个结构,sk_stream_alloc_skb会最终调用到__alloc_skb。在这个函数里面,除了分配一个sk_buff结构之外,还要分配sk_buff指向的数据区域。这段数据区域分为下面这几个部分。

第一部分是连续的数据区域。紧接着是第二部分,一个struct skb_shared_info结构。这个结构是对于网络包发送过程的一个优化,因为传输层之上就是应用层了。按照TCP的定义,应用层感受不到下面的网络层的IP包是一个个独立的包的存在的。反正就是一个流,往里写就是了,可能一下子写多了,超过了一个IP包的承载能力,就会出现上面MSS的定义,拆分成一个个的Segment放在一个个的IP包里面,也可能一次写一点,一次写一点,这样数据是分散的,在IP层还要通过内存拷贝合成一个IP包。

为了减少内存拷贝的代价,有的网络设备支持分散聚合(Scatter/Gather)I/O,顾名思义,就是IP层没必要通过内存拷贝进行聚合,让散的数据零散的放在原处,在设备层进行聚合。如果使用这种模式,网络包的数据就不会放在连续的数据区域,而是放在struct skb_shared_info结构里面指向的离散数据,skb_shared_info的成员变量skb_frag_t frags[MAX_SKB_FRAGS],会指向一个数组的页面,就不能保证连续了。

于是我们就有了第四步。在注释/* Where to copy to? */后面有个if-else分支。if分支就是skb_add_data_nocache将数据拷贝到连续的数据区域。else分支就是skb_copy_to_page_nocache将数据拷贝到struct skb_shared_info结构指向的不需要连续的页面区域。

第五步,就是要发生网络包了。第一种情况是积累的数据报数目太多了,因而我们需要通过调用__tcp_push_pending_frames发送网络包。第二种情况是,这是第一个网络包,需要马上发送,调用tcp_push_one。无论__tcp_push_pending_frames还是tcp_push_one,都会调用tcp_write_xmit发送网络包。

至此,tcp_sendmsg解析完了。

解析tcp_write_xmit函数

接下来我们来看,tcp_write_xmit是如何发送网络包的。

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;
......
    max_segs = tcp_tso_segs(sk, mss_now);
    while ((skb = tcp_send_head(sk))) {
        unsigned int limit;
......
        tso_segs = tcp_init_tso_segs(skb, mss_now);
......
        cwnd_quota = tcp_cwnd_test(tp, skb);
......
        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
            is_rwnd_limited = true;
            break;
        }
......
        limit = mss_now;
        if (tso_segs > 1 && !tcp_urg_mode(tp))
            limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle);

        if (skb->len > limit &&
            unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))
            break;
......
        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;
    }
......
}

这里面主要的逻辑是一个循环,用来处理发送队列,只要队列不空,就会发送。

在一个循环中,涉及TCP层的很多传输算法,我们来一一解析。

第一个概念是TSO(TCP Segmentation Offload)。如果发送的网络包非常大,就像上面说的一样,要进行分段。分段这个事情可以由协议栈代码在内核做,但是缺点是比较费CPU,另一种方式是延迟到硬件网卡去做,需要网卡支持对大数据包进行自动分段,可以降低CPU负载。

在代码中,tcp_init_tso_segs会调用tcp_set_skb_tso_segs。这里面有这样的语句:DIV_ROUND_UP(skb->len, mss_now)。也就是sk_buff的长度除以mss_now,应该分成几个段。如果算出来要分成多个段,接下来就是要看,是在这里(协议栈的代码里面)分好,还是等待到了底层网卡再分。

于是,调用函数tcp_mss_split_point,开始计算切分的limit。这里面会计算max_len = mss_now * max_segs,根据现在不切分来计算limit,所以下一步的判断中,大部分情况下tso_fragment不会被调用,等待到了底层网卡来切分。

第二个概念是拥塞窗口的概念(cwnd,congestion window),也就是说为了避免拼命发包,把网络塞满了,定义一个窗口的概念,在这个窗口之内的才能发送,超过这个窗口的就不能发送,来控制发送的频率。

那窗口大小是多少呢?就是遵循下面这个著名的拥塞窗口变化图。

一开始的窗口只有一个mss大小叫作slow start(慢启动)。一开始的增长速度的很快的,翻倍增长。一旦到达一个临界值ssthresh,就变成线性增长,我们就称为拥塞避免。什么时候算真正拥塞呢?就是出现了丢包。一旦丢包,一种方法是马上降回到一个mss,然后重复先翻倍再线性对的过程。如果觉得太过激进,也可以有第二种方法,就是降到当前cwnd的一半,然后进行线性增长。

在代码中,tcp_cwnd_test会将当前的snd_cwnd,减去已经在窗口里面尚未发送完毕的网络包,那就是剩下的窗口大小cwnd_quota,也即就能发送这么多了。

第三个概念就是接收窗口rwnd的概念(receive window),也叫滑动窗口。如果说拥塞窗口是为了怕把网络塞满,在出现丢包的时候减少发送速度,那么滑动窗口就是为了怕把接收方塞满,而控制发送速度。

滑动窗口,其实就是接收方告诉发送方自己的网络包的接收能力,超过这个能力,我就受不了了。因为滑动窗口的存在,将发送方的缓存分成了四个部分。

  • 第一部分:发送了并且已经确认的。这部分是已经发送完毕的网络包,这部分没有用了,可以回收。
  • 第二部分:发送了但尚未确认的。这部分,发送方要等待,万一发送不成功,还要重新发送,所以不能删除。
  • 第三部分:没有发送,但是已经等待发送的。这部分是接收方空闲的能力,可以马上发送,接收方收得了。
  • 第四部分:没有发送,并且暂时还不会发送的。这部分已经超过了接收方的接收能力,再发送接收方就收不了了。

因为滑动窗口的存在,接收方的缓存也要分成了三个部分。

  • 第一部分:接受并且确认过的任务。这部分完全接收成功了,可以交给应用层了。
  • 第二部分:还没接收,但是马上就能接收的任务。这部分有的网络包到达了,但是还没确认,不算完全完毕,有的还没有到达,那就是接收方能够接受的最大的网络包数量。
  • 第三部分:还没接收,也没法接收的任务。这部分已经超出接收方能力。

在网络包的交互过程中,接收方会将第二部分的大小,作为AdvertisedWindow发送给发送方,发送方就可以根据他来调整发送速度了。

在tcp_snd_wnd_test函数中,会判断sk_buff中的end_seq和tcp_wnd_end(tp)之间的关系,也即这个sk_buff是否在滑动窗口的允许范围之内。如果不在范围内,说明发送要受限制了,我们就要把is_rwnd_limited设置为true。

接下来,tcp_mss_split_point函数要被调用了。

static unsigned int tcp_mss_split_point(const struct sock *sk,
                                        const struct sk_buff *skb,
                                        unsigned int mss_now,
                                        unsigned int max_segs,
                                        int nonagle)
{
        const struct tcp_sock *tp = tcp_sk(sk);
        u32 partial, needed, window, max_len;

        window = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
        max_len = mss_now * max_segs;

        if (likely(max_len <= window && skb != tcp_write_queue_tail(sk)))
                return max_len;

        needed = min(skb->len, window);

        if (max_len <= needed)
                return max_len;
......
        return needed;
}

这里面除了会判断上面讲的,是否会因为超出mss而分段,还会判断另一个条件,就是是否在滑动窗口的运行范围之内,如果小于窗口的大小,也需要分段,也即需要调用tso_fragment。

在一个循环的最后,是调用tcp_transmit_skb,真的去发送一个网络包。

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
                gfp_t gfp_mask)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    struct inet_sock *inet;
    struct tcp_sock *tp;
    struct tcp_skb_cb *tcb;
    struct tcphdr *th;
    int err;

    tp = tcp_sk(sk);

    skb->skb_mstamp = tp->tcp_mstamp;
    inet = inet_sk(sk);
    tcb = TCP_SKB_CB(skb);
    memset(&opts, 0, sizeof(opts));

    tcp_header_size = tcp_options_size + sizeof(struct tcphdr);
    skb_push(skb, tcp_header_size);

    /* Build TCP header and checksum it. */
    th = (struct tcphdr *)skb->data;
    th->source      = inet->inet_sport;
    th->dest        = inet->inet_dport;
    th->seq         = htonl(tcb->seq);
    th->ack_seq     = htonl(tp->rcv_nxt);
    *(((__be16 *)th) + 6)   = htons(((tcp_header_size >> 2) << 12) |
                    tcb->tcp_flags);

    th->check       = 0;
    th->urg_ptr     = 0;
......
    tcp_options_write((__be32 *)(th + 1), tp, &opts);
    th->window  = htons(min(tp->rcv_wnd, 65535U));
......
    err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
......
}

tcp_transmit_skb这个函数比较长,主要做了两件事情,第一件事情就是填充TCP头,如果我们对着TCP头的格式。

这里面有源端口,设置为inet_sport,有目标端口,设置为inet_dport;有序列号,设置为tcb->seq;有确认序列号,设置为tp->rcv_nxt。我们把所有的flags设置为tcb->tcp_flags。设置选项为opts。设置窗口大小为tp->rcv_wnd。

全部设置完毕之后,就会调用icsk_af_ops的queue_xmit方法,icsk_af_ops指向ipv4_specific,也即调用的是ip_queue_xmit函数。

const struct inet_connection_sock_af_ops ipv4_specific = {
        .queue_xmit        = ip_queue_xmit,
        .send_check        = tcp_v4_send_check,
        .rebuild_header    = inet_sk_rebuild_header,
        .sk_rx_dst_set     = inet_sk_rx_dst_set,
        .conn_request      = tcp_v4_conn_request,
        .syn_recv_sock     = tcp_v4_syn_recv_sock,
        .net_header_len    = sizeof(struct iphdr),
        .setsockopt        = ip_setsockopt,
        .getsockopt        = ip_getsockopt,
        .addr2sockaddr     = inet_csk_addr2sockaddr,
        .sockaddr_len      = sizeof(struct sockaddr_in),
        .mtu_reduced       = tcp_v4_mtu_reduced,
};

总结时刻

这一节,我们解析了发送一个网络包的一部分过程,如下图所示。

这个过程分成几个层次。

  • VFS层:write系统调用找到struct file,根据里面的file_operations的定义,调用sock_write_iter函数。sock_write_iter函数调用sock_sendmsg函数。
  • Socket层:从struct file里面的private_data得到struct socket,根据里面ops的定义,调用inet_sendmsg函数。
  • Sock层:从struct socket里面的sk得到struct sock,根据里面sk_prot的定义,调用tcp_sendmsg函数。
  • TCP层:tcp_sendmsg函数会调用tcp_write_xmit函数,tcp_write_xmit函数会调用tcp_transmit_skb,在这里实现了TCP层面向连接的逻辑。
  • IP层:扩展struct sock,得到struct inet_connection_sock,根据里面icsk_af_ops的定义,调用ip_queue_xmit函数。

课堂练习

如果你对TCP协议的结构不太熟悉,可以使用tcpdump命令截取一个TCP的包,看看里面的结构。

欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。

精选留言(14)
  • 是男人就开巴巴托斯 👍(10) 💬(5)

    有一次分别在服务器端和客户端抓包 服务器端的包都是好几k 十几k. 客户端的包是1400多 一开始没弄明白都mtu了为什么还有好几k的包 后来查到内核可以配置网络参数,不把拆包交给网卡固件,自己分包。

    2019-08-13

  • 佳俊 👍(1) 💬(3)

    sk_buff已经对mss做了分片处理了,为什么还要在ip再做一次分片处理。sk_buff有最大的限制吗?

    2020-05-24

  • W.jyao 👍(1) 💬(2)

    老师,请教一个问题,为什么流媒体服务器发送的rtp包都要小于1500左右,也就是小于MTU,理论上不是大于1500会分片吗?但是好像实现的代码都会小于Mtu,为什么呢?

    2019-07-11

  • 一梦如是 👍(0) 💬(2)

    老师好,请教一个困惑很久的问题,cpu的L1,L2,L3级cache,缓存的数据是以内存的页为单位的吗 oracle sga在大内存时,通常会配置hugepage以减少TLB的压力和swap的交换用来提高性能,linux(centos)下默认是2M,而一般cpu L1是32+32K,L2是256K,是不是就意味着没法使用这两级缓存了

    2019-07-11

  • 谛听 👍(8) 💬(2)

    VFS: 拿到 file 中的 socket,进而得到 sock,进而调用 tcp_sendmsg tcp_sendmsg: 将用户数据拷到 sk_buff,不断循环发送,发送过程中计算 MSS,拆分成一个个的 Segment 放在一个个的 IP 包里面,数据可拷贝到连续的区域,也可以拷到不连续的区域 ( 需要网络设备支持分散聚合),最后调用 tcp_write_xmit 发送网络包 tcp_write_xmit:TSO--分段可由内核做,比较耗CPU,也延迟到网卡做; 拥塞窗口--避免把网络塞满; 滑动窗口--避免把接收端塞满 tcp_transmit_skb:填充tcp报文,发送网络包

    2019-11-24

  • 忆水寒 👍(6) 💬(0)

    从网络协议专栏看完过来的,tcp协议实际上很熟悉,所以看这篇文章大概都懂。但是每次读都能有新的体会。

    2020-02-09

  • stackWarn 👍(1) 💬(0)

    作为一个运维,这节算是这里面听的最轻松的一次了,之前看过这部分的代码,函数名都有点印象哈哈

    2020-10-13

  • Mr小公熊 👍(0) 💬(0)

    在 Linux 内核的 TCP/IP 协议栈中,tcp_sendmsg 函数用于发送 TCP 数据。这个函数会处理数据的包装和发送,包括将数据分割成多个片段(如果需要的话)以及构造适当的网络数据包(sk_buff)。 struct skb_shared_info 是否在 tcp_sendmsg 中使用,并不是由硬件决定的。这个决策完全取决于内核协议栈的逻辑。具体来说,当 TCP 数据的大小超过一个网络数据包的最大传输单元(MTU)时,内核需要将数据分割成多个片段来发送。在这种情况下,struct skb_shared_info 就会被用来存储关于这些额外片段的信息。 以下是一些关键点: 数据分片:如果 TCP 数据的大小超过了 MTU,tcp_sendmsg 会调用 tcp_fragment 函数来分割数据。这个函数会创建多个 sk_buff 结构体,并使用 struct skb_shared_info 来管理这些片段。 硬件无关:这个决策过程完全在软件层面进行,与硬件无关。硬件只负责发送和接收原始的网络数据包,而不知道或不关心这些数据包是如何在软件中被构造和管理的。 协议栈逻辑:tcp_sendmsg 和相关函数根据当前的 TCP 连接状态、可用缓冲区大小、MTU 等因素来决定是否使用 struct skb_shared_info。 因此,struct skb_shared_info 在 tcp_sendmsg 中的使用是由内核协议栈的逻辑决定的,而不是由硬件决定的。这是内核网络栈处理数据包分割和发送的一部分,旨在优化数据传输效率和处理大型或分段的数据包。 好像是否存入shared里不是由软硬件决定的啊。。。

    2024-03-06

  • Geek_2b44d4 👍(0) 💬(0)

    请教一下,这里进行分段后,每个段是否时类似链表结构?分包发送的时候,是不是每个包里面都标记了上一个与下一个的标识,这样接收方收到后就可以重排了,不知道是不是这样?

    2022-05-12

  • 程序员老王 👍(0) 💬(0)

    send 很大数据。假如网络正常,回失败吗?skb回自动增加,没有阻塞和阻塞者 一说吧?

    2022-05-02

  • herongwei 👍(0) 💬(0)

    老师讲的真的非常好!常看常新

    2022-03-24

  • jackji 👍(0) 💬(0)

    老师 文中代码对应的kernel版本是?

    2021-03-03

  • bbbi 👍(0) 💬(0)

    老师一个sk_buff多大呢?这个链表可以无限长吗?

    2019-12-17

  • 记事本 👍(0) 💬(0)

    老师好 这部分的内容有没有参考书可以看看的?

    2019-11-25