嵌入式Linux——kmsg:分析/proc/kmsg文件以及写自己的/proc/mymsg

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/W1107101310/article/details/80582314

简介:

    本文主要分析/proc/kmsg文件的形成过程以及使用cat /proc/kmsg查看log_buf中的信息时所要经历的代码。并结合上面的分析写自己的 /proc/mymsg和myprintk 。

 Linux内核:linux-2.6.22.6

 所用开发板:JZ2440 V3(S3C2440A)

声明:

    本文主要是看完韦东山老师的视频后,自己分析代码所写。同时我在写这篇文章的时候也参考了一些网友的文章。我相信这样可以让我们的知识点变得更加全面。

分析/proc/kmsg:

    我们都知道当我们使用cat /proc/kmsg时,我们可以看到缓存在log_buf中的内容,那么/proc/kmsg文件是在什么时候创建的?同时我们是如何使用cat /proc/kmsg获得log_buf中的信息,这就成了我们要问的问题了。下面我们会通过分析代码来回答上面的问题。

    我先将上篇文章中的一幅图引入这里,希望对大家有帮助:


    首先我们看/proc/kmsg文件是什么时候创建的。我们打开内核的文件我们会发现有fs(文件系统)文件,并在这个文件下面有proc文件,在proc文件下有proc_misc.c文件,我们看这个文件的入口函数:proc_misc_init

void __init proc_misc_init(void)
{
	·······
#ifdef CONFIG_PRINTK
	{
		struct proc_dir_entry *entry;
		entry = create_proc_entry("kmsg", S_IRUSR, &proc_root);
		if (entry)
			entry->proc_fops = &proc_kmsg_operations;
	}
#endif
        ·······
}

    从上面看代码可能的意思是,当我们定义了CONFIG_PRINTK时,我们就要在/porc目录下创建一个kmsg的子条目,而子条目的文件操作函数为proc_kmsg_operations。下面我们慢慢分析代码看是不是我说的这个意思。从上面程序看,先定义了一个proc_dir_entry的结构体。

/*
 * 这个结构体不会完全的运用。但是他的想法是:
 * 在内存中创建一个proc_dir_entries的树(就像真实的/proc文件系统树)
 * 这样我们就可以动态的添加一个新的文件到/proc
 */
struct proc_dir_entry {
	unsigned int low_ino;
	unsigned short namelen;   //名字长度
	const char *name;         //名字
	mode_t mode;             //操作属性 
	nlink_t nlink;
	uid_t uid;
	gid_t gid;
	loff_t size;
	const struct inode_operations *proc_iops;
	const struct file_operations *proc_fops;    //文件操作函数
	get_info_t *get_info;
	struct module *owner;
	struct proc_dir_entry *next, *parent, *subdir;  //下一个条目,父条目,子条目
	void *data;
	read_proc_t *read_proc;
	write_proc_t *write_proc;
	atomic_t count;		/* use count */
	int deleted;		/* delete flag */
	void *set;
};

    从上面proc_dir_entry的注释我们可以知道,这个结构体就是要将我们注册到/proc目录下的子条目填写到这个结构体中。而通过create_proc_entry函数来完成这个任务,同时create_proc_entry函数还会将这个子条目注册到/proc目录下,然后通过返回填好的proc_dir_entry结构体。我们现在看create_proc_entry函数

struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent)
{
	struct proc_dir_entry *ent;
	nlink_t nlink;
        //对子条目的操作属性进行检查
	if (S_ISDIR(mode)) {    
		if ((mode & S_IALLUGO) == 0)
			mode |= S_IRUGO | S_IXUGO;
		nlink = 2;
	} else {
		if ((mode & S_IFMT) == 0)
			mode |= S_IFREG;
		if ((mode & S_IALLUGO) == 0)
			mode |= S_IRUGO;
		nlink = 1;
	}
        //创建一个名为name,属性为mode,而他的父目录为parent的子条目
	ent = proc_create(&parent,name,mode,nlink);
	if (ent) {    
		if (S_ISDIR(mode)) {
			ent->proc_fops = &proc_dir_operations;
			ent->proc_iops = &proc_dir_inode_operations;
		}
		if (proc_register(parent, ent) < 0) {  //将这个子条目注册进父目录
			kfree(ent);
			ent = NULL;
		}
	}
	return ent;
}

    在上面的代码中我们看到了要设置这个子条目的操作属性,那么他都有什么属性那?我们看代码:

#define S_IRWXU 00700   //用户rwx:111
#define S_IRUSR 00400   //用户rwx:100
#define S_IWUSR 00200   //用户rwx:010
#define S_IXUSR 00100   //用户rwx:001

#define S_IRWXG 00070   //同组用户rwx:111
#define S_IRGRP 00040   //同组用户rwx:100
#define S_IWGRP 00020   //同组用户rwx:010
#define S_IXGRP 00010   //同组用户rwx:001

#define S_IRWXO 00007   //其他用户rwx:111
#define S_IROTH 00004   //其他用户rwx:100
#define S_IWOTH 00002   //其他用户rwx:010
#define S_IXOTH 00001   //其他用户rwx:001

    从上面代码看,他的属性与我们在Linux中运用chmod命令来定义文件的操作属性是一样的,都是使用一个二进制位来表示读写可执行中的一个。

    而同时我们使用下面的代码来判断子条目的文件类型:

#define S_IFMT  00170000
#define S_IFSOCK 0140000    //是否是socket
#define S_IFLNK	 0120000    //是否是连接
#define S_IFREG  0100000    //是否是寄存器
#define S_IFBLK  0060000   //是否是块设备
#define S_IFDIR  0040000   //是否是文件
#define S_IFCHR  0020000   //是否是字符设备
#define S_IFIFO  0010000
    而通过对子条目的文件类型的判断我们会为他加上合适的文件操作属性。而通过上面的判断我们的子条目为只读属性。

    而注册完子条目,我们就要为他添加文件操作函数了。我们知道在Linux中一些皆文件。所以对这个子条目的操作其实就是要操作这个条目的文件操作函数:proc_kmsg_operations 。    

const struct file_operations proc_kmsg_operations = {
	.read		= kmsg_read,
	.poll		= kmsg_poll,
	.open		= kmsg_open,
	.release	= kmsg_release,
};

    我们看到上面的代码是不是很熟悉啊,这就是我们以前写字符驱动时的file_operations。而这里也是这个结构体。那么下面的讲解就要简单些了,因为道理都是相同的。我们在上面介绍了这个子条目为只读属性。所以我们主要在意的是使用 cat /proc/kmsg来对log_buf中的内容进行读取。所以我们在这里主要看kmsg_read函数。

static ssize_t kmsg_read(struct file *file, char __user *buf,
			 size_t count, loff_t *ppos)
{
	if ((file->f_flags & O_NONBLOCK) && !do_syslog(9, NULL, 0)) //如果文件为非阻塞方式,并且log_buf中为空,我们返回错误
		return -EAGAIN;
	return do_syslog(2, buf, count);  //否则我们调用do_syslog函数
}

    而同时我们发现其他几个函数主要也是调用do_syslog函数。那么我们接下来最主要的就是对do_syslog函数进行分析了:

/*
 * Commands to do_syslog:
 *
 * 	0 -- Close the log.  Currently a NOP.
 * 	1 -- Open the log. Currently a NOP.
 * 	2 -- Read from the log.
 * 	3 -- Read all messages remaining in the ring buffer.
 * 	4 -- Read and clear all messages remaining in the ring buffer
 * 	5 -- Clear ring buffer.
 * 	6 -- Disable printk's to console
 * 	7 -- Enable printk's to console
 *	8 -- Set level of messages printed to console
 *	9 -- Return number of unread characters in the log buffer
 *     10 -- Return size of the log buffer
 */
int do_syslog(int type, char __user *buf, int len)
{
	unsigned long i, j, limit, count;
	int do_clear = 0;
	char c;
	int error = 0;

	error = security_syslog(type);
	if (error)
		return error;

	switch (type) {
	case 0:		/* Close log */
		break;
	case 1:		/* Open log */
		break;
	case 2:		/* Read from log */
		error = -EINVAL;
		if (!buf || len < 0)
			goto out;
		error = 0;
		if (!len)
			goto out;
		if (!access_ok(VERIFY_WRITE, buf, len)) {
			error = -EFAULT;
			goto out;
		}
		error = wait_event_interruptible(log_wait,
							(log_start - log_end));
		if (error)
			goto out;
		i = 0;
		spin_lock_irq(&logbuf_lock);
		while (!error && (log_start != log_end) && i < len) {
			c = LOG_BUF(log_start);
			log_start++;
			spin_unlock_irq(&logbuf_lock);
			error = __put_user(c,buf);
			buf++;
			i++;
			cond_resched();
			spin_lock_irq(&logbuf_lock);
		}
		spin_unlock_irq(&logbuf_lock);
		if (!error)
			error = i;
		break;
	case 4:		/* Read/clear last kernel messages */
		do_clear = 1;
		/* FALL THRU */
	case 3:		/* Read last kernel messages */
		error = -EINVAL;
		if (!buf || len < 0)
			goto out;
		error = 0;
		if (!len)
			goto out;
		if (!access_ok(VERIFY_WRITE, buf, len)) {
			error = -EFAULT;
			goto out;
		}
		count = len;
		if (count > log_buf_len)
			count = log_buf_len;
		spin_lock_irq(&logbuf_lock);
		if (count > logged_chars)
			count = logged_chars;
		if (do_clear)
			logged_chars = 0;
		limit = log_end;
		/*
		 * __put_user() could sleep, and while we sleep
		 * printk() could overwrite the messages
		 * we try to copy to user space. Therefore
		 * the messages are copied in reverse. <manfreds>
		 */
		for (i = 0; i < count && !error; i++) {
			j = limit-1-i;
			if (j + log_buf_len < log_end)
				break;
			c = LOG_BUF(j);
			spin_unlock_irq(&logbuf_lock);
			error = __put_user(c,&buf[count-1-i]);
			cond_resched();
			spin_lock_irq(&logbuf_lock);
		}
		spin_unlock_irq(&logbuf_lock);
		if (error)
			break;
		error = i;
		if (i != count) {
			int offset = count-error;
			/* buffer overflow during copy, correct user buffer. */
			for (i = 0; i < error; i++) {
				if (__get_user(c,&buf[i+offset]) ||
				    __put_user(c,&buf[i])) {
					error = -EFAULT;
					break;
				}
				cond_resched();
			}
		}
		break;
	case 5:		/* Clear ring buffer */
		logged_chars = 0;
		break;
	case 6:		/* Disable logging to console */
		console_loglevel = minimum_console_loglevel;
		break;
	case 7:		/* Enable logging to console */
		console_loglevel = default_console_loglevel;
		break;
	case 8:		/* Set level of messages printed to console */
		error = -EINVAL;
		if (len < 1 || len > 8)
			goto out;
		if (len < minimum_console_loglevel)
			len = minimum_console_loglevel;
		console_loglevel = len;
		error = 0;
		break;
	case 9:		/* Number of chars in the log buffer */
		error = log_end - log_start;
		break;
	case 10:	/* Size of the log buffer */
		error = log_buf_len;
		break;
	default:
		error = -EINVAL;
		break;
	}
out:
	return error;
}

    其中do_syslog实现了对log_buf的操作,而我们可以用图表示为:


    syslog 调用(在内核中调用 ./linux/kernel/printk.c 的 do_syslog)是一个相对较小的函数,它能够读取和控制内核环缓冲区。

    type 参数是用于传递所执行的命令,它指定了可选的缓冲区长度。有一些命令(如清除环缓冲)是忽略 buf 和 len 这两个参数的。虽然前面两个命令类型不会对内核进行任何操作,但是其余命令则是用于读取日志消息或控制日志。其中有三个命令是用于读取日志消息的。SYSLOG_ACTION_READ(2) 用于阻塞操作,直至日志消息到达后才释放该操作,然后将它们返回到所提供的缓冲区。这个命令会处理这些消息(旧的消息将不会出现在这个命令的后续调用中)。SYSLOG_ACTION_READ_ALL(3) 命令会从日志读取最后 n 个字符(而 n 是在传递给 klogctl 的参数 'len' 中定义的)。SYSLOG_ACTION_READ_CLEAR (4)命令会先执行 SYSLOG_ACTION_READ_ALL(3)操作,然后执行SYSLOG_ACTION_CLEAR(5)命令(清除环缓冲区)。SYSLOG_ACTION_CONSOLE ON(7) 和 OFF(6) 可以将日志级别设置为激活或禁用日志消息输出到控制台,而 SYSLOG_CONSOLE_LEVEL(8) 则允许调用者定义控制台所接受的日志消息级别。最后,SYSLOG_ACTION_SIZE_BUFFER (10)是用于返回内核环缓冲区大小,而 SYSLOG_ACTION_SIZE_UNREAD (9)则返回当前内核环缓冲区可读取的字符数。下表显示了 SYSLOG 命令的完整清单。

     文件 /proc/kmsg 实现了少数等同于内部 do_syslog 的文件操作。在内部,open 调用与 SYSLOG_ACTION_OPEN 有关,而 SYSLOG_ACTION_CLOSE 则与 release 有关(每一个调用都实现为一个 No Operation Performed [NOP])。这个轮循操作会等待文件活动的完成,然后才调用 SYSLOG_ACTION_SIZE_UNREAD 确定可以读取的字符数。最后,read 操作会被映射到 SYSLOG_ACTION_READ,以处理可用的日志消息。

    上面描述来自:内核日志:API 及实现

    我们现在分析程序,我们主要是分析type为2时,从log_buf中读取信息,程序为;

	case 2:		/* Read from log */
		error = -EINVAL;
		if (!buf || len < 0)  //当用户buf不存在或者要读取的数据小于0时,表示出错
			goto out;
		error = 0;
		if (!len)
			goto out;
		if (!access_ok(VERIFY_WRITE, buf, len)) {
			error = -EFAULT;
			goto out;
		}
		error = wait_event_interruptible(log_wait,(log_start - log_end));//当log_start与log_end相等时,线程休眠,等待唤醒
		if (error)
			goto out;
		i = 0;
		spin_lock_irq(&logbuf_lock);
		while (!error && (log_start != log_end) && i < len) {  //当log_start与log_end不相等并且线程被唤醒
			c = LOG_BUF(log_start);   //从log_buf中读出一字节数据
			log_start++;             //读位置加一
			spin_unlock_irq(&logbuf_lock);
			error = __put_user(c,buf);     //将读出的数据放到buf中,然后发送到用户空间,相当于copy_to_user函数
			buf++;                    
			i++;
			cond_resched();
			spin_lock_irq(&logbuf_lock);
		}
		spin_unlock_irq(&logbuf_lock);
		if (!error)
			error = i;
		break

    上面就是一个读数据的过程了,即先对要读取的范围等进行判断,然后就是将log_buf中的数据读出并发送到用户空间。而do_syslog函数中的其他选项则主要是对log_start和log_end的判断和操作了。

    我现在为大家解释在cat /proc/dmsg 时我们为什么可以将log_buf中的信息打印出来。


    从上面图中我们知道cat /proc/dmsg就相当于是:调用kmsg_open函数,然后在while循环中不断调用kmsg_read函数。而有上面的分析我们知道这其实就是从log_buf中读取数据。由于我们读完一次后log_start指到与log_end相等的位置。所以我们只显示一次的结果。

    而关于log_buf环形缓存区的知识我建议大家看:Linux-分析并制作环形缓冲区

写自己的/proc/mymsg:   

    好了,有了上面的知识我们就可以写自己的/proc/mymsg了。我先在这里说一下我们的目标。我们的目标是:仿照printk建一个自己的环形缓冲区:mylog_buf,并在/proc下创建一个自己的mymsg子条目。当我们在其他驱动中加myprintk函数时,会将myprintk中要打印的信息放到mylog_buf中。如果想要看这些信息可以使用cat /proc/mymsg来查看存在mylog_buf中的信息。

    我们按着上面分析程序时的步骤来写代码。所以我们同样要先申请注册一个proc_dir_entry结构体。程序为:

struct proc_dir_entry *myentry;

static int mymsg_init(void)
{
	/* 创建一个名为mymsg,属性为S_IRUSR,而父类为proc_root的proc_dir_entry结构体,并注册 */
	myentry = create_proc_entry("mymsg", S_IRUSR, &proc_root);
	if (myentry)
	myentry->proc_fops = &proc_mymsg_operations;  /* 创建成功,加文件操作函数 */

	return 0;
}

    我们既然定义了proc_dir_entry结构体的文件处理函数proc_mymsg_operations,我们就要完善它,所以:

static struct file_operations proc_mymsg_operations = {
	.read = mymsg_read,  //read函数
};

    我们现在本应该要完善mymsg_read函数,但是我们知道read函数是要操作到mylog_buf上的,而这里我们还没有定义这个环形缓冲区,并且没有写他相应函数。所以我们先定义环形缓冲区:

#define MYLOG_BUF_LEN 1024   //定义环形缓冲区的大小为1024

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 int is_mylog_empty(void)
{
	return (mylog_w == mylog_r);
}
/* 判断环形缓冲区是否满 */
static int is_mylog_full(void)
{
	return (((mylog_w+1)%MYLOG_BUF_LEN) == (mylog_r%MYLOG_BUF_LEN));
}
/* 向环形缓冲区添加一个字符 */
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(&mylog_waitq);
}
/* 从环形缓冲区读取一个字符 */
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;	
}

    完成这些之后我们要完成myprintk函数,来将要打印的信息放到mylog_buf中,这里我们参考sprintf函数。将myprintk函数写为:

int myprintk(const char *fmt,...)
{
	va_list args;
	int i,j;
	va_start(args,fmt); 
	i = vsnprintf(tmp_buf,INT_MAX,fmt, args);  /* 参考sprintf函数,这里返回值i为放入的值的个数 */
	va_end(args);
	for(j=0;j<i;j++){
		mylog_putc(tmp_buf[j]); 
	}

	return i;
}

    完成这里之后我们就可以写读函数mymsg_read了:

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 */
	if ((file->f_flags & O_NONBLOCK) &&  !is_mylog_empty_for_read())
		return -EAGAIN;
	error = wait_event_interruptible(mylog_waitq, !is_mylog_empty_for_read());

	while(!error && mylog_getc_for_read(&c) && i<count){
		error = __put_user(c, buf);
		buf++;
		i++;
	}

	if(!error)
		error = i;
	
	return error;
}

    好了,讲到这里我们关于写自己的mymsg就介绍完了。而我将代码放到了:做自己的mymsg

参考文章:

内核日志:API 及实现
35.Linux-分析并制作环形缓冲区
Linux内核调试技术之自构proc
Linux修改内核使得普通用户可以打印kmsg内容

猜你喜欢

转载自blog.csdn.net/W1107101310/article/details/80582314