kernel module编程(七):通过读取proc文件进行debug

  本文也即《Linux Device Drivers》,LDD3的第四章Debuging Techniques的读书笔记之二,但我们不限于此内容。

  在linux中,例如读取CPU,可以使用cat /proc/cpuinfo,通过这个我们可以在程序中采用读文件的方式获取CPU,这种大容量高性能的服务中非常常用,例如在cpu大于60%的时候,我们将拒绝所有的业务请求,直至cpu恢复到40%一下。我们可以根据此进行多级CPU过载保护,这在电信基本的系统中非常常用。由于采用读取CPU的方式,也满足JAVA程序的获取。如果我们开发一个长期运行的稳定的业务系统,过载保护是应当给予考虑。 【编程思想1:CPU过载保护】

  /proc是特殊的文件系统,通过软件创建,由kernel给出信息。我们之前通过printk给出调测信息,虽然通过宏定义,在编译的时候觉得是否给出printk或者给出某部分的printk,但是一旦模块加载后,printk也就定下来。不能够做到需要时给出,不需要是不要去写/var/log/messages文件。使用/proc文件系统可以满足这个要求。我们在需要的时候去获取信息。但是这样做是有风险的,例如我们在读取的同时卸载模块,有或者两个不同模块使用同一/proc/filename来进行信息输出。而书中的作者更是给了一个ugly的例子(作者自己原话:somewhat ugly),为了跑通,花费了我很多的时间,而examples(可以通过Google检索ldd3 examples得到随书光盘的例子)有误导,让我兜了很多圈子。

  我们通过下面函数创建和删除/proc下面文件,文件名为scullmems,他们分别在加载和卸载模块时候调用。

create_proc_read_entry ("scullmem"/* 文件名 */ ,
      0 /* default mode */ ,
      NULL /* parent dir,NULL表示缺省在/proc路径下 */ ,
      scull_read_procmem/* 读取proc文件时调用的函数 */ ,
      NULL /* client data */ );
remove_proc_entry ("scullmem", NULL /* parent dir */ );

  关键是如何写scull_read_procmem,他的结构是int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data); 其中返回值是char ** start和int *eof,void *data是创建文件时携带的参数,kernel不处理,但会在出发调用时传递,我们可以利用来携带某些信息,其他的都是输入值。

  如果内核模块返回的信息,大于允许存放的最大空间,就好出现cat /proc/scullmems是触发多次该函数的调用,这一点不仅麻烦,而且需要非常小心,LDD3建议使用self_file(还没看到)而不是这种方式,这可能就是作者自嘲ugly的原因。但是我觉得真正ugly的是example中的sculld的例子,将buff前向放内容,然后转为后向存放数据,搞得我迷惑了很久。在scull的例子中,我的机器page大小为1024字节,所有信息不能一次传递完成,需要多次,而文章对这部分的处理说明语焉不详。下面是我实践得到的经验,可能描述得不一定十分准确,但是管用。来看一个实验例子,我们不去处理那个复杂的scull数组结构 ,只是希望输出一定的信息,增加了printk用来进行跟踪:

static int my_index = 0,total_index = 10;
int scull_read_procmem(char * buf, char ** start, off_t offset, int count, int * eof, void *data)
{
        int i ,len = 0;
        int limit = count - 80;
        printk("==============buf %p *start %p offset %li len %d eof %d count %i limit %i /n",
         buf,*start,offset,len, * eof, count,limit);

        if(my_index >= total_index){
                printk("=_= return len = %i limit = %d eof = %d /n",len,limit, * eof);
                return 0;
        }
        /* if(offset > count /2){
                * start = buf;
        }else{
                buf = buf + offset;
                * start = buf;
                limit = count - offset -80;
        }*/

        if(offset > 0)
                * start = buf;

        printk("===buf %p *start %p offset %li len %d eof %d count %i limit %i /n",
         buf,*start,offset,len, * eof, count,limit);
        for(i = my_index; i< total_index && len <= limit; i ++){
                printk( "%03d len=%d 12345678901234567890123456789901234567890/n",i,len);
                len += sprintf(buf + len,"%03d len=%d 12345678901234567890123456789901234567890/n",
                 i,len);
                my_index ++;
        }
      
       * eof = 1;
        printk("=== return len = %i limit = %d eof = %d index = %d /n",len,limit, * eof, my_index);
        return len;
}

  第一个参数char * buf,给出的输出信息的buf位置,count表示表示buf空间的大小,通常是根据page来进行分配,在我的机器中,count=1024。

  关键之一是我们需要告诉用户,是否这次已经读完,还是需要继续读,这个在LDD3中说明不太清楚。这和返回值以及返回参数* eof有关。*eof的缺省输入值为0,如果我们需要告诉用户信息尚未读完,仍需要进一步读取,我们设置*eof为1,并且返回本次读出的内容长度。如果这次读取能够读到信息,即返回有效长度,在我的实验中无论eof设置为何值,都会进行下一次调用。只要我们设置* eof = 0并return 0,表示已无进一步的信息。在我们的实验中,有以下的规律

  • 连续两次返回值为0,不再读取
  • 当* eof 且和上次有效返回是的数值不一样(且上次不能为0),并返回值为0,不再读取。
  • 如果有效进行读取,我们应设置*eof为一个正整数。

 

  offset的理解有些意思,我开始理解为buf中的偏移量,但是我们是一次一次地输出信息,如果上一次的输出如果还占据buf的空间,我们就无法有足够的空间给出新的信息。对于多次输出,offset实际是已经有效读取的内容长度,他是一个累计的数值。作为的偏移量不是指读取buf的 偏移量,而是指我们输出信息的偏移量,即已完成读取的长度。

  在上面的例子中,为了保证存放在有效的buf空间内,我们假定每一行的输出不会超过80字节,因此我们每次写入(sprintf)的时候都要判断,是否有足够的空间。

  char ** start用于大量数据,需要多次读取,当offset不为0的时候,即表示不是第一次读时,我们应该指出数据存放的起始位置,即* start。一般来讲,系统会使用同一个buf来读取,在非第一次的读取中,我们可以选择在buf+offset的位置上开始存放,直至buf满,我们也可以自由设定我们存放数据的起始位置。例如在上面的例子中,可以修改为每次只有一条信息就返回,而不是试图读多次。简单来讲在非第一次读取时,我们都应该设置* start,指明初始位置。对于第一次读取,缺省从buf的第一个字节开始,可能设置也可以把不设置*start的值。

   下面是total_index = 100的dmesg的部分输入结果:

==============buf effb1000 *start 00000000 offset 0 len 0 eof 0 count 1024 limit 944
===buf effb1000 *start effb1000 offset 0 len 0 eof 0 count 1024 limit 944
000 len=0 12345678901234567890123456789901234567890
001 len=52 12345678901234567890123456789901234567890
002 len=105 12345678901234567890123456789901234567890
003 len=159 12345678901234567890123456789901234567890
004 len=213 12345678901234567890123456789901234567890
005 len=267 12345678901234567890123456789901234567890
006 len=321 12345678901234567890123456789901234567890
007 len=375 12345678901234567890123456789901234567890
008 len=429 12345678901234567890123456789901234567890
... ...
017 len=915 12345678901234567890123456789901234567890
=== return len = 969 limit = 944 eof = 1 index = 18
==============buf effb1000 *start 00000000 offset 969 len 0 eof 0 count 1024 limit 944
===buf effb1000 *start effb1000 offset 969 len 0 eof 0 count 1024 limit 944
018 len=0 12345678901234567890123456789901234567890
... ....
035 len=915 12345678901234567890123456789901234567890
=== return len = 969 limit = 944 eof = 1 index = 36
==============buf effb1000 *start 00000000 offset 1938 len 0 eof 0 count 1024 limit 944
===buf effb1000 *start effb1000 offset 1938 len 0 eof 0 count 1024 limit 944
036 len=0 12345678901234567890123456789901234567890

... ....

089 len=915 12345678901234567890123456789901234567890
=== return len = 969 limit = 944 eof = 1 index = 90
==============buf effb1000 *start 00000000 offset 4845 len 0 eof 0 count 1024 limit 944
===buf effb1000 *start effb1000 offset 4845 len 0 eof 0 count 1024 limit 944
090 len=0 12345678901234567890123456789901234567890
... ...
099 len=483 12345678901234567890123456789901234567890
=== return len = 537 limit = 944 eof = 1 index = 100
==============buf effb1000 *start 00000000 offset 5382 len 0 eof 0 count 1024 limit 944
=_= return len = 0 limit = 944 eof = 0

我们下面给出scull内存信息的例子,如果设备SCULLx的scull_qset中含有数据,我们就在qset队列中每一个quantum的位置显示出来,包括没有分配的quantum(显示0,这样一般都需要多次读取)。对于多次读取,最大的问题是,我们需要记住上次已经读取了多少信息,即这次应当从那个开始读取。在LDD3的上一章中说到goto语句在内核程序是比较常见的,这和我们在学习编程的时候,将goto骂得狗血淋头的情况不一样,这和kernel的事件触发机制有关,在这个例子中当本次读取buffer快满的时候,我们通过goto给出统一的出口,这样整个程序显得整洁和易读。

static int store_dev_num = 0, store_qs_num = 0 , store_quan_num = 0;

int scull_read_procmem(char * buf, char ** start, off_t offset, int count, int * eof, void *data)
{
        int i, j, len = 0, k = 0;
        int limit = count - 80;

        /*如果已经全部读完,store_dev_num将等于SCULL_DEV_NUM,我们使用下面这三个参数,分别记录正在读取哪个设备,读到哪个qset,以及qset中的哪个quantum*/
        if(store_dev_num >= SCULL_DEV_NUM){
                store_dev_num = 0;
                store_qs_num  = 0;
                store_quan_num = 0;
                return 0;
        }
        /* 用于多次读取*/
        if(offset > 0){
                * start = buf ;
        }

        for(i = store_dev_num ; i < SCULL_DEV_NUM && len <= limit ; i ++){
                struct scull_dev * dev = & mydev[i];
                struct scull_qset * qs = dev->data;
                if( len > limit)
                       
goto buffer_full;

                if(down_interruptible(&dev->sem))
                        return -ERESTARTSYS;
                if(! store_qs_num  && ! store_quan_num ){
                        len += sprintf(buf + len, "/n Device Scull%d: qset %i, q %i sz %li/n",
                         i,dev->qset, dev->quantum, dev->size);
                }
                k = 0;

                for(; qs ; qs=qs->next, k++){
                        if(k < store_qs_num)
                                continue;
                        if(len > limit){
                                up(&dev->sem);
                                goto buffer_full;
                        }

                        if(!store_quan_num ){
                                len +=  sprintf(buf + len ,"  item at %p, qset %d at %p/n", qs, k++, qs->data);
                        }

                        if(qs->data ){
                                j = store_quan_num ;

                                /*下面注释的部分用于全部显示队列中的quantum位置,已保证输出信息足够,
                                 * 而最后程序将恢复,只显示有效部分。*/

                                for(; j < dev->qset /* && qs->data[ j ] */ ;j++){
                                        if(len > limit){
                                                up(&dev->sem);
                                                goto buffer_full;
                                        }

                                        len +=  sprintf(buf + len, "/t%4i:%8p/n",  j,qs->data[ j ]);
                                        store_quan_num ++;

                                }
                                store_quan_num = 0;
                        }
                        store_qs_num ++;
                }
                up(&dev->sem);

                store_dev_num ++;
                store_qs_num  = 0;
        }

buffer_full:
        * eof = 1;
        return len;
}

从上面代码看,有一个潜在的危险,就是读取信息过程中,如果正在对scull设备进行写操作。在一次读写完成后,我们释放了信号量,等待下一次读写,在这个过程中,信号量可能会被写操作所占有。这个例子,我们只是读取位置信息,即使出现问题,也不会有太多的影响,但是在实际应用过程中,我们应当在一次信号量持有中完成对某设备的操作 ,例如考虑缓存没有读取的信息,等待下次读取等等,或者尽量减少不必要的信息。【编程思想2:信号量持有和操作】

相关技术文章:
我的与kernel module有关的文章
我的与编程思想相关的文章

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值