https://blog.csdn.net/lishanmin11/article/details/77098136
这篇文章介绍最近用到的一个监听TCP信息的内核模块:tcpprobe.
主要说说这个内核模块:
- tcpprobe是什么?
- tcpprobe的基本用法?
- tcpprobe是怎么实现的?
- 实践中如何用好tcpprobe?
1. tcp probe 是什么?
下面摘录LINUX FOUNDATION对于tcpprobe上对于tcpprobe的一段解释:
tcpprobe is a module that records the state of a TCP connection in response to incoming packets. It works by inserting a hook into the tcp_recv processing path using kprobe, so that the congestion window and sequence number can be captured.
首先tcpprobe是一个内核模块,可以按需加载与卸载。在下文的实践中如何用好tcpprobe可以看出模块化的好处。
同时tcpprobe是用于监听某些特定TCP的,而不是整个TCP/IP协议栈的内容。在使用tcpprobe模块时需要指定特定的端口号用于监听。
这个限制主要是出于效率上的考虑,但是基本上也能满足大部分的需求了,后面我们也将介绍如何根据需要实现我们自己的限制条件。
最后tcpprobe是基于kprobe机制实现的,它能捕捉cwnd和seq等信息。这些内容将在后续的内容中具体介绍。
2. tcpprobe的基本用法
Linux Foundation上有一个简单地例子,这里我结合我的实际使用给出一个更加具体的例子。
2.1 编译tcpprobe模块
// 方法1:tcpprobe是一个内核模块,在Linux 3.10中它的实现代码是net/ipv4/tcp_probe.c // 可以设置在编译内核时,将tcpprobe也编译了。方法就是在.config中添加 CONFIG_NET_TCPPROBE=m // 方法2:可以像一个普通内核模块一样,只编译tcpprobe模块,然后加载 # vim Makefile // 为tcpprobe模块建立一个新Makefile文件,最好把整个tcp_probe.c和Makefile放到单独的一个文件夹中 obj-m += tcp_probe.o all: make -C /lib/modules/`uname -r`/build M=${PWD} modules clean: make -C /lib/modules/`uname -r`/build M=${PWD} clean
2.2 tcpprobe模块的加载与卸载
// 如果是通过修改.config来编译tcpprobe的,可以使用modprobe加载 # modprobe tcp_probe port=5001 // 监听所有本地端口是5001的TCP流,仅在cwnd变化时捕捉信息 # modprobe tcp_probe full=1 port=5001 // full选项表示每次收到数据包都捕捉信息 // 如果是普通内核模块形式编译的,则使用insmod加载 # insmod tcp_probe.ko full=1 port=5001 // 卸掉tcpprobe模块的命令也很简单,只要当前没有任务在使用tcpprobe模块即可卸载 # rmmod tcp_probe
2.3 tcpprobe模块的使用
加载tcpprobe模块后,会新增一个/proc/net/tcpprobe的接口,可以通过这个接口获取tcpprobe捕捉的信息。
# cat /proc/net/tcpprobe > data.out & // tcpprobe捕捉的信息是持续性的,因此读这个接口可以放到后台读 # pid=$! // 保存上一个读命令的pid,用于结束读tcpprobe接口 # iperf -c otherhost // 使用iperf建立一个TCP流 # kill $pid
记录在data.out中的数据大致是这个样子:
1 2 3 | 11 .172120204 193 .168 .0 .2 :8089 193 .168 .0 .25 :54320 32 0 x842ccad 0 x84299c5 10 168 14608 143 11 .172141113 193 .168 .0 .2 :8089 193 .168 .0 .25 :54320 32 0 x842ccad 0 x842b60d 11 168 29088 137 11 .172283353 193 .168 .0 .2 :8089 193 .168 .0 .25 :54320 32 0 x842f9ed 0 x842bbb5 11 168 31984 133 |
每行的各列分别为:
timestamp saddr:port // 源IP及端口,我的数据是在发送端捕捉的,所以port是固定的8089 daddr:port // 目的IP及端口 skb->len // 收到的数据包skb大小,收到的都是ACK包,所以len都比较小。 snd_nxt // 下一个待发送数据的序列号 snd_una // 待确认数据的序列号 snd_cwnd // 拥塞窗口大小 ssthresh // 慢启动阈值 snd_wnd // 接收窗口大小 srtt // smoothed RTT
可以使用简单地文本处理及gnuplot将得到的数据进行可视化,不过这里就不展开了。
3. tcpprobe是怎么实现的?
tcpprobe是一个机遇kprobe机制实现的模块,代码量其实不大,这节算是我的一个学习笔记吧。
首先tcpprobe定义了两个由于记录捕捉信息的数据结构:struct tcp_log和static struct tcp_probe。
tcp_log的元素基本就是上面介绍的一行中各列的数据,而tcp_probe结构体算是一个管理结构体,比如spinlock和用于判断cwnd是否改变的lastcwnd。
作为一个内核模块,必不可少的就是初始化和退出函数。下面先总体上说下这两个函数完成的任务。
static __init int tcpprobe_init(void) => spin_lock_init(&tcp_probe.lock) // 初始化tcp_probe结构体中的内容 => tcp_probe.log = kcalloc(bufsize, sizeof(struct tcp_log), GFP_KERNEL) // 为捕捉数据分配内存空间 => proc_create(procname, S_URUSR, init_net.proc_net, &tcpprobe_fops) // 为tcpprobe模块创建一个proc文件系统接口 => register_jprobe(&tcp_jprobe) // 将tcp_jprobe注册到内核中 static __exit void tcpprobe_exit(void) => remove_proc_entry(procname, init_net.proc_net); // 删除tcpprobe模块的proc接口 => unregister_jprobe(&tcp_jprobe); // 注销tcp_jprobe => kfree(tcp_probe.log) // 释放内存
tcp_probe中锁的初始化和内存分配就不说了,先介绍一下proc_create()函数创建的proc接口,再说说register_jprobe()函数。
proc_create()函数的原型如下,在include/linux/proc_fs.h中实现。
static inline struct proc_dir_entry *proc_create( const char *name, umode_t mode, struct proc_dir_entry *parent, const struct file_operations *proc_fops)
tcpprobe模块中的各个参数的含义如下:
- name = “tcpprobe”
- mode = S_IRUSR, 允许读文件
- parent = init_net.proc_net,新建的proc接口将出现在/proc/net/目录中
- proc_fops = &tcpprobe_fops
tcpprobe_fops结构体的声明如下
static const struct file_operations tcpprobe_fops = { .owner = THIS_MODULE, .open = tcpprobe_open, .read = tcpprobe_read, .llseek = noop_llseek, };
proc接口也是被当做文件来处理的,且由于是只读的,所以主要需要实现open和read这两个函数。
tcpprobe_open()主要就是将tcp_probe结构体的各项内容进行reset,
tcpprobe_read()则是读取已经记录的数据,当没有数据可读时则睡眠,正常情况就将tcp_probe.log数组的最后一项读取出来。
至此还只是建好了一个只读的proc接口,但是数据的采集函数并没有注册到内核中,接下来就简单说说tcp_jprobe这个数据结构体。
register_jprobe()函数的原型如下,在kernel/kprobes.c中实现。
int __kprobes register_jprobe(struct jprobe *jp)
tcp_jprobe结构体的定义如下:
static struct jprobe tcp_jprobe = { .kp = { .symbol_name = "tcp_rcv_established", }, .entry = jtcp_rcv_established, };
可以看出来,tcpprobe其实是用的jprobe方式实现的,监听tcp_rcv_established函数(这也是为什么tcpprobe不会抓取三次握手时候的包信息的原因)。
对kprobe和jprobe感兴趣的话,可以看看最后给出的几个参考资料。
最后说说jtcp_rcv_established()函数,由于它是用于监听tcp_rcv_established()函数的,因此函数的参数需要完全一致,从而jtcp_rcv_established()就拿到了一个TCP流最关键的sk结构体。
具体实现就是如果这个包的Port是监听的那一个,则往tcp_probe.log中新写入一项,然后唤醒等待数据的读进程。
还有一点就是tcp_probe.log是以一个循环数组形式实现的,再具体点就是那种保留一项不用的实现。代码就补解释了。
4. 实践中如何用好tcpprobe?
在实践中往往会发现tcpprobe的功能还不够强大,但只要适当修改,利用tcpprobe的实现原理还是能做很多的事情的。
tcpprobe算是目前为止我了解到的用于分析Linux内核TCP流的最赞的工具之一了。
首先,由于tcpprobe能够拿到TCP流的sk结构体,其实它能用于打印输出的信息远远不止是cwnd。
sk结构体是内核中记录一个TCP流最关键的数据结构,里面包含一条TCP流几乎任何的信息。
比如内存相关的可以看发送缓冲区的大小和占用情况,接收缓存区的大小。
其实只要在内核中拿到了一条TCP流的sk结构体,几乎可以对这条TCP流做任何事情。
所以建议在使用tcpprobe的时候,根据自己的需求修改tcp_probe.log结构体的内容。
其次,有时候无法或不便知道端口号,tcpprobe的现有实现就无法满足要求了。但是其实只要根据实际情况适当的修改tcpprobe模块,或许就能达到目的。比如如果我们知道src IP和dst IP,那么只要修改jtcp_rcv_established()函数记录数据的判断条件即可。
最后如果对TCP的内核实现熟悉的话,甚至可以改掉tcpprobe的监听入口函数。比如我们想监听skb发送时的一些信息,可以把监听入口函数改为tcp_transmit_skb()。