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:信号量持有和操作】

你可能感兴趣的:(数据结构,编程,linux,读书,电信)