LDD: Ch 4 调试技术


1. 内核调试的困难:

内核不和特定的进程关联,所以无法很容易的用调试器来调试,而且很难跟踪。



2. 打印调试

2.1 printk

printk的形式如下:

printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE__);

形式和printf相当类似,其中KERN_DEBUG代表的是当前打印的优先级。

打印的优先级包括如下几种:

#define KERN_EMERG  KERN_SOH "0"    /* system is unusable */
#define KERN_ALERT  KERN_SOH "1"    /* action must be taken immediately */
#define KERN_CRIT   KERN_SOH "2"    /* critical conditions */
#define KERN_ERR    KERN_SOH "3"    /* error conditions */
#define KERN_WARNING    KERN_SOH "4"    /* warning conditions */
#define KERN_NOTICE KERN_SOH "5"    /* normal but significant condition */
#define KERN_INFO   KERN_SOH "6"    /* informational */
#define KERN_DEBUG  KERN_SOH "7"    /* debug-level messages */

定义优先级的目的是为了尽量减少不必要的输出信息,所以在编写的代码时候要注意优先级的使用。

对于printk的每个优先级,还有相对的宏可以直接使用:

#define pr_emerg(fmt, ...) \
    printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert(fmt, ...) \
    printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit(fmt, ...) \
    printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err(fmt, ...) \
    printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warning(fmt, ...) \
    printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn pr_warning
#define pr_notice(fmt, ...) \
    printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) \
    printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
#define pr_debug(fmt, ...) \
    printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)


2.2 输出控制台的指定

内核输出的控制台,是由内核启动参数来确定的,如下面的例子:

console=ttyS0,115200


2.3信息如何被记录

printk函数将信息记录在一个循环缓冲区中,并唤醒所有在等待消息后者读取/proc/kmsg的进程。

/proc/kmsg类似一个FIFO的文件,读取的动作会block在read中等待更多数据的到来,当然read之后数据也会被消耗。

klogd,syslogd

klogd是用户空间的daemon,读取/proc/kmsg的信息并分发出去,而syslogd则是接受klogd分发出来的消息进行处理。

在android中有一个logd的daemon,这个daemon也支持直接读取/proc/kmsg来读取kernel的输出信息。


2.4 printk打印的输出优先级

内核的打印信息在输出到控制台的时候,会按照控制台指定的输出优先级进行过滤,低于该优先级的输出会被过滤掉。
我们通过下面的proc接口来设定/读取控制台的输出优先级:

/proc/sys/kernel/printk


2.5 输出速率的限制

当代码中printk输出太多,很可能导致输出设备受到影响,设置可能导致系统响应很慢。kernel中提供了下面的函数

int printk_ratelimit(void);

这个函数会告诉我们输出设备是否处于极度繁忙之中,如果是的话,返回0,如果不是,返回1。我们在会频繁输出的信息前,
加上这个函数来判断是否要输出信息:

    if (printk_ratelimit())
        dev_err(dev, "RX: Can't reallocate skb to %d; "
                            "RX dropped\n", rx_size);

在kernel 3.14中,printk_ratelimit实际是一个宏。


2.6 打印设备编号

设备编号的打印最好使用特定的函数,以保持向后兼容。
kernel提供了下面两个函数

int print_dev_t(char* buffer, dev_t dev);
char* format_dev_t(char* buffer, dev_tdev);

两个函数都是讲dev_t输入到缓冲区buffer中,只是返回值不一样。
print_dev_t返回的是buffer中占用的字节数。
format_dev_t返回的是缓冲区的指针(即char*buffer参数)。

为了保证后续64位的兼容,要保证buffer长度在20以上。



3. 用查询来调试

太多的打印会拖慢系统的运行,所以最好的调试方法是在需要的时候,产生你需要的信息,而不是一直产生信息。
内核提供了下面几种方式来查询系统信息:
+ /proc文件系统
+ 特定的ioctl命令
+ sysfs属性 (本章暂不讨论)
+ debugfs


3.1 /proc文件系统

/proc是一个特殊的文件系统,内核通过他来向外部输出信息。目前/proc已经过于庞大复杂,远离了原本的设计意图,建议新代码中使用sysfs来代替。

书中的proc相关接口已经不用了,下面是使用的3.14内核中的接口。

procfs接口的使用

proc操作的接口声明在头文件

struct proc_dir_entry * proc_mkdir(const char *name, struct proc_dir_entry * parent); 
struct proc_dir_entry * proc_mkdir_data(const char *name, umode_t mode, struct proc_dir_entry *parent, void*data);

proc_mkdir第一个参数是要创建的目录名字,第二个是该目录的parent目录的proc_dir_entry结构,如果设为NULL,则目录建立在/proc的根目录,返回的是新建目录的proc_dir_entry结构。
proc_mkdir_data功能一样,只是可以做额外的控制,第二个参数mode是用来设置建立好的目录的访权限,第四个参数data是赋值给建立好的proc_dir_entry.data。

  • proc文件的建立
struct proc_dir_entry* proc_create_data(const char*name, umode_t mode, struct proc_dir_entry* dir,
                                        const struct file_operations* fop, void* data);

参数dir就是第一步中建立的目录的proc_dir_entry结构。
参数fop需要我们根据需要自己定义。
参数data是自定义的额外信息,会被赋值给返回的proc_dir_entry结构中的data成员。

  • proc节点的删除
int remove_proc_entry(const char* name, struct proc_dir_entry * parent);

proc目录和文件都通过这个函数来删除。
注意parent参数是创建文件时候所用的parent的proc_dir_entry,而不是该文件本身的proc_dir_entry。

使用seq_file实现proc文件操作

seq_file的说明在common/Documents/filesystems/seq_file.txt。
procfs是建立在内存中的文件系统,他所对应的的内容也是建立在内存中的,为了安全方便的操作内存文件,内核提供了seq_file的机制来实现。
seq_file的使用会单独说明。


3.2 ioctl方法

模块开发者可以在driver中保留一个隐秘的ioctl command,在需要debug的时候使用,不过相对麻烦的地方在于,需要修改代码才能使用这种debug方式。

3.3.1 debugfs的使用

debugfs的api只export给GPL-ONLY的模块,头文件是

struct dentry *debugfs_create_dir(const char *name, struct dentry *parent);

debugfs_create_dir会在parent所指定的目录中建立名字为name的目录,如果parent为NULL,则目录建立在debugfs的root目录。
函数返回的dentry用于后续在该目录下建立文件。

  • 建立debug文件
struct dentry *debugfs_create_file(const char *name, umode_t mode,
                   struct dentry *parent, void *data,
                   const struct file_operations *fops);

name是要创建的目标文件名称,mode是文件的访问权限,parent是文件所在目录的dentry也就是第一步创建的dentry,data是文件的私有数据会被记录在生成的文件的inode.i_private中,fops则是文件操作的定义(fops一般至少要包括read/write这两个操作)。

  • debugfs的预设file operations
    对于debugfs来说,最常用的就是read/write这两个操作,内核中针对不同类型的数据提供了预设好的操作函数。

针对整数的debug fs操作:

    struct dentry *debugfs_create_u8(const char *name, umode_t mode,
                     struct dentry *parent, u8 *value);
    struct dentry *debugfs_create_u16(const char *name, umode_t mode,
                      struct dentry *parent, u16 *value);
    struct dentry *debugfs_create_u32(const char *name, umode_t mode,
                      struct dentry *parent, u32 *value);
    struct dentry *debugfs_create_u64(const char *name, umode_t mode,
                      struct dentry *parent, u64 *value);

这些接口会预设好read/write操作。
除此之外,针对16进制的数据,bool型,blob(二进制)数据,都有相对象的预设接口可以使用。

  • debugfs的其他操作
    struct dentry *debugfs_rename(struct dentry *old_dir, 
                      struct dentry *old_dentry,
                          struct dentry *new_dir, 
                  const char *new_name);

    struct dentry *debugfs_create_symlink(const char *name, 
                                          struct dentry *parent,
                          const char *target);

debugfs_rename是对debugfs文件重命名,new_name必须是之前不存在的名字,new_dir是新目录的名字。
debugfs_create_symlink是用来建立一个符号链接到指定的debugfs文件。

  • debugfs文件/目录的移除
    void debugfs_remove(struct dentry *dentry);

如果debugfs在模块中使用,在模块被卸载掉的时候需要将debugfs文件移除掉。dentry是建立文件/目录时候返回的dentry,如果是NULL,那么什么都不会做。
当debugfs目录中包含的文件太多的时候,逐个操作显然很麻烦,可以使用下面的函数来删除整个目录下的所有文件/目录,
这里的dentry是最上层的目录的dentry结构。

    void debugfs_remove_recursive(struct dentry *dentry);


4. 观察程序来调试

观察用户空间程序的行为,可以帮助我们判断驱动是否正确工作。
strace命令是一个很强力的工具,他可以显示所有用户空间发出的系统调用,并且显示调用的参数和返回值。这样我们使用strace命令会方便定位问题。

你可能感兴趣的:(LDD笔记)