socket的write系统调用栈:

write最终调用tcp_sendmsg发送消息。
tcp_sendmsg主要是对用户的消息按MSS进行分段、添加到发送队列并将用户数据拷贝到分段里面,根据相关判断设置PSH标志,最后调用__tcp_push_pending_frames、tcp_push_one、tcp_push函数发送报文。代码与机械工业出版社《Linux内核源码剖析:TCP/IP实现(下册)》对应内核版本代码基本一致,代码注释基本可以从该书拷贝过来即可,具体的细节及流程参考机械工业出版社《Linux内核源码剖析:TCP/IP实现(下册) "30.3.3 传输接口层的实现"》。
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;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;}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.*/if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) && // 非ESTABLISHED或者CLOSE_WAIT状态?只在ESTABLISHED或者CLOSE_WAIT这两种状态,接收窗口打开的,才能接收数据!tcp_passive_fastopen(sk)) { // 非fastopenerr = sk_stream_wait_connect(sk, &timeo); // 调用sk_stream_wait_connect等待建立起连接if (err != 0) // 超时或者其他错误goto do_error; // 跳转到do_error,处理错误}if (unlikely(tp->repair)) { // TCP热迁移相关(参考https://lwn.net/Articles/495304/)if (tp->repair_queue == TCP_RECV_QUEUE) {copied = tcp_send_rcvq(sk, msg, size);goto out_nopush;}err = -EINVAL;if (tp->repair_queue == TCP_NO_QUEUE)goto out_err;/* 'common' sending to sendq */}/* This should be in poll */sk_clear_bit(SOCKWQ_ASYNC_NOSPACE, sk); // 清楚表示异步情况下套接口发送队列已满的标志mss_now = tcp_send_mss(sk, &size_goal, flags); // 调用tcp_send_mss获取当前有效MSS,获取发送数据报到达网络设备时数据段的最大长度(参考机械工业出版社《Linux内核源码剖析:TCP/IP实现(下册)》P323及后续代码)/* Ok commence sending. */copied = 0; // 初始化已从用户数据块复制到skb的字节数为0err = -EPIPE; // 初始化错误码为EPIPEif (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN)) // 判断此时套接口是否存在错误,以及该套接口是否允许发送数据goto out_err; // 如果有错误或者不允许发送数据,则跳转到out_err处作处理sg = !!(sk->sk_route_caps & NETIF_F_SG); // 网卡分散聚合while (msg_data_left(msg)) { // 用户数据块大小是否为0,是否还有数据需要拷贝发送int copy = 0;int max = size_goal; // GSO相关skb = tcp_write_queue_tail(sk); // 获取传输控制块发送队列尾的SKB(最后一个报文段可能不是最大报文段,需要追加数据,使该报文段构成一个最大报文段)if (tcp_send_head(sk)) { // 发送队列是否为空(发送队列不为空,发送队列尾的SKB才有效,否则队列尾也应该为空,发送队列不为空,队列尾才不为空)if (skb->ip_summed == CHECKSUM_NONE) // GSO特性相关,参考网上其他资料max = mss_now; // 不支持GSO,报文段最大为mss_nowcopy = max - skb->len; // 计算发送队列尾的SKB构成一个最大报文段还需要拷贝的数据大小}if (copy <= 0) { // 发送队列尾已经是一个最大的报文段
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; // 跳转到wait_for_sndbuf等待发送缓存skb = sk_stream_alloc_skb(sk,select_size(sk, sg),sk->sk_allocation,skb_queue_empty(&sk->sk_write_queue)); // 分配SKBif (!skb) // 分配SKB失败goto wait_for_memory; // 跳转到wait_for_memory,等待内存/** Check whether we can use HW checksum.*/if (sk_check_csum_caps(sk)) // 根据目的路由网络设备的特性,确定释放设置由硬件执行校验和标志skb->ip_summed = CHECKSUM_PARTIAL;skb_entail(sk, skb); // 将该SKB添加到发送队列尾部(这里还没拷贝数据到SKB)copy = size_goal; // 初始化本次拷贝的字节数为size_goal(不支持GSO的情况下就是MSS)max = size_goal; // 初始化本次最大拷贝的字节数为size_goal(不支持GSO的情况下就是MSS)/* All packets are restored as if they have* already been sent. skb_mstamp isn't set to* avoid wrong rtt estimation.*/if (tp->repair)TCP_SKB_CB(skb)->sacked |= TCPCB_REPAIRED;}/* Try to append data to the end of skb. */if (copy > msg_data_left(msg)) // copy大于剩余消息数据大小(不需要拷贝这么多数据,按消息数据大小构造报文段)copy = msg_data_left(msg); // 更新拷贝数据的大小为剩余发送数据的大小/* Where to copy to? */if (skb_availroom(skb) > 0) { // 判断SKB的线性存储区底部是否还有空间/* We have some space in skb head. Superb! */copy = min_t(int, copy, skb_availroom(skb)); // 取copy和SKB的线性存储区底部剩余空间较小的值为本次拷贝的值(底部空间不够copy大小,则只能按底部空间大小拷贝)err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy); // 从用户空间复制长度为copy的数据到SKB中(拷贝数据通过设置DOMAIN来访问用户地址空间)if (err) // 复制失败?goto do_fault; // 复制失败,跳转到do_fault处} else { // SKB的线性存储区底部已没有空间了,那就需要把数据复制到支持分散聚会I/O分页中bool merge = true; // 是否在最后一个分页中添加数据int i = skb_shinfo(skb)->nr_frags; // 获取当前SKB的分散片段数istruct page_frag *pfrag = sk_page_frag(sk); // 获取最后一个分片的页面pageif (!sk_page_frag_refill(sk, pfrag)) // 检查分片页面是否有可用空间,如果有,返回true,如果没有,调用alloc_pages分配页面,如果还是分配失败,返回falsegoto wait_for_memory; // 分片页面没有可用空间并且分配新页面失败,跳转到wait_for_memory等待内存if (!skb_can_coalesce(skb, i, pfrag->page,pfrag->offset)) { // 判断最后一个分页是否能追加数据if (i == sysctl_max_skb_frags || !sg) { // 分页数达到了上限或者网卡不支持分散聚合tcp_mark_push(tp, skb); // SKB设置PSH标志位,并记录最后一个PSH的序号(如果PSH之后的数据有很长一段没有设置过PSH,在后面代码会强制设置PSH)goto new_segment; // 不能追加合并发送队列末尾的报文段,跳转到new_segment,新建一个报文段}merge = false; // 最后一个分页不能追加数据,设置merge为false}copy = min_t(int, copy, pfrag->size - pfrag->offset); // pfrag->size - pfrag->offset为分页剩余空间大小,更新copy为copy与分页剩余空间大小的较小者if (!sk_wmem_schedule(sk, copy)) // 判断用于输出使用的缓存是否达到上限goto wait_for_memory; // 跳转到wait_for_memory等待内存err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,pfrag->page,pfrag->offset,copy); // 拷贝数据(内核态通过改变DOMAIN来访问用户空间的数据)if (err) // 拷贝失败?goto do_error; // 跳转到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; // 更新分页偏移(可用空间页内偏移)}if (!copied) // 复制数据长度为零?(tcp_sendmsg的第一个分段)TCP_SKB_CB(skb)->tcp_flags &= ~TCPHDR_PSH; // 取消PSH标志tp->write_seq += copy; // 更新发送队列中的最后一个序号write_seq(下一个要发送的段的序号)TCP_SKB_CB(skb)->end_seq += copy; // 设置SKB最后一个序列tcp_skb_pcount_set(skb, 0); // tcp_gso_segscopied += copy; // 更新已复制字节数if (!msg_data_left(msg)) { // 所有的数据都已全部复制到发送队列?tcp_tx_timestamp(sk, skb);goto out; // 跳转到out处理}if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair)) // 如果当前SKB中的数据长度小于MSS,说明还可以往里填充数据,或者发送的是带外数据...continue; // 继续复制数据到SKBif (forced_push(tp)) { // 检查是否必须立即发送,即检查自上次发送产生的数据是否已超过对方曾经通告的最大窗口值的一半(从forced_push代码看,简单理解就是两次PSH之前的数据是否已超过对方曾经通告的最大窗口值的一半)tcp_mark_push(tp, skb); // SKB设置PSH标志(并更新pushed_seq,记录最后一次PSH的序号)__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH); // 调用__tcp_push_pending_frames将在发送队列上的SKB从sk_send_head开始发送出去(发送的不是当前报文,而是发送队列可以发送的数据)} else if (skb == tcp_send_head(sk)) // 如果没有必要立即发送,且发送队列上只存在这个段tcp_push_one(sk, mss_now); // 调用tcp_push_one只发送当前段continue;wait_for_sndbuf: // 等待发送缓存(套接口的发送缓存大小是有上限的,因此,一旦发送队列中段数据总长度达到发送缓冲区的长度上限,就不能再分配SKB了,只能等待... lwIP实现,除了发送检查发送缓存总的缓存数据外,还检查总的段数,总的段数也有上限)set_bit(SOCK_NOSPACE, &sk->sk_socket->flags); // 设置SOCK_NOSPACE标志,表示套接口发送队列已满
wait_for_memory: // 如果直接跳转到这里,则表示整个系统内存不够(发送队列没有满,但是系统内存不够,没有足够内存分配给发送队列)if (copied) // 虽然分配SKB失败,单如果之前已有数据从用户空间复制过来,则调用tcp_push将其发送出去tcp_push(sk, flags & ~MSG_MORE, mss_now,TCP_NAGLE_PUSH, size_goal);err = sk_stream_wait_memory(sk, &timeo); // 调用sk_stream_wait_memory进入睡眠,等待内存空闲信号(当前线程挂载到sk->sk_wq,然后将当前线程调度出去,有数据被确认等释放内存的情况下,会唤醒sk->sk_wq的阻塞线程)if (err != 0) // 如果在超时时间内没有得到该信号,则跳转到do_error处处理goto do_error;mss_now = tcp_send_mss(sk, &size_goal, flags); // 等待内存未超时,有空闲的内存可以使用了。睡眠后,MSS有可能发生了变化,因此需要重新获取当前的MSS和TSO分段段长}out:if (copied) // 已复制了数据?tcp_push(sk, flags, mss_now, tp->nonagle, size_goal); // 调用tcp_push将其发送出去
out_nopush: // TCP热迁移相关(参考https://lwn.net/Articles/495304/)release_sock(sk);return copied + copied_syn;do_fault:if (!skb->len) { // 如果SKB中的数据长度为零,则说明该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_wmem_free_skb(sk, skb); // 释放SKB}do_error:if (copied + copied_syn) // 如果已复制了部分数据,那么即使发生了错误,也可以发送数据包,因此跳转到out处发送数据包goto out;
out_err:err = sk_stream_error(sk, flags, err);/* make sure we wake any epoll edge trigger waiter */if (unlikely(skb_queue_len(&sk->sk_write_queue) == 0 && err == -EAGAIN)) // 发送队列长度为0,并且错误码为EAGAINsk->sk_write_space(sk); // 唤醒epoll边沿触发的写线程release_sock(sk);return err;
}
从代码上看,tcp_sendmsg并不是在一次write的最后添加PSH标志,lwIP实现是在一次write之后添加PSH标志,所以linux接收方不能通过收到PSH来判断一次write数据,而lwIP就一定程度上可以根据PSH知道收到了一次write。
这个PSH主要是在读socket的数据的时候,读到PSH标志就会立即返回,不等读指定的字节数,简单说就是发送了两次"hello world!",lwIP就会在每次发送的后面添加PSH标志,接收的时候也会带这个PSH标志,调用read读到PSH标志就立即返回,不管第二个"hello world!",两个"hello world!"本身不存在什么关联,读到第一个"hello world!"就可以处理该"hello world!"了,没必要把第二个"hello world!"读上来之后处理第一个"hello world!"。
tcp_sendmsg只是将消息分成一个个最大的段,最后一个可能没这么多数据,所以下一次写的时候可能需要追加数据到最后一个不够最大段的段里面,tcp_sendmsg调用其他接口发送报文,这些接口最终都是调用tcp_write_xmit发送数据。
tcp_write_xmit主要是根据拥塞窗口等调用tcp_transmit_skb将数据发送到网络层,并更新发送队列发送窗口相关信息,组装TCP报文的工作在tcp_transmit_skb函数中实现,最后调用tcp_event_new_data_sent,tcp_event_new_data_sent调用tcp_rearm_rto启动超时重传定时器ICSK_TIME_RETRANS,基本流程与机械工业出版社《Linux内核源码剖析:TCP/IP实现(下册)》P880 "1. 输出发送队列上的段"相同,具体可以参考该书籍相关内容。
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;int result;bool is_cwnd_limited = false;u32 max_segs;sent_pkts = 0;if (!push_one) { // 调用tcp_write_xmit的地方不多,只有__tcp_push_pending_frames、tcp_push_one、tcp_send_loss_probe,push_one为真(不为0)表示发送一个段,tcp_push_one、tcp_send_loss_probe调用tcp_write_xmit时,push_one不为0,这两个函数只要发送一个报文就行,__tcp_push_pending_frames调用tcp_write_xmit时,push_one为0,尽可能发送更多报文/* Do MTU probing. */result = tcp_mtu_probe(sk); // 发送MTU探测,if (!result) { // tcp_mtu_probe返回0,拥塞窗口不可用(需要等待拥塞窗口),不能发送报文return false; // 不能发送报文返回false} else if (result > 0) { // tcp_mtu_probe返回1,表示发送了一个MTU探测报文sent_pkts = 1; // 已发送的包数记为1(一个MTU探测报文)}}max_segs = tcp_tso_autosize(sk, mss_now); // how many segs we'd like on a TSO packet, to send one TSO packet per mswhile ((skb = tcp_send_head(sk))) { // 发送队列不为空,则准备发送段(未发送的第一个段)unsigned int limit;tso_segs = tcp_init_tso_segs(skb, mss_now); // TSO相关,暂不考虑TSO,具体参考机械工业出版社《Linux内核源码剖析:TCP/IP实现(下册)》P881BUG_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 */}cwnd_quota = tcp_cwnd_test(tp, skb); // 拥塞窗口限额(拥塞控制相关允许发送的窗口大小,书上及一些网上解释为拥塞窗口,这个不是很准确,具体看tcp_cwnd_test代码实现)if (!cwnd_quota) { // 拥塞窗口限额为0(拥塞窗口已满)if (push_one == 2) // tcp_send_loss_probe调用tcp_write_xmit强制发送loss probe pkt/* Force out a loss probe pkt. */cwnd_quota = 1; // 强制发送loss probe pkt,只需要也只允许发送一个loss probe pktelsebreak; // 其他拥塞窗口已满的情况下,不允许发送报文,退出循环}if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) // 检测当前段是否完全处在发送窗口内,如果是则可以发送,否则目前不能发送(这里可能发送窗口满了,或者剩余的发送窗口不够发送一个报文)break; // 退出循环if (tso_segs == 1) { // TSO不分段if (unlikely(!tcp_nagle_test(tp, skb, mss_now,(tcp_skb_is_last(sk, skb) ?nonagle : TCP_NAGLE_PUSH)))) // 检测是否使用Nagle算法,并确定当前内否立即发送该段break; // 不立即发送该段,退出循环} else { // TSO相关,具体参考机械工业出版社《Linux内核源码剖析:TCP/IP实现(下册)》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)) // // TSO相关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))) // 如果SKB中的数据长度大于分段段长,则调用tso_fragment根据该段长进行分段,如果分段失败则目前暂不发送(tso_fragment分段成功返回0,不需要分段或者分段成功,if就为false,就不会跳出循环,继续发送该段)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);limit = min_t(u32, limit, sysctl_tcp_limit_output_bytes);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.*/smp_mb__after_atomic();if (atomic_read(&sk->sk_wmem_alloc) > limit)break;}if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) // 调用tcp_transmit_skb发送段到网络层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); // 如果发送的段小于MSS,则更新最近发送的小包的最后一个字节序号sent_pkts += tcp_skb_pcount(skb); // 更新在函数中已发送的总段数if (push_one) // tcp_push_one/tcp_send_loss_probe只发送一个段的情况下,退出循环(循环只执行一次,只发送一个段)break;}if (likely(sent_pkts)) { // 本次有数据发送if (tcp_in_cwnd_reduction(sk)) // CWR | Recoverytp->prr_out += sent_pkts; // Total number of pkts sent during Recovery/* Send one loss probe per tail loss episode. */if (push_one != 2) // 非tcp_send_loss_probe调用tcp_write_xmittcp_schedule_loss_probe(sk); // Tail loss probe,参考https://lwn.net/Articles/542642/is_cwnd_limited |= (tcp_packets_in_flight(tp) >= tp->snd_cwnd); // in flight报文大于等于拥塞窗口tcp_cwnd_validate(sk, is_cwnd_limited); // 对TCP拥塞窗口进行确认return false; // 有数据发送,返回false,从__tcp_push_pending_frames函数看,返回false就不需要启动ICSK_TIME_PROBE0定时器,有数据发送,就会有超时重传定时器,不需要ICSK_TIME_PROBE0定时器}return !tp->packets_out && tcp_send_head(sk); // 如果发送队列不为空并且packets_out为0,那么应该就是有数据要发送但是发送窗口为0,返回true,需要启动ICSK_TIME_PROBE0定时器
}
调用tcp_write_xmit相关的函数:

tcp_write_xmit函数调用栈:


(第一个调用栈,应该是有发送缓存及内存时的调用栈;第二个调用栈应该是阻塞模式,没有发送缓存或者队列,用户阻塞在socket上,中断程序收到报文时由backlog来处理报文,也就是由用户线程处理报文,接收端不读数据,发送端不断发送数据,就会导致接收端的接收窗口变满,发送端发送窗口为0,发送端不能发送数据,用户write的数据就会缓存到发送队列,发送队列满了就会阻塞等待发送缓存,服务器接收确认数据就会导致发送端释放发送缓存,唤醒发送端阻塞的线程)
启动超时重传定时器ICSK_TIME_RETRANS调用栈:

tcp_transmit_skb主要就是构造TCP报文,填充TCP首部等信息,最后调用ip_queue_xmit发送TCP报文到网络层,网络层主要就是通过路由找到下一跳(路由可能缓存在SKB里面,没有的话就通过路由子系统找到路由缓存)以及目的输出函数(输出到本地、丢弃还是输出到哪里),再通过邻居子系统及ARP协议获取下一跳的目的地址,一层层协议封装之后,最后通过路由缓存项的网卡发送出去。
组装TCP报文相关的内容具体参考机械工业出版社《Linux内核源码剖析:TCP/IP实现(下册)》P883 "2. 输出到网络层"以及《TCP/IP详解卷 1:协议》"第17章 TCP:传输控制协议"。
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 tcp_out_options opts;unsigned int tcp_options_size, tcp_header_size;struct tcp_md5sig_key *md5;struct tcphdr *th;int err;BUG_ON(!skb || !tcp_skb_pcount(skb));if (clone_it) { // 需要拷贝数据skb_mstamp_get(&skb->skb_mstamp);if (unlikely(skb_cloned(skb)))skb = pskb_copy(skb, gfp_mask);elseskb = skb_clone(skb, gfp_mask);if (unlikely(!skb))return -ENOBUFS;}inet = inet_sk(sk);tp = tcp_sk(sk);tcb = TCP_SKB_CB(skb);memset(&opts, 0, sizeof(opts));if (unlikely(tcb->tcp_flags & TCPHDR_SYN))tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);elsetcp_options_size = tcp_established_options(sk, skb, &opts,&md5);tcp_header_size = tcp_options_size + sizeof(struct tcphdr);/* if no packet is in qdisc/device queue, then allow XPS to select* another queue. We can be called from tcp_tsq_handler()* which holds one reference to sk_wmem_alloc.** TODO: Ideally, in-flight pure ACK packets should not matter here.* One way to get this would be to set skb->truesize = 2 on them.*/skb->ooo_okay = sk_wmem_alloc_get(sk) < SKB_TRUESIZE(1);skb_push(skb, tcp_header_size);skb_reset_transport_header(skb);skb_orphan(skb);skb->sk = sk;skb->destructor = skb_is_tcp_pure_ack(skb) ? sock_wfree : tcp_wfree;skb_set_hash_from_sk(skb, sk);atomic_add(skb->truesize, &sk->sk_wmem_alloc);/* Build TCP header and checksum it. */th = tcp_hdr(skb);th->source = inet->inet_sport; // TCP首部填充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);if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) {/* RFC1323: The window in SYN & SYN/ACK segments* is never scaled.*/th->window = htons(min(tp->rcv_wnd, 65535U));} else {th->window = htons(tcp_select_window(sk));}th->check = 0;th->urg_ptr = 0;/* The urg_mode check is necessary during a below snd_una win probe */if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {if (before(tp->snd_up, tcb->seq + 0x10000)) {th->urg_ptr = htons(tp->snd_up - tcb->seq);th->urg = 1;} else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) {th->urg_ptr = htons(0xFFFF);th->urg = 1;}}tcp_options_write((__be32 *)(th + 1), tp, &opts);skb_shinfo(skb)->gso_type = sk->sk_gso_type;if (likely((tcb->tcp_flags & TCPHDR_SYN) == 0))tcp_ecn_send(sk, skb, tcp_header_size);#ifdef CONFIG_TCP_MD5SIG/* Calculate the MD5 hash, as we have all we need now */if (md5) {sk_nocaps_add(sk, NETIF_F_GSO_MASK);tp->af_specific->calc_md5_hash(opts.hash_location,md5, sk, skb);}
#endificsk->icsk_af_ops->send_check(sk, skb);if (likely(tcb->tcp_flags & TCPHDR_ACK))tcp_event_ack_sent(sk, tcp_skb_pcount(skb));if (skb->len != tcp_header_size)tcp_event_data_sent(tp, sk);if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,tcp_skb_pcount(skb));tp->segs_out += tcp_skb_pcount(skb);/* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);/* Our usage of tstamp should remain private */skb->tstamp.tv64 = 0;/* Cleanup our debris for IP stacks */memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),sizeof(struct inet6_skb_parm)));err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); // 调用ip_queue_xmit发送报文到网络层if (likely(err <= 0))return err;tcp_enter_cwr(sk);return net_xmit_eval(err);
}
tcp_transmit_skb调用ip_queue_xmit发送数据到网络层的函数调用栈:

发送超时,调用tcp_retransmit_timer,tcp_retransmit_timer调用tcp_retransmit_skb、__tcp_retransmit_skb、tcp_transmit_skb重传报文。
tcp_retransmit_timer函数调用栈:

tcp收到带有ACK的报文并且有数据被确认,调用tcp_snd_una_update更新发送窗口。

tcp_snd_una_update函数调用栈:

发送的报文被确认接收之后,调用tcp_clean_rtx_queue从发送队列清除对应的数据。

tcp_clean_rtx_queue函数调用栈:

上一篇:有哪些描写「不眠之夜」的诗句?
下一篇:对生活有感觉的句子