内核中的TCP的追踪分析-11-TCP(IPV4)的socket连接-续3

2020-05-25 00:00:00 函数 代码 调用 结构 钩子

关于tcp的ipv4的socket连接非常长,之所以其复杂,很多朋友对此望而却步,主要是涉及的协议理论太复杂,而我对2.6.26的分析只从代码出处,意从简单入手,深入浅出剖析出tcp的socket原理,所以请朋友们阅读我的分析过程会感觉到没有太多的协议内容,这样简化了难度更加让朋友理解整个过程。我是无名小卒,转载的朋友请注明出处。
我们继续昨天的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;
int tcp_header_size;
#ifdef CONFIG_TCP_MD5SIG
struct tcp_md5sig_key *md5;
__u8 *md5_hash_location;
#endif
struct tcphdr *th;
int sysctl_flags;
int err;
BUG_ON(!skb || !tcp_skb_pcount(skb));
/* If congestion control is doing timestamping, we must
* take such a timestamp before we potentially clone/copy.
*/
if (icsk->icsk_ca_ops->flags & TCP_CONG_RTT_STAMP)
__net_timestamp(skb);
if (likely(clone_it)) {
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);
tcp_header_size = tp->tcp_header_len;
#define SYSCTL_FLAG_TSTAMPS 0x1
#define SYSCTL_FLAG_WSCALE 0x2
#define SYSCTL_FLAG_SACK 0x4
上面代码中有一个结构struct tcp_skb_cb我们未曾看过,这个结构是为了保存每一个TCP包的一些控制信息及参数,是等待发送包队列传递给接收方的每一个TCP包的控制信息,在传递时,它会保存到 struct sk_buff中的cb数组,cb数组的长度是40字节,而struct tcp_skb_cb在ipv4中是36字节,在ipv6中是40字节。

struct tcp_skb_cb {
union {
struct inet_skb_parm h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct inet6_skb_parm h6;
#endif
} header; /* For incoming frames */
__u32 seq; /* Starting sequence number */
__u32 end_seq; /* SEQ + FIN + SYN + datalen */
__u32 when; /* used to compute rtt's */
__u8 flags; /* TCP header flags. */
/* NOTE: These must match up to the flags byte in a
* real TCP header.
*/
#define TCPCB_FLAG_FIN 0x01
#define TCPCB_FLAG_SYN 0x02
#define TCPCB_FLAG_RST 0x04
#define TCPCB_FLAG_PSH 0x08
#define TCPCB_FLAG_ACK 0x10
#define TCPCB_FLAG_URG 0x20
#define TCPCB_FLAG_ECE 0x40
#define TCPCB_FLAG_CWR 0x80
__u8 sacked; /* State flags for SACK/FACK. */
#define TCPCB_SACKED_ACKED 0x01 /* SKB ACK'd by a SACK block */
#define TCPCB_SACKED_RETRANS 0x02 /* SKB retransmitted */
#define TCPCB_LOST 0x04 /* SKB is lost */
#define TCPCB_TAGBITS 0x07 /* All tag bits */
#define TCPCB_EVER_RETRANS 0x80 /* Ever retransmitted frame */
#define TCPCB_RETRANS (TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS)
__u16 urg_ptr; /* Valid w/URG flags is set. */
__u32 ack_seq; /* Sequence number ACK'd */
};
所以这个结构是与skbuff结构中的cb数组密切联系的,二者的长度是严格对正的。另外还有一个结构是我们以前看到过的
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your defines"
#endif
__be16 window;
__sum16 check;
__be16 urg_ptr;
};
这个结构体是用于tcp的头部所使用的结构体。其内部的变量我们随着代码来看。然后我们看到代码中检查参数clone_it是否设置为1来克隆出一个新的sk_buff结构并加以初始化
我们看余下的代码
sysctl_flags = 0;
if (unlikely(tcb->flags & TCPCB_FLAG_SYN)) {
tcp_header_size = sizeof(struct tcphdr) + TCPOLEN_MSS;
if (sysctl_tcp_timestamps) {
tcp_header_size += TCPOLEN_TSTAMP_ALIGNED;
sysctl_flags |= SYSCTL_FLAG_TSTAMPS;
}
if (sysctl_tcp_window_scaling) {
tcp_header_size += TCPOLEN_WSCALE_ALIGNED;
sysctl_flags |= SYSCTL_FLAG_WSCALE;
}
if (sysctl_tcp_sack) {
sysctl_flags |= SYSCTL_FLAG_SACK;
if (!(sysctl_flags & SYSCTL_FLAG_TSTAMPS))
tcp_header_size += TCPOLEN_SACKPERM_ALIGNED;
}
} else if (unlikely(tp->rx_opt.eff_sacks)) {
/* A SACK is 2 pad bytes, a 2 byte header, plus
* 2 32-bit sequence numbers for each SACK block.
*/
tcp_header_size += (TCPOLEN_SACK_BASE_ALIGNED +
(tp->rx_opt.eff_sacks *
TCPOLEN_SACK_PERBLOCK));
}
if (tcp_packets_in_flight(tp) == 0)
tcp_ca_event(sk, CA_EVENT_TX_START);
#ifdef CONFIG_TCP_MD5SIG
/*
* Are we doing MD5 on this segment? If so - make
* room for it.
*/
md5 = tp->af_specific->md5_lookup(sk, sk);
if (md5)
tcp_header_size += TCPOLEN_MD5SIG_ALIGNED;
#endif
skb_push(skb, tcp_header_size);
skb_reset_transport_header(skb);
skb_set_owner_w(skb, sk);
/* Build TCP header and checksum it. */
th = tcp_hdr(skb);
th->source = inet->sport;
th->dest = inet->dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(tp->rcv_nxt);
*(((__be16 *)th) + 6) = htons(((tcp_header_size >> 2) 12) |
tcb->flags);
if (unlikely(tcb->flags & TCPCB_FLAG_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;
if (unlikely(tp->urg_mode &&
between(tp->snd_up, tcb->seq + 1, tcb->seq + 0xFFFF))) {
th->urg_ptr = htons(tp->snd_up - tcb->seq);
th->urg = 1;
}
if (unlikely(tcb->flags & TCPCB_FLAG_SYN)) {
tcp_syn_build_options((__be32 *)(th + 1),
tcp_advertise_mss(sk),
(sysctl_flags & SYSCTL_FLAG_TSTAMPS),
(sysctl_flags & SYSCTL_FLAG_SACK),
(sysctl_flags & SYSCTL_FLAG_WSCALE),
tp->rx_opt.rcv_wscale,
tcb->when,
tp->rx_opt.ts_recent,
#ifdef CONFIG_TCP_MD5SIG
md5 ? &md5_hash_location :
#endif
NULL);
} else {
tcp_build_and_update_options((__be32 *)(th + 1),
tp, tcb->when,
#ifdef CONFIG_TCP_MD5SIG
md5 ? &md5_hash_location :
#endif
NULL);
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) {
tp->af_specific->calc_md5_hash(md5_hash_location,
md5,
sk, NULL, NULL,
tcp_hdr(skb),
sk->sk_protocol,
skb->len);
}
#endif
icsk->icsk_af_ops->send_check(sk, skb->len, skb);
if (likely(tcb->flags & TCPCB_FLAG_ACK))
tcp_event_ack_sent(sk, tcp_skb_pcount(skb));
if (skb->len != tcp_header_size)
tcp_event_data_sent(tp, skb, sk);
if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
TCP_INC_STATS(TCP_MIB_OUTSEGS);
err = icsk->icsk_af_ops->queue_xmit(skb, 0);
if (likely(err = 0))
return err;
tcp_enter_cwr(sk, 1);
return net_xmit_eval(err);
#undef SYSCTL_FLAG_TSTAMPS
#undef SYSCTL_FLAG_WSCALE
#undef SYSCTL_FLAG_SACK
}
上面代码中首先是根据头部结构tcb是否有TCPCB_FLAG_SYN标志,以及窗口大小和时间戳来确定头部的长度tcp_header_size。这里注意sysctl_tcp_sack,涉及到sack (Selective Acknowledgment, 选择性确认)选项,这个选项使tcp的socket能够明确是否应该重发数据。在确定了tcp_header_size的长度后,函数进入tcp_packets_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;
}
static inline unsigned int tcp_left_out(const struct tcp_sock *tp)
{
return tp->sacked_out + tp->lost_out;
}
上面是通过tcp的sock的计数器的运算得到的。如果没有数据包发送了就会进入tcp_ca_event()函数
static inline void tcp_ca_event(struct sock *sk, const enum tcp_ca_event event)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
if (icsk->icsk_ca_ops->cwnd_event)
icsk->icsk_ca_ops->cwnd_event(sk, event);
}
这里调用了我们以前在创建tcp的socket时挂入的钩子函数,请朋友们看一下
http://blog.chinaunix.net/u2/64681/showart_1360583.html
在那篇文章中我们回忆一下在创建tcp的socket时有一句代码icsk->icsk_ca_ops = &tcp_init_congestion_ops;
这个结构在/net/ipv4/tcp_cong.c中的408行处
struct tcp_congestion_ops tcp_init_congestion_ops = {
.name = "",
.owner = THIS_MODULE,
.ssthresh = tcp_reno_ssthresh,
.cong_avoid = tcp_reno_cong_avoid,
.min_cwnd = tcp_reno_min_cwnd,
};
我们看到在这个钩子函数中没有设置cwnd_event钩子函数所以tcp_ca_event()函数直接返回了。接着我们往下看到函数调用了skb_push()
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
{
skb->data -= len;
skb->len += len;
if (unlikely(skb->dataskb->head))
skb_under_panic(skb, len, __builtin_return_address(0));
return skb->data;
}
这个函数在skb载送的数据长度为tcp头部留出空间并增加了整个skb的数据长度。接着下边调用skb_reset_transport_header()来确定这次发送的数据包长度,接着我们看到调用了skb_set_owner_w()函数,这个函数在
http://blog.chinaunix.net/u2/64681/showart_1355078.html
《13-socket的实践到内核--TCP的socket数据的发送》那节中我们曾经分析过了,函数就是将我们的sk_buff与sock结构挂起钩来,关键的是设置了释放的钩子函数sock_wfree()这样当接收方收到数据包后可以通过这个钩子函数来释放数据包。接下来我们看到代码中进一步对tcp的头部结构变量th做了一系列的设置。我们在这段代码中要注意结构tcp_skb_cb的变量tcb是从我们的sk_buff中的cb[0]的元素中读取到的,代码中我们看到这个tcp的头部控制结构决定着th的一些设置。我们还要注意这里th已经指向了sk_buff数据位置上了
th = tcp_hdr(skb);结合看一下调用的tcp_hdr函数
static inline struct tcphdr *tcp_hdr(const struct sk_buff *skb)
{
return (struct tcphdr *)skb_transport_header(skb);
}
所以引后调用的tcp_syn_build_options()函数以及tcp_build_and_update_options()函数和TCP_ECN_send()函数中我们都看到是对th+1为起点位置都是对sk_buff中载送的数据的初始化操作。接下来我们在代码中看到了
icsk->icsk_af_ops->send_check(sk, skb->len, skb);
这个函数还要结合我们在创建tcp的socket的文章
http://blog.chinaunix.net/u2/64681/showart_1360583.html
在那里调用tcp_v4_init_sock()函数中挂入的是icsk->icsk_af_ops = &ipv4_specific;
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,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.remember_stamp = tcp_v4_remember_stamp,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.bind_conflict = inet_csk_bind_conflict,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_ip_setsockopt,
.compat_getsockopt = compat_ip_getsockopt,
#endif
};
很显然我们这里是调用了tcp_v4_send_check()钩子函数
void tcp_v4_send_check(struct sock *sk, int len, struct sk_buff *skb)
{
struct inet_sock *inet = inet_sk(sk);
struct tcphdr *th = tcp_hdr(skb);
if (skb->ip_summed == CHECKSUM_PARTIAL) {
th->check = ~tcp_v4_check(len, inet->saddr,
inet->daddr, 0);
skb->csum_start = skb_transport_header(skb) - skb->head;
skb->csum_offset = offsetof(struct tcphdr, check);
} else {
th->check = tcp_v4_check(len, inet->saddr, inet->daddr,
csum_partial((char *)th,
th->doff 2,
skb->csum));
}
}
这个函数是为我们计算出了ipv4的tcp检验和checksum.
下面摘自百度的一段文字叙述
http://baike.baidu.com/view/93743.htm

Checksum
  Checksum-检验和,校验和。在数据处理和数据通信领域中,用于校验目的的一组数据项的和。这些数据项可以是数字或在计算检验和过程中看作数字的其它字符串。
  它通常是以十六进制为数制表示的形式,如:
  十六进制串: 0102030405060708
  的效验和是: 24 (十六进制)
  如果效验和的数值超过十六进制的FF,也就是255. 就要求其补码作为效验和.
  通常用来在通信中,尤其是远距离通信中保证数据的完整性和准确性.
回到tcp_transmit_skb函数我们继续往下看,接着是对inet_connection_sock结构变量icsk应答模式的设置,这是通过tcp_event_ack_sent ()调用tcp_dec_quickack_mode()函数执行的。以及调用inet_csk_clear_xmit_timer()对定时器的设置。接着调用tcp_event_data_sent()函数对tcp的sock结构变量tp进一步的设置,上面没有分析这些函数都是与具体的协议密切联系的,我们先抓住主线将来再分析协议相关的初始化部分。函数下面调用了
err = icsk->icsk_af_ops->queue_xmit(skb, 0);
这句代码结合上面的ipv4_specific钩子结构,我们看到是执行的钩子函数ip_queue_xmit()这是一个发送数据包的函数,我们在进入这个重要的函数之前先把tcp_transmit_skb()函数分析完比,假设我们成功调用了ip_queue_xmit()函数发送了数据包,返回后会进入tcp_enter_cwr()函数中
void tcp_enter_cwr(struct sock *sk, const int set_ssthresh)
{
struct tcp_sock *tp = tcp_sk(sk);
const struct inet_connection_sock *icsk = inet_csk(sk);
tp->prior_ssthresh = 0;
tp->bytes_acked = 0;
if (icsk->icsk_ca_state TCP_CA_CWR) {
tp->undo_marker = 0;
if (set_ssthresh)
tp->snd_ssthresh = icsk->icsk_ca_ops->ssthresh(sk);
tp->snd_cwnd = min(tp->snd_cwnd,
tcp_packets_in_flight(tp) + 1U);
tp->snd_cwnd_cnt = 0;
tp->high_seq = tp->snd_nxt;
tp->snd_cwnd_stamp = tcp_time_stamp;
TCP_ECN_queue_cwr(tp);
tcp_set_ca_state(sk, TCP_CA_CWR);
}
}
这个函数是对tcp的socket结构进行了一些状态位的设置,不过这里会调用我们前面贴出的钩子结构tcp_init_congestion_ops中的函数tcp_reno_ssthresh()在这个函数中也是对snd_cwnd窗口拥塞计数器的设置。后tcp_transmit_skb函数检查一下是否有丢包等错误发生通过net_xmit_eval()宏来判断确定是否执行发送成功。关于ip_queue_xmit()数据包发送函数我们明天继续分析。


文章来源CU社区:内核中的TCP的追踪分析-11-TCP(IPV4)的socket连接-续3

相关文章