4.3 查询调试
驱动程序开发者可以使用一些查询技术来调试程序,比如说:
/proc
创建文件;- 使用驱动程序的ioctl方法;
sysfs
导出属性;
使用sysfs
需要有驱动程序模型的背景知识,所以这将在第14章讨论。
4.3.1 /proc
/proc
文件系统是一个特殊的,由软件创建的文件系统,内核用它将信息导出到外界。/proc
下的每个文件都与内核函数绑定在一起,当用户读取其中的文件时,该函数动态地生成文件的“内容”。例如,/proc/modules
总是返回当前已加载模块的列表。
Linux
系统中,大量使用/proc
。许多工具,比如ps
,top
,uptime
,都是通过/proc
获取它们所需的信息。设备驱动程序当然也可以从/proc
导出信息。/proc
文件系统是动态的,因此模块程序可以随时添加、删除文件。
完整的/proc
文件是非常复杂的,并且是可读写的。但是大部分时候,/proc
中的文件是只读的。本文中,假设都是只读的。如果想要实现更为复杂的/proc
文件,请参阅linux内核源代码。
但是,不鼓励在/proc
下添加文件,推荐使用方式是sysfs
。但是,使用sysfs
需要了解Linux设备模型,这个到第14章,我们才会讨论。同时,在/proc
中创建文件又是非常简易的,且符合调试的目的,因此,我们先讨论它。
4.3.1.1 /proc实现文件
所有使用/proc
的模块,必须包含头文件<linux/proc_fs.h>
。
为了创建一个只读的/proc
文件,驱动程序必须实现一个函数,当该文件被读时,产生数据。当某个进程读这个文件时(调用read
系统调用),请求通过该函数送达给驱动程序。首先,看一下这个函数,稍后再看其注册接口。
当进程从创建的/proc
文件中读取时,内核会分配一页内存(即PAGE_SIZE
字节),驱动程序可以在其中写入要返回给用户空间的数据。该缓冲区被传递给名为read_proc的函数:
int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
参数说明:
page
是要写入数据的缓冲区;
start
说明感兴趣的数据在页面中写入的位置(稍后会详细介绍);
offset
含义与read方法相同。
count
含义与read方法相同。
eof
指向一个整数的指针,驱动程序必须设置该整数以表明它没有更多数据要返回;
data
是指向驱动程序特定数据的指针,可用于用户记录。
这个函数返回写入到page
缓冲区内的数据的字节数,就像read
方法一样。eof
是一个简单的标志,但是start
的使用比较复杂;它的目的就是帮助实现更大的/proc
文件(大于一页)。
参数start
有一些非常规的用途。它的目的是指出要在哪里(页面内)找到要返回给用户的数据。当proc_read
方法被调用时,*start
设为NULL
。如果将其设为NULL
,则内核假定数据从偏移量为0的地址开始写入,就像参数offset等于0一样;换句话说,就是忽略掉参数offset
,不论其值为多少。相反,如果将*start
设为非NULL
值,则内核假定*start+offset
为开始地址,然后将数据返回给用户。一般来说,只有少量的数据的话,直接忽略掉参数start
就好。更复杂的方法考虑start
和offset
参数配合使用。
参数start
可以解决长久以来困扰/proc
文件的一个主要问题。有时候,连续调用read
系统调用时,ASCII表示的内核数据结构会发生变化,读取文件的进程会发现两次调用之间的数据不一致。如果*start
被设为一个较小值,用它来增加filp->f_pos,然后据此读取数据,不再需要返回的数据量,从而使f_pos
成为read_proc
过程的内部记录号。
例如,如果read_proc
函数正在从大型结构数组中返回信息,并且在第一次调用中返回了其中5个结构,则可以将* start
设置为5。下一次调用将提供与偏移量相同的值; 然后驱动程序知道开始从数组中的第六个结构返回数据。可以在fs/proc/generic.c
中看到。
请注意,有更好的方法来实现大型/proc
文件; 它被称为seq_file
,我们很快就会讨论它。 首先,先看一下普通的/proc
的示例。 这是一个简单的scull设备的read_proc实现:
int scull_read_procmem(char *buf, char **start, off_t offset, int count,
int *eof, void *data)
{
int i,j,len=0;
int limit = count - 80; /* 打印不能超过这个大小 */
for (i = 0; i < scull_nr_devs && len <= limit; i++)
{
struct scull_dev *d = &scull_devices[i];
struct scull_qset *qs = d->data;
if (down_interruptible(&d->sem))
{
return -ERESTARTSYS;
}
len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n",
i, d->qset, d->quantum, d->size);
for (; qs && len <= limit; qs = qs->next) /* scan the list */
{
len += sprintf(buf + len, " item at %p, qset at %p\n",
qs, qs->data);
if (qs->data && !qs->next) /* dump only the last item */
{
for (j = 0; j < d->qset; j++)
{
if (qs->data[j])
{
len += sprintf(buf + len," % 4i: %8p\n",
j, qs->data[j]);
}
}
}
}
up(&scull_devices[i].sem);
}
*eof = 1;
return len;
}
这是一个相当典型的read_proc
实现。 它假定永远不会需要生成多个页面的数据,因此忽略start
和offset
。 但是,小心不要超出缓冲区,以防万一。