1. 综述
Linux的TCP协议非常复杂,看了几天Linux内核的tcp实现犹如雾里看花。在这里主要是根据书籍《The Linux Networking Architecture》 24章,以及结合网上的资料,走读一遍tcp协议的数据流的发送和接收。而内核源码是Centos 7.2的标准内核3.10。这个版本和书中的介绍的函数有点不一样。
tcp的数据流就是tcp数据的发送和接收两个方面。具体的系统调用如下图所示,图中的某些函数可能与最新内核有所不同了。
tcp数据报文的处理过程如下图所示:
2. 处理输入tcp数据段
IP数据报文,经过IP层的数据筛选,然后将属于6号协议(TCP协议代号),交付给tcp协议层进行处理。TCP协议层处理输入数据报文的任务的函数是tcp_v4_rcv()(net/ipv4/tcp_ipv4.c)。
tcp处理接收数据报文的过程如下图所示:
首先来看一下tcp_v4_rcv() 这个函数
tcp_v4_rev() //net/ipv4/tcp_ipv4.c
tcp_v4_rcv(skb, len) 函数的主要任务是检查socket buff结构体(skb),验证数据报文的地址是否执行本机(skb->pkt_type == PACKET_HOST)。如果是,则将ip数据报文的头部移去,协议处理继续进行。如果不是,则将该socket buff丢弃。
__inet_lookup_skb 函数在活跃的socket的hashtable中,查找这个socket是否合法。如果这个socket是存在的,则使用tcp_v4_do_rcv() 函数根据连接状态进行处理。如果socket不存在,跳转到no_tcp_socket,使用tcp_send_reset() 发送一个RESET报文。
tcp_v4_do_rcv() //net/ipv4/tcp_ipv4.c
首先,如果socket filter是活跃的,使用sk_filter() 函数检查socket buffer。如果检查失败,则丢包该包。否则,根据tcp的连接状态(sk->state),进行包处理。
- TCP_ESTABLISHED : 这个状态表示tcp已经建立连接,socket buffer将会交给tcp_rcv_established() 进行处理。
- 如果是其他状态,则提交给函数 * tcp_rcv_state_process() * 对socket buffer进行处理。
这里先忽略tcp的状态转移部分,重点关注tcp的数据流是如何交互的。tcp的状态转移也涉及到很多协议和代码。
tcp_rcv_established() //net/ipv4/tcp_input.c
tcp_rcv_established(sk, skb, th, len) 主要是处理已经连理连接的输入的tcp数据包。tcp_rcv_established实际上包含了两条路径用于处理不同目的的数据包。
- 快速路径 使用快速路径只进行最少的处理,如处理数据段、发生ACK、存储时间戳等。
- 慢速路径 使用慢速路径可以处理乱序数据段、PAWS、socket内存管理和紧急数据等。
Linux通过预测标志来区分这两种处理模式,预测标志存储在tp->pred_flags,生成这个标志的函数是__tcp_fast_path_on和tcp_fast_path_on,TCP会直接使用这两个函数来生成预测标记,也可以调用tcp_fast_path_check来完成这一任务。
static inline void __tcp_fast_path_on(struct tcp_sock *tp, u32 snd_wnd)
{
tp->pred_flags = htonl((tp->tcp_header_len << 26) |
ntohl(TCP_FLAG_ACK) |
snd_wnd);
}
static inline void tcp_fast_path_on(struct tcp_sock *tp)
{
__tcp_fast_path_on(tp, tp->snd_wnd >> tp->rx_opt.snd_wscale);
}
static inline void tcp_fast_path_check(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
if (skb_queue_empty(&tp->out_of_order_queue) &&
tp->rcv_wnd &&
atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf &&
!tp->urg_data)
tcp_fast_path_on(tp);
}
tcp_fast_path_check会先检查条件是否满足,如果满足再设置预测标记。条件是:
(1)没有乱序数据(629)
(2)接收窗口不为0(630)
(3)接收缓存未耗尽(631)
(4)没有紧急数据(632)
反之,则进入slow path处理;另外当连接新建立时处于slow path。
具体代码如下:
/* pred_flags is 0xS?10 << 16 + snd_wnd
* if header_prediction is to be made
* 'S' will always be tp->tcp_header_len >> 2
* '?' will be 0 for the fast path, otherwise pred_flags is 0 to
* turn it off (when there are holes in the receive
* space for instance)
* PSH flag is ignored.
*/
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
int tcp_header_len = tp->tcp_header_len;
//快速路径
}else{
//慢速路径
}
从fast path进入slow path的触发条件(进入slow path 后pred_flags清除为0):
在tcp_data_queue中接收到乱序数据包
在tcp_prune_queue中用完缓存并且开始丢弃数据包
在tcp_urgent_check中遇到紧急指针
在tcp_select_window中发送的通告窗口下降到0.
从slow_path进入fast_path的触发条件:
When we have read past an urgent byte in tcp_recvmsg() . Wehave gotten an urgent byte and we remain
in the slow path mode until we receive the urgent byte because it is handled in the slow path in
tcp_rcv_established() .当在tcp_data_queue中乱序队列由于gap被填充而处理完毕时,运行tcp_fast_path_check。
tcp_ack_update_window()中更新了通告窗口。
2.1 快速路径
A. 首先判断能否进入fast path。
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))
TCP_HP_BITS 的作用就是排除flag中的PSH标志位。只有在头部预测满足并且数据包以正确的顺序(该数据包的第一个序号就是下个要接收的序号)到达才能进入fast path。
B. 进行时间戳的检查校验PAWS(Protect Against Wrapped Sequence numbers)
/* Timestamp header prediction: tcp_header_len
* is automatically equal to th->doff*4 due to pred_flags
* match.
*/
/* Check timestamp */
if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
/* No? Slow path! */
if (!tcp_parse_aligned_timestamp(tp, th))
goto slow_path;
/* If PAWS failed, check it more carefully in slow path */
if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
goto slow_path;
/* DO NOT update ts_recent here, if checksum fails
* and timestamp was corrupted part, it will result
* in a hung connection since we will drop all
* future packets due to the PAWS test.
*/
}
C. 如果发送来的仅是一个TCP头的话(没有捎带数据或者接收端检测到有乱序数据这些情况时都会发送一个纯粹的ACK包)
if (len <= tcp_header_len) {
/* Bulk data transfer: sender */
if (len == tcp_header_len) {
/* Predicted packet is in window by definition.
* seq == rcv_nxt and rcv_wup <= rcv_nxt.
* Hence, check seq<=rcv_wup reduces to:
*/
if (tcp_header_len ==
(sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);
/* We know that such packets are checksummed
* on entry.
*/
tcp_ack(sk, skb, 0);
__kfree_skb(skb);
tcp_data_snd_check(sk);
return;
} else { /* Header too small */
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);
goto discard;
}
} else {
主要的工作如下:
保存对方的最近时戳 tcp_store_ts_recent。通过前面的if判断可以看出tcp总是回显2次时戳回显直接最先到达的数据包的时戳,
rcv_wup只在发送数据(这时回显时戳)时重置为rcv_nxt,所以接收到前一次回显后第一个数据包后,rcv_nxt增加了,但是
rcv_wup没有更新,所以后面的数据包处理时不会调用该函数来保存时戳。
ACK处理。这个函数非常复杂,包含了拥塞控制机制,确认处理等等。
检查是否有数据待发送 tcp_data_snd_check。
D. 如果该数据包中包含了数据的话
} else {
int eaten = 0;
bool fragstolen = false;
/* 此数据包刚好是下一个读取的数据,并且用户空间可存放下该数据包*/
if (tp->ucopy.task == current &&
tp->copied_seq == tp->rcv_nxt &&
len - tcp_header_len <= tp->ucopy.len &&
sock_owned_by_user(sk)) { /* 如果该函数在进程上下文中调用并且sock被用户占用的话*/
__set_current_state(TASK_RUNNING); /* 进程有可能被设置为TASK_INTERRUPTIBLE */
if (!tcp_copy_to_iovec(sk, skb, tcp_header_len)) { /* 直接copy数据到用户空间*/
/* Predicted packet is in window by definition.
* seq == rcv_nxt and rcv_wup <= rcv_nxt.
* Hence, check seq<=rcv_wup reduces to:
*/
if (tcp_header_len ==
(sizeof(struct tcphdr) +
TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);
/* 更新RCV RTT,Dynamic Right-Sizing算法*/
tcp_rcv_rtt_measure_ts(sk, skb);
__skb_pull(skb, tcp_header_len);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITSTOUSER);
eaten = 1;
}
}
if (!eaten) { /* 没有直接读到用户空间*/
if (tcp_checksum_complete_user(sk, skb))
goto csum_error;
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5;
/* Predicted packet is in window by definition.
* seq == rcv_nxt and rcv_wup <= rcv_nxt.
* Hence, check seq<=rcv_wup reduces to:
*/
if (tcp_header_len ==
(sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);
tcp_rcv_rtt_measure_ts(sk, skb);
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITS);
/* Bulk data transfer: receiver */ /* 进入receive queue 排队,以待tcp_recvmsg读取*/
eaten = tcp_queue_rcv(sk, skb, tcp_header_len,
&fragstolen);
}
/* 数据包接收后续处理*/
tcp_event_data_recv(sk, skb);
/* ACK 处理*/
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
/* Well, only one small jumplet in fast path... */
tcp_ack(sk, skb, FLAG_DATA);
tcp_data_snd_check(sk);
if (!inet_csk_ack_scheduled(sk))
goto no_ack;
}
/* ACK发送处理*/
__tcp_ack_snd_check(sk, 0);
no_ack:
if (eaten) /* eaten为1,表示数据直接copy到了用户空间,这时无需提醒用户进程数据的到达,否则需调用sk_data_ready来通知,因为此时数据到达了receive queue*/
kfree_skb_partial(skb, fragstolen);
sk->sk_data_ready(sk, 0);
return;
}
}
2.2 慢速路径
TCP报文段进入慢速路径处理的条件有:
- 收到乱序数据或乱序队列非空
- 收到紧急指针
- 接收缓存耗尽
- 收到0窗口通告
- 收到的报文中含有除了PUSH和ACK之外的标记,如SYN、FIN、RST
- 报文的时间戳选项解析失败
- 报文的PAWS检查失败
慢速路径的代码如下,具体细节先不深挖。
slow_path:
if (len < (th->doff << 2) || tcp_checksum_complete_user(sk, skb)) /*报文长度非法或检验和错误*/
goto csum_error;
if (!th->ack && !th->rst && !th->syn)
goto discard;
/*
* Standard slow path.
*/
if (!tcp_validate_incoming(sk, skb, th, 1)) /*其他合法性检查*/
return;
step5:
if (tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) < 0) /*ack非法丢弃之*/
goto discard;
tcp_rcv_rtt_measure_ts(sk, skb); /*更新RTT估计量*/
/* Process urgent data. */
tcp_urg(sk, skb, th); /*紧急指针 */
/* step 7: process the segment text */
tcp_data_queue(sk, skb); /*处理数据,包括乱序数据*/
tcp_data_snd_check(sk); /*试图发送队列中的数据 */
tcp_ack_snd_check(sk); /*发送ACK */
return;
综上所述,tcp数据段的接收,从IP层将数据接收之后进行处理,处理过程需要根据tcp的状态进行处理,如果处于已连接状态,则进行函数tcp_rcv_established()进行处理,该函数的处理方式又分为快速路径和慢速路径,分别用于不同数据包的处理。而其他状态的数据则给tcp_rcv_state_process()进行处理。
3. tcp数据段发送
这一节主要是tcp数据报文段是怎么发送的,以及一些其他机制的实现,如ack报文、delay ack定时器等。
socket连接建立之后,用户态程序使用send来实现tcp报文发送。 send系统调用实质上是调用了tcp_sendmsg()来实现。这个函数注册在tcp_prot结构体中(net/ipv4/tcp_ipv4.c)。tcp_sendmsg()的调用过程如下所示:
tcp_sendmsg() //net/ipv4/tcp.c
tcp_sendmsg函数的主要功能是:
如果发送队列尾部的skb尚未发送而且还有剩余空间,则将用户缓存中的数据copy进去;如果没有这样的空间则申请一个数据空间固定大小的skb,再copy数据;
一个skb的空间如果不够就再申请一个固定大小的skb,再copy数据,直到数据全部copy完毕,或skb的缓存空间无法申请,或发送缓存达到限制为止;若是后两种情况,如果socket是非阻塞的则立即返回,否则会等待能够得到空间或超时
将申请的skb放入发送队列尾部,再调用tcp_push、__tcp_push_pending_frames或tcp_push_one函数发送队列中的skb
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 = tcp_write_queue_tail(sk);
if (!(flags & MSG_MORE) || forced_push(tp))
tcp_mark_push(tp, skb);
tcp_mark_urg(tp, flags); /*如果客户设置要发送OOB数据,则记录紧急数据的下一个字节的序列号,其实紧急数据就是当前数据包的最后一个字节数据 */
if (tcp_should_autocork(sk, skb, size_goal)) {
/* avoid atomic op if TSQ_THROTTLED bit is already set */
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.
*/
if (atomic_read(&sk->sk_wmem_alloc) > skb->truesize)
return;
}
if (flags & MSG_MORE)
nonagle = TCP_NAGLE_CORK;
__tcp_push_pending_frames(sk, mss_now, nonagle); /*最后发送数据*/
}
__tcp_push_pending_frames、tcp_push_one都会调用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) {
/* Do MTU probing. */
result = tcp_mtu_probe(sk);
if (!result) {
return false;
} else if (result > 0) {
sent_pkts = 1;
}
}
max_segs = tcp_tso_autosize(sk, mss_now);
while ((skb = tcp_send_head(sk))) { /*获取没有发送的最老的skb*/
unsigned int limit;
sk->sk_write_queue
tso_segs = tcp_init_tso_segs(sk, skb, mss_now); /*获取网卡将这个skb分割成的段的个数*/
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); /*计算拥塞窗口允许发送的字节数 */
if (!cwnd_quota) {
is_cwnd_limited = true;
if (push_one == 2) /*如果要发送TCP探测包*/
/* Force out a loss probe pkt. */
cwnd_quota = 1; /*允许发送一个字节*/
else
break;
}
/*检查发送窗口是否允许发送数据*/
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
break;
/*网卡会将此skb按一个包发送*/
if (tso_segs == 1) {
/*查看nagle算法是否运行发送当前包*/
if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
(tcp_skb_is_last(sk, skb) ?
nonagle : TCP_NAGLE_PUSH))))
break;
/*网卡会将此skb分割成多个包发送*/
} else {
if (!push_one &&
tcp_tso_should_defer(sk, skb, &is_cwnd_limited,
max_segs))
break;
}
limit = mss_now;
/*网卡会将此skb分割成多个包发送并且没有紧急数*/
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; /*如果包过大,就得拆成两个包,当前包的大小会减小为与limit一致*/
/* 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.
* 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;
}
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); /*send_head指向下一个要发送的包*/
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_write_xmit函数会根据拥塞窗口、发送窗口、nagle算法等条件判断是否发送数据以及发送多少,即新放入发送队列中的数据不一定会立即发送。而且发送包时是发送skb的副本,原来的skb会一直呆在发送队列中,如果发生了数据丢失则TCP会将发送队列中的skb再发送一次,直到数据被确认时才能删除。
可以发送时tcp_write_xmit函数会调用tcp_transmit_skb函数发送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) { /*clone一个副本发送出去,原本留在队列中等待ACK确认后再删除*/
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); /*构建SYN包的选项信息*/
else
tcp_options_size = tcp_established_options(sk, skb, &opts,
&md5);/*构建非SYN包的选项信息*/
tcp_header_size = tcp_options_size + sizeof(struct tcphdr);/*计算TCP头长度*/
if (tcp_packets_in_flight(tp) == 0)
tcp_ca_event(sk, CA_EVENT_TX_START);/*没有停留在网络中的包(即发送后未被确认的包,包括重传包)*/
/* if no packet is in qdisc/device queue, then allow XPS to select
* another queue.
*/
skb->ooo_okay = sk_wmem_alloc_get(sk) == 0;
skb_push(skb, tcp_header_size);/*skb->data指向TCP头*/
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; //设置源端口
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);
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, NULL, skb);
}
#endif
//调用tcp_v4_send_check或tcp_v6_send_check计算TCP检验和
icsk->icsk_af_ops->send_check(sk, skb);
if (likely(tcb->tcp_flags & TCPHDR_ACK)) //包中有ACK标记
tcp_event_ack_sent(sk, tcp_skb_pcount(skb)); //可以取消延迟ACK定时器,因为即将发送的包中已经携带ACK标记
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));
/* Our usage of tstamp should remain private */
skb->tstamp.tv64 = 0;
err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);
//调用ip_queue_xmit或inet6_csk_xmit构建IP头并将包发送出去
if (likely(err <= 0))
return err;
tcp_enter_cwr(sk);
return net_xmit_eval(err);
}
综上,TCP发送数据的系统调用会将用户指定缓冲区的内容复制到内核中,内核存储数据的结构是skb。每个skb存储数据的空间大小的固定的,即,用户进程指定的连续缓存中的数据可能会被TCP拆成多个固定长度的skb来存储并发送,这被称为“分段”。发送时一个skb会被封成一个TCP报文,进而封装成一个IP报文。skb包在放入队列时是一个一个添加到队列尾部顺序放入,发送时是从队列头开始一个一个按序发送,从而保证了发送数据的顺序与用户缓存中的顺序是一致的。发数据的系统调用返回值是“成功发送的字节数”,但实际上只是“成功放入TCP发送队列中的字节数”,放入队列的数据不一定会立即发送,但TCP会负责将这些数据可靠的发送给对端,在成功之前不会删除。也就是说,用户进程可以认为数据已经发送成功了。TCP也可以copy大段数据到一个skb中从而减少分段,这种技术名为“TSO (TCP Segmentation Offload)”。
4. 参考资料
- http://blog.youkuaiyun.com/u011130578/article/details/44181503
- The Linux Networking Architecture
- linux-3.10.0-327.28.3.el7