内核不和特定的进程关联,所以无法很容易的用调试器来调试,而且很难跟踪。
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__)
内核输出的控制台,是由内核启动参数来确定的,如下面的例子:
console=ttyS0,115200
printk函数将信息记录在一个循环缓冲区中,并唤醒所有在等待消息后者读取/proc/kmsg的进程。
/proc/kmsg类似一个FIFO的文件,读取的动作会block在read中等待更多数据的到来,当然read之后数据也会被消耗。
klogd是用户空间的daemon,读取/proc/kmsg的信息并分发出去,而syslogd则是接受klogd分发出来的消息进行处理。
在android中有一个logd的daemon,这个daemon也支持直接读取/proc/kmsg来读取kernel的输出信息。
内核的打印信息在输出到控制台的时候,会按照控制台指定的输出优先级进行过滤,低于该优先级的输出会被过滤掉。
我们通过下面的proc接口来设定/读取控制台的输出优先级:
/proc/sys/kernel/printk
当代码中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实际是一个宏。
设备编号的打印最好使用特定的函数,以保持向后兼容。
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以上。
太多的打印会拖慢系统的运行,所以最好的调试方法是在需要的时候,产生你需要的信息,而不是一直产生信息。
内核提供了下面几种方式来查询系统信息:
+ /proc文件系统
+ 特定的ioctl命令
+ sysfs属性 (本章暂不讨论)
+ debugfs
/proc是一个特殊的文件系统,内核通过他来向外部输出信息。目前/proc已经过于庞大复杂,远离了原本的设计意图,建议新代码中使用sysfs来代替。
书中的proc相关接口已经不用了,下面是使用的3.14内核中的接口。
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。
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成员。
int remove_proc_entry(const char* name, struct proc_dir_entry * parent);
proc目录和文件都通过这个函数来删除。
注意parent参数是创建文件时候所用的parent的proc_dir_entry,而不是该文件本身的proc_dir_entry。
seq_file的说明在common/Documents/filesystems/seq_file.txt。
procfs是建立在内存中的文件系统,他所对应的的内容也是建立在内存中的,为了安全方便的操作内存文件,内核提供了seq_file的机制来实现。
seq_file的使用会单独说明。
模块开发者可以在driver中保留一个隐秘的ioctl command,在需要debug的时候使用,不过相对麻烦的地方在于,需要修改代码才能使用这种debug方式。
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用于后续在该目录下建立文件。
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这两个操作)。
针对整数的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(二进制)数据,都有相对象的预设接口可以使用。
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文件。
void debugfs_remove(struct dentry *dentry);
如果debugfs在模块中使用,在模块被卸载掉的时候需要将debugfs文件移除掉。dentry是建立文件/目录时候返回的dentry,如果是NULL,那么什么都不会做。
当debugfs目录中包含的文件太多的时候,逐个操作显然很麻烦,可以使用下面的函数来删除整个目录下的所有文件/目录,
这里的dentry是最上层的目录的dentry结构。
void debugfs_remove_recursive(struct dentry *dentry);
观察用户空间程序的行为,可以帮助我们判断驱动是否正确工作。
strace命令是一个很强力的工具,他可以显示所有用户空间发出的系统调用,并且显示调用的参数和返回值。这样我们使用strace命令会方便定位问题。