驱动程序调试方法之printk——printk的原理与直接使用

1、基本原理
(1)在UBOOT里设置console=ttySAC0或者console=tty1
这里是设置控制终端, tySAC0 表示串口,  tty1 表示lcd
(2)内核用printk打印
内核就会根据命令行参数来找到对应的硬件操作函数,并将信息通过对应的硬件终端打印出来!

2、printk的使用
(1)printk函数的信息如何才能在终端显示出来
在内核代码include/linux/kernel.h中,定义了控制台的级别:
extern int console_printk[];
#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])

我们在到kernel/printk.c里找到console_printk的定义:

/* printk's without a loglevel use this.. */
#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */

/* We show everything that is MORE important than this.. */
#define MINIMUM_CONSOLE_LOGLEVEL 1 /* Minimum loglevel we let people use */
#define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG */

DECLARE_WAIT_QUEUE_HEAD(log_wait);

int console_printk[4] = {
        DEFAULT_CONSOLE_LOGLEVEL,       /* console_loglevel */
        DEFAULT_MESSAGE_LOGLEVEL,       /* default_message_loglevel */
        MINIMUM_CONSOLE_LOGLEVEL,       /* minimum_console_loglevel */
        DEFAULT_CONSOLE_LOGLEVEL,       /* default_console_loglevel */
};

于是我们知道控制台的级别是:
7   4     1     7
我们当然可以再这里修改,但是还有一个更简单的修改方法,即在用户空间使用下面的命令:

echo “1 4 1 7” > /proc/sys/kernel/printk

将1 4 1 7写入   /proc/sys/kernel/printk即可!

当我们使用printk函数时往往要加上信息级别,比如:

printk(KERN_WARNING"there is a warning here!\n")

其中 KERN_WARNING就表示信息的级别,相关宏在函数include/linux/kernel.h中:

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

如果没有指明信息级别的话,就会采用默认的信息级别,这个默认的信息级别我们在上面见到过的,就是:
#define default_message_loglevel (console_printk[1])
没有改动的情况下是4

上面我们说到了信息级别和控制台级别,下面我们要说到重点了!当信息级别的数值小于控制台的级别时,printk要打印的信息才会在终端打印出来,否则不会显示在终端!

(2)串口控制台
printk
        vprintk(fmt, args);
                 vscnprintf(printk_buf, sizeof(printk_buf), fmt, args);
                          vsnprintf(buf,size,fmt,args); //先把输出信息输入到临时buffer
                           //把临时buffer里面的数据稍作处理,写入log_buffer
                          //可以将信息级别与信息合并
                          //在用户空间使用命令dmesg可以把log_buffer里面的数据打印出来
                          release_console_sem();
                                  call_console_drivers(_con_start, _log_end);
                                          _call_console_drivers(start_print, cur_index, msg_level);
                                                   __call_console_drivers(start, end);
                                                            con->write(con, &LOG_BUF(start), end - start); //调用具体的输出函数

这个输出函数要把数据从串口输出的话,肯定要调用到串口硬件相关的函数
我们到文件:drivers/serial/s3c2410.c里面去这里有个串口初始化函数:

s3c24xx_serial_initconsole
       register_console(&s3c24xx_serial_console);

我们来看看它的注册函数:
static struct console s3c24xx_serial_console =
{
.name = S3C24XX_SERIAL_NAME,
.device = uart_console_device,
.flags = CON_PRINTBUFFER,
.index = -1,
.write = s3c24xx_serial_console_write,
.setup = s3c24xx_serial_console_setup
};
里面的确有个write函数!

但是我们还不知道printk选择的控制台为什么是串口呢?

我们知道uboot传入了参数:console=ttySAC0 或者 console=tty1

内核就通过如下的函数来处理传入的参数:
__setup("console=", console_setup);
这是个宏 ,它的作用就是用函数 console_setup来处理 我们传入的参数
console_ setup
          //先解码字符串为:name, idx, options,然后就使用这些:name, idx, options
        add_preferred_console(name, idx, options);
                  strcmp(console_cmdline[i].name, name)
                  console_cmdline[i].index == idx    //将索引和名字都记录在了console_cmdline数组中了

我们记住这个数组,回过头来再来看:
 register_console(&s3c24xx_serial_console);
         if (strcmp(console_cmdline[i].name, console->name) != 0)
                 continue;
我们看到在注册串口控制台的时候,会将串口控制台的名字与uboot传进来的参数相比较,一旦匹配才会注册。这样的话,只有与uboot传进来的控制台参数相一致的控制台才能注册成功。那么也就是说,printk会通过uboot设置的控制台的write函数,将信息打印出来!

另外还有一点需要牢记的就是,printk输出的信息会先保存在缓冲区log_buf中,所以我们当然可以通过查看log_buf来看输出信息了!而这个查看命令就是:dmesg
实际上,dmesg这个命令的作用就是去读/proc/kmsg这个文件。也就是说log_buf里面的内容是存放在 /proc/kmsg这个文件里面的!

(3)使用printk
方法一:
我们可以再内核中使用如下打印语句:
#define DEG_PRINTK printk
//#define DEG_PRINTK(x...)

DEG_PRINTK("%s %s %d\n",_FILE_,_FUNCTION_,_LINE_);
这行打印语句的意思就是讲本行代码所在的文件的名(包括路径)、所在的函数、所在的行打印出来!
当我们需要调试的时候,就使用 #define DEG_PRINTK printk这个宏,当不需要调试的时候,就使用 #define DEG_PRINTK(x...)这个宏。其中 #define DEG_PRINTK(x...)里面的"..."的意思是 DEG_PRINTK的参数是可变的!
当代码比较少的时候,我们可以在每一行都加上这个打印语句,这样很容易就会发现错误的位置!
当代码比较多的时候,我们可以采用对半查找的方法!先在代码中间加上打印语句!然后判断出错位置在打印语句之前还是之后,如果出现在之前,就在之前的代码代码里面再次采用对半查找!

方法二:
上面我们说到过,要打印的信息会存放在log_buf中,通过文件/proc/kmsg可以来访问这个buf,然后将信息打印出来。由此我们就想了,我们是否可以构造这样一个mylog_buf,里面存放我们所需要的打印信息,通过一个/proc/kmsg文件可以访问该buf,然后打印出来?答案是肯定的!下面我们就一步步来完成!
首先我们需要弄清楚proc机制,来看看fs/proc/proc_misc.c这个文件,从入口函数开始看:
proc_misc_init(void)
        #ifdef CONFIG_PRINTK
{
struct proc_dir_entry *entry;
entry = create_proc_entry("kmsg", S_IRUSR, &proc_root); //这里创建了一个proc入口kmsg
if (entry)
entry->proc_fops = &proc_kmsg_operations; //设置操作函数,见注释1
}

注释1:
const struct file_operations proc_kmsg_operations = {
.read = kmsg_read,
.poll = kmsg_poll,
.open = kmsg_open,
.release = kmsg_release,
};
这个操作函数就用于多kmsg来进行操作

于是我们可以仿照来设计一下!
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/proc_fs.h>

struct proc_dir_entry *myentry;

const struct file_operations proc_mymsg_operations = {
};

static int mymsg_init(void)
{
myentry = create_proc_entry("mymsg", S_IRUSR, &proc_root);
if (myentry)
    myentry->proc_fops = &proc_mymsg_operations ;

    return 0;
}

void  mymsg_eixt(void)
{
    remove_proc_entry("mymsg", &proc_root);

module_init(mymsg_init);
module_exit(mymsg_eixt);

这个函数只是在proc目录下面创建了一个入口而已!我们加载后:
ls /proc/mymsg -l
打印出如下信息:
-r--------    1 0        0               0 Feb  4 13:37 /proc/mymsg

如果我们想查看 /proc/mymsg内容的话:
# cat /proc/mymsg
打印出来
cat: read error: Invalid argument
这也是理所当然的,因为我们根本没有读函数嘛!

所以我们接下来要做的就是来完成这个读函数,而这这个读函数里面我们要做的就是将mylog_buf中的中的数据拷贝到用户空间。有一个很关键的地方就是,我们的  mylog_buf 应该是一个环形队列,关于环形队列的概念我们先来说一下:
front指向队列首部,rear指向队列尾部,size表示队列长度。读只能从首部读,写的话只能从尾部写!写到尾部的话,就回去重头开始写!
当front==rear的时候,可以判断队列是空的。
当(rear+1)/size==front的时候,可以判断队列是满的。程序如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/proc_fs.h>

#define MYLOG_BUF_LEN 1024

struct proc_dir_entry *myentry;

static char mylog_buf[MYLOG_BUF_LEN];
static char tmp_buf[MYLOG_BUF_LEN];
static int mylog_r = 0;     //用来标识读
static int mylog_w = 0;   //用来标识写

static DECLARE_WAIT_QUEUE_HEAD(mymsg_waitq);

//判断环形缓冲区是否为空
static int is_mylog_empty(void)
{
return (mylog_r == mylog_w);
}
//判断环形缓冲区是否已满
static int is_mylog_full(void)
{
return ((mylog_w + 1)% MYLOG_BUF_LEN == mylog_r);
}

/*写缓冲区:如果缓冲区已满的话,就让覆盖掉下一个要读的数据
*否则就直接写入
此外在写缓冲区函数里面还需要做的一件事情就是唤醒等待队列,
这是因为当缓冲区为空的时候,如果调用读函数的话,就会使进程
进入等待队列,理当在写入数据的时候唤醒进程
*/
static void mylog_putc(char c)
{
if (is_mylog_full())
{
/* 丢弃一个数据 */
mylog_r = (mylog_r + 1) % MYLOG_BUF_LEN;
}

mylog_buf[mylog_w] = c;
mylog_w = (mylog_w + 1) % MYLOG_BUF_LEN;

/* 唤醒等待数据的进程 */
    wake_up_interruptible(&mymsg_waitq);    /* 唤醒休眠的进程 */
}

/*读缓冲区:如果缓冲区为空的话,就返回0
否则从首部读出一个数据,返回1
*/
static int mylog_getc(char *p)
{
if (is_mylog_empty())
{
return 0;
}
*p = mylog_buf[mylog_r];
mylog_r = (mylog_r + 1) % MYLOG_BUF_LEN;
return 1;
}

/*打印函数:这个函数是参考sprintf函数得编写的
*它将传递进来的参数转换为固定的格式之后,放入到一个临时缓冲区里面
*然后将环形缓冲区的值写入到mylog_buf缓冲区里面,详见注释2
*/
int myprintk(const char *fmt, ...)
{
va_list args;
int i;
int j;

va_start(args, fmt);
i = vsnprintf(tmp_buf, INT_MAX, fmt, args); //将传进来的参数转换后放入tmp_buf
va_end(args);
for (j = 0; j < i; j++)
mylog_putc(tmp_buf[j]); //将tmp_buf里面的东东放入mylog_buf缓冲区里面
return i;
}

/*读函数:当在应用空间调用命令:cat /proc/mymsg的时候,会调用这个函数
*
*/
static ssize_t mymsg_read(struct file *file, char __user *buf,
 size_t count, loff_t *ppos)
{
int error = 0;
int i = 0;
char c;

/* 把mylog_buf的数据copy_to_user, return */

        //如果为非阻塞且mylog_buf为空,那么就出错返回
if ((file->f_flags & O_NONBLOCK) && is_mylog_empty())
return -EAGAIN;

         //如果mylog_buf为空的话进程进入等待队列,还记得我们在写缓冲区
       //函数里面会唤醒进程这件事情吧!
error = wait_event_interruptible(mymsg_waitq, !is_mylog_empty());

/* copy_to_user */
        //首先从缓冲区里面获得一个字符,然后拷贝到用户空间
        //如果缓冲区还有信息的话,就再次获得字符,拷贝到用户
       //空间,直到缓冲区为空
while (!error && (mylog_getc(&c)) && i < count) {
error = __put_user(c, buf); //将c的内容拷贝到用户空间
buf++;
i++;
}
if (!error)
error = i;
return error;
}

const struct file_operations proc_mymsg_operations = {
.read = mymsg_read,
};

static int mymsg_init(void)
{
myentry = create_proc_entry("mymsg", S_IRUSR, &proc_root);
if (myentry)
myentry->proc_fops = &proc_mymsg_operations;
return 0;
}

static void mymsg_exit(void)
{
remove_proc_entry("mymsg", &proc_root);
}

module_init(mymsg_init);
module_exit(mymsg_exit);

/*因为myprintk是我们自己写的打印语句
*所以需要导出才能被使用,使用的时候还需要声明一下:
extern int myprintk(const char *fmt,...);
*/
EXPORT_SYMBOL(myprintk);

MODULE_LICENSE("GPL");

我们在来总结一下:在本文件里面我们做了两件事情,一件事情是定义了一个写函数,当我们在用户空间使用命令:cat /proc/mymsg的时候,就会调用到这个读函数,这个读函数会将mylog_buf中的数据拷贝到用户空间,那么mylog_buf里面的数据哪里来的呢?这就是我们做的另外一件事情,我们定义了一个打印函数,这个打印函数会将要打印的数据写入一个临时缓冲区,然后又从临时缓冲区里面取出数据放入mylog_buf中。cat /proc/mymsg的候就会将mylog_buf中的数据拷贝到用户空间,就可以显示出来了!

注释2:
int sprintf(char * buf, const char *fmt, ...)
{
va_list args;
int i;

va_start(args, fmt);
i=vsnprintf(buf, INT_MAX, fmt, args);
va_end(args);
return i;
}
这个就是sprintf函数,它将传递进来的参数放入buf缓冲区,我们做的就是将这个缓冲区里面的值在放入mylog_buf缓冲区里面!相当的巧妙啊!

接下来我们不妨测试一下,测试函数如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

xtern int myprintk(const char *fmt, ...);

static int first_drv_init(void)
{
myprintk("first_drv_init\n");

return 0;
}

static void first_drv_exit(void)
{
    myprintk("abcdefhg\n");
}

module_init(first_drv_init);
module_exit(first_drv_exit);


MODULE_LICENSE("GPL");

先加载proc.ko
在加载test.ko
cat /proc/mymsg,打印如下信息:
first_drv_init
这正是我们打印的信息,成功了!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值