注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

gmd20的个人空间

// 编程和生活

 
 
 

日志

 
 

Linux 内核的tcp连接状态分析(systemtap的tcp_trace.stp和call_granh.stp脚本)  

2014-08-04 13:16:06|  分类: linux相关 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
做一个网络断开导致tcp连接错误的测试,
程序本来是想依赖keepalive来探测到tcp死链接,结果测试中发现的情况有3种:
1.
如果网络断开时,tcp连接双方没有数据通讯,那么等tcp keepalive时间到了之后,
keepalive timer生效发送几次keepalive探测包之后对方没有回应,内核会关闭连接。
这个keepalive超时的总时间除了keepalive相关的几个选项之后,用户设置的
TCP_USER_TIMEOUT选项的超时时间也起作用。这样可以快速探测到网络错误。
2.
如果网络断开时,tcp有网络包已经发送出去了,但还没有收到对方的ack。
那么内核启动重传定时器(retransmission timer)。这个重传超时时间受
tcp_retries1 tcp_retries2两个选项控制,默认15分钟左右。用户设置的TCP_USER_TIMEOUT
超时也会使的重传定时器在重传失败之后快速返回错误。
3.
如果网络断开时,有数据需要发送,但还没有实际发送出去的(没有第2种情况的
有unack的数据包已经在网络上)。 那么tcp会尝试发送网络包出去的时候得到失败的返回
(通过iptables直接丢掉某个端口的网络包就是这种情况,tcp层 会得到netfilter丢包
导致发送skb错误通知)。
这种情况下内核在发送skb错误之后,会启动probe定时器(坚持定时器TCP Persist
Timer http://www.pcvr.nl/tcpip/tcp_pers.htm). 也是认为错误的时候是由于0 windows size
导致的?? 虽然这个keepalive 定时器也会被周期性的触发,但优先起作用是probe定时器。
probe定时器的超时和默认的重传15分钟超时一样受两个retries选项控制。但probe不受用户设置的
TCP_USER_TIMEOUT 超时控制。所以这种iptables导致连接失效的情况,linux的tcp要过
15分钟左右才能探测到网络错误关闭连接。

所以最可能的快速检测tcp网络错误的方式还是 上层应用自己实现自己的类似SCTP协议的
heartbeat机制,自己双向不停的每隔一段时间就发心跳检查包。只有双向的心跳包都收到
了才当作连接正常”。


下面是测试过程中找到的一些跟踪/查看linux的tcp连接状态(主要是为了查看定时器)的工具和方法。

内核proc文件有导出tcp的状态的
============================
cat /proc/net/tcp
http://lxr.free-electrons.com/source/net/ipv4/tcp_ipv4.c?v=3.2
触发函数
static void get_tcp4_sock(struct sock *sk, struct seq_file *f, int i, int
    *len)

这个会打印具体的tcp连接的状态,
root@debian01:/home/bright# cat /proc/net/tcp
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 00000000:8B45 00000000:0000 0A 00000000:00000000 00:00000000 00000000   105        0 5822 1 f7275580 100 0 0 10 0
   7: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 6871 1 f5faeac0 100 0 0 10 0
   8: 0100007F:177A 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 798696 1 f7658ac0 100 0 0 10 0
  17: 6638A8C0:03C0 0138A8C0:3382 01 00000000:00000000 02:000002A4 00000000     0        0 771794 2 f7388580 40 4 30 10 -1
  18: 6638A8C0:0016 0138A8C0:6BAE 01 00000034:00000000 01:0000002A 00000000     0        0 798586 4 f77a0580 42 4 31 10 -1

 tr  是对于的timer类型,比如1是 重传定时器,2是probe0定时器,0
 是没有定时器等。参考get_tcp4_sock 的源码。
 timeout对应icsk_probes_out,表示keepalive超时了几次等等。


netstat也有很很多tcp状态的信息,比如查看当前tcp连接定时器状态的
===========================================
“netstat --timers” 也可以查看各个timer的状态, 应该就是直接读取/proc/net/tcp
文件信息得到的吧。




内核源码的相关函数
==================
内核相关的3种定时器函数
http://lxr.free-electrons.com/source/net/ipv4/tcp_timer.c?v=3.2

void tcp_retransmit_timer(struct sock *sk)
static void tcp_keepalive_timer (unsigned long data)
static void tcp_probe_timer(struct sock *sk)

--------
tcp_sock, inet_connection_sock, inet_sock, sock结构的关系,
这几个结构包含大量的状态信息,因为结构都是放在结构嵌套的最开始的位置的,
所以这个4个struct其实地址都是是一样的,可以简单的通过地址转换得到各个结构指针。
struct tcp_sock {
  struct inet_connection_sock     inet_conn {
    struct inet_sock          icsk_inet {
      struct inet_sock {
        struct sock             sk {
           struct sk_buff          *sk_send_head; //还有tcp网络包需要发送出去,待发送的skb的队列头。
        }
        __be16                  inet_dport;
        __be16                  inet_sport;
      }
      u32                       icsk_user_timeout; // TCP_USER_TIMEOUT用户自定义超时设置
      __u8                      icsk_probes_out;   // keepalive 或者probe的包已经发送了几次
    }
  }
  u32     packets_out;  //时候有网络包已经发送出去
}


指针转换的例子,参考内核源码
517 static void tcp_keepalive_timer (unsigned long data)
518 {
519         struct sock *sk = (struct sock *) data;
520         struct inet_connection_sock *icsk = inet_csk(sk);
521         struct tcp_sock *tp = tcp_sk(sk);





---------------------------------------


http://lxr.free-electrons.com/source/net/ipv4/tcp_timer.c?v=3.2
517 static void tcp_keepalive_timer (unsigned long data)
518 {
519         struct sock *sk = (struct sock *) data;
520         struct inet_connection_sock *icsk = inet_csk(sk);
521         struct tcp_sock *tp = tcp_sk(sk);
522         u32 elapsed;
523
524         /* Only process if socket is not in use. */
525         bh_lock_sock(sk);
526         if (sock_owned_by_user(sk)) {
527                 /* Try again later. */
528                 inet_csk_reset_keepalive_timer (sk, HZ/20);
529                 goto out;
530         }
531
532         if (sk->sk_state == TCP_LISTEN) {
533                 tcp_synack_timer(sk);
534                 goto out;
535         }
536
537         if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
538                 if (tp->linger2 >= 0) {
539                         const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;
540
541                         if (tmo > 0) {
542                                 tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
543                                 goto out;
544                         }
545                 }
546                 tcp_send_active_reset(sk, GFP_ATOMIC);
547                 goto death;
548         }
549
550         if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)
551                 goto out;
552
553         elapsed = keepalive_time_when(tp);
554
555         /* It is alive without keepalive 8) */
556         if (tp->packets_out || tcp_send_head(sk))  // 如果有数据包在路上,或者tcp_send_head这个等待发送的skb队列不为空,就忽略keepalive 机制

557                 goto resched;
558
559         elapsed = keepalive_time_elapsed(tp);
560
561         if (elapsed >= keepalive_time_when(tp)) {
562                 /* If the TCP_USER_TIMEOUT option is enabled, use that
563                  * to determine when to timeout instead.
564                  */
565                 if ((icsk->icsk_user_timeout != 0 &&
566                     elapsed >= icsk->icsk_user_timeout &&
567                     icsk->icsk_probes_out > 0) ||
568                     (icsk->icsk_user_timeout == 0 &&              // 用户设置的TCP_USER_TIMEOUT
569                     icsk->icsk_probes_out >= keepalive_probes(tp))) {
570                         tcp_send_active_reset(sk, GFP_ATOMIC);
571                         tcp_write_err(sk);
572                         goto out;
573                 }
574                 if (tcp_write_wakeup(sk) <= 0) {
575                         icsk->icsk_probes_out++;
576                         elapsed = keepalive_intvl_when(tp);
577                 } else {
578                         /* If keepalive was lost due to local congestion,
579                          * try harder.
580                          */
581                         elapsed = TCP_RESOURCE_PROBE_INTERVAL;
582                 }
583         } else {
584                 /* It is tp->rcv_tstamp + keepalive_time_when(tp) */
585                 elapsed = keepalive_time_when(tp) - elapsed;
586         }
587
588         sk_mem_reclaim(sk);
589
590 resched:
591         inet_csk_reset_keepalive_timer (sk, elapsed);
592         goto out;
593
594 death:
595         tcp_done(sk);
596
597 out:
598         bh_unlock_sock(sk);
599         sock_put(sk);
600 }




271 static void tcp_probe_timer(struct sock *sk)
272 {
273         struct inet_connection_sock *icsk = inet_csk(sk);
274         struct tcp_sock *tp = tcp_sk(sk);
275         int max_probes;
276 
277         if (tp->packets_out || !tcp_send_head(sk)) {   // packets_out 表示有数据包在路上了,   tcp_send_head 是等待发送skb队列,不为空表示有需要发送出去的数据包,才去做probe
278                 icsk->icsk_probes_out = 0;
79                 return;
280         }
281 
282         /* *WARNING* RFC 1122 forbids this
283          *
284          * It doesn't AFAIK, because we kill the retransmit timer -AK
285          *
286          * FIXME: We ought not to do it, Solaris 2.5 actually has fixing
287          * this behaviour in Solaris down as a bug fix. [AC]
288          *
289          * Let me to explain. icsk_probes_out is zeroed by incoming ACKs
290          * even if they advertise zero window. Hence, connection is killed only
291          * if we received no ACKs for normal connection timeout. It is not killed
292          * only because window stays zero for some time, window may be zero
293          * until armageddon and even later. We are in full accordance
294          * with RFCs, only probe timer combines both retransmission timeout
295          * and probe timeout in one bottle.                             --ANK
296          */
297         max_probes = sysctl_tcp_retries2;
298 
299         if (sock_flag(sk, SOCK_DEAD)) {
300                 const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX);
301 
302                 max_probes = tcp_orphan_retries(sk, alive);
303 
304                 if (tcp_out_of_resources(sk, alive || icsk->icsk_probes_out <= max_probes))
305                         return;
306         }
307 
308         if (icsk->icsk_probes_out > max_probes) {
309                 tcp_write_err(sk); //这上面的等于超时重传的时间,不判断TCP_USER_TIMEOUT
310         } else {
311                 /* Only send another probe if we didn't close things up. */
312                 tcp_send_probe0(sk);
313         }
314 }



void tcp_retransmit_timer(struct sock *sk)  和
static void tcp_keepalive_timer (unsigned long data)
检查用户自定义设置的TCP_USER_TIMEOUT超时设置,
但tcp_probe_timer 不忽略TCP_USER_TIMEOUT超时时间的检查,默认的probe超时时间
和正常的重传超时是差不多的,为15分钟左右。

如果probe和keepalive 定时器两个同时触发。keepalive定时器的超时检查会失去作用。
tcp_keepalive_timer 里面的 if (tp->packets_out || tcp_send_head(sk))
检查tcp_send_head 会检查有数据需要发送,直接跳出keepalive的超时机制。

----------
启动probe定时器 路径1
tcp_ack
  tcp_ack_probe    // 如果等待发送skb的序列号大于 tcp的 snd_wnd,  即windows
  size没法容纳指定skb发送出去的时候,就设置probe定时器,发送windows
  size的probe网络包

----------------
启动probe定时器 路径2
__tcp_push_pending_frames
   if (tcp_write_xmit(sk, cur_mss, nonagle, 0, GFP_ATOMIC)) //如果发送失败就启动proble定时器
    tcp_check_probe_timer
      inet_csk_reset_xmit_timer


1819 /* Push out any pending frames which were held back due to
1820  * TCP_CORK or attempt at coalescing tiny packets.
1821  * The socket must be locked by the caller.
1822  */
1823 void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
1824                                int nonagle)
1825 {
1826         /* If we are closed, the bytes will have to remain here.
1827          * In time closedown will finish, we empty the write queue and
1828          * all will be happy.
1829          */
1830         if (unlikely(sk->sk_state == TCP_CLOSE))
1831                 return;
1832 
1833         if (tcp_write_xmit(sk, cur_mss, nonagle, 0, GFP_ATOMIC))
1834                 tcp_check_probe_timer(sk); //如果上面tcp_write_xmit发送失败就调用probe定时器
1835 }


862 static inline void tcp_check_probe_timer(struct sock *sk)
863 {
864         const struct tcp_sock *tp = tcp_sk(sk);
865         const struct inet_connection_sock *icsk = inet_csk(sk);
866 
867         if (!tp->packets_out && !icsk->icsk_pending) //如果packets_out,那应该是有数据发送出去了,还没有ack的,应该是重传定时器起作用了。
868                 inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, //启动probe定时器
869                                           icsk->icsk_rto, TCP_RTO_MAX);
870 


 static int tcp_transmit_skb}
       err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);
       如果queue_xmit 失败就触发probe

 http://lxr.free-electrons.com/source/net/ipv4/tcp_ipv4.c?v=3.2#L1830
 const struct inet_connection_sock_af_ops ipv4_specific = {
 .queue_xmit        = ip_queue_xmit,


tcp层可以在发送的时候得到netfilter丢包的错误返回,进而启动proble定时器的调用流程。
tcp_write_xmit
  tcp_transmit_skb
    icsk->icsk_af_ops->queue_xmit
      ip_queue_xmit
         ip_local_out
            __ip_local_out
               nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT




systemtap跟踪tcp连接的状态
==========================
systemtap 自带 tcp_trace.stp call_graph.stp, tcp_monitor.stp
等几个例子, 特别是tcp_trace.stp 这个简直是追踪tcp连接状态的神器。

tcp_trace可以指定过滤端口和ip,打印出tcp连接的所有状体变动,可以看到状态、各种
定时器的启动和变化,接受和发送buffer和窗口的大小等。
脚本里面有示范用法和各个参数的详细说明,比如下面这个可以用于实时查看timer的变化:
                                   source addr  remote addr
stap tcp_trace.stp filter=all  *.*.*.*:*-*.*.*.*:10011
stap tcp_trace.stp filter=state,timer,txq  *.*.*.*:*-192.168.56.1:10011
stap tcp_trace.stp filter=state,timer,txq,snd_wnd   *.*.*.*:*-192.168.56.1:10011


可以根据需要在这个脚本基础增加更多信息的输出:

systemtap自带了很多内置函数,比如直接用tcp_sock获取端口的:
laddr = tcpmib_local_addr(sk);
raddr = tcpmib_remote_addr(sk);
lport = tcpmib_local_port(sk);
rport = tcpmib_remote_port(sk);
其他ip地址等函数可以参考systamp的文档。


可以用systemtap的 @cast语法转换函数指针类型,打印不同结果的成员,比如下面这个用法
probe kernel.function("tcp_keepalive_timer")
{
if ( !timer_flg ) next;

sk = $data;
key = filter_key(sk)
if ( !key ) next;

  printf ("icsk_probes_out=%d, packets_out=%d, sk_send_head=%p\n",
      @cast($data, "inet_connection_sock")->icsk_probes_out,
      @cast($data, "tcp_sock")->packets_out,
      @cast($data, "sock")->sk_send_head
      );

is_packet_updated(key,sk)
length[key] = 0
tx_timer[key] = 4;
print_packet_info(key, 1)
tx_timer[key] = 0;
}



可以根据 call_graph.stp  和para-callgraph.stp
脚本为模板,打印对应的tcp函数相互调用过程的trace

-----------------call_graph.stp -----------------------
// stap -v call_graph.stp  ssh

function is_this:long(name:string)
{
return isinstr(name, @1)
}

probe  kernel.function("*@net/socket.c").call
{
if (is_this(execname()))
printf("%s --> %s\n", thread_indent(4), probefunc())
}

probe  kernel.function("*@net/socket.c").return
{
if (is_this(execname()))
printf("%s <-- %s\n", thread_indent(-4), probefunc())
}

probe  kernel.function("*@net/ipv4/tcp_input.c").call
{
if (is_this(execname()))
printf("%s --> %s\n", thread_indent(4), probefunc())
}

probe  kernel.function("*@net/ipv4/tcp_input.c").return
{
if (is_this(execname()))
printf("%s <-- %s\n", thread_indent(-4), probefunc())
}

--------------para-callgraph.stp----------------------------------

#! /usr/bin/env stap

function trace(entry_p, extra) {
  %( $# > 1 %? if (tid() in trace) %)
  printf("%s%s%s %s\n",
         thread_indent (entry_p),
         (entry_p>0?"->":"<-"),
         ppfunc (),
         extra)
}


%( $# > 1 %?
global trace
probe $2.call {
  trace[tid()] = 1
}
probe $2.return {
  delete trace[tid()]
}
%)

probe $1.call   { trace(1, $$parms) }
probe $1.return { trace(-1, $$return) }

// 调用的例子第一个参数是需要记录的函数。第二个参数是触发点。
// stap para-callgraph.stp 'kernel.function("*@fs/*.c")' 'kernel.function("sys_read")':
------------------------------------------------------


结合使用tcp_trace.stp 和 call_graph
两个脚本,我觉的应该时所有的tcp连接状态都可以分析的清楚了吧。
  评论这张
 
阅读(998)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017