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代码中都不能睡眠!

SystemTap----初始化和遍历数组

   SystemTap数组中的索引项可以是long或者string类型(systemtap的基本类型),访问数组项的方式和C语言类似,数组名[索引]。例如odds[x],odds是数组名,x是索引。SystemTap中的数组必须声明为全局变量,不能是局部变量。
下面的示例是以long为索引项,如下所示:
global odds
probe begin {
printf("probe begin\n");

for (i = 0; i < 10; ++i) {
odds[i] = i;
}

exit();
}

probe end {
for (i = 0; i < 10; ++i) {
printf("odds[%d] = %d\n", i, odds[i]);
}

printf("probe end\n");
}
熟悉C语言的同学看到这个脚本是不是很熟悉!脚本在开始的位置定义了一个全局变量odds,我们在这里将odds作为数组使用,索引项是整数,使用的是for循环。
除了for循环的遍历方式外,还可以使用foreach循环,如果使用过面向对象语言的话,对foreach也不会陌生,非常方便的一种遍历方式,使用起来也非常简单,实例如下:

global odds
probe begin {
printf("probe begin\n");

for (i = 0; i < 10; ++i) {
odds[i] = i;
}

exit();
}

probe end {
foreach(i+ in odds) {
printf("odds[%d] = %d\n", i, odds[i]);
}

printf("probe end\n");
}
注意,使用foreach遍历数组的时候,获取的不是数组项,而是索引!细心的同学会发现在i的后面有一个“+”,这个表示的按照顺序进行遍历(默认情况也是如此,可以不加),即索引遍历顺序从0到9.这个“+”号还可以添加到odds后边。既然有顺序遍历,就有逆序遍历,只需要把“+”换成“-”,同样加在i后边或odds后边都可以。
如果是使用string类型做索引,则只能使用foreach来遍历了,遍历的方式和以long为索引一样。
systemtap虽然支持long和string类型做索引,但是单个数组的索引要么都是long类型,要么都是string类型,如果混合,则编译时会报错。数组项的值也一样,要么是long类型,要么是string类型。注意,这里说的是索引项或数组项的值要统一,但是索引项是long类型,数组项的值是string类型是可以的,反之亦可。
在测试的过程,还发现了一个有趣的地方,就是如果在probe begin块中初始化了数组,但是在probe end块中没有访问数组,则在脚本退出时,会自动顺序打印出数组,示例脚本和输出结果如下所示:
global odds
probe begin {
printf("probe begin\n");

for (i = 0; i < 10; ++i) {
odds[i] = "123";
}

exit();
}

probe end {
printf("probe end\n");
}
输出结果:
[root@CentOS_190 systemtap]# stap syst1.stp
probe begin
probe end
odds[0]="123"
odds[1]="123"
odds[2]="123"
odds[3]="123"
odds[4]="123"
odds[5]="123"
odds[6]="123"
odds[7]="123"
odds[8]="123"
odds[9]="123"
如果不想在退出的时候打印的话,可以在probe end块中加入下面的语句:
delete odds;
这条语句会清空数组中的元素。
update:
SystemTap中数组的索引项还是可以是由long和string类型混合,中间以","分割,示例如下:
global odds
probe begin {
printf("probe begin\n");
odds["789","Jack", 10] = 2;
odds["123","Tom", 11] = 3;

exit();
}

probe end {
foreach ([x,y,z] in odds) {
printf("odds[%s,%s,%d] = %d\n", x, y, z, odds[x,y,z]);
}
printf("End\n");
}
除了上述方式还,还可以使用下面的方式遍历,
probe end {
foreach (var=[x,y,z] in odds) {
printf("odds[%s,%s,%d] = %d\n", x, y, z, var);
}
printf("End\n");
}
SystemTap中的数组真是和哈希表一样!

2013年8月14日星期三

netpoll浅析

   netpoll只是一种框架和一些接口,只有依赖这个框架和接口实现的netpoll实例,netpoll才能发挥它的功能。类似于kernel中的vfs,vfs本身并不会去做具体的文件操作,只是为不同的文件系统提供了一个框架。netpoll不依赖于网络协议栈,因此在内核网络及I/O子系统尚未可用时,也可以发送或接收数据包。当然netpoll能够处理的数据包类型也很有限,只有UDP和ARP数据包,并且只能是以太网报文。注意这里对UDP数据包的处理并不像四层的UDP协议那样复杂,并且netpoll可以发挥作用要依赖网络设备的支持。
1、netpoll结构和netpoll_info结构
netpoll结构用来描述接收和发送数据包的必要信息,每一个依赖netpoll的模块在使用这个框架前都必须实现并注册netpoll实例。
netpoll结构定义如下:

struct netpoll {
struct net_device *dev;
char dev_name[IFNAMSIZ];

const char *name;

void (*rx_hook)(struct netpoll *, int, char *, int);

__be32 local_ip, remote_ip;
u16 local_port, remote_port;

u8 remote_mac[ETH_ALEN];
};
dev成员存储的是绑定的网络设备实例,netpoll实例只能通过特定的网络设备接收和发送数据包。该设备在注册netpoll实例时设置。
dev_name存储的是网络设备名,通过它调用dev_get_by_name()获取指定的网络设备实例,并保存在dev中。
name是netpoll实例的名称。
netpoll实例有两种:能接收数据包和不能接收数据包。如果要接收数据包的话,必须实现rx_hook接口。如果不接收数据包的话,则不用。
local_ip和remote_ip分别存储的是远端和本地的IP,由netpoll实例指定。
local_port和remote_port分别存储的是远端和本地的port。
remote_mac存储的MAC地址。netpoll只支持以太网数据包,所以这里的MAC地址是以太网MAC地址。
当支持netpoll时,网络设备的net_device实例必须实现npinfo成员,即网络设备的netpoll_info信息块,描述结构为netpoll_info,定义如下:

struct netpoll_info {

atomic_t refcnt;

int rx_flags;

spinlock_t rx_lock;

struct netpoll *rx_np; /* netpoll that registered an rx_hook */

struct sk_buff_head arp_tx; /* list of arp requests to reply to */

struct sk_buff_head txq;
struct delayed_work tx_work;
};
refcnt是引用计数。每个netpoll_info实例被多个netpoll实例引用,每次引用时都对该成员加1.
rx_flags是标识接收的特性,可取的值为NETPOLL_RX_ENABLED和NETPOLL_RX_DROP(尚未使用)。如果所属的netpoll实例允许接收数据包,则会设置为NETPOLL_RX_ENABLED,否则为0.
rx_lock用来保证同一时刻只有一个CPU在进行相关的netpoll的输入操作。除此之外,在清理netpoll实例操作与netpoll的输入操作互斥,参见netpoll_cleanup().
如果注册的netpoll实例可以接收数据包,则将实例存储在rx_np成员中,不过该成员在发送数据包时也会使用,参见arp_reply().
arp_tx存储的是接收到的ARP报文。这里存储的ARP报文是在service_arp_queue()中处理的,而调用该函数的是netpoll_poll(),后面再讨论netpoll_poll()函数。
如果netpoll没有能成功发送数据包或者设备繁忙,则将待输出报文缓存到txq队列中,重新调度tx_work工作队列,等待再次尝试发送。
2、netpoll的输入
netpoll_rx()函数是netpoll接收数据包的入口函数,在netif_rx()和netif_receive_skb()中都会调用到。如果该函数返回0,则表示当前数据包不是netpoll想要的,继续传递到上层协议栈继续处理;如果返回1,则表示由netpoll来处理,不再向上层传递。
如果是ARP包,是否接收还要看静态变量trapped。trapped默认状态下是0,只有在poll_one_api()中调用网络设备的poll接口接收数据包前,才会加1,接收完后又会减1,如下所示:
static int poll_one_napi(struct netpoll_info *npinfo,
struct napi_struct *napi, int budget)
{
int work;

/* net_rx_action's ->poll() invocations and our's are
* synchronized by this test which is only made while
* holding the napi->poll_lock.
*/

if (!test_bit(NAPI_STATE_SCHED, &napi->state))
return budget;

npinfo->rx_flags |= NETPOLL_RX_DROP;
atomic_inc(&trapped);
set_bit(NAPI_STATE_NPSVC, &napi->state);

work = napi->poll(napi, budget);
trace_napi_poll(napi);

clear_bit(NAPI_STATE_NPSVC, &napi->state);
atomic_dec(&trapped);
npinfo->rx_flags &= ~NETPOLL_RX_DROP;

return budget - work;
}
poll_one_api()是由poll_napi()调用的,如果当前CPU和接收数据包的CPU不是一个CPU,并且此时网卡被放置到轮询列表,即设置了NAPI_STATE_SCHED,才会去执行接收操作。所以netpoll在调度接收网卡的数据包过程中会trap数据包(trapped不为0),这种情况下ARP包会被接收。如果trapped为0,即不trap数据包,并且是ARP数据包,则会传递到上层协议栈。不过,在__netpoll_rx()中返回之前,trapped此时不为0,会丢弃ARP包。
如果是UDP数据包,则主要是检查校验和和IP地址、端口号等信息,确定是否是netpoll想要的数据包,如果不是,则根据trapped决定是丢弃数据包还是传递到上层协议栈。如果是netpoll实例感兴趣的报文,则会调用其注册的rx_hook接口来接收,然后释放掉SKB包(注意,是在调用rx_hook之后立即释放)。
还有一点需要注意,如果netpoll在调度接收网卡的数据包,即trapped不为0,这个过程中会直接释放掉所有不是netpoll想要的数据包(只是netpoll实例绑定的网卡上的数据包)。个人理解是,netpoll只有在发送数据包没有成功或者分配skb失败(尝试10次)时(都是在向外输出数据包的时候)才会调度网卡接收数据包,如果出现这种情况,则说明网卡非常繁忙,并且很多数据包没来得及处理,此时丢掉数据包也是合理的。
3、netpoll的输出
如果接收到ARP报文,会调用arp_reply()来发送ARP响应;如果是UDP报文,则由netpoll实例处理,发送数据包调用的是netpoll_send_udp()。不过这两个接口最终都是在封装好要发送的数据包后,交给netpoll_send_skb()来发送,如下所示:
static void netpoll_send_skb(struct netpoll *np, struct sk_buff *skb)
{
......

/* don't get messages out of order, and no recursion */
if (skb_queue_len(&npinfo->txq) == 0 && !netpoll_owner_active(dev)) {
struct netdev_queue *txq;
unsigned long flags;

txq = netdev_get_tx_queue(dev, skb_get_queue_mapping(skb));

local_irq_save(flags);
/* try until next clock tick */
for (tries = jiffies_to_usecs(1)/USEC_PER_POLL;
tries > 0; --tries) {
if (__netif_tx_trylock(txq)) {
if (!netif_tx_queue_stopped(txq)) {
status = ops->ndo_start_xmit(skb, dev);
if (status == NETDEV_TX_OK)
txq_trans_update(txq);
}
__netif_tx_unlock(txq);

if (status == NETDEV_TX_OK)
break;

}

/* tickle device maybe there is some cleanup */
netpoll_poll(np);

udelay(USEC_PER_POLL);
}

WARN_ONCE(!irqs_disabled(),
"netpoll_send_skb(): %s enabled interrupts in poll (%pF)\n",
dev->name, ops->ndo_start_xmit);

local_irq_restore(flags);
}

if (status != NETDEV_TX_OK) {
skb_queue_tail(&npinfo->txq, skb);
schedule_delayed_work(&npinfo->tx_work,0);
}
}
从上面的代码我们可以看到,只有在(skb_queue_len(&npinfo->txq) == 0 && !netpoll_owner_active(dev))为真时才会尝试,发送数据包,否则直接缓存到txq队列中。
如果npinfo->txq队列不为空,说明tx_work工作队列已经被调度执行,此时直接将数据包缓存到txq队列中,通过tx_work工作队列来输出。注意,这里调用schedule_delayed_work()的时候,延迟时间设置的是0,所以如果重新调度的话,tx_work工作队列会立即开始执行。
如果npinfo->txq队列为空,是否将数据包直接缓存到txq队列,取决于netpoll_owner_active()的返回值。netpoll_owner_active()源码如下:
static int netpoll_owner_active(struct net_device *dev)
{
struct napi_struct *napi;

list_for_each_entry(napi, &dev->napi_list, dev_list) {
if (napi->poll_owner == smp_processor_id())
return 1;
}
return 0;
}

poll_owner是在接收数据包的软中断处理函数net_rx_action()中设置的,保存的是当前处理软中断的CPU的ID。如果netpoll实例绑定的网卡没有在接收数据包,也就是网卡没有放到设备轮询列表上,此时会直接返回0.如果此时网卡被放到轮询列表上,但是接收数据包的CPU不是当前的CPU,也会返回0。如果此时绑定的网卡正在接收数据包,并且是当前CPU,才会返回1,这时netpoll在发送SKB包时,会直接将数据包放到txq队列中,等待tx_work工作队列发送。
如果不是上述情况,netpoll_send_skb()会立即调用网络设备的ndo_start_xmit接口发送数据包。如果发送失败,则会尝试多次,直到下一次时钟节拍。如果仍然没有发送成功,则会将数据包缓存到txq队列中。
在尝试重新发送的过程中,netpoll对调用netpoll_poll()接口来模拟网络设备接收到数据包的中断,然后借助其他CPU来接收数据包,源码如下:
void netpoll_poll(struct netpoll *np)
{
struct net_device *dev = np->dev;
const struct net_device_ops *ops;

if (!dev || !netif_running(dev))
return;

ops = dev->netdev_ops;
if (!ops->ndo_poll_controller)
return;

/* Process pending work on NIC */
ops->ndo_poll_controller(dev);

poll_napi(dev);

/*
* 处理arp_tx队列中的ARP报文
*/

service_arp_queue(dev->npinfo);

zap_completion_queue();
}
模拟中断的接口是ndo_poll_controller,如果网卡不支持,则直接返回。模拟中断后,网卡设备会被放到轮询列表上,在poll_api()中会检查接收数据包的CPU和当前CPU是否是同一个CPU,如果不是,则会调用poll_one_napi()去使用网络设备的poll接口来接收数据包,否则直接返回,避免在UP上出现递归的情况。如果可以接收数据包,则trapped会加1,此时netpoll会trap数据包,该网卡上不是netpoll想要的数据包都会被直接丢掉,也只有在这段时间netpoll才可以接收ARP报文。所以我们看到,处理netpoll接收到的ARP包的接口,只在netpoll_poll()中调用,也只有在此时才有必要去处理接收到的ARP包。
综上所述,netpoll_poll()会加速网卡对数据包的处理,这样下次发送数据包时就更容易成功。
4、netpoll应用
netconsole是依赖netpoll实现的,可以将本机的dmesg系统信息,通过网络的方式输出到另一台主机上。这样就可以实现远程监控某些主机的dmesg信息,给开发人员调试内核提供了非常方便的途径。netconolse的使用方法参见内核文档netconsole.txt,里面介绍的非常详细   

2013年8月11日星期日

实战RPM包制作

 在开发中经常会用到一些rpm包,但是一直没有自己手动制作过。今天在制作的时候意外地还解决了自己以前一直困惑的问题,就是怎么制作rpm debuginfo包,类似CentOS官网那样的debuginfo包。原来在制作rpm时如果没有特殊设置就会在RPMS目录下同时生成rpm包及对应的debuginfo包。
下面总结一下制作rpm包的流程以及遇到的一些问题的解决。
1、配置工作路径
在制作rpm包之前,首先要配置工作路径,也就是制作rpm包所在的目录,当前的工作路径可以通过rpmbuild命令查看,如下所示:

工作路径是由_topdir变量指定的,默认情况下是当前用户目录下的rpmbuild目录。如果你在制作rpm包之前不知道需要设置工作路径的话,只是单纯地照搬网上的一些文章,在后面制作的时候很有可能会报找不到源码包等类似的错误。如果你不想在用户目录下的rpmbuild目录制作rpm包,可以在当前用户目录下的.rpmmacros文件(如果没有,则创建)中修改,格式为:


% _topdir 你的目录

我这里设置的是/usr/src/redhat,后面的制作过程都是在这个目录下进行。
默认情况下会生成debuginfo包,如果不需要debuginfo包,在.rpmmacros文件中添加"%debug_package %{nil}"。
2、建立所需要的目录
mkdir -pv /usr/src/redhat/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
目录说明:

目录 用途
BUILD rpmbuild命令在这个目录下编译软件
RPMS 生成的二进制RPM包和debuginfo包存储在这个目录
SOURCES 将源码放在这个目录
SPECS 生成rpm包的spec文件放置在这个目录
SRPMS 存放rpmbuild生成的源码包
3、编写spec文件
spec文件是制作rpm包的关键,定义了rpmbuild命令生成时需要遵循的一些规则。下面的spec文件是以nginx为例来制作rpm包:

# 这个区域定义的Name、Version这些字段对应的值可以在后面
# 通过%{name},%{version}这样的方式来引用,类似于C语言中的宏

# Name制定了软件的名称
Name: nginx
# 软件版本
Version: 1.5.2
# 释出号,也就是第几次制作rpm
Release: 1%{?dist}
# 软件的介绍,必须设置,最好不要超过50个字符
Summary: Nginx from WangYing

# 软件的分组,可以通过/usr/share/doc/rpm-4.8.0/GROUPS文件中选择,也可以
# 在相应的分类下,自己创建一个新的类型,例如这里的Server
Group: Application/Server
# 许可证类型
License: GPL
# 软件的源站
URL: http://nginx.org
# 制作rpm包的人员信息
Packager: WangYing <justlinux2010@gmail.com>
# 源码包的名称,在%_topdir/SOURCE下,如果有多个源码包的话,可以通过
# Source1、Source2这样的字段来指定其他的源码包
Source0: %{name}-%{version}.tar.gz
# BuildRoot指定了make install的测试安装目录,通过这个目录我们可以观察
# 生成了哪些文件,方便些files区域。如果在files区域中写的一些文件报
# 不存在的错误,可以查看%_topdir/BUILDROOT目录来检查有哪些文件。
BuildRoot: %_topdir/BUILDROOT
# 指定安装的路径
Prefix: /usr/local/nginx-1.5.2

# 制作过程需要的工具或软件包
BuildRequires: gcc,make
# 安装时依赖的软件包
Requires: pcre,pcre-devel,openssl

# 软件的描述,这个可以尽情地写
%description
Nginx is a http server

# %prep指定了在编译软件包之前的准备工作,这里的
# setup宏的作用是静默模式解压并切换到源码目录中,
# 当然你也可以使用tar命令来解压
%prep
%setup -q

# 编译阶段,和直接编译源代码类似,具体的操作或指定的一些参数由configure文件决定。
%build
CFLAGS="-pipe -O2 -g -W -Wall -Wpointer-arith -Wno-unused-parameter -Werror" ./configure --prefix=%{prefix}
# make后面的意思是:如果是多处理器,则并行编译
make %{?_smp_mflags}

# 安装阶段
%install
# 先删除原来的测试安装的,只有在制作失败了%{buildroot}目录才会有内容,
# 如果成功的话,目录下会被清除。
# %{buildroot}指向的目录不是BuildRoot(%_topdir/BUILDROOT)指定的目录,
# 而是该目录下名称与生成的rpm包名称相同的子目录。例如我的是
# /usr/src/redhat/BUILDROOT/nginx-1.5.2-1.el6.x86_64
rm -rf %{buildroot}
# 指定安装目录,注意不是真实的安装目录,是在制作rpm包的时候指定的
# 安装目录,如果不指定的话,默认就会安装到configure命令中指定的prefix路径,
# 所以这里一定要指定DESTDIR
make install DESTDIR=%{buildroot}

# 安装前执行的脚本,语法和shell脚本的语法相同
%pre

# 安装后执行的脚本
%post

# 卸载前执行的脚本,我这里的做的事情是在卸载前将nginx服务器停掉
%preun
MSG=`ps aux | grep nginx | grep -v "grep"`
if [ -z "$MSG" ];then
killall nginx 1>/dev/null 2>/dev/null
fi

# 卸载完成后执行的脚本
%postun
rm -rf %{prefix}

# 清理阶段,在制作完成后删除安装的内容
%clean
rm -rf %{buildroot}

#指定要包含的文件
%files
#设置默认权限,如果没有指定,则继承默认的权限
%defattr (-,root,root,0755)
%{prefix}
如果在制作的过程中报类似下面的错误,检查你的files区域,看要包含的文件是否存在。

+ /usr/lib/rpm/check-buildroot
+ /usr/lib/rpm/redhat/brp-compress
+ /usr/lib/rpm/redhat/brp-strip-static-archive /usr/bin/strip
+ /usr/lib/rpm/redhat/brp-strip-comment-note /usr/bin/strip /usr/bin/objdump
+ /usr/lib/rpm/brp-python-bytecompile
+ /usr/lib/rpm/redhat/brp-python-hardlink
+ /usr/lib/rpm/redhat/brp-java-repack-jars
Processing files: nginx-1.5.2-1.el6.x86_64
error: File not found: /usr/src/redhat/BUILDROOT/nginx-1.5.2-1.el6.x86_64/usr/local/nginx


RPM build errors:
File not found: /usr/src/redhat/BUILDROOT/nginx-1.5.2-1.el6.x86_64/usr/local/nginx