存档

‘Linux专区’ 分类的存档

linux内存泄露,不再困难!Valgrind工具-击破内存泄露!

2010年3月30日 没有评论

Linux下用Valgrind防止内存泄露 用C/C++开发其中最令人头疼的一个问题就是内存管理,有时候为了查找一个内存泄漏或者一个内存访问越界,需要要花上好几天时间,如果有一款工具能够帮助我们做这件事情就好了,valgrind正好就是这样的一款工具。

Valgrind是一款基于模拟linux下的程序调试器和剖析器的软件套件,可以运行于x86, amd64和ppc32架构上。valgrind包含一个核心,它提供一个虚拟的CPU运行程序,还有一系列的工具,它们完成调试,剖析和一些类似的任务。valgrind是高度模块化的,所以开发人员或者用户可以给它添加新的工具而不会损坏己有的结构。

valgrind的官方网址是:http://valgrind.org

可以在它的网站上下载到最新的valgrind,它是开放源码和免费的。

一、介绍

valgrind包含几个标准的工具,它们是:

1、memcheck

memcheck探测程序中内存管理存在的问题。它检查所有对内存的读/写操作,并截取所有的malloc/new/free/delete调用。因此memcheck工具能够探测到以下问题:

1)使用未初始化的内存

2)读/写已经被释放的内存

3)读/写内存越界

4)读/写不恰当的内存栈空间

5)内存泄漏

6)使用malloc/new/new[]和free/delete/delete[]不匹配。

2、cachegrind
cachegrind是一个cache剖析器。它模拟执行CPU中的L1, D1和L2 cache,因此它能很精确的指出代码中的cache未命中。如果你需要,它可以打印出cache未命中的次数,内存引用和发生cache未命中的每一行代码,每一个函数,每一个模块和整个程序的摘要。如果你要求更细致的信息,它可以打印出每一行机器码的未命中次数。在x86和amd64上,cachegrind通过CPUID自动探测机器的cache配置,所以在多数情况下它不再需要更多的配置信息了。

3、helgrind

helgrind查找多线程程序中的竞争数据。helgrind查找内存地址,那些被多于一条线程访问的内存地址,但是没有使用一致的锁就会被查出。这表示这些地址在多线程间访问的时候没有进行同步,很可能会引起很难查找的时序问题。

二、valgrind对你的程序都做了些什么

valgrind被设计成非侵入式的,它直接工作于可执行文件上,因此在检查前不需要重新编译、连接和修改你的程序。要检查一个程序很简单,只需要执行下面的命令就可以了

valgrind –tool=tool_name program_name

比如我们要对ls -l命令做内存检查,只需要执行下面的命令就可以了

valgrind –tool=memcheck ls -l

不管是使用哪个工具,valgrind在开始之前总会先取得对你的程序的控制权,从可执行关联库里读取调试信息。然后在valgrind核心提供的虚拟CPU上运行程序,valgrind会根据选择的工具来处理代码,该工具会向代码中加入检测代码,并把这些代码作为最终代码返回给valgrind核心,最后valgrind核心运行这些代码。

如果要检查内存泄漏,只需要增加–leak-check=yes就可以了,命令如下

valgrind –tool=memcheck –leak-check=yes ls -l

不同工具间加入的代码变化非常的大。在每个作用域的末尾,memcheck加入代码检查每一片内存的访问和进行值计算,代码大小至少增加12倍,运行速度要比平时慢25到50倍。

valgrind模拟程序中的每一条指令执行,因此,检查工具和剖析工具不仅仅是对你的应用程序,还有对共享库,GNU C库,X的客户端库都起作用。

三、现在开始

下载以后,解压文件

tar xvf valgrind-3.5.0.tar.bz2
cd valgrind-3.5.0
./configure  –prefix=/where/you/want/it/installed
make
make install

然后:测试程序。
首先,在编译程序的时候打开调试模式(gcc编译器的-g选项)。如果没有调试信息,即使最好的valgrind工具也将中能够猜测特定的代码是属于哪一个函数。打开调试选项进行编译后再用valgrind检查,valgrind将会给你的个详细的报告,比如哪一行代码出现了内存泄漏。

当检查的是C++程序的时候,还应该考虑另一个选项 -fno-inline。它使得函数调用链很清晰,这样可以减少你在浏览大型C++程序时的混乱。比如在使用这个选项的时候,用memcheck检查openoffice就很容易。当然,你可能不会做这项工作,但是使用这一选项使得valgrind生成更精确的错误报告和减少混乱。

一些编译优化选项(比如-O2或者更高的优化选项),可能会使得memcheck提交错误的未初始化报告,因此,为了使得valgrind的报告更精确,在编译的时候最好不要使用优化选项。

如果程序是通过脚本启动的,可以修改脚本里启动程序的代码,或者使用–trace-children=yes选项来运行脚本。

下面是用memcheck检查ls -l命令的输出报告,在终端下执行下面的命令

valgrind –tool=memcheck ls -l

程序会打印出ls -l命令的结果,最后是valgrind的检查报告如下:

==8439== Memcheck, a memory error detector.
==8439== Copyright (C) 2002-2006, and GNU GPL’d, by Julian Seward et al.
==8439== Using LibVEX rev 1658, a library for dynamic binary translation.
==8439== Copyright (C) 2004-2006, and GNU GPL’d, by OpenWorks LLP.
==8439== Using valgrind-3.2.1, a dynamic binary instrumentation framework.
==8439== Copyright (C) 2000-2006, and GNU GPL’d, by Julian Seward et al.
==8439== For more details, rerun with: -v
==8439==
==8439== Invalid write of size 4
==8439==    at 0x4004C6: main (11.c:6)
==8439==  Address 0x4C2F038 is 8 bytes inside a block of size 10 alloc’d
==8439==    at 0x4A05809: malloc (vg_replace_malloc.c:149)
==8439==    by 0x4004B0: main (11.c:5)
==8439==
==8439== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 1)
==8439== malloc/free: in use at exit: 10 bytes in 1 blocks.
==8439== malloc/free: 1 allocs, 0 frees, 10 bytes allocated.
==8439== For counts of detected errors, rerun with: -v
==8439== searching for pointers to 1 not-freed blocks.
==8439== checked 63,528 bytes.
==8439==
==8439==
==8439== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==8439==    at 0x4A05809: malloc (vg_replace_malloc.c:149)
==8439==    by 0x4004B0: main (11.c:5)
==8439==
==8439== LEAK SUMMARY:
==8439==    definitely lost: 10 bytes in 1 blocks.
==8439==      possibly lost: 0 bytes in 0 blocks.
==8439==    still reachable: 0 bytes in 0 blocks.
==8439==         suppressed: 0 bytes in 0 blocks.
==8439== Reachable blocks (those to which a pointer was found) are not shown.
==8439== To see them, rerun with: –show-reachable=yes

这里的“8439”指的是执行ls -l的进程ID,这有利于区别不同进程的报告。memcheck会给出报告,分配置和释放了多少内存,有多少内存泄漏了,还有多少内存的访问是可达的,检查了多少字节的内存。
下面举两个用valgrind做内存检查的例子
例子一 (11.c):

#include <string.h>
int main(int argc, char *argv[])
{
    char *ptr;
    ptr = (char*) malloc(10);
    strcpy(ptr, “01234567890″);
    return 0;
}

编译程序

gcc -g -o 11 11.c

用valgrind执行命令

valgrind –tool=memcheck –leak-check=yes ./11

报告如下

==8427== Memcheck, a memory error detector.
==8427== Copyright (C) 2002-2006, and GNU GPL’d, by Julian Seward et al.
==8427== Using LibVEX rev 1658, a library for dynamic binary translation.
==8427== Copyright (C) 2004-2006, and GNU GPL’d, by OpenWorks LLP.
==8427== Using valgrind-3.2.1, a dynamic binary instrumentation framework.
==8427== Copyright (C) 2000-2006, and GNU GPL’d, by Julian Seward et al.
==8427== For more details, rerun with: -v
==8427==
==8427== Invalid write of size 4
==8427==    at 0x4004C6: main (in /root/11)
==8427==  Address 0x4C2F038 is 8 bytes inside a block of size 10 alloc’d
==8427==    at 0x4A05809: malloc (vg_replace_malloc.c:149)
==8427==    by 0x4004B0: main (in /root/11)
==8427==
==8427== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 1)
==8427== malloc/free: in use at exit: 10 bytes in 1 blocks.
==8427== malloc/free: 1 allocs, 0 frees, 10 bytes allocated.
==8427== For counts of detected errors, rerun with: -v
==8427== searching for pointers to 1 not-freed blocks.
==8427== checked 63,528 bytes.
==8427==
==8427==
==8427== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==8427==    at 0x4A05809: malloc (vg_replace_malloc.c:149)
==8427==    by 0x4004B0: main (in /root/11)
==8427==
==8427== LEAK SUMMARY:
==8427==    definitely lost: 10 bytes in 1 blocks.
==8427==      possibly lost: 0 bytes in 0 blocks.
==8427==    still reachable: 0 bytes in 0 blocks.
==8427==         suppressed: 0 bytes in 0 blocks.
==8427== Reachable blocks (those to which a pointer was found) are not shown.
==8427== To see them, rerun with: –show-reachable=yes

从这份报告可以看出,进程号是8427,test.c的第8行写内存越界了,引起写内存越界的是strcpy函数,
第7行泄漏了10个字节的内存,引起内存泄漏的是malloc函数。
例子二(test2.c)

#include <stdio.h>
int foo(int x)
{
    if (x < 0) {
        printf(“%d “, x);
    }
    return 0;
}

int main(int argc, char *argv[])
{
    int x;  
    foo(x);
    return 0;
}

编译程序

gcc -g -o test2 test2.c

用valgrind做内存检查

valgrind –tool=memcheck ./test2

输出报告如下

==4285== Memcheck, a memory error detector.
==4285== Copyright (C) 2002-2006, and GNU GPL’d, by Julian Seward et al.
==4285== Using LibVEX rev 1606, a library for dynamic binary translation.
==4285== Copyright (C) 2004-2006, and GNU GPL’d, by OpenWorks LLP.
==4285== Using valgrind-3.2.0, a dynamic binary instrumentation framework.
==4285== Copyright (C) 2000-2006, and GNU GPL’d, by Julian Seward et al.
==4285== For more details, rerun with: -v
==4285==
==4285== Conditional jump or move depends on uninitialised value(s)
==4285== at 0×8048372: foo (test2.c:5)
==4285== by 0x80483B4: main (test2.c:16)
==4285==p p
==4285== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 12 from 1)
==4285== malloc/free: in use at exit: 0 bytes in 0 blocks.
==4285== malloc/free: 0 allocs, 0 frees, 0 bytes allocated.
==4285== For counts of detected errors, rerun with: -v
==4285== All heap blocks were freed — no leaks are possible.

从这份报告可以看出进程PID是4285,test2.c文件的第16行调用了foo函数,在test2.c文件的第5行foo函数使用了一个未初始化的变量。
valgrind还有很多使用选项,具体可以查看valgrind的man手册页和valgrind官方网站的在线文档。

分类: Linux专区 标签: , ,

内存谨慎操作—mysql_free_result()引起的15G内存泄露问题…

2010年3月30日 1 条评论

我们的流量测试系统上的做系统测试,以前都用的随机用户ID,由于数据量过大,我做了一个简单过滤,只插入前5W条记录,这样程序运行很好,速度也在可接受之内。后来添加上用户,由于用户ID是存在数据库里面的,所以我们考虑到了使用内存表缓存关键字:ID和IP,然后每次通过IP匹配id,这样便有了多次数据库查询,由于不在监控记录内的IP我们都是丢掉的,所以便出现了如下代码:

  sprintf(strQuery,"select userid from heap_user_ip where ip=%lld",host_ip);

   if(mysql_query(mysql,strQuery))
   {
     DBG("select userid  error: %s\n", mysql_error(mysql));
    return 0;
   }
   res=mysql_store_result(mysql);
   row = mysql_fetch_row(res);
   if(row!=NULL)
    userid=atoi(row[0]);    
   else
  {
     userid=0; 

     return 0;
    }
 mysql_free_result(res);

开始的时候代码运行还是可以的,没有什么问题,但是运行时间久了,就发现这个程序特别吃内存,又一次运行了一天以后居然,吃了整整 15个G的内存,这下把我吓了一跳,一看就杯具了,一直在怀疑我的Hash 桶 和 链表数据有问题,但是把hash桶以及链表全部算上 估计出最大值也不会有2G的内存消耗。然后就怀疑是不是链表出现了,内存的泄露。查了一天,打印了N多地方。还是没有查出问题。

突然想起了,用top看下变化,能看到,这个程序每3秒钟就有一个+5MB内存,后来查看源码,发现全部是更新操作,没有插入,就可以知道链表是没有malloc内存的,怎么会是这样呢?

最后看到这个程序的用户ID查找时,有一个问题:如果该IP不在统计表里面,程序会直接退出,那么res便没有释放!我的天,原来在这里;在退出先加上

<mysql_free_result(res);

问题解决!

我们可以推测,mysql 开辟的不是栈上的内存,应该是类似 堆上的,要不然,函数退出改结果就会自动释放!

所以,做事要细心!想到每一步!

Linux 下 实现软件多功能日志(log)的记录。

2010年3月2日 没有评论

    前段时间调试万兆的项目,为了调试,发现错误,我们便直接使用了printf 打印到stdout上,大家都知道,Linux 终端显示的缓冲是有限的(默认的显示几百行),所以如果我们程序printf行数较多,或者程序运行时间较长,难免有一些记录被冲掉,进而影响查看bug。特别是对随机的、少重现错错误,想通过屏幕上printf出来的数据调试,就难免困难一些,当然我们也可以使用重定向实现

程序>log.txt

但是这种方式,打印到文件上便不能显示到屏幕上了,所以有一定的局限性。

于是写了一个程序用意实现。

改程序有以下几个文件

Debug.c

Debug.h

 

改打印log程序可以关闭和打开文件输出、屏幕输出详细说明如下:

       debug_term_on();//将打印信息输出到屏幕上

       debug_term_off();//不将打印信息输出到屏幕上

       debug_file_off();//不将打印信息输出到文件中

       debug_set_dir( “./log” )  ; //设置log文件夹

       debug_file_on();//将打印信息输出到文件中

 

输出例子:


#include <stdio.h>
#include "debug.c"

int main(int argc, char *argv[])
{
	/*--------------------初始化---------------------*/
	char* log_dir = "./log";//the dir
	unsigned int  log_terminal = 1;//debug on terminal: yes or not

/* init the debug info */
	if( log_terminal )
	{
		debug_term_on();
	}
	else
	{
		debug_term_off();
	}
	if( log_dir == NULL ){
		debug_file_off();
	}else{
		debug_set_dir( log_dir );
		debug_file_on();
	}

	/*-------------------使用测试---------------------*/
  DBG("I printf on stdout and logfile...\n");
  return 0;
}

代码下载

Linux下监控某个服务进程情况,并通过Email和Fetion告警

2010年1月14日 3 条评论

     前一段时间我们做的万兆流量测量服务器,还在测试期,后来相对来说比较稳定了,但是还需要观察稳定性:有时候程序退出了,我们还不能实时的观测,所以我就抽时间做了一个检测软件。用着比较好用,推荐使用。闲话少说,直接步入正题:

源码下载

本程序有以下模块:

  1. 服务器报错模块
  2. 监控信息模块
  3. 邮件发送模块
  4. 飞信发送模块

下面就简单阐述改系统的原理:

该系统使用了管道通信

流程图如上面描述,改程序不需要改变被监控服务器的构架,只需要在被监控程序的出错处理代码中添加写管道代码即可。

                   故障原因传入管道
服务器出现故障——————->监控程序接受管道信息<—————
                                        |                            |
                                        |                            |
                                        |                            |
                                    判断是否出错                     |
                                    这里我用了502做信令              |
                                        |                            |
                                        |                            |
                                    是出错,调用邮件、飞信发送程序   |
                                        |                            |
                                        |                            |
                                        —————————–

        监控程序上图所示,改程序启动以后就处于阻塞状态,不耗费使用CUP资源,只有被监控程序出错,才会激发改程序。如果你只需要email监控,就可以配置代码flag取消Fetion发送。具体看源代码注释。

邮件收发,该模块我修改了zzx的邮件发送,添加了DNS的解析,可以直接输入邮箱地址。不用先ping获得IP地址。使用时,加上具体自己发送的SMTP地址、邮箱账号、以及内容。看源代码。

Fetion的发送借用了solrex的用飞信发送天气预报中的部分源码。十分感谢他共享源码,他的联系方见源码。

下面说下使用方法:

Monitor.c中修改email 以及飞信的 dest@qq.com(推荐用qq邮箱,实时性高)

修改飞信的配置:execl(“sendsms”,”sendsms”,”-vlf”,” 137********”,”-p”,”passoword***”,”-t”,”137******”,”please check the log on the server!\n “,NULL);

账号和密码,以及发送目的手机号。

全部配置妥当,运行Monitor,你就可以去睡懒觉了,有问题了,他就会给你发短信或者发邮件哦!

如有疑问请登陆www.yqshare.com 留言。 

源码下载

孤儿进程和僵尸进程[详解]

2010年1月11日 1 条评论

一、定义:什么是孤儿进程和僵尸进程
   僵尸进程:一个子进程在其父进程还没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。
   孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
注:
    僵尸进程将会导致资源浪费,而孤儿则不会。

子进程持续10秒钟的僵尸状态(EXIT_ZOMBIE)
——————————————————
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0)
        printf(“error occurred!\n”);
    else if(pid == 0) {
        printf(“Hi father! I’m a ZOMBIE\n”);
        exit(0);      //(1)
    }
    else {
        sleep(10);
        wait(NULL);   //(2)
    }
}

(1) 向父进程发送SIGCHILD信号
(2) 父进程处理SIGCHILD信号

执行exit()时根据其父进程的状态决定自己的状态:
    如果父进程已经退出(没有wait),则该子进程将会成为孤儿进程过继给init进程
    如果其父进程还没有退出,也没有wait(),那么该进程将向父进程发送SIGCHILD信号,进入僵尸状态等待父进程为其收尸。如果父进程一直没有执行wait(),那么该子进程将会持续处于僵尸状态。
子进程将成为孤儿进程
——————————————————
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0)
        printf(“error occurred!\n”);
    else if(pid == 0) {
        sleep(6);
        printf(“I’m a orphan\n”);
        exit(0);
    }
    else {
        sleep(1);
        printf(“Children Bye!\n”);
    }
}

# ./a.out
Children Bye!
# I’m a orphan
(回车后将会进入#)
#
二、影响:
僵尸进程会占用系统资源,如果很多,则会严重影响服务器的性能
孤儿进程不会占用系统资源
处理流程:
只要父进程不等wait(sys/wait.h)子进程,子进程都将成为孤魂野鬼zombie(zombie),unix中默认父进程总是想看子进程死后的状态 
  if  父进程比子进程先退出 
    子进程将被init(id   =   1)收养,最后的结果是zombie子进程彻底再见,系统资源释放 
  else   
      { 
        子进程的zombie将一直存在,系统资源占用… 
        if   父进程dead   
            子进程将被init(id   =   1)收养,最后的结果是zombie子进程彻底再见,系统资源释放 
  
      else   类似的子进程zombie越来越多,系统就等死了!!! 
    } 
三、如何防止僵尸进程
首先明白如何产生僵尸进程:
1、子进程结束后向父进程发出SIGCHLD信号,父进程默认忽略了它
2、父进程没有调用wait()或waitpid()函数来等待子进程的结束
第一种方法:  捕捉SIGCHLD信号,并在信号处理函数里面调用wait函数

转贴Richard Steven的Unix Network Programming代码

int main(int argc, char **argv)
{
                …
        Signal(SIGCHLD, sig_chld);
                for(;
                }
                …
}

void sig_chld(int signo)
{
        pid_t        pid;
        int         stat;

        while ( (pid = waitpid(-1, &stat, WNOHANG)) >; 0)
                printf(“child %d terminated\n”, pid);
        return;
}
第二种方法:两次fork():转载
在《Unix 环境高级编程》里关于这个在8.6节有非常清楚的说明。

实例
回忆一下8 . 5节中有关僵死进程的讨论。如果一个进程要fork一个子进程,但不要求它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一要求的诀窍是调用fork两次。程序8 – 5实现了这一点。在第二个子进程中调用sleep以保证在打印父进程ID时第一个子进程已终止。在fork之后,父、子进程都可继续执行——我们无法预知哪一个会先执行。如果不使第二个子进程睡眠,则
在fork之后,它可能比其父进程先执行,于是它打印的父进程ID将是创建它的父进程,而不是init进程(进程ID1)。

#include        <sys/types.h>
#include        <sys/wait.h>
#include        “ourhdr.h”

int main(void)
{
        pid_t        pid;

        if ( (pid = fork()) < 0)
                err_sys(“fork error”);
        else if (pid == 0) {               
                if ( (pid = fork()) < 0)
                        err_sys(“fork error”);
                else if (pid > 0)
                        exit(0);       

                sleep(2);
                printf(“second child, parent pid = %d\n”, getppid());
                exit(0);
        }

        if (waitpid(pid, NULL, 0) != pid)       
                err_sys(“waitpid error”);

        exit(0);
}
//avoid zombie process by forking twice

分类: Linux专区 标签:

linux网络编select用法

2010年1月11日 没有评论

select()函数主要是建立在fd_set类型的基础上的。fd_set(它比较重要所以先介绍一下)是一组文件描述字(fd)的集合

,它用一位来表示一个fd(下面会仔细介绍),对于fd_set类型通过下面四个宏来操作: 

    fd_set set;

    FD_ZERO(&set);      

    FD_SET(fd, &set);   

    FD_CLR(fd, &set);   

    FD_ISSET(fd, &set);        

过去,一个fd_set通常只能包含<32的fd(文件描述字),因为fd_set其实只用了一个32位矢量来表示fd;现在,UNIX系统

通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,其值通常是1024,这样就能

表示<1024的fd。根据fd_set的位矢量实现,我们可以重新理解操作fd_set的四个宏: 

    fd_set set;

FD_ZERO(&set);     

FD_SET(0, &set);   

FD_CLR(4, &set);     

FD_ISSET(5, &set);   

―――――――――――――――――――――――――――――――――――――――

注意fd的最大值必须<FD_SETSIZE。

――――――――――――――――――――――――――――――――――――――― 

select函数的接口比较简单:

    int select(int nfds, fd_set *readset, fd_set *writeset,

fd_set* exceptset, struct tim *timeout); 

功能:

测试指定的fd可读?可写?有异常条件待处理?     

参数:

nfds    

需要检查的文件描述字个数(即检查到fd_set的第几位),数值应该比三组fd_set中所含的最大fd值更大,一般设为三组

fd_set中所含的最大fd值加1(如在readset,writeset,exceptset中所含最大的fd为5,则nfds=6,因为fd是从0开始的)

。设这个值是为提高效率,使函数不必检查fd_set的所有1024位。

readset   

     用来检查可读性的一组文件描述字。

writeset

     用来检查可写性的一组文件描述字。

exceptset

     用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)

timeout

有三种可能:

1.        timeout=NULL(阻塞:直到有一个fd位被置为1函数才返回)

2.        timeout所指向的结构设为非零时间(等待固定时间:有一个fd位被置为1或者时间耗尽,函数均返回)

3.        timeout所指向的结构,时间设为0(非阻塞:函数检查完每个fd后立即返回) 

返回值:     

返回对应位仍然为1的fd的总数。 

Remarks:

三组fd_set均将某些fd位置0,只有那些可读,可写以及有异常条件待处理的fd位仍然为1。

使用select函数的过程一般是:

先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,接着调用函数select测试fd_set

中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1。 

以下是一个测试单个文件描述字可读性的例子:

     int isready(int fd)

     {

         int rc;

         fd_set fds;

         struct tim tv;    

         FD_ZERO(&fds);

         FD_SET(fd,&fds);

         tv.tv_sec = tv.tv_usec = 0;    

      rc = select(fd+1, &fds, NULL, NULL, &tv);

         if (rc < 0)   //error

           return -1;    

         return FD_ISSET(fd,&fds) ? 1 : 0;

     }

下面还有一个复杂一些的应用:

//这段代码将指定测试Socket的描述字的可读可写性,因为Socket使用的也是fd

uint32 SocketWait(TSocket *s,bool rd,bool wr,uint32 timems)    

{

     fd_set rfds,wfds;

#ifdef _WIN32

     TIM tv;

#else

     struct tim tv;

#endif    

     FD_ZERO(&rfds);

     FD_ZERO(&wfds); 

     if (rd)     //TRUE

          FD_SET(*s,&rfds);   //添加要测试的描述字 

     if (wr)     //FALSE

          FD_SET(*s,&wfds); 

     tv.tv_sec=timems/1000;     //second

     tv.tv_usec=timems%1000;     //ms 

     for (;;) //如果errno==EINTR,反复测试缓冲区的可读性

          switch(select((*s)+1,&rfds,&wfds,NULL,

              (timems==TIME_INFINITE?NULL:&tv)))  //测试在规定的时间内套接口接收缓冲区中是否有数据可读

         {                                              //0--超时,-1--出错

         case 0:    

              return 0; 

         case (-1):   

              if (SocketError()==EINTR)

                   break;              

              return 0; //有错但不是EINTR 

          default:

              if (FD_ISSET(*s,&rfds)) //如果s是fds中的一员返回非0,否则返回0

                   return 1;

              if (FD_ISSET(*s,&wfds))

                   return 2;

              return 0;

         };

}

分类: Linux专区 标签:

[转]bash脚本调试方法

2010年1月11日 没有评论

本文全面系统地介绍了shell脚本调试技术,包括使用echo, tee, trap等命令输出关键信息,跟踪变量的值,在脚本中植入调试钩子,使用“-n”选项进行shell脚本的语法检查,使用“-x”选项实现shell脚本逐条语句的跟踪,巧妙地利用shell的内置变量增强“-x”选项的输出信息等。
    
一. 前言
shell编程在unix/linux世界中使用得非常广泛,熟练掌握shell编程也是成为一名优秀的unix/linux开发者和系统管理员的必经之路。脚本调试的主要工作就是发现引发脚本错误的原因以及在脚本源代码中定位发生错误的行,常用的手段包括分析输出的错误信息,通过在脚本中加入调试语句,输出调试信息来辅助诊断错误,利用调试工具等。但与其它高级语言相比,shell解释器缺乏相应的调试机制和调试工具的支持,其输出的错误信息又往往很不明确,初学者在调试脚本时,除了知道用echo语句输出一些信息外,别无它法,而仅仅依赖于大量的加入echo语句来诊断错误,确实令人不胜其繁,故常见初学者抱怨shell脚本太难调试了。本文将系统地介绍一些重要的shell脚本调试技术,希望能对shell的初学者有所裨益。

本文的目标读者是unix/linux环境下的开发人员,测试人员和系统管理员,要求读者具有基本的shell编程知识。本文所使用范例在Bash3.1+Redhat Enterprise Server 4.0下测试通过,但所述调试技巧应也同样适用于其它shell。
    
二. 在shell脚本中输出调试信息
通过在程序中加入调试语句把一些关键地方或出错的地方的相关信息显示出来是最常见的调试手段。Shell程序员通常使用echo(ksh程序员常使用print)语句输出信息,但仅仅依赖echo语句的输出跟踪信息很麻烦,调试阶段在脚本中加入的大量的echo语句在产品交付时还得再费力一一删除。针对这个问题,本节主要介绍一些如何方便有效的输出调试信息的方法。

1. 使用trap命令
trap命令用于捕获指定的信号并执行预定义的命令。
其基本的语法是:
trap ‘command’ signal
其中signal是要捕获的信号,command是捕获到指定的信号之后,所要执行的命令。可以用kill –l命令看到系统中全部可用的信号名,捕获信号后所执行的命令可以是任何一条或多条合法的shell语句,也可以是一个函数名。
shell脚本在执行时,会产生三个所谓的“伪信号”,(之所以称之为“伪信号”是因为这三个信号是由shell产生的,而其它的信号是由操作系统产生的),通过使用trap命令捕获这三个“伪信号”并输出相关信息对调试非常有帮助。

shell伪信号
信号名 何时产生
EXIT 从一个函数中退出或整个脚本执行完毕
ERR 当一条命令返回非零状态时(代表命令执行不成功)
DEBUG 脚本中每一条命令执行之前

通过捕获EXIT信号,我们可以在shell脚本中止执行或从函数中退出时,输出某些想要跟踪的变量的值,并由此来判断脚本的执行状态以及出错原因,其使用方法是:
trap ‘command’ EXIT 或 trap ‘command’ 0
通过捕获ERR信号,我们可以方便的追踪执行不成功的命令或函数,并输出相关的调试信息,以下是一个捕获ERR信号的示例程序,其中的$LINENO是一个shell的内置变量,代表shell脚本的当前行号。
$ cat -n exp1.sh
     1 ERRTRAP()
     2 {
     3    echo “[LINE:$1] Error: Command or function exited with status $?”
     4 }
     5 foo()
     6 {
     7    return 1;
     8 }
     9 trap ‘ERRTRAP $LINENO’ ERR
    10 abc
    11 foo
    
其输出结果如下:
$ sh exp1.sh
exp1.sh: line 10: abc: command not found
[LINE:10] Error: Command or function exited with status 127
[LINE:11] Error: Command or function exited with status 1

在调试过程中,为了跟踪某些变量的值,我们常常需要在shell脚本的许多地方插入相同的echo语句来打印相关变量的值,这种做法显得烦琐而笨拙。而通过捕获DEBUG信号,我们只需要一条trap语句就可以完成对相关变量的全程跟踪。
以下是一个通过捕获DEBUG信号来跟踪变量的示例程序:
$ cat –n exp2.sh
     1 #!/bin/bash
     2 trap ‘echo “before execute line:$LINENO, a=$a,b=$b,c=$c”’ DEBUG
     3 a=1
     4 if [ "$a" -eq 1 ]
     5 then
     6     b=2
     7 else
     8     b=1
     9 fi
    10 c=3
    11 echo “end”

其输出结果如下:
$ sh exp2.sh
before execute line:3, a=,b=,c=
before execute line:4, a=1,b=,c=
before execute line:6, a=1,b=,c=
before execute line:10, a=1,b=2,c=
before execute line:11, a=1,b=2,c=3
end

从运行结果中可以清晰的看到每执行一条命令之后,相关变量的值的变化。同时,从运行结果中打印出来的行号来分析,可以看到整个脚本的执行轨迹,能够判断出哪些条件分支执行了,哪些条件分支没有执行。

2. 使用tee命令
在shell脚本中管道以及输入输出重定向使用得非常多,在管道的作用下,一些命令的执行结果直接成为了下一条命令的输入。如果我们发现由管道连接起来的一批命令的执行结果并非如预期的那样,就需要逐步检查各条命令的执行结果来判断问题出在哪儿,但因为使用了管道,这些中间结果并不会显示在屏幕上,给调试带来了困难,此时我们就可以借助于tee命令了。

tee命令会从标准输入读取数据,将其内容输出到标准输出设备,同时又可将内容保存成文件。例如有如下的脚本片段,其作用是获取本机的ip地址:
ipaddr=`/sbin/ifconfig | grep ‘inet addr:’ | grep -v ’127.0.0.1′
| cut -d : -f3 | awk ‘{print $1}’`
#注意=号后面的整句是用反引号(数字1键的左边那个键)括起来的。
echo $ipaddr

运行这个脚本,实际输出的却不是本机的ip地址,而是广播地址,这时我们可以借助tee命令,输出某些中间结果,将上述脚本片段修改为:
ipaddr=`/sbin/ifconfig | grep ‘inet addr:’ | grep -v ’127.0.0.1′
| tee temp.txt | cut -d : -f3 | awk ‘{print $1}’`
echo $ipaddr

之后,将这段脚本再执行一遍,然后查看temp.txt文件的内容:
$ cat temp.txt
inet addr:192.168.0.1 Bcast:192.168.0.255 Mask:255.255.255.0

我们可以发现中间结果的第二列(列之间以:号分隔)才包含了IP地址,而在上面的脚本中使用cut命令截取了第三列,故我们只需将脚本中的cut -d : -f3改为cut -d : -f2即可得到正确的结果。
具体到上述的script例子,我们也许并不需要tee命令的帮助,比如我们可以分段执行由管道连接起来的各条命令并查看各命令的输出结果来诊断错误,但在一些复杂的shell脚本中,这些由管道连接起来的命令可能又依赖于脚本中定义的一些其它变量,这时我们想要在提示符下来分段运行各条命令就会非常麻烦了,简单地在管道之间插入一条tee命令来查看中间结果会更方便一些。

3. 使用”调试钩子”
在C语言程序中,我们经常使用DEBUG宏来控制是否要输出调试信息,在shell脚本中我们同样可以使用这样的机制,如下列代码所示:
if [ “$DEBUG” = “true” ]; then
echo “debugging” #此处可以输出调试信息
fi

这样的代码块通常称之为“调试钩子”或“调试块”。在调试钩子内部可以输出任何您想输出的调试信息,使用调试钩子的好处是它是可以通过DEBUG变量来控制的,在脚本的开发调试阶段,可以先执行export DEBUG=true命令打开调试钩子,使其输出调试信息,而在把脚本交付使用时,也无需再费事把脚本中的调试语句一一删除。
如果在每一处需要输出调试信息的地方均使用if语句来判断DEBUG变量的值,还是显得比较繁琐,通过定义一个DEBUG函数可以使植入调试钩子的过程更简洁方便,如下面代码所示:
$ cat –n exp3.sh
     1 DEBUG()
     2 {
     3 if [ "$DEBUG" = "true" ]; then
     4      $@  
     5 fi
     6 }
     7 a=1
     8 DEBUG echo “a=$a”
     9 if [ "$a" -eq 1 ]
    10 then
    11       b=2
    12 else
    13       b=1
    14 fi
    15 DEBUG echo “b=$b”
    16 c=3
    17 DEBUG echo “c=$c”
在上面所示的DEBUG函数中,会执行任何传给它的命令,并且这个执行过程是可以通过DEBUG变量的值来控制的,我们可以把所有跟调试有关的命令都作为DEBUG函数的参数来调用,非常的方便。
    
三. 使用shell的执行选项
上一节所述的调试手段是通过修改shell脚本的源代码,令其输出相关的调试信息来定位错误的,那有没有不修改源代码来调试shell脚本的方法呢?答案就是使用shell的执行选项,本节将介绍一些常用选项的用法:

-n 只读取shell脚本,但不实际执行
-x 进入跟踪方式,显示所执行的每一条命令
-c “string” 从strings中读取命令

“-n”可用于测试shell脚本是否存在语法错误,但不会实际执行命令。在shell脚本编写完成之后,实际执行之前,首先使用“-n”选项来测试脚本是否存在语法错误是一个很好的习惯。因为某些shell脚本在执行时会对系统环境产生影响,比如生成或移动文件等,如果在实际执行才发现语法错误,您不得不手工做一些系统环境的恢复工作才能继续测试这个脚本。
“-c”选项使shell解释器从一个字符串中而不是从一个文件中读取并执行shell命令。当需要临时测试一小段脚本的执行结果时,可以使用这个选项,如下所示:
sh -c ‘a=1;b=2;let c=$a+$b;echo “c=$c”‘
“-x”选项可用来跟踪脚本的执行,是调试shell脚本的强有力工具。“-x”选项使shell在执行脚本的过程中把它实际执行的每一个命令行显示出来,并且在行首显示一个”+”号。 “+”号后面显示的是经过了变量替换之后的命令行的内容,有助于分析实际执行的是什么命令。 “-x”选项使用起来简单方便,可以轻松对付大多数的shell调试任务,应把其当作首选的调试手段。

如果把本文前面所述的trap ‘command’ DEBUG机制与“-x”选项结合起来,我们就可以既输出实际执行的每一条命令,又逐行跟踪相关变量的值,对调试相当有帮助。
仍以前面所述的exp2.sh为例,现在加上“-x”选项来执行它:
$ sh –x exp2.sh
+ trap ‘echo “before execute line:$LINENO, a=$a,b=$b,c=$c”‘ DEBUG
++ echo ‘before execute line:3, a=,b=,c=’
before execute line:3, a=,b=,c=
+ a=1
++ echo ‘before execute line:4, a=1,b=,c=’
before execute line:4, a=1,b=,c=
+ ‘[' 1 -eq 1 ']‘
++ echo ‘before execute line:6, a=1,b=,c=’
before execute line:6, a=1,b=,c=
+ b=2
++ echo ‘before execute line:10, a=1,b=2,c=’
before execute line:10, a=1,b=2,c=
+ c=3
++ echo ‘before execute line:11, a=1,b=2,c=3′
before execute line:11, a=1,b=2,c=3
+ echo end
end

在上面的结果中,前面有“+”号的行是shell脚本实际执行的命令,前面有“++”号的行是执行trap机制中指定的命令,其它的行则是输出信息。
shell的执行选项除了可以在启动shell时指定外,亦可在脚本中用set命令来指定。 “set -参数”表示启用某选项,”set +参数”表示关闭某选项。有时候我们并不需要在启动时用”-x”选项来跟踪所有的命令行,这时我们可以在脚本中使用set命令,如以下脚本片段所示:
set -x    #启动”-x”选项
要跟踪的程序段
set +x     #关闭”-x”选项
set命令同样可以使用上一节中介绍的调试钩子—DEBUG函数来调用,这样可以避免脚本交付使用时删除这些调试语句的麻烦,如以下脚本片段所示:
DEBUG set -x    #启动”-x”选项
要跟踪的程序段
DEBUG set +x    #关闭”-x”选项
    
四. 对”-x”选项的增强
“-x”执行选项是目前最常用的跟踪和调试shell脚本的手段,但其输出的调试信息仅限于进行变量替换之后的每一条实际执行的命令以及行首的一个”+”号提示符,居然连行号这样的重要信息都没有,对于复杂的shell脚本的调试来说,还是非常的不方便。幸运的是,我们可以巧妙地利用shell内置的一些环境变量来增强”-x”选项的输出信息,下面先介绍几个shell内置的环境变量:

$LINENO
代表shell脚本的当前行号,类似于C语言中的内置宏__LINE__
$FUNCNAME
函数的名字,类似于C语言中的内置宏__func__,但宏__func__只能代表当前所在的函数名,而$FUNCNAME的功能更强大,它是一个数组变量,其中包含了整个调用链上所有的函数的名字,故变量${FUNCNAME[0]}代表shell脚本当前正在执行的函数的名字,而变量${FUNCNAME[1]}则代表调用函数${FUNCNAME[0]}的函数的名字,余者可以依此类推。
$PS4
主提示符变量$PS1和第二级提示符变量$PS2比较常见,但很少有人注意到第四级提示符变量$PS4的作用。我们知道使用“-x”执行选项将会显示shell脚本中每一条实际执行过的命令,而$PS4的值将被显示在“-x”选项输出的每一条命令的前面。在Bash Shell中,缺省的$PS4的值是”+”号。(现在知道为什么使用”-x”选项时,输出的命令前面有一个”+”号了吧?)。

利用$PS4这一特性,通过使用一些内置变量来重定义$PS4的值,我们就可以增强”-x”选项的输出信息。例如先执行export PS4=’+{$LINENO:${FUNCNAME[0]}} ‘, 然后再使用“-x”选项来执行脚本,就能在每一条实际执行的命令前面显示其行号以及所属的函数名。
以下是一个存在bug的shell脚本的示例,本文将用此脚本来示范如何用“-n”以及增强的“-x”执行选项来调试shell脚本。这个脚本中定义了一个函数isRoot(),用于判断当前用户是不是root用户,如果不是,则中止脚本的执行
$ cat –n exp4.sh
     1 #!/bin/bash
     2 isRoot()
     3 {
     4          if [ "$UID" -ne 0 ]
     5                  return 1
     6          else
     7                  return 0
     8          fi
     9 }
    10 isRoot
    11 if ["$?" -ne 0 ]
    12 then
    13          echo “Must be root to run this script”
    14          exit 1
    15 else
    16          echo “welcome root user”
    17          #do something
    18 fi

首先执行sh –n exp4.sh来进行语法检查,输出如下:
$ sh –n exp4.sh
exp4.sh: line 6: syntax error near unexpected token `else’
exp4.sh: line 6: `      else’
发现了一个语法错误,通过仔细检查第6行前后的命令,我们发现是第4行的if语句缺少then关键字引起的(写惯了C程序的人很容易犯这个错误)。我们可以把第4行修改为if [ "$UID" -ne 0 ]; then来修正这个错误。再次运行sh –n exp4.sh来进行语法检查,没有再报告错误。接下来就可以实际执行这个脚本了,执行结果如下:
$ sh exp4.sh
exp2.sh: line 11: [1: command not found
welcome root user

尽管脚本没有语法错误了,在执行时却又报告了错误。错误信息还非常奇怪“[1: command not found”。现在我们可以试试定制$PS4的值,并使用“-x”选项来跟踪:
$ export PS4='+{$LINENO:${FUNCNAME[0]}} ‘
$ sh –x exp4.sh
+{10:} isRoot
+{4:isRoot} ‘[' 503 -ne 0 ']‘
+{5:isRoot} return 1
+{11:} ‘[1' -ne 0 ']‘
exp4.sh: line 11: [1: command not found
+{16:} echo 'welcome root user'
welcome root user

从输出结果中,我们可以看到脚本实际被执行的语句,该语句的行号以及所属的函数名也被打印出来,从中可以清楚的分析出脚本的执行轨迹以及所调用的函数的内部执行情况。由于执行时是第11行报错,这是一个if语句,我们对比分析一下同为if语句的第4行的跟踪结果:
+{4:isRoot} '[' 503 -ne 0 ']‘
+{11:} ‘[1' -ne 0 ']‘

可知由于第11行的[号后面缺少了一个空格,导致[号与紧挨它的变量$?的值1被shell解释器看作了一个整体,并试着把这个整体视为一个命令来执行,故有“[1: command not found”这样的错误提示。只需在[号后面插入一个空格就一切正常了。
shell中还有其它一些对调试有帮助的内置变量,比如在Bash Shell中还有BASH_SOURCE, BASH_SUBSHELL等一批对调试有帮助的内置变量,您可以通过man sh或man bash来查看,然后根据您的调试目的,使用这些内置变量来定制$PS4,从而达到增强“-x”选项的输出信息的目的。
    
五. 总结
现在让我们来总结一下调试shell脚本的过程:
首先使用“-n”选项检查语法错误,然后使用“-x”选项跟踪脚本的执行,使用“-x”选项之前,别忘了先定制PS4变量的值来增强“-x”选项的输出信息,至少应该令其输出行号信息(先执行export PS4='+[$LINENO]‘,更一劳永逸的办法是将这条语句加到您用户主目录的.bash_profile文件中去),这将使你的调试之旅更轻松。也可以利用trap,调试钩子等手段输出关键调试信息,快速缩小排查错误的范围,并在脚本中使用“set -x”及“set +x”对某些代码块进行重点跟踪。这样多种手段齐下,相信您已经可以比较轻松地抓出您的shell脚本中的臭虫了。如果您的脚本足够复杂,还需要更强的调试能力,可以使用shell调试器bashdb,这是一个类似于GDB的调试工具,可以完成对shell脚本的断点设置,单步执行,变量观察等许多功能,使用bashdb对阅读和理解复杂的shell脚本也会大有裨益。关于bashdb的安装和使用,不属于本文范围,您可参阅http://bashdb.sourceforge.net/上的文档并下载试用。

分类: Linux专区 标签:

Linux下进程状态深入分析

2010年1月11日 没有评论

1)进程的状态的概述:

1.1)Running(R),运行或将要运行
1.2)Interruptible(S),被阻断而等待一个事件,可能会被一个信号激活
1.3)Uninterruptible(D),被阻断而等待一个事件,不会被信号激活
1.4)Stopped(T),由于任务的控制或者外部的追踪而被终止,比如:strace
1.5)Zombie(Z),僵死,但是它的父进程尚未调用wait函数.
1.6)Deal(X),这个永远看不见

在内核源代码中的定义如下:
=====================================================
/usr/src/linux/fs/proc/array.c
static const char *task_state_array[] = {
        “R (running)”,         
        “S (sleeping)”,        
        “D (disk sleep)”,      
        “T (stopped)”,         
        “T (tracing stop)”,    
        “Z (zombie)”,          
        “X (dead)”             
};
=====================================================

在ps命令的帮助中定义如下:

PROCESS STATE CODES
       Here are the different values that the s, stat and state output specifiers (header “STAT” or “S”) will display to
       describe the state of a process.
       D    Uninterruptible sleep (usually IO)
       R    Running or runnable (on run queue)
       S    Interruptible sleep (waiting for an event to complete)
       T    Stopped, either by a job control signal or because it is being traced.
       W    paging (not valid since the 2.6.xx kernel)
       X    dead (should never be seen)
       Z    Defunct (“zombie”) process, terminated but not reaped by its parent.

       For BSD formats and when the stat keyword is used, additional characters may be displayed:
       <    high-priority (not nice to other users)
       N    low-priority (nice to other users)
       L    has pages locked into memory (for real-time and custom IO)
       s    is a session leader
       l    is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
       +    is in the foreground process group
======================================================
2)分析不可被中断的睡眠进程:
2.1)重现:

终端1)
strace -o/tmp/dd.log dd if=/dev/zero f=/share/test count=65535 bs=65535

终端2)
more /tmp/dd.log
出现大量的write和read函数调用,即调用65535次,每次读写65535个字节
write(1, “\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0″…, 65535) = 65535
read(0, “\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0″…, 65535) = 65535
write(1, “\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0″…, 65535) = 65535
read(0, “\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0″…, 65535) = 65535
write(1, “\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0″…, 65535) = 65535
read(0, “\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0″…, 65535) = 65535

终端3)
ps aux|grep dd
root       313 16.9  0.0  3588  260 pts/1    D    00:12   0:10 dd if /dev/zero of test count 65535 bs 65535
2.2)分析:
系统进入这种不可中断是很少发生的,即使发生也是一个短暂的状态,引起这种状态的发生一般是驱动程序.
例如:驱动程序可能正在特殊的设备上等待通过检测的响应,但又要保证自己不在可中断睡眠状态(S)被中断.所以驱动程序会把进程切换到不可中断的睡眠状态,直到硬件已返回到已知状态.
可以通过访问一个慢设备来观察不可中断的睡眠状态,比如CDROM这样的设备
例如:
dd if=/dev/cdrom f=/dev/null &
进程在一个不可中断的状态是十分危险的,你不能用kill -9杀掉它
例如:
一个有问题的驱动程序访问一个有问题的设备,设备不给驱动程序响应,驱动程序永远得不到响应,而又永远等待响应.

3)分析被跟踪或被停止的进程状态(T)
3.1)重现被跟踪时的状态:

终端1)
strace top

终端2)
ps auxf|grep top
root       980  9.4  0.0  1716  608 pts/0    S    00:31   0:12  |       \_ strace top
root       981  3.7  0.1 10084 7076 pts/0    T    00:31   0:05  |           \_ top

在用strace跟踪top执行的时候,top进程为T的状态
3.2)重现被停止的进程状态:

停止进程有三种手段:
3.2.1)发送SIGTSTP信停止进程.
-SIGTSTP的信号相当于CTRL+Z的组合键来终止正在前台运行的进程.

终端1)
vi /etc/passwd

终端2)
kill -SIGTSTP 12029

查看进程状态:
ps auxf

root     10297  0.0  1.0   5124  2696 pts/0    Ss+  Dec16   0:00      \_ -bash
root     12029  0.0  0.8   5348  2200 pts/0    T    05:15   0:00      |   \_ vi test.c
终端1)
查看放到后台的作业
jobs
[1]+  Stopped                 vi test.c

用fg将作业切换到前台
fg
3.2.2)进程自已终止自己,标准输入引起进程停止

一个终端利用常规的后台和前台进程管理进程,一个终端有且只有一个前台进程,只有这个进程可以接受键盘的输入,其它任何开始于终端的进程都被认为是后台进程,但是当后台进程试图从标准办入读取数据时,终端将发送一个SIGTTIN终端它,因为这里只有一个输入设备键盘,只能用于前台进程.
这里的前后台进程概念仅限于终端的范围.

SIGTTIN 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN 信号. 缺省时这些进程会停止执行.
终端1)

尝试在后台运行read命令,因为后台进程不能从终端获取标准输入,所以进程将会收到信号SIGTTIN,使进程进入停止状态.
read x &
[1] 12057

[1]+  Stopped                 read x

终端2)
jobs
[1]+  Stopped                 read x
查看进程,12057这个PID就是read x&,现在看到是-bash,它的状态已经是T了
ps aux
root     12057  0.0  0.5   5124  1476 pts/0    T    05:26   0:00 -bash

用SIGCONT来唤醒
kill -SIGCONT 12057

终端1)
输入回车后,12057的进程依然会进入停止状态,也就是阻塞,只有会放到前台后,它才能完成输入.
fg
read x
hello

3.2.3)进程自已终止自己,标准输出引起进程停止

终端有一个tostop(终端输出终止)设置,在大多数系统里默认是关闭.
当是关闭的状态时,后台进程可以随时在终端写内容,如果是开启状态时,后台进程向标准输出写数据时就会被终止.
开启tostop
stty tostop

向标准输出写数据,被停止了
echo hello world &
[1] 12125

[1]+  Stopped                 echo hello world

jobs
[1]+  Stopped                 echo hello world

关闭tostop
stty -tostop

jobs
[1]+  Stopped                 echo hello world

向标准输出写数据恢复正常了
fg
echo hello world
hello world

4)分析进程的可中断睡眠态与运行态
 
编写一个小程序测试睡眠态与运行态之后的转换 :
=====================================================
#include <stdio.h>
#include <math.h>
#include <unistd.h>
#include <stdlib.h>

void
run_status(void)
{
    double pi = M_PI;
    double pisqrt;
    long i;
  
    for (i=0; i<100000000; ++i){
    pisqrt = sqrt(pi);
    }
}

int
main (void)
{
    run_status();   
    sleep(10);
    run_status();

    exit(EXIT_SUCCESS);
}
======================================================
编译链接后:
gcc -Wall -o pisqrt a.c -lm
终端1)
监控pisqrt进程
watch -n 1 “ps aux|grep pisqrt|grep -v ps|awk ‘{print $2}’|sort -k 8″
终端2)
strace ./pisqrt
显示如下:
read(3, “\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\200X\1″…, 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1572440, …}) = 0
old_mmap(NULL, 1279916, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x49e000
old_mmap(0x5d1000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0×132000) = 0x5d1000
old_mmap(0x5d4000, 10156, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x5d4000
close(3)                                = 0
set_thread_area({entry_number:-1 -> 6, base_addr:0xb75e3a60, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
munmap(0xb75e4000, 75579)               = 0

此时切换到终端1看pisqrt进程状态,此时为R状态:
root      3792 99.9  0.0  1516  268 pts/2    R    02:40   0:01 ./pisqrt    
root      3801  0.0  0.0  3700  672 pts/1    S    02:40   0:00 grep pisqrt
root      3791  1.0  0.0  1728  564 pts/2    S    02:40   0:00 strace ./pisqr

之后pisqrt进程进入S状态,因为执行了sleep(10)函数,10秒之后pisqrt再次进入R状态.最终退出.

分析:
pisqrt占用CPU时间片时状态为R,而在调用sleep函数后为S,系统大多数进程状态都为S,比如APACHE和ORACLE,
而处于S状态不一定是调用了sleep函数,因为IO也会让进程处于睡眠态.
而我们可以启动多个pisqrt程序,这时在系统中会有多个R状态的进程.也就是说CPU个各数与R进程是没有直接关联的.
5)分析进程的僵死态(Z)

当一个进程退出时,它并不是完全消失,而是等到它的父进程发出wait系统调用才会完全消失,除非父进程发出wait系统调用终止进程,
否则该进程将一直处于所谓的僵死状态,等待它的父进程终止它.如果父进程终止了运行而没有撤销子进程,那么这些进程将被进程init收养.
init进程定期调用wait来收养这些未被撤消的进程.

先制造一段僵尸程序,如下:
=============================
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int
main (){
    if(!fork()){
    printf(“child pid=%d\n”, getpid());
    exit(5);
}
    sleep(20);
    printf(“parent pid=%d\n”, getpid());
    exit(EXIT_SUCCESS);
}
===========================================
编译
gcc -Wall defunct.c -o defunct

终端1:
watch -n 1 “ps auxf|grep defunct|grep -v ps|grep -v grep|awk ‘{print $2}’|sort -k 8″
终端2:
执行./defunct

查看终端1:
root      7280  0.0  0.0  1380  240 pts/2    S    03:05   0:00  |       \_ ./defunct
root      7281  0.0  0.0     0    0 pts/2    Z    03:05   0:00  |           \_ [defunct <defunct>]

20秒后查看终端2:
child pid=7281
parent pid=7280

关于信号集的描述:/usr/include/bits/signum.h

#define SIGCLD          SIGCHLD
#define SIGCHLD         17     

在上面程序的基础上加入wait函数即可将SIGCHLD信号回收

修改后的程序如下:
=================================
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int
main (){
    int status,i;
    if(!fork()){
    printf(“child pid=%d\n”, getpid());
    exit(5);
}
    wait(&status);
    i = WEXITSTATUS(status);
    sleep(20);
    printf(“parent pid=%d,child process exit/status=%d\n”, getpid(),i);
    exit(EXIT_SUCCESS);
}
==========================================
6)最后的进程X (dead)
指死掉的进程

最后4种附加的状态….
W状态:不驻留内存
<状态:nice小于0
N状态:nice大于0
L状态:有锁住的页面

这部分在ps的源代码(output.c)有描述:
===================================
static int
pr_stat(void)
{
        int end = 0;
        outbuf[end++] = pp->state;
        if (pp->rss == 0 && pp->state != ‘Z’)
                outbuf[end++] = ‘W’;
        if (pp->nice < 0)
                outbuf[end++] = ‘<’;
        if (pp->nice > 0)
                outbuf[end++] = ‘N’;
        if (pp->vm_lock)
                outbuf[end++] = ‘L’;
        outbuf[end] = ‘\0′;
        return end;
}
=====================================

分类: Linux专区 标签:

Linux TCP/IP协议栈笔记(3)

2009年11月14日 没有评论

五、队列层

1、软中断与下半部
当用中断处理的时候,为了减少中断处理的工作量,比如,一般中断处理时,需要屏蔽其它中断,如果中断处理时间过长,那么其它中断
有可能得不到及时处理,也以,有一种机制,就是把“不必马上处理”的工作,推迟一点,让它在中断处理后的某一个时刻得到处理。这就
是下半部。

下半部只是一个机制,它在Linux中,有多种实现方式,其中一种对时间要求最严格的实现方式,叫“软中断”,可以使用:

open_softirq()

来向内核注册一个软中断,
然后,在合适的时候,调用

raise_softirq_irqoff()

触发它。

如果采用中断方式接收数据(这一节就是在说中断方式接收,后面,就不用这种假设了),同样也需要软中断,可以调用

open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);

向内核注册一个名为NET_RX_SOFTIR的软中断,net_rx_action是软中断的处理函数。

然后,在驱动中断处理完后的某一个时刻,调用

raise_softirq_irqoff(NET_RX_SOFTIRQ);

触发它,这样net_rx_action将得到执行。

2、队列层
什么是队列层?通常,在网卡收发数据的时候,需要维护一个缓冲区队列,来缓存可能存在的突发数据,类似于前面的DMA环形缓冲区。
队列层中,包含了一个叫做struct softnet_data:

struct softnet_data
{

int throttle;

int cng_level;
int avg_blog;

struct sk_buff_head input_pkt_queue;

struct list_head poll_list;
struct net_device *output_queue;
struct sk_buff *completion_queue;

struct net_device backlog_dev;
};

内核使用了一个同名的变量softnet_data,它是一个Per-CPU变量,每个CPU都有一个。

net/core/dev.c

DECLARE_PER_CPU(struct
softnet_data,softnet_data);

 

static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;

BUG_ON(!dev_boot_phase);

net_random_init();

if (dev_proc_init())
goto out;

if (netdev_sysfs_init())
goto out;

INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < 16; i++)
INIT_LIST_HEAD(&ptype_base[i]);

for (i = 0; i < ARRAY_SIZE(dev_name_head);
i++)
INIT_HLIST_HEAD(&dev_name_head[i]);

for (i = 0; i < ARRAY_SIZE(dev_index_head);
i++)
INIT_HLIST_HEAD(&dev_index_head[i]);

for (i = 0; i < NR_CPUS; i++) {
struct softnet_data *queue;

queue = &per_cpu(softnet_data, i);

skb_queue_head_init(&queue->input_pkt_queue);

queue->throttle = 0;
queue->cng_level = 0;
queue->avg_blog = 10;
queue->completion_queue = NULL;
INIT_LIST_HEAD(&queue->poll_list);

set_bit(__LINK_STATE_START,
&queue->backlog_dev.state);
queue->backlog_dev.weight = weight_p;

queue->backlog_dev.poll = process_backlog;
atomic_set(&queue->backlog_dev.refcnt,
1);
}

#ifdef OFFLINE_SAMPLE
samp_timer.expires = jiffies + (10 * HZ);
add_timer(&samp_timer);
#endif

dev_boot_phase = 0;

open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);

hotcpu_notifier(dev_cpu_callback, 0);
dst_init();
dev_mcast_init();
rc = 0;
out:
return rc;
}

这样,初始化完成后,在驱动程序中,在中断处理函数中,会调用netif_rx将数据交上来,这与采用轮询技术,有本质的不同:

int netif_rx(struct sk_buff *skb)
{
int this_cpu;
struct softnet_data *queue;
unsigned long flags;

if (netpoll_rx(skb))
return NET_RX_DROP;

if (!skb->stamp.tv_sec)
net_timestamp(&skb->stamp);

local_irq_save(flags);

this_cpu = smp_processor_id();
queue = &__get_cpu_var(softnet_data);

__get_cpu_var(netdev_rx_stat).total++;

if (queue->input_pkt_queue.qlen <=
netdev_max_backlog) {
if (queue->input_pkt_queue.qlen) {
if (queue->throttle)
goto drop;

enqueue:
dev_hold(skb->dev);
__skb_queue_tail(&queue->input_pkt_queue,
skb);
#ifndef OFFLINE_SAMPLE
get_sample_stats(this_cpu);
#endif
local_irq_restore(flags);
return queue->cng_level;
}

if (queue->throttle)
queue->throttle = 0;

netif_rx_schedule(&queue->backlog_dev);

goto enqueue;
}

if (!queue->throttle) {
queue->throttle = 1;
__get_cpu_var(netdev_rx_stat).throttled++;
}

drop:
__get_cpu_var(netdev_rx_stat).dropped++;
local_irq_restore(flags);

kfree_skb(skb);
return NET_RX_DROP;
}


这段代码的分析中,我们可以看到,当第一个数据包被接收后,因为qlen==0,所以首先会调用netif_rx_schedule触发软中断,然后利用
goto跳转至入队。因为软中断被触发后,将执行出队操作,把数据交往上层处理。而当这个时候,又有数据包进入,即网卡中断产生,因为它的优先级高过软中
断,这样,出队操作即被中断,网卡中断程序再将被调用,netif_rx函数又再次被执行,如果队列未满,就入队返回。中断完成后,软中断的执行过程被恢
复而继续执行出队——如此生产者/消费者循环不止,生生不息……

netif_rx调用netif_rx_schedule进一步处理数据包,我们注意到:
1、前面讨论过,采用轮询技术时,同样地,也是调用netif_rx_schedule,把设备自己传递了过去;
2、这里,采用中断方式,传递的是队列中的一个“伪设备”,并且,这个伪设备的poll函数指针,指向了一个叫做process_backlog的函数;

netif_rx_schedule函数完成两件重要的工作:
1、将bakclog_dev设备加入“处理数据包的设备”的链表当中;
2、触发软中断函数,进行数据包接收处理;

这样,我们可以猜想,在软中断函数中,不论是伪设备bakclog_dev,还是真实的设备(如前面讨论过的e100),都会被软中断函数以:

dev->poll()
的形式调用,对于e100来说,poll函数的接收过程已经分析了,而对于其它所有没有采用轮询技术的网络设备来说,它们将统统调用

process_backlog函数(我觉得把它改名为pseudo-poll是否更合适一些^o^)。

OK,我想分析到这里,关于中断处理与轮询技术的差异,已经基本分析开了……

继续来看,netif_rx_schedule进一步调用__netif_rx_schedule:

static inline void netif_rx_schedule(struct net_device *dev)
{
if (netif_rx_schedule_prep(dev))
__netif_rx_schedule(dev);
}

 

static inline void __netif_rx_schedule(struct net_device
*dev)
{
unsigned long flags;

local_irq_save(flags);
dev_hold(dev);

list_add_tail(&dev->poll_list,
&__get_cpu_var(softnet_data).poll_list);
if (dev->quota < 0)
dev->quota += dev->weight;
else
dev->quota = dev->weight;

__raise_softirq_irqoff(NET_RX_SOFTIRQ);
local_irq_restore(flags);
}

软中断被触发,注册的net_rx_action函数将被调用:

static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *queue =
&__get_cpu_var(softnet_data);
unsigned long start_time = jiffies;
int budget = netdev_max_backlog;

local_irq_disable();

while
(!list_empty(&queue->poll_list))
{
struct net_device *dev;

if (budget <= 0 || jiffies – start_time
> 1)
goto softnet_break;

local_irq_enable();

dev = list_entry(queue->poll_list.next,
    struct
net_device, poll_list);
netpoll_poll_lock(dev);

if (dev->quota <= 0 ||
dev->poll(dev, &budget)) {
netpoll_poll_unlock(dev);

local_irq_disable();
list_del(&dev->poll_list);
list_add_tail(&dev->poll_list,
&queue->poll_list);
if (dev->quota < 0)
dev->quota += dev->weight;
else
dev->quota = dev->weight;
} else {
netpoll_poll_unlock(dev);
dev_put(dev);
local_irq_disable();
}
}
out:
local_irq_enable();
return;

softnet_break:
__get_cpu_var(netdev_rx_stat).time_squeeze++;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
}

对于dev->poll(dev,
&budget)的调用,一个真实的poll函数的例子,我们已经分析过了,现在来看process_backlog,

static int process_backlog(struct net_device
*backlog_dev, int *budget)
{
int work = 0;
int quota = min(backlog_dev->quota, *budget);
struct softnet_data *queue =
&__get_cpu_var(softnet_data);
unsigned long start_time = jiffies;

backlog_dev->weight = weight_p;

for (;;) {
struct sk_buff *skb;
struct net_device *dev;

local_irq_disable();
skb =
__skb_dequeue(&queue->input_pkt_queue);

if (!skb)
goto job_done;
local_irq_enable();

dev = skb->dev;

netif_receive_skb(skb);

dev_put(dev);

work++;

if (work >= quota || jiffies – start_time
> 1)
break;

}

backlog_dev->quota -= work;
*budget -= work;
return -1;

job_done:
backlog_dev->quota -= work;
*budget -= work;

list_del(&backlog_dev->poll_list);

smp_mb__before_clear_bit();
netif_poll_enable(backlog_dev);

if (queue->throttle)
queue->throttle = 0;
local_irq_enable();
return 0;
}

这个函数重要的工作,就是出队,然后调用netif_receive_skb()将数据包交给上层,这与上一节讨论的poll是一样的。这也是为什么,

在网卡驱动的编写中,采用中断技术,要调用netif_rx,而采用轮询技术,要调用netif_receive_skb啦!

到了这里,就处理完数据包与设备相关的部分了,数据包将进入上层协议栈……  

分类: Linux专区 标签:

Linux TCP/IP协议栈笔记(2)

2009年11月14日 没有评论

四、网卡的数据接收

内核如何从网卡接受数据,传统的经典过程:
1、数据到达网卡;
2、网卡产生一个中断给内核;
3、内核使用I/O指令,从网卡I/O区域中去读取数据;

我们在许多网卡驱动中,都可以在网卡的中断函数中见到这一过程。

但是,这一种方法,有一种重要的问题,就是大流量的数据来到,网卡会产生大量的中断,内核在中断上下文中,会浪费大量的资源来处理中断本身。所以,一个问
题是,“可不可以不使用中断”,这就是轮询技术,所谓NAPI技术,说来也不神秘,就是说,内核屏蔽中断,然后隔一会儿就去问网卡,“你有没有数据
啊?”……

从这个描述本身可以看到,哪果数据量少,轮询同样占用大量的不必要的CPU资源,大家各有所长吧,呵呵……

OK,另一个问题,就是从网卡的I/O区域,包括I/O寄存器或I/O内存中去读取数据,这都要CPU去读,也要占用CPU资源,“CPU从I/O区域
读,然后把它放到内存(这个内存指的是系统本身的物理内存,跟外设的内存不相干,也叫主内存)中”。于是自然地,就想到了DMA技术——让网卡直接从主内
存之间读写它们的I/O数据,CPU,这儿不干你事,自己找乐子去:
1、首先,内核在主内存中为收发数据建立一个环形的缓冲队列(通常叫DMA环形缓冲区)。
2、内核将这个缓冲区通过DMA映射,把这个队列交给网卡;
3、网卡收到数据,就直接放进这个环形缓冲区了——也就是直接放进主内存了;然后,向系统产生一个中断;
4、内核收到这个中断,就取消DMA映射,这样,内核就直接从主内存中读取数据;

——呵呵,这一个过程比传统的过程少了不少工作,因为设备直接把数据放进了主内存,不需要CPU的干预,效率是不是提高不少?

对应以上4步,来看它的具体实现:
1、分配环形DMA缓冲区
Linux内核中,用skb来描述一个缓存,所谓分配,就是建立一定数量的skb,然后把它们组织成一个双向链表;

2、建立DMA映射
内核通过调用
dma_map_single(struct device *dev,void *buffer,size_t size,enum
dma_data_direction direction)
建立映射关系。
struct device *dev,描述一个设备;
buffer:把哪个地址映射给设备;也就是某一个skb——要映射全部,当然是做一个双向链表的循环即可;
size:缓存大小;
direction:映射方向——谁传给谁:一般来说,是“双向”映射,数据在设备和内存之间双向流动;

对于PCI设备而言(网卡一般是PCI的),通过另一个包裹函数pci_map_single,这样,就把buffer交给设备了!设备可以直接从里边读/取数据。

3、这一步由硬件完成;

4、取消映射
dma_unmap_single,对PCI而言,大多调用它的包裹函数pci_unmap_single,不取消的话,缓存控制权还在设备手里,要调用它,把主动权掌握在CPU手里——因为我们已经接收到数据了,应该由CPU把数据交给上层网络栈;

当然,不取消之前,通常要读一些状态位信息,诸如此类,一般是调用
dma_sync_single_for_cpu()
让CPU在取消映射前,就可以访问DMA缓冲区中的内容。

关于DMA映射的更多内容,可以参考《Linux设备驱动程序》“内存映射和DMA”章节相关内容!

OK,有了这些知识,我们就可以来看e100的代码了,它跟上面讲的步骤基本上一样的——绕了这么多圈子,就是想绕到e100上面了,呵呵!

在e100_open函数中,调用e100_up,我们前面分析它时,略过了一个重要的东东,就是环形缓冲区的建立,这一步,是通过

e100_rx_alloc_list函数调用完成的:

static int e100_rx_alloc_list(struct nic
*nic)
{
struct rx *rx;
unsigned int i, count =
nic->params.rfds.count;

nic->rx_to_use = nic->rx_to_clean =
NULL;
nic->ru_running = RU_UNINITIALIZED;

if(!(nic->rxs = kmalloc(sizeof(struct rx) * count,
GFP_ATOMIC)))
return -ENOMEM;
memset(nic->rxs, 0, sizeof(struct rx) *
count);

for(rx = nic->rxs, i = 0; i < count;
rx++, i++) {
rx->next = (i + 1 < count) ? rx + 1 :
nic->rxs;
rx->prev = (i == 0) ? nic->rxs +
count – 1 : rx – 1;
if(e100_rx_alloc_skb(nic, rx)) {
e100_rx_clean_list(nic);
return -ENOMEM;
}
}

nic->rx_to_use = nic->rx_to_clean =
nic->rxs;
nic->ru_running = RU_SUSPENDED;

return 0;
}

#define RFD_BUF_LEN (sizeof(struct rfd) +
VLAN_ETH_FRAME_LEN)
static inline int e100_rx_alloc_skb(struct nic *nic, struct rx
*rx)
{

if(!(rx->skb = dev_alloc_skb(RFD_BUF_LEN +
NET_IP_ALIGN)))
return -ENOMEM;

rx->skb->dev =
nic->netdev;
skb_reserve(rx->skb, NET_IP_ALIGN);

memcpy(rx->skb->data,
&nic->blank_rfd, sizeof(struct
rfd));

rx->dma_addr =
pci_map_single(nic->pdev,
rx->skb->data,
RFD_BUF_LEN, PCI_DMA_BIDIRECTIONAL);

if(pci_dma_mapping_error(rx->dma_addr)) {
dev_kfree_skb_any(rx->skb);
rx->skb = 0;
rx->dma_addr = 0;
return -ENOMEM;
}

if(rx->prev->skb) {
struct rfd *prev_rfd = (struct rfd
*)rx->prev->skb->data;

put_unaligned(cpu_to_le32(rx->dma_addr),
(u32 *)&prev_rfd->link);
wmb();
prev_rfd->command &=
~cpu_to_le16(cb_el);
pci_dma_sync_single_for_device(nic->pdev,
rx->prev->dma_addr,
sizeof(struct rfd), PCI_DMA_TODEVICE);
}

return 0;
}

e100_rx_alloc_list函数在一个循环中,建立了环形缓冲区,并调用e100_rx_alloc_skb为每个缓冲区分配了空间,并做了

DMA映射。这样,我们就可以来看接收数据的过程了。

前面我们讲过,中断函数中,调用netif_rx_schedule,表明使用轮询技术,系统会在未来某一时刻,调用设备的poll函数:

static int e100_poll(struct net_device
*netdev, int *budget)
{
struct nic *nic = netdev_priv(netdev);
unsigned int work_to_do = min(netdev->quota,
*budget);
unsigned int work_done = 0;
int tx_cleaned;

e100_rx_clean(nic, &work_done, work_to_do);
tx_cleaned = e100_tx_clean(nic);

if((!tx_cleaned && (work_done ==
0)) || !netif_running(netdev)) {
netif_rx_complete(netdev);
e100_enable_irq(nic);
return 0;
}

*budget -= work_done;
netdev->quota -= work_done;

return 1;
}

目前,我们只关心rx,所以,e100_rx_clean函数就成了我们关注的对像,它用来从缓冲队列中接收全部数据(这或许是取名为clean的原因吧!):

static inline void e100_rx_clean(struct nic
*nic, unsigned int *work_done,
unsigned int work_to_do)
{
struct rx *rx;
int restart_required = 0;
struct rx *rx_to_start = NULL;

if(RU_SUSPENDED == nic->ru_running)
restart_required = 1;

for(rx = nic->rx_to_clean; rx->skb;
rx = nic->rx_to_clean = rx->next)
{
int err = e100_rx_indicate(nic, rx, work_done, work_to_do);
if(-EAGAIN == err) {

restart_required = 0;
break;
} else if(-ENODATA == err)
break;
}

if(restart_required)
rx_to_start = nic->rx_to_clean;

for(rx = nic->rx_to_use; !rx->skb; rx
= nic->rx_to_use = rx->next) {
if(unlikely(e100_rx_alloc_skb(nic, rx)))
break;
}

if(restart_required) {
// ack the rnr?
writeb(stat_ack_rnr,
&nic->csr->scb.stat_ack);

e100_start_receiver(nic, rx_to_start);
if(work_done)
(*work_done)++;
}
}

static inline int e100_rx_indicate(struct
nic *nic, struct rx *rx,
unsigned int *work_done, unsigned int work_to_do)
{
struct sk_buff *skb = rx->skb;
struct rfd *rfd = (struct rfd *)skb->data;
u16 rfd_status, actual_size;

if(unlikely(work_done && *work_done
>= work_to_do))
return -EAGAIN;

pci_dma_sync_single_for_cpu(nic->pdev,
rx->dma_addr,
sizeof(struct rfd), PCI_DMA_FROMDEVICE);
rfd_status = le16_to_cpu(rfd->status);

DPRINTK(RX_STATUS, DEBUG, “status=0x%04X\n”, rfd_status);

if(unlikely(!(rfd_status & cb_complete)))
return -ENODATA;

actual_size = le16_to_cpu(rfd->actual_size)
& 0x3FFF;
if(unlikely(actual_size > RFD_BUF_LEN -
sizeof(struct rfd)))
actual_size = RFD_BUF_LEN – sizeof(struct rfd);

pci_unmap_single(nic->pdev,
rx->dma_addr,
RFD_BUF_LEN, PCI_DMA_FROMDEVICE);

if(le16_to_cpu(rfd->command) &
cb_el)
nic->ru_running = RU_SUSPENDED;

skb_reserve(skb, sizeof(struct rfd));

skb_put(skb, actual_size);

skb->protocol = eth_type_trans(skb,
nic->netdev);

if(unlikely(!(rfd_status & cb_ok))) {

nic->net_stats.rx_dropped++;
dev_kfree_skb_any(skb);
} else if(actual_size >
nic->netdev->mtu + VLAN_ETH_HLEN)
{

nic->rx_over_length_errors++;
nic->net_stats.rx_dropped++;
dev_kfree_skb_any(skb);
} else {

nic->net_stats.rx_packets++;
nic->net_stats.rx_bytes += actual_size;
nic->netdev->last_rx = jiffies;
netif_receive_skb(skb);
if(work_done)
(*work_done)++;
}

rx->skb = NULL;

return 0;
}

网卡驱动执行到这里,数据接收的工作,也就处理完成了。但是,使用这一种方法的驱动,省去了网络栈中一个重要的内容,就是
“队列层”,让我们来看看,传统中断接收数据包模式下,使用netif_rx函数调用,又会发生什么。

PS:九贱没有去研究过所谓的“零拷贝”技术,不太清楚,它同这种DMA直取方式有何不同?难道是把网卡中的I/O内存直接映射到主内存中,这样CPU就
可以像读取主内存一样,读取网卡的内存,但是这要求设备要有好大的I/O内存来做缓冲呀!!^o^,外行了……希望哪位DX提点!

分类: Linux专区 标签: