linux驱动常用输出和调试手段


结合scull驱动代码,来观察其实现使用。

1.   创建/proc文件调试

在/proc 下的每个文件都绑到一个内核函数上, 当文件被读的时候即时产生文件内容.

使用 /proc 的模块需要包含 <linux/proc_fs.h>

当一个进程读模块的 /proc 文件, 内核分配了一页内存(就是说, PAGE_SIZE 字节), 驱动可以写入数据来返回给用户空间. 那个缓存区传递给你的函数, 是一个file_operations的函数集方法:

struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,

                                        struct proc_dir_entry *parent,

                                        const struct file_operations *proc_fops,

                                        void *data)

在驱动中通过如下调用:

proc_create_data("scullmem", 0 /* default mode */,

                        NULL /* parent dir */, &scullmem_proc_ops,

                        NULL /* client data */);

在/proc文件夹下创建scullmem文件,其对应的操作函数集合是

scullmem_proc_ops,如下:

static struct file_operations scullmem_proc_ops = {

        .owner   = THIS_MODULE,

        .open    = scullmem_proc_open,

        .read    = seq_read,

        .llseek  = seq_lseek,

        .release = single_release

};

主要是scullmem_proc_open函数,就一条语句,调用scull_read_procmem.

static int scullmem_proc_open(struct inode *inode, struct file *file)

{

        return single_open(file, scull_read_procmem, NULL);

}

其中single_open在内核中定义如下:

int single_open(struct file *file, int (*show)(struct seq_file *, void *), void *data) 

而scull_read_procmem函数实现真正的输出操作。

移除/proc下目录:

void remove_proc_entry(const char *name, struct proc_dir_entry *parent)

/proc处理大文件时候容易出错。内核提供了seq_file接口。

2.   seq_file

需要包含 <linux/seq_file.h> ,必须创建 4 个 iterator 方 法, 称为 start, next, stop, 和 show.

创建的proc下目录如下,并关联proc文件相关的操作:

proc_create("scullseq", 0, NULL, &scullseq_proc_ops);

其中scullseq_proc_ops如下:

static struct file_operations scullseq_proc_ops = {

        .owner   = THIS_MODULE,

        .open    = scullseq_proc_open,

        .read    = seq_read,

        .llseek  = seq_lseek,

        .release = seq_release

};

在scullseq_proc_open中会调用seq_open函数,如下,并关联打开相关的函数集scull_seq_ops。

static int scullseq_proc_open(struct inode *inode, struct file *file)

{

        return seq_open(file, &scull_seq_ops);

}

static struct seq_operations scull_seq_ops = {

        .start = scull_seq_start,

        .next  = scull_seq_next,

        .stop  = scull_seq_stop,

        .show  = scull_seq_show

};

然后seq_open在内核中如下:

/**

 *      seq_open -      initialize sequential file

 *      @file: file we initialize

 *      @op: method table describing the sequence

 *

 *      seq_open() sets @file, associating it with a sequence described

 *      by @op.  @op->start() sets the iterator up and returns the first

 *      element of sequence. @op->stop() shuts it down.  @op->next()

 *      returns the next element of sequence.  @op->show() prints element

 *      into the buffer.  In case of error ->start() and ->next() return

 *      ERR_PTR(error).  In the end of sequence they return %NULL. ->show()

 *      returns 0 in case of success and negative number in case of error.

 *      Returning SEQ_SKIP means "discard this element and move on".

 *      Note: seq_open() will allocate a struct seq_file and store its

 *      pointer in @file->private_data. This pointer should not be modified.

 */

int seq_open(struct file *file, const struct seq_operations *op)

3.   ioctl

ioctl是一个系统调用, 作用于一个文件描述符;接收一个确定要进行的命令的数字和(可选地)另一个参数,常常是一个指针。可以作为使用 /proc 文件系统的替代,可以实现几个用来调试用的 ioctl 命令. 这些命令可以从 驱动拷贝相关的数据结构到用户空间, 这里你可以检查它们.

使用 ioctl 来获取信息比使用/proc 困难,运行比读取/proc快,不要求划分数据为小于一页的片段。

另外,ioctl 命令可能不记录文档并不为人知。如果驱动发生了怪异的事情, 仍将在那里.缺点是模块可能会稍微大些.

4.   strace

strace其中最有用 的是 -t 来显示每个调用执行的时间, -T 来显示调用中花费的时间, -e 来限制被跟踪调 用的类型, 以及-o 来重定向输出到一个文件。

strace 从内核自身获取信息,所以不管它是否带有调试支持编译(对 gcc 是 -g 选项)以及不管它是否 strip 过,都可以跟踪。

5.   gdb调试内核

使用gdb方式,gdb 不能修改内核数据,也不可能设置断点或观察点, 或单步内核函数。

#gdb /usr/src/linux/vmlinux /proc/kcore

(gdb)p jiffies

也可添加模块的符号:

(gdb)add-symbol-file scull.ko 0xffffffffc0184000 -s .bss 0xffffffffc0188d40 -s .data 0xffffffffc0188000

然后可以打印驱动中相关参数:

(gdb) p scull_devices[0]

$1 = {data = 0x0 <irq_stack_union>, quantum = 4000, qset = 1000, size = 0, access_key = 0, sem = {lock = {raw_lock = {val = {

          counter = 0}}}, count = 1, wait_list = {next = 0xffff89b335861028, prev = 0xffff89b335861028}}, cdev = {kobj = {

      name = 0x0 <irq_stack_union>, entry = {next = 0xffff89b335861040, prev = 0xffff89b335861040}, parent = 0x0 <irq_stack_union>,

      kset = 0x0 <irq_stack_union>, ktype = 0xffffffffba661120, sd = 0x0 <irq_stack_union>, kref = {refcount = {refs = {counter = 1}}},

      state_initialized = 1, state_in_sysfs = 0, state_add_uevent_sent = 0, state_remove_uevent_sent = 0, uevent_suppress = 0},

    owner = 0xffffffffc0188a00, ops = 0xffffffffc0188000 <scull_fops>, list = {next = 0xffff89b335861088, prev = 0xffff89b335861088},

    dev = 260046848, count = 1}}

其中地址符号来自/sys/module/scull/sections/.data

/sys/module/scull/sections/.text

/sys/module/scull/sections/.bss

要设置断点可以使用kdb,不过是汇编级别的。要代码级别的可以使用kgdb,只是需要两台机器通过串口来进行调试。

 

上一篇:linux中btt工具详解


下一篇:NVMe驱动详解系列_第一部:NVMe驱动初始化与注销