内核中的TCP的追踪分析-22-TCP(IPV4)的客户端数据的发送-续

我们今天接着看tcp_write_xmit(),在开始函数之前我想提醒朋友们,如果你是一名研发工程师的话请注意我的提醒,近来很多公司打着招聘的名义窃取项目计划和机密,这些公司在招聘人才时要求简历写出其所做过的项目情况,看似展示一个人的工作经验和才能,背后隐藏着不可告人的意图,通过与工程师的这种不合理的要求轻松取得了一些有价值的项目资料。这往往是做技术的工程师想不到欺诈行为,假设曾经与所在公司有过约定的话,那么就已经违约在先了。在此提醒广大朋友如果遇到此类要求介绍你的项目情况,请用技术语言概括你的技能即可,完全没必要将项目的具体情况暴露给对方。我们先看函数的主体。
static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
unsigned int tso_segs, sent_pkts;
int cwnd_quota;
int result;
/* If we are closed, the bytes will have to remain here.
* In time closedown will finish, we empty the write queue and all
* will be happy.
*/
if (unlikely(sk->sk_state == TCP_CLOSE))
return 0;
sent_pkts = 0;
/* Do MTU probing. */
if ((result = tcp_mtu_probe(sk)) == 0) {
return 0;
} else if (result > 0) {
sent_pkts = 1;
}
while ((skb = tcp_send_head(sk))) {
unsigned int limit;
tso_segs = tcp_init_tso_segs(sk, skb, mss_now);
BUG_ON(!tso_segs);
cwnd_quota = tcp_cwnd_test(tp, skb);
if (!cwnd_quota)
break;
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
break;
if (tso_segs == 1) {
if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
(tcp_skb_is_last(sk, skb) ?
nonagle : TCP_NAGLE_PUSH))))
break;
} else {
if (tcp_tso_should_defer(sk, skb))
break;
}
limit = mss_now;
if (tso_segs > 1)
limit = tcp_mss_split_point(sk, skb, mss_now,
cwnd_quota);
if (skb->len > limit &&
unlikely(tso_fragment(sk, skb, limit, mss_now)))
break;
TCP_SKB_CB(skb)->when = tcp_time_stamp;
if (unlikely(tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC)))
break;
/* 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++;
}
if (likely(sent_pkts)) {
tcp_cwnd_validate(sk);
return 0;
}
return !tp->packets_out && tcp_send_head(sk);
}
函数代码中,首先是检测一下我们的socket的状态,我们以前分析的文章中经常看到在函数的开头类似的判断状态语句,这是为了防止在中断的过程socket的状态改变了,而进程恢复运行时不做状态检查就往下执行是非常危险和不必要的。接下来是对MTU最大传输单元的检查tcp_mtu_probe()函数实现的,我们不看这个关于mtu的函数了,函数内部是结合mss来确定的。我们继续往下看到进入一个while循环中,取得了要发送的数据包,然后通过tcp_init_tso_segs()函数计算一下tcp分段卸载的个数,注意TSO(TCP Segment Offload)tcp分段卸载
static int tcp_init_tso_segs(struct sock *sk, struct sk_buff *skb,
unsigned int mss_now)
{
int tso_segs = tcp_skb_pcount(skb);
if (!tso_segs || (tso_segs > 1 && tcp_skb_mss(skb) != mss_now)) {
tcp_set_skb_tso_segs(sk, skb, mss_now);
tso_segs = tcp_skb_pcount(skb);
}
return tso_segs;
}
函数中首先是取得GSO的个数
static inline int tcp_skb_pcount(const struct sk_buff *skb)
{
return skb_shinfo(skb)->gso_segs;
}
也就是直接将GSO的总数,Generic Segmentation Offload通用的分段卸载,关于GSO的介绍已经在
http://blog.chinaunix.net/u2/64681/showart.php?id=1411408
文章的末尾处介绍了,那里曾经提到最好在网卡驱动里面分段。这里的函数代码是直接将GSO的总数传递给TSO使用了。如果TSO的个数是0或者虽然不为0但是数据包的GSO 的大小与mss值不同的话就会进入tcp_set_skb_tso_segs()函数,我们一并把tcp_skb_mss() 函数的代码列在下面
static inline int tcp_skb_mss(const struct sk_buff *skb)
{
return skb_shinfo(skb)->gso_size;
}
static void tcp_set_skb_tso_segs(struct sock *sk, struct sk_buff *skb,
unsigned int mss_now)
{
if (skb->len = mss_now || !sk_can_gso(sk)) {
/* Avoid the costly divide in the normal
* non-TSO case.
*/
skb_shinfo(skb)->gso_segs = 1;
skb_shinfo(skb)->gso_size = 0;
skb_shinfo(skb)->gso_type = 0;
} else {
skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss_now);
skb_shinfo(skb)->gso_size = mss_now;
skb_shinfo(skb)->gso_type = sk->sk_gso_type;
}
}
很明显要根据mss的值重新设置数据包中的struct skb_shared_info内的关于GSO的内容项。最后将得到的TSO总数返回给tcp_write_xmit()中,回到tcp_write_xmit函数后我们继续往下看到执行了tcp_cwnd_test()函数根据窗口规定的阻塞要求计算出还能发送的数据段数量。
static inline unsigned int tcp_cwnd_test(struct tcp_sock *tp,
struct sk_buff *skb)
{
u32 in_flight, cwnd;
/* Don't be strict about the congestion window for the final FIN. */
if ((TCP_SKB_CB(skb)->flags & TCPCB_FLAG_FIN) &&
tcp_skb_pcount(skb) == 1)
return 1;
in_flight = tcp_packets_in_flight(tp);
cwnd = tp->snd_cwnd;
if (in_flight cwnd)
return (cwnd - in_flight);
return 0;
}
我们看到就象在unix的socket看到的计数“飞行状态”中的数据包那样,这里也有一个in_flight来记录还有多少数据还处在未被服务器端接收仍在网络的旅途之中
static inline unsigned int tcp_packets_in_flight(const struct tcp_sock *tp)
{
return tp->packets_out - tcp_left_out(tp) + tp->retrans_out;
}
这是通过使用tcp的sock中的几个计数器运算得出的,我们可以看出关于tcp的sock结构体中的这几个变量的作用了。适用了我们所说的“用时查看”的数据结构学习方法。packets_out是用来记录还有多少在“飞行”状态的总数,这样的计算能够得到比较准备的网络阻塞情况。下面函数接着调用了tcp_snd_wnd_test()来检查我们的数据包序列号是否在二手手游账号地图发送窗口要求之内。接着根据TSO的分段数量对数据包进行分割并挂入客户端的sock发送等待队列sk_write_queue中,这主要是通过tso_fragment()函数完成的,我们就不看这些过程了。
接下我们看到函数中也调用了tcp_transmit_skb(),这个函数的代码请朋友们看
http://blog.chinaunix.net/u2/64681/showart.php?id=1415963
那节关于socket的连接文章,很明显这里的数据发送与那里的连接可以说是过程完全一样了。朋友们可以顺着连接过程,直到完成到服务器的连接过程回忆一下。我们在将来还会再次修正这些内容。这里我们假定已经完成了对服务器数据包的发送过程,程序接着往下执行,我们接着往下看,数据发送后,接下来就要调用
static void tcp_event_new_data_sent(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
unsigned int prior_packets = tp->packets_out;
tcp_advance_send_head(sk, skb);
tp->snd_nxt = TCP_SKB_CB(skb)->end_seq;
/* Don't override Nagle indefinately with F-RTO */
if (tp->frto_counter == 2)
tp->frto_counter = 3;
tp->packets_out += tcp_skb_pcount(skb);
if (!prior_packets)
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
}
这个函数对tcp的sock中的计数器进行调整,其作用我们在上面计算阻塞情况时看到了。并且调用tcp_advance_send_head()来调整发送头使其指向下一个数据包
static inline void tcp_advance_send_head(struct sock *sk, struct sk_buff *skb)
{
sk->sk_send_head = skb->next;
if (sk->sk_send_head == (struct sk_buff *)&sk->sk_write_queue)
sk->sk_send_head = NULL;
}
如果数据包全部发送完了,sk_write_queue发送队列也就空了,所以将sk_send_head发送头置空。到这里我们客户端的数据发送主要过程也就完成了,函数其余的代码无非是调整窗口和相关的计数器,我们不分析了,请朋友们自行根据协议看一下这些变量的作用。函数层层返回tcp_sendmsg()中,其余的代码也就很简单了,无非是我们前面看到对出错、等待等情况的处理过程。那些过程我们不再列出了相信朋友们通过以前的阅读分析已经可以自己理解了。到此,我们的ipv4的整个分析过程就结束了。回忆一下我们从客户端socet到其硬件网卡再到服务器的硬件网卡至服务器端的socket,不管是连接请求还是握手过程以及发送、接收过程我们都已经分析过了,这个主线分析了,其余的过程朋友们可以举一反三完成自己的学习过程。本次是第一版的分析文章,计划在不久的将来完成第二版的结合理论协议让朋友即在阅读理论的同时又能结合代码的分析过程达到切实领会、掌握的境界。谢谢朋友们的支持,欢迎阅读我的二版分析过程资料。

上一篇:网桥


下一篇:RTL8169网卡驱动