1. 问题描述
梳理内核函数dump_stack的源码实现时,PC上竟然没有该函数打印内容的日志截图,又不想从网上盗图,所以学习下如何在内核中打印Hello World。
2. 解决方案
科普:Kernel Module是一个二进制文件,用于运行时拓展内核功能,如果功能不被需要时,可被卸载。直观上看,与dlopen机制类似,程序运行时显示加载so,当不使用该库时,调用dlclose关闭so。
解决思路:基于Kernel Module机制,执行命令"insmod + 模块名" 安装模块,执行打印。
2.1 基础版
注意:测试使用的系统是Ubuntu,目录/lib/modules下已有内核相应的头文件,无需自己安装源码。
- 模块被加载时,内核执行函数hello_init,卸载则执行hello_exit。源文件名:hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
static int hello_init(void)
{
pr_info("%s\n", "Hello World!");
return 0;
}
static void hello_exit(void)
{
pr_info("%s\n", "Byebye");
return;
}
module_init(hello_init);
module_exit(hello_exit);
仅从代码来看,该机制类似于Java中调用System.loadLibrary加载so,so的JNI_OnLoad方法被执行。
- Makefile,不要迟疑,文件名也是Makefile
obj-m := hello.o
KDIR := /lib/modules/`uname -r`/build
modules:
make -C $(KDIR) M=`pwd`
clean:
make -C $(KDIR) M=`pwd` clean
- 代码目录结构如下所示:
执行make命令即可,编译成功后,将生成hello.ko文件
- 安装模块,命令:insmod hello.ko
日志文件 /var/log/syslog,可看到如下打印。
使用命令lsmod,可查看内核加载的模块。
从图中也不难看出,hello模块也确实被加载到内核。
5. 卸载模块,命令:rmmod hello
2.2 进阶版
需求场景:用户态进程使用C++ std::thread启动线程,该线程内部调用Linux系统调用,然后系统调用内部使用dump_stack打印内核堆栈。显然只有"Hello World"还是不够的。如果实现该需求场景,将面临一个问题,那就是如何让系统调用将用户请求转发到自己写的Kernel Module中来呢 ?
浏览Linux内核打印Hello World相关的技术文章时,偶然碰见有一篇文章使用了字符设备驱动的方法,如果仅仅是为了完成该任务,可以不假思索地采取该办法,如果仔细思考一下,为什么要使用这个方法 ?有没有更简单的方法呢 ?
通过学习kernel module的概念,了解到大多数的设备驱动都以kernel module的形式存在,那么设备驱动是什么呢 ?在此之前,对于驱动这个名词,脑海中其实并没有很准确的概念,仅仅是从同事那里听说,驱动开发之类的名词。其实,设备驱动是与硬件设备交互的内核组件,更细致来讲,用户通过读写特定的设备文件来完成硬件设备的访问。这样看来,我要做的事情其实就是写一个驱动来处理用户的请求。接下来便是学习驱动的相关基础概念,驱动包含两种类型,字符类型和块类型,选择哪一种类型呢 ?用户的系统调用直接转发至字符设备驱动的相关函数,而块设备与系统调用之间还夹杂着文件系统等诸多模块,文件系统又是另外一件复杂的事情,实现块设备驱动对于目前的我而言,是一件不可能完成的事情,而且对于要实现的需求,也没有必要,所以选择字符设备驱动是唯一的选择。
修改后的hello.c文件,内容如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static int hello_open(struct inode *inode, struct file *file)
{
pr_info("%s\n", "hello open");
dump_stack();
return 0;
}
static int hello_release(struct inode *inode, struct file *file)
{
pr_info("%s\n", "hello release");
return 0;
}
static ssize_t hello_read(struct file *file, char __user *user_buffer,
size_t size, loff_t *offset)
{
pr_info("%s\n", "hello read");
return 0;
}
static ssize_t hello_write(struct file *file, const char __user *user_buffer,
size_t size, loff_t * offset)
{
pr_info("%s\n", "hello write");
return 0;
}
#define MY_MAJOR 99
struct cdev dev;
struct class *hello_char_class;
const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,
};
MODULE_LICENSE("GPL");
static int hello_init(void)
{
pr_info("%s\n", "Hello Driver!");
int err;
err = register_chrdev_region(MKDEV(MY_MAJOR, 0), 1,
"hello_driver");
if (err != 0) {
return err;
}
cdev_init(&dev, &hello_fops);
cdev_add(&dev, MKDEV(MY_MAJOR, 0), 1);
hello_char_class = class_create(THIS_MODULE, "hello_driver");
device_create(hello_char_class, NULL, MKDEV(MY_MAJOR, 0), NULL, "hello_driver1");
return 0;
}
static void hello_exit(void)
{
pr_info("%s\n", "Byebye Driver!");
device_destroy(hello_char_class, MKDEV(MY_MAJOR, 0));
class_destroy(hello_char_class);
cdev_del(&dev);
unregister_chrdev_region(MKDEV(MY_MAJOR, 0), 1);
return;
}
module_init(hello_init);
module_exit(hello_exit);
编译,安装,卸载模块等过程同上,安装成功后,在/dev/目录可以查看到设备文件hello_driver1。
执行命令 cat hello_driver1,便可触发dump_stack函数。
到此为止,使用c++ std::thread新建线程执行系统调用,在内核中打印dump_stack的任务已完成99%,剩下的一丢丢c++相关代码就不在此处继续呈现了。
3. 总结
通过上述实验及查找相关资料,学习到的知识点如下:
- kernel module的概念,安装,卸载module的简易实践。
- 设备的标识符的组成原理,MAJOR, MINOR等表示的多种含义。
- 设备驱动的类别,及不同类别驱动之间的比较,应用场景等,字符设备驱动的简易实践。
概括一下,实践完上述过程,能够更具体地认知驱动,设备等概念,更近一步地了解linux。