linux网络协议栈源码分析 - 传输层(TCP的输出)
Posted arm7star
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux网络协议栈源码分析 - 传输层(TCP的输出)相关的知识,希望对你有一定的参考价值。
1、TCP write系统调用(tcp_sendmsg)
1.1、write系统调用
socket的write系统调用栈:
write最终调用tcp_sendmsg发送消息。
1.2、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)) // 非fastopen
err = 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的字节数为0
err = -EPIPE; // 初始化错误码为EPIPE
if (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_now
copy = 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)); // 分配SKB
if (!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的分散片段数i
struct page_frag *pfrag = sk_page_frag(sk); // 获取最后一个分片的页面page
if (!sk_page_frag_refill(sk, pfrag)) // 检查分片页面是否有可用空间,如果有,返回true,如果没有,调用alloc_pages分配页面,如果还是分配失败,返回false
goto 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_segs
copied += 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; // 继续复制数据到SKB
if (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,并且错误码为EAGAIN
sk->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!"。
2、TCP报文输出(tcp_write_xmit)
tcp_sendmsg只是将消息分成一个个最大的段,最后一个可能没这么多数据,所以下一次写的时候可能需要追加数据到最后一个不够最大段的段里面,tcp_sendmsg调用其他接口发送报文,这些接口最终都是调用tcp_write_xmit发送数据。
2.1、报文段发送(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 ms
while ((skb = tcp_send_head(sk))) // 发送队列不为空,则准备发送段(未发送的第一个段)
unsigned int limit;
tso_segs = tcp_init_tso_segs(skb, mss_now); // TSO相关,暂不考虑TSO,具体参考机械工业出版社《Linux内核源码剖析:TCP/IP实现(下册)》P881
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 */
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 pkt
else
break; // 其他拥塞窗口已满的情况下,不允许发送报文,退出循环
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 | Recovery
tp->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_xmit
tcp_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调用栈:
2.2、发送到网络层(tcp_transmit_skb)
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);
else
skb = 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);
else
tcp_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);
#endif
icsk->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发送数据到网络层的函数调用栈:
2.3、tcp超时重传
发送超时,调用tcp_retransmit_timer,tcp_retransmit_timer调用tcp_retransmit_skb、__tcp_retransmit_skb、tcp_transmit_skb重传报文。
tcp_retransmit_timer函数调用栈:
3、ACK接收
3.1、发送窗口更新(tcp_snd_una_update)
tcp收到带有ACK的报文并且有数据被确认,调用tcp_snd_una_update更新发送窗口。
tcp_snd_una_update函数调用栈:
3.2、清除已发送的报文(tcp_clean_rtx_queue)
发送的报文被确认接收之后,调用tcp_clean_rtx_queue从发送队列清除对应的数据。
tcp_clean_rtx_queue函数调用栈:
以上是关于linux网络协议栈源码分析 - 传输层(TCP的输出)的主要内容,如果未能解决你的问题,请参考以下文章
linux网络协议栈源码分析 - 传输层(TCP连接的终止)
linux网络协议栈源码分析 - 传输层(TCP连接的终止)
linux网络协议栈源码分析 - 传输层(TCP连接的终止)
linux网络协议栈源码分析 - 传输层(TCP连接的建立)