零、写在开头的话
在这里整理一些关于ldd3这本书的学习笔记,由于才开始学习,故此这里写下的是自己的理解,可能会有一些偏差,不过学习是积累的过程,后面学习有了新的发现再回过头来进行修改,也是可以的。
使用CSDN博客来记录是因为可以有较好的格式,在代码的表现上比Word好太多了,并且可以得到保存和分类。话不多说,开始整理一些内容
一、通过查询方式调试
在ldd3中,查询调试方法的前一节为打印调试。在该节中使用了printk
来进行调试,为了得到足够的信息来判断错误,需要大量的使用该语句,导致的结果是系统性能显著降低。所以这一节提供了更好的方法——在需要的时候才去查询系统信息,而不是持续不断的产生数据。
本节介绍了两种查询调试方法,一种是在/proc
文件系统中创建文件,另一种是使用驱动程序的ioctl
方法。这里主要记录在实际操作过程中,对于第一种方法的理解和遇到问题的整理。
二、/proc文件系统
首先简单介绍一下/proc
文件系统,其实一种特殊的、由软件创建的文件系统,内核使用它向外界导出信息。/proc
下面每个文件都绑定于一个内核函数,用户读取其中的文件时,该函数动态地生成文件的“内容”。因为其文件系统时动态的,所以驱动程序模块可以在任何时候添加或删除其中的入口项。当然,/proc
文件不仅可以用于读出数据,也可以用于写入数据,本节只是简单涉及只读情形。
在/proc中实现文件
所有使用/proc
的模块必须包含头文件<linux/proc_fs.h>
,为了在/proc
目录下创建一个只读文件,我们需要在设备的驱动程序中实现一个函数,该函数的作用是:在使用命令读取/proc
目录下的文件时,生成数据。具体来说,就是当某个进程读取1/proc
目录下的只读文件时,该读取请求会通过函数发送到驱动程序模块,而驱动程序将所需要的数据通过一个内存页返回到用户空间,这个内存页是在某进程读取/proc
文件时,由内核分配的(即PAGE_SIZE字节的内存块)。
这个函数称为read_proc
方法:
int (*read_proc) (char *page, char **start, off_t offset, int count, int *eof, void *data);
函数的参数含义按照书上内容抄写如下:
参数表中
page
指针指向用来写入数据的缓冲区;使用二重指针start
返回实际的数据写到内存页的哪个位置;参数count
是请求传输的数据长度;offset
是一个指向长偏移量类型对象的指针,用来指明用户在文件中进行存取操作的位置;参数eof
指向一个整型数,在没有数据可返回时,驱动程序必须设置这个参数;data
参数是提供给驱动程序的专用数据指针,可用于内部记录。
书上这里给出了一个简单的read_proc()
函数的实现,后面给出了一个更好的方法实现/proc
文件,为seq_file()
函数,本篇笔记首先实现第一种函数。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; /* Don't print more than this */
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
值。但是,要小心不要超出缓冲区,以防万一。
创建自己的/proc文件
定义好的read_proc
函数可以将数据传入缓冲区,而将其与一个/proc
入口项连接起来,这便需要create_proc_read_entry
来实现,并在卸载模块时,使用函数remove_proc_entry
来删除/proc
中的入口项。
以下是scull设备调用该函数创建/proc
文件的代码:
create_proc_read_entry("scullmem", 0 /* default mode */,NULL /* parent dir */,scull_read_procmem,NULL /* client data */);
删除时的代码如下:
remove_proc_entry("scullmem", NULL /* parent dir */);
三、具体实现
上面是ldd3书本上的知识,在实际操作过程中,会有一些细小的地方需要规范或者注意,这里记录下实际中所遇到的一些问题。
首先对于源代码做以说明:笔者在网上下载到ldd3配套的源程序,但是发现其程序一劳永逸的将scull设备的所用内容全部编写进去,故笔者在使用时,按照ldd3上的编写顺序,以及参照下载得来的源程序的编写规范,编写了仅实现scull设备基础功能的代码,并根据学习的进度,随时在该代码上添加内容。所以这里的具体实现是指在笔者自己(或按照书本顺序)的基础代码上对于/proc
文件的简单实现。
read_proc()
函数的实现
此函数在书中已经给出,直接添加到程序当中即可。所需注意的是在基础程序(仅实现scull设备基本功能的代码)中,是不含DEBUG功能的,而为了方便打开和关闭调试功能,使用了#ifdef
预编译命令,故在使用read_proc()
函数时先定义DEBUG,具体代码如下:
#ifdef SCULL_DEBUG/* use proc only if debugging */
/*
* The proc filesystem: function to read and entry
*/
int scull_read_procmem(char *buf, char **start, off_t offset,int count, int *eof, void *data)
{
代码内容上文已有
}
create
和remove
函数的实现
即为创建目录入口项和卸载删除入口项的函数,其主要语句上文(ldd3书中)以及给出,所需要所得仅是定义一个函数即可,具体代码如下:
/*
* Actually create (and remove) the /proc file(s).
*/
static void scull_create_proc(void)
{
create_proc_read_entry(上文已有);
}
static void scull_remove_proc(void)
{
remove_proc_entry(上文已有);
}
#endif /* SCULL_DEBUG */(这里和read_proc程序前的#ifdef相对应)
到这里仅是定义了创建和删除两个函数,并未使用,需要实现在安装模块时创建,在卸载模块时删除的功能,因此需要将这两个函数分别与安装模块和卸载模块语句里调用一次、同样因方便开启和关闭调试的缘故,需使用预编译。具体语句如下:
int scull_init_module(void)
{
...
#ifdef SCULL_DEBUG /* only when debugging */
scull_create_proc();
#endif
...
void scull_cleanup_module(void)
{
...
#ifdef SCULL_DEBUG /* use proc only if debugging */
scull_remove_proc();
#endif
...
}
以上即是在scull.c
源程序上的改动,在scull.h
这个头文件中,需要增加SCULL_DEBUG
这个宏定义,如此即可完成/proc
文件系统的添加。
实际调试
在编写完成,make编译通过之后,运行脚本sh scull_load
完成设备安装,之后执行命令,查询/proc
目录下只读文件,命令为:cat /proc/scullmem
,发现并未顺利完成功能,出现错误如下图所示:
接上
在图中,可以看到如EIP is at __down_interruptible+0x56/0x11f
的字样,从这里我们可以看出,故障所在的函数是down_interruptible
,若是语句最后有中括号,则表明该函数位于括号里的模块内,十六进制的数据表明指令指针在该函数的56字节处,而函数本身是11f字节长。浏览代码,该语句定位到下面函数:
int scull_read_procmem(char *buf, char **start, off_t offset,int count, int *eof, void *data)
{
...
if (down_interruptible(&d->sem))
return -ERESTARTSYS;
在将该语句注释掉后,再次编译执行,不在报错,显示如下图所示:
在写入一些数据到scull0设备中后,再次执行可以观察该只读文件工作正常,如下图所示:
后记
1、对于down_interruptible语句为何会报错暂时还未了解,待解决后给出方案
2、卸载设备时,内核崩溃……
读取操作使用read系统调用 ↩︎