2013年8月28日星期三

IP数据包的输入与输出

IP层主要函数之间的调用关系如下图所示:

上面的图主要是拷贝的《Linux内核源码剖析----TCP/IP实现上册》中的图11.3,原图中有部分错误,所以这里重新绘制了一下,并且去掉了一些冗余的部分。
下面简述一下数据包传递的大致过程:
一、IP数据包的输入
ip_rcv()是网络层(IPv4,以下同)接收数据包的入口函数,链路层在接收到数据包后调用netif_receive_skb()将数据包传递到网络层。网络层的packet_type实例为ip_packet_type,在Internet协议族的初始化函数inet_init()中调用dev_add_pack()来注册到ptype_base散列表中。
ip_rcv()中接收到数据包后会检查是否是一个完整的、没有错误的数据包。如果是合法的数据包,会传递到netfilter的NF_INET_PRE_ROUTING钩子点进行处理,如果钩子处理函数中没有截获数据包,则传递到ip_rcv_finish()进行下一阶段的处理。
ip_rcv_finish()会检查是否已设置路由缓存项,如果没有,则调用ip_route_input()来查找路由,如果查找失败,则丢弃数据包。如果找到路由项,在路由缓存项描述结构dst_entry的input和outpu接口中设置下一个阶段的处理函数。
如果是要转发的数据包,input接口设置的是ip_forward()函数,output接口设置的是ip_output()。ip_forward()中会检查数据包的IP选项,并做相应处理。如果没有问题,将数据包的TTl值减1,然后将数据包传递到NF_INET_FORWARD钩子点进行处理。如果钩子处理函数没有截获数据包,则调用路由缓存项的output接口输出数据包,即ip_output()函数。
如果是要交给本地的数据包,input接口设置的ip_local_deliver()函数,output接口设置的ip_rt_bug()。ip_local_deliver()中会检查接收到的数据包是否是IP报文的分片,如果是,则调用ip_defrag()组装分片的各个部分。如果分片没有到齐或出错,则直接返回,当前报文不再向传输层传递。如果不是IP报文的分片或IP报文的所有分片组装成功,则将报文传递到netfilter的NF_INET_LOCAL_IN钩子点进行处理。如果钩子处理函数没有截获报文,则调用ip_local_deliver_finish()将报文传递到传输层。在传递到传输层之前,ip_local_deliver_finish()中会将sk_buff中的成员指向传输层报文的位置。
二、IP数据包的输出
传输层向IP层输出数据包主要是调用ip_queue_xmit()和ip_push_pending_frames()。TCP协议主要使用ip_queue_xmit()来发送数据,UDP协议主要使用ip_push_pending_frames(),不过这两者都是由本地发送的数据包,需要转发的数据包也需要IP层来处理。本地发送的数据包需要传递到netfilter的NF_INET_LOCAL_OUT钩子点处理,转发的数据包则不需要。如果没有钩子处理函数截获数据包,则继续进行处理。
如果是组播数据包,则output接口设置的是ip_mc_output()接口;如果是单播数据包,则output接口设置的ip_output。这里只讨论单播数据包。ip_output()中只是简单地将路由缓存项中存储的网络设备设置到数据包的dev成员中,并且设置三层的协议类型(protocol成员),然后将数据包传递到netfilter的NF_INET_POST_ROUTING钩子点。如果没有钩子处理函数截获数据包,则将数据包传递到ip_finish_output()中处理。
ip_finish_output()中会检查报文的长度是否大于MTU。如果大于MTU,则调用ip_fragment()对数据包进行分片,然后再调用ip_finish_output2()将数据包通过邻居子系统传递到网络设备。如果不需要分片,则直接调用ip_finish_output2()处理。

2013年8月25日星期日

Linux中查看是否是固态硬盘(SSD)

   最近在准备测试,需要看看哪些机器挂载的是ssd硬盘,Google了一圈看到了许多方法,但都云里雾里的,不知道怎么确定。ssd硬盘貌似使用的也是scsi接口,所以根据盘符的名称也是判断不出来的。最后群里eric大神告知lsscsi工具,试了一下,非常简单,显示的也很直接,分享一下。
废话不多说,直接上图和结果,如下所示:

[root@FWD_YF_009_110 ~]# lsscsi
[0:0:0:0] disk SEAGATE ST3300657SS ES62 -
[0:0:1:0] disk ATA INTEL SSDSA2CW16 0362 /dev/sda
[0:0:2:0] disk ATA INTEL SSDSA2CW16 0362 /dev/sdb
[0:0:3:0] disk ATA INTEL SSDSA2CW16 0362 /dev/sdc
[0:1:0:0] disk Dell VIRTUAL DISK 1028 /dev/sdd
[3:0:0:0] cd/dvd TEAC DVD-ROM DV-28SW R.2A /dev/sr0
看第四列就知道是否是SSD硬盘了,感兴趣的可以试下

SystemTap----将SystemTap脚本编译成内核模块

  当运行SystemTap脚本时,会根据脚本生成一个内核模块,然后插入到系统中执行后退出。这个过程总共分为5个阶段:parse, elaborate, translate, compile, run,对应的编号为1-5.stap命令的-p选项可以用来指定在哪个阶段停止,利用这个选项可以将脚本编译成内核模块。
正常情况下,SystemTap脚本只能在部署了SystemTap执行环境(安装内核的开发包和debuginfo包)的机器,如果要在十台机器上执行,就要在这些机器上都部署这样的环境。如果将脚本编译成内核模块,借助staprun命令(需要systemtap-runtime包)就可以像直接使用stap命令执行脚本一样。当然,也可以直接使用insmod或modprobe命令直接将内核模块插入到系统中运行。除此之外,还可以利用生成的内核模块来修改系统的一些行为,具体可以参考褚霸的文章《Systemtap辅助设置tcp_init_cwnd,免对操作系统打Patch》。
下面介绍一下具体的步骤:
1、编写脚本
示例脚本如下:
probe begin {
%{ printk(KERN_ALERT "Hello, World!\n") %};
exit();
}
在上面的脚本中嵌入了C代码,将“hello World"输入到系统日志,这个脚本其实很简单,之所以贴出来,是因为在分别使用insmod和staprun命令来执行生成的内核模块时,表现不一样,后面再说。将脚本保存为hello.stp。
2、生成内核模块
命令如下:
stap -p4 -gu -m hello hello.stp
-p选项用来指定在哪个阶段停止,这里是在compile阶段。
因为在脚本中嵌入了C代码,所以要指定-g选项进入guru模式,-u选项是禁止优化的选项,可选。
-m是指定生成的内核模块名称,这里生成的内核模块就是hello.ko。如果不使用-m选项,systemtap会将生成的内核模块放在用户目录的.systemtap目录下。没有指定的情况下,我的机器上生成的路径为/root/.systemtap/cache/e9/stap_e9ff1bc604b35641b8cec15699c7bfa0_791.ko,所以你懂的,最好要指定一下哦。
3、运行内核模块
命令如下:
staprun hello.ko
这里生成的hello.ko模块在使用staprun命令运行的时候,会在系统日志中显示"Hello,World!",但是直接使用insmod时,则没有看到,具体原因目前不详,有知道的希望不吝赐教,拜谢。


   除非必须,不要把日志输出到系统日志,特别是量比较大的时候,否则会将重要的系统信息给冲掉。还是建议大家在使用的时候使用staprun命令来执行生成的内核模块,不要使用insmod或modprobe。在使用staprun的时候还可以使用-o选项来指定的输出的文件,如果不指定的话,脚本中的printf函数输出的信息是看不到的。

2013年8月24日星期六

SystemTap----利用stap命令来查找内核函数定义

   我们知道stap命令的-l(或-L)选项可以列出指定的某个probe描述中所有符合的probe点的列表,例如可以使用下面的命令,看到所有可以probe的函数:
stap -l 'kernel.function("*")'
今天在看书的时候,突然想到可以利用这个选项来找到一些内核函数的定义,例如sys_open()的定义。不管是source insight还是vim+ctag+cscope+taglist这样的组合看代码时,如果要想找到某个系统调用的定义,都需要在工程里搜索,因为这些系统调用在定义的时候都是用SYSCALL_DEFINE0、SYSCALL_DEFINE1等这样的宏来定义的,这些工具都没法直接找到其准确定义的位置。但是现在我们可以利用stap命令的-l选项,先准确地找到其具体的位置,然后精确地去某个文件的某一行就可以找到,非常快捷方便。下面就以sys_open()为例来演示这个过程。
1、找到具体的位置
命令和输出如下:

[root@CentOS____190 ~]# stap -l 'kernel.function("sys_open")'
kernel.function(sys_open@fs/open.c:913)
2、找到定义的位置
如下所示:

上面的步骤只是一个思路,如果想进一步简化的话,可以直接写一个脚本,每次只需要输入要查找的函数名称就行了,避免做重复的劳动。

SystemTap----thread_indent()函数分析

thread_indent是systemtap中一个非常有用的函数,声明如下:
thread_indent:string(delta:long)
它可以输出当前probe所处的可执行程序名称、线程id、函数执行的相对时间和执行的次数(通过空格的数量)信息,它的返回值就是一个字符串。参数delta是在每次调用时增加或移除的空白数量。
在没有看thread_indent函数的实现之前,对delta参数的作用非常疑惑,也很好奇它的格式是怎么输出的,所以决定看看这个函数是怎么实现的。
systemtap中使用的一些库函数在tapset目录下(我的是/usr/local/systemtap-2.3/share/systemtap/tapset),使用grep命令找到thread_indent()函数在indent.stp文件中定义,实现如下所示:

function thread_indent:string (delta:long)
{
return _generic_indent (tid(), sprintf("%s(%d)", execname(), tid()), delta)
}
我们看到thread_indent()是直接调用的_generic_indent()函数,第一个参数是当前的线程id(tid()函数的返回值),第二个参数是一个字符串,由可执行程序名称和线程id组成,格式为"EXECNAME(TID)",第三个参数就是thread_indent()的delta参数。
接下来看_generic_indent()中是怎么处理的,其源码如下:
global _indent_counters, _indent_timestamps

function _generic_indent (idx, desc, delta)
{
ts = __indent_timestamp ()
if (! _indent_counters[idx]) _indent_timestamps[idx] = ts

# pre-increment for positive delta and post-decrement for negative delta
x = _indent_counters[idx] + (delta > 0 ? delta : 0)
_indent_counters[idx] += delta

return sprintf("%6d %s:%-*s", (ts - _indent_timestamps[idx]), desc, (x>0 ? x-1 : 0), "")
}
_generic_indent()中首先调用__indent_timestamp()获取当前的Unix时间,时间存储在局部变量ts中。__indent_timestamp()直接调用gettimeofday_us()来获取的时间,参见indent_default.stp文件。
如果_indent_counters[idx]为0,即线程id为idx中是第一次调用indent_timestamp(),则将当前的时间存储在数组_indent_timestamps中以线程idx为索引的位置上。
接下来就会用到thread_indent()中的参数delta。从后面的sprintf中可以看出x的值将决定在":"后面输出的空格的数量,如果x是正值的话,则x的值就是空格的数量;如果x是负数,则在":"后面不会输出空格。
每次thread_indent()调用时,对应_indent_counters数组中的项(以线程id为索引)都会加上delta。
如果delta的值是0,则_indent_counters数组中对应的项一直为0,则x的值也一直是0,_indent_timestamps数组中对应项存储的时间每次也会被更新,所以在输出时(ts - _indent_timestamps[idx])总是0,并且不会输出空格。
如果delta的值为负数,则_indent_counters数组中对应的项也一直是负数,则x的值也是负数,这样在sprintf中也不会输出空格,但是,_indent_timestamps数组中对应项的时间值不会每次都更新,所以在输出的时候还是可以看到相对运行时间。
如果delta的值为正值,则indent_counters数组中对应的项是正数,并且会一直增加,则x的值也会增加,这样在后面的打印中输出的空格数量会越来越多,也可以输出运行的相对时间。
至此可以看出thread_indent()中的delta参数可以控制输出的空白的数量,也即空格数数量,还可以控制是否输出相对时间。
通过这个函数的输出结果你可以知道某个函数的执行一次的时间,找到系统中可能会造成瓶颈的一些点,具体的例子参见indent.stp文件的末尾。
如果这个函数不满足你的需求的话,可以参照其思路构造自己的thread_indent()函数,非常地方便!

2013年8月18日星期日

SystemTap----常用变量、宏、函数和技巧

后面会持续更新,方便自己,方便大家.......
一、宏
1. kderef
从指定的地址处读取指定大小的值
格式为:
kderef(size, address);
其中address为要读取的地址值,size是要是读取的值的大小,返回值就是所读取的值。
2.kread
在嵌入的C代码中安全地读取指针值
格式为:
kread(&(address))
二、函数
1.execname()
获取当前进程的名称,即可执行文件的名称
2. pid()
获取当前进程的PID
3.pp()
获取当前的probe点。例如 probe process.syscall,process.end { /* scripts */},在块中调用pp()可能会返回"process.syscall"和"process.end"。
4.probefunc()
获取当前probe的函数名称。例如probe sys_read函数,在probe块中调用该函数就会返回sys_read。
三、技巧
1.@cast()操作
如果将一个获取的值(可能是一个类型的地址值)存储到SystemTap中定义的变量,但是在读取的时候需要根据特定的类型去读取,此时,可以使用@cast()操作来读取。
其格式为
@cast(p, "type_name"[,"module"])->member
在systemtap中使用cast来将指定的地址值转换为C语言中的类型,并且可以去获取相应的值(例如结构体成员)示例如下
function is_tcp_packet:long(iphdr) {
protocol = @cast(iphdr, "iphdr")->protocol
return (protocol == %{ IPPROTO_TCP %}) /* <-- expression */ }
如果是在probe内,还可以直接使用$ptr来获取成员的值,例如:
probe begin {
printf("SystemTap Scripts start.....\n");
}

probe kernel.function("tcp_v4_rcv") {
printf("skb->len = %d\n", $skb->len);
}

@cast()操作中还可以指定类型所在的头文件,示例如下:
@cast(tv, "timeval", "<sys/time.h>")->tv_sec
@cast(task, "task_struct", "kernel<linux/sched.h>")->tgid
2.在使用嵌入C代码作为函数体的辅助函数中获取参数和设置返回值
如果版本是1.8或更新的,则使用STAP_ARG_(参数名)来获取参数,例如STAP_ARG_arg,其中arg是参数名。设置返回值的形式是STAP_RETVALUE=value。
如果版本是1.7或更老的,则使用THIS->(参数名)来获取参数,例如THIS->arg,其中arg是参数名。设置返回值的形式是THIS->__retvalue=value。
3.获取probe函数的参数
如果带debuginfo,即DWARF probes,则可以直接使用参数的名称加'$'即可,例如sys_read()中的第一个参数fd,就可以通过$fd来获取其值。
如果缺少debuginfo,即DWARF-less probing,则需要通过uint_arg(),pointer_arg()和ulong_arg()等来获取,这些函数都需要指定当前要获取的参数是第几个参数,编号从1开始。例如asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)中,uint_arg(1)获取的是fd的值,pointer_arg(2)获取的是buf的地址值,ulong_arg(3)获取的是count参数。更多的获取参数的函数参见man page index
如果是通过process.syscall、process("PATH").syscall或者process(PID).syscall来probe系统调用,则可以通过$syscall来获取系统调用号,通过$arg1,$arg2等来获取相应的参数值。
在probe用户程序时,可以通过$$parms来获取所有的参数(是字符串,不是具体的值)。
4."."字符窜连接符
如果想将一个函数返回的字符串和一个常量字符串拼接,则在两者之间加入"."即可,例如probefunc()."123"。
"."运算符还支持".=",即拼接后赋值。
5、获取stap命令行参数
如果要获取命令行参数准确的值,则使用$1、$2....$<NN>来获取对应的参数。如果想将命令行参数转换为字符串,则使用@1、@2...@<NN>来获取参数对应的字符串。
6、next操作
如果在probe函数中,发现某个条件没有满足,则结束本次probe的执行,等待下次事件的到来。示例如下:


global i

probe begin {
printf("SystemTap Scripts start.....\n");
}

probe kernel.function("sys_read") {
++i;
if (i % 2) {
next;
}
printf("i = %d\n", i);
}

2013年8月17日星期六

SystemTap----嵌入C代码

SystemTap支持guru模式,通过-g选项来以这种模式执行SystemTap脚本。在guru模式下,嵌入的C代码在“%{"和“%}"标记之间,这些代码会原封不动地放到生成的模块中。嵌入的C代码不仅可以作为函数体,还可以出现在SystemTap描述中(例如函数等),示例如下:
%{
#include <linux/in.h>
#include <linux/ip.h>
%} /* <-- top level */

function read_iphdr:long(skb:long) %{ /* pure */
struct iphdr *iph = ip_hdr((struct sk_buff *)STAP_ARG_skb);
STAP_RETVALUE = (long)iph;
%} /* <-- function body */

/* Determines whether an IP packet is TCP, based on the iphdr: */
function is_tcp_packet:long(iphdr) {
protocol = @cast(iphdr, "iphdr")->protocol
return (protocol == %{ IPPROTO_TCP %}) /* <-- expression */
}

probe begin {
printf("SystemTap start!\n");
}

probe kernel.function("ip_local_deliver") {
iph = read_iphdr(pointer_arg(1));
printf("tcp packet ? %s\n", is_tcp_packet(iph) ? "yes" : "no");
}
在这里read_iphdr函数就是使用嵌入的C代码作为函数体,is_tcp_packet中是作为systemtap辅助函数中的一部分。
在使用嵌入C代码作为函数体的函数中,访问参数的值是以STAP_ARG_+参数名的形式,这种方式是最新版本的SystemTap中的方式。1.7及更早的版本是通过THIS->+参数名的方式。CentOS6.4中的SystemTap版本是1.8,所以你如果在SystemTap脚本中仍然使用老的访问方式会报错。同样,最新的设置返回值的方式是STAP_RETVALUE,1.7及更早的版本是THIS->__retvalue。
由于在guru模式下,SystemTap对嵌入的C代码没有做任何的处理,所以如果在C代码中出现异常的访问或者其他错误,就会导致内核crash。不过SystemTap提供了kread宏来安全地访问指针,如下所示:

struct net_device *dev;
char *name;
dev = kread(&(skb->dev));
name = kread(&(dev->name));
还有一点要特别注意,所有的SystemTap函数和probe都是在关闭中断下执行,所以在所有嵌入的C代码中都不能睡眠!