使用Linux内核序列文件

现代编程的一个特征是使用全球网络作为参考信息的来源,特别是用于解决特定程序员未知或鲜为人知的问题的模式的来源。 这种方法可以节省大量时间,而且通常可以得到定性的结果。 但是,尽管通常正确,但网络中提出的解决方案并未始终考虑解决问题的所有细微之处,这导致通常在正常情况下工作的部分出现在源代码中,但在不太标准的情况下会成为问题。令人不愉快的惊喜的来源。

考虑在Linux内核中使用序列文件的主题,这些文件被认为是从内核模式进行打印的最方便的机制。 但是实际上,正确使用它们比您想象的要困难得多。

可在线获取有关此主题的许多材料。 最好的是内核本身的源代码,其中包含相当详细的注释。 这种信息来源的问题在于它的数量。 如果您不确切知道要寻​​找什么,那么最好是只限于有限的时间而不尝试。 对我而言,当我对该主题感兴趣时,Google提供了一些与搜索有关的看似出色的信息资源:著名的《 Linux内核模块编程指南》Rob Day的一系列文章 。 这些来源不是新鲜事物,而是非常可靠的。

首先,让我们更自然地考虑何时使用序列文件。 最常见的情况是在/ proc文件系统中创建自己的文件。 通过阅读该系统的文件,您可以获得有关所用设备,其驱动程序,RAM,进程等的各种信息。

看起来任何东西的打印输出都是编程中最简单的任务。 但是,在内核模式下,操作系统施加了许多限制,这些限制对于应用程序软件的开发人员而言似乎是完全无法想象的。 在内核模式下,打印缓冲区的大小受虚拟内存页面的大小限制。 对于x86架构,它为4 KB。 因此,一个好的程序在打印大量数据时必须首先达到最大程度地填充缓冲区,然后再打印,然后重复此迭代直到打印数据完全用尽。 当然,您可以逐个字符地打印,这将大大简化一切,但是我们正在谈论好的程序。

上述消息来源比预期的要差一些。 例如,在书中,某些信息通常被认为是不正确的,这正是促使我编写此笔记的原因。 通常认为以方案图片的形式给出的信息最容易理解和使用。 但是在本书中与主题有关的图片是不正确的。 使用这种方案可能会导致严重的错误,尽管本书中的示例可以正确运行并遵循这种方案。 这是由于以下事实:在此示例中,访问/ proc / iter时一次仅打印几个字节。 如果将它用作打印大于一页内存的文本的模板,将会出乎意料。 上述文章系列没有明显的错误,但是没有报告一些对于理解该主题很重要的细节。

因此,让我们首先考虑如何使用序列文件的正确方案。



要使用这样的文件,您需要创建函数start(),stop(),next()和show()。 这些功能的名称可以是任何名称,我选择的含义与功能的动作相对应的最短单词。 如果存在此类功能并将其正确连接到内核系统,则访问/ proc目录中与它们相关的文件时,它们将自动开始工作。 最令人困惑的是使用stop()函数,可以在三种不同的上下文中调用该函数。 在start()之后调用它意味着结束打印作业。 在show()之后调用它意味着对缓冲区的最后一次打印操作(通常使用seq_printf函数)使页面缓冲区溢出,并且此打印操作被取消。 在next()之后调用它是在将某些数据打印到缓冲区结束时发生的最有趣的情况,您需要完成工作或使用新数据。 例如,假设/ proc目录中的文件在访问时首先生成有关块设备的信息,然后生成有关字符设备的信息。 首先,start()函数初始化块设备的打印,next()和可能的show()函数使用此初始化数据来打印有关块设备的逐步信息。 当一切准备就绪时,在最后一次调用next()之后,将进行考虑的stop()调用,然后调用start(),这一次应该已经启动了字符设备的进一步打印。

我从Rob Day的文章中给出了一个稍作修改的示例(文件evens.c的内容)。 我不得不用现代的内核替换一个函数的调用,而该函数实际上是等效的。 评论也略有变化。

#include <linux/module.h> #include <linux/moduleparam.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/proc_fs.h> #include <linux/fs.h> #include <linux/seq_file.h> #include <linux/slab.h> static int limit = 10; //default value, it can be changed here or module_param(limit, int, S_IRUGO); //transfered as a module parameter static int* even_ptr; //we will work with dynamic memory /** * start */ static void *ct_seq_start(struct seq_file *s, loff_t *pos) { printk(KERN_INFO "Entering start(), pos = %Ld, seq-file pos = %lu.\n", *pos, s->count); if (*pos >= limit) { // are we done? printk(KERN_INFO "Apparently, we're done.\n"); return NULL; } //Allocate an integer to hold our increasing even value even_ptr = kmalloc(sizeof(int), GFP_KERNEL); if (!even_ptr) // fatal kernel allocation error return NULL; printk(KERN_INFO "In start(), even_ptr = %pX.\n", even_ptr); *even_ptr = (*pos)*2; return even_ptr; } /** * show */ static int ct_seq_show(struct seq_file *s, void *v) { printk(KERN_INFO "In show(), even = %d.\n", *(int*)v); seq_printf(s, "The current value of the even number is %d\n", *(int*)v); return 0; } /** * next */ static void *ct_seq_next(struct seq_file *s, void *v, loff_t *pos) { printk(KERN_INFO "In next(), v = %pX, pos = %Ld, seq-file pos = %lu.\n", v, *pos, s->count); (*pos)++; //increase my position counter if (*pos >= limit) //are we done? return NULL; *(int*)v += 2; //to the next even value return v; } /** * stop */ static void ct_seq_stop(struct seq_file *s, void *v) { printk(KERN_INFO "Entering stop().\n"); if (v) printk(KERN_INFO "v is %pX.\n", v); else printk(KERN_INFO "v is null.\n"); printk(KERN_INFO "In stop(), even_ptr = %pX.\n", even_ptr); if (even_ptr) { printk(KERN_INFO "Freeing and clearing even_ptr.\n"); kfree(even_ptr); even_ptr = NULL; } else printk(KERN_INFO "even_ptr is already null.\n"); } /** * This structure gathers functions which control the sequential reading */ static struct seq_operations ct_seq_ops = { .start = ct_seq_start, .next = ct_seq_next, .stop = ct_seq_stop, .show = ct_seq_show }; /** * This function is called when a file from /proc is opened */ static int ct_open(struct inode *inode, struct file *file) { return seq_open(file, &ct_seq_ops); }; /** * This structure gathers functions for a /proc-file operations */ static struct file_operations ct_file_ops = { .owner = THIS_MODULE, .open = ct_open, .read = seq_read, .llseek = seq_lseek, .release = seq_release }; /** * This function is called when this module is loaded into the kernel */ static int __init ct_init(void) { proc_create("evens", 0, NULL, &ct_file_ops); return 0; } /** * This function is called when this module is removed from the kernel */ static void __exit ct_exit(void) { remove_proc_entry("evens", NULL); } module_init(ct_init); module_exit(ct_exit); MODULE_LICENSE("GPL"); 


与序列文件一起使用的函数使用两个具有重叠功能的指针(这也有些令人困惑)。 其中之一应通过show()指向要打印到缓冲区的当前对象-它是程序中的“ v”指针。 另一个指针“ pos”通常用于指向计数器。

对于那些可能第一次想要在内核模式下运行其程序的人,我举一个成功构建的Makefile示例。 当然,要成功构建,必须在系统中具有Linux内核源标头。

 obj-m += evens.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean 


使用以下命令sudo insmod evens.ko连接到内核,然后使用cat /proc/evens sudo insmod evens.ko命令检查/ proc / evens-file的功能(通过此命令查看cat /proc/evens ,并使用以下命令解释系统操作:命令sudo cat /var/log/messages

要使页面缓冲区溢出,请将limit参数设置为一个较大的值,例如200。此值可以输入到程序的文本中,或者在使用命令sudo insmod events.ko limit=200加载模块时使用。

日志分析可以解释剩余的不清楚之处。 例如,您可能会注意到,在next()或start()之后调用stop()之前,系统将变量'v'清零。 您可能还会注意到,在调用stop()之后再调用start()之前,系统将打印缓冲区的内容。

如果有人报告我的笔记中发现的任何不准确之处或应提及的其他任何内容,我将不胜感激。

源代码也可以在这里获得

Source: https://habr.com/ru/post/zh-CN444620/


All Articles