通过此文,你会了解到怎么创建一个内核模块,并且加载到Linux内核中。然后修改这个内核模块,以便在/proc文件系统中创建一个条目。
开发内核模块的优势就是,这是一个与内核交互的相对而言比较容易的方法,因为可以让你写程序直接调用内核函数。很重要的是,你必须记住,你实际是在写直接于内核交互的内核代码。也就是说,你代码中的任何错误都会有可能导致系统的崩溃。
内核模块概览
首先是创建模块并把模块插入Linux内核中。你可以通过如下命令来列出当前加载的所有内核模块:
![2e208c35124f5dc53c0f44b7a29e15bd.png](https://img-blog.csdnimg.cn/img_convert/2e208c35124f5dc53c0f44b7a29e15bd.png)
这个命令会把当前的内核模块列为3列,分别是名字,大小,以及在哪里被使用。
下面的代码是一个非常基本的内核模块,当它被加载或卸载时,会打印出适当的信息。
![3402c277965b0f58293c2da0d49f35b9.png](https://img-blog.csdnimg.cn/img_convert/3402c277965b0f58293c2da0d49f35b9.png)
函数simple_init()是模块入口点module entry point,代表当模块被加载时会调用的函数。同样的,函数simple_exit()模块出口点module entry point,当模块被从内核中移除时,这个函数会被调用。
模块入口点函数必须返回一个整数,0代表成功,其他任何值表示失败。模块出口点函数返回空。模块入口点和出口点函数都不能传递任何参数。下面两个宏是在内核中注册模块的入口和出口点的。
![8f7b1cc8c7f5cbae9daa9df6d2c34616.png](https://img-blog.csdnimg.cn/img_convert/8f7b1cc8c7f5cbae9daa9df6d2c34616.png)
这里需要注意的是之前代码途中,模块入口点和出口点的函数都调用了函数printk()。printk是内核中相当于printf()功能的函数,但它会把输出发送到内核日志缓存中,可以通过dmesg命令读取。printf()与printk()的一个区别是,printk()指定了优先级,这些值在中声明。在上文中,优先级是KERN_INFO,这是定义一条informational消息。
最后几行的MODULE_LICENSE(),MODULE_DESSCRIPTION(),以及MODULE_AUTHOR(),代表了有关软件许可证,模块的描述以及作者的相信信息。对于我们来说,其实是不需要的,但我们这么做是因为这是开发内核模块时的标准实际操作之一。
因为是在ubuntu中演示,首先下载Linux源代码:
sudo apt-get install linux-headers-$(uname -r)
sudo apt-get install linux-source
以下是测试目录结构:
![d977c493e423c51301ff6bfae42f42fc.png](https://img-blog.csdnimg.cn/img_convert/d977c493e423c51301ff6bfae42f42fc.png)
下面是Makefile中的内容:
![899f997ae857e47e3769ffd48155bde6.png](https://img-blog.csdnimg.cn/img_convert/899f997ae857e47e3769ffd48155bde6.png)
在命令行中输入:
![b3c49839bd6e450b4124e650a86cd115.png](https://img-blog.csdnimg.cn/img_convert/b3c49839bd6e450b4124e650a86cd115.png)
编译会产生几个文件,其中simple.ko就是编译出的内核模块。下面是怎么插入到内核中的步骤:
加载和移除内核模块
内核模块的加载是通过使用insmod命令实现的,如下运行:
![6e90c19d00f3230db85b0c48b5aeb1b1.png](https://img-blog.csdnimg.cn/img_convert/6e90c19d00f3230db85b0c48b5aeb1b1.png)
为了验证模块是否被加载,可以运行lsmod命令,然后查找simple。
![4a1463d2272f1578fae0e6c5d1dcd006.png](https://img-blog.csdnimg.cn/img_convert/4a1463d2272f1578fae0e6c5d1dcd006.png)
因为我们在模块的入口点调用的函数会打印信息。信息,通过如下核实在内核日志缓存中的消息:
![56582063132a68859dca051203afeeb2.png](https://img-blog.csdnimg.cn/img_convert/56582063132a68859dca051203afeeb2.png)
移除内核模块是通过调用rmmod命令实现的,这里需要注意的是后缀.ko不是必须的:
![cfcaa2846fca8b09bc33d85dd267e156.png](https://img-blog.csdnimg.cn/img_convert/cfcaa2846fca8b09bc33d85dd267e156.png)
再次通过dmesg确认模块已经被移除:
![a85b5334c8e8d8802d1dff6e6d6d9e91.png](https://img-blog.csdnimg.cn/img_convert/a85b5334c8e8d8802d1dff6e6d6d9e91.png)
因为内核日志缓存很容易被填满,所以定期性的清空缓存是有意义的,命令如下:
![bec7d4674445f89d8e1f07d3a3c78922.png](https://img-blog.csdnimg.cn/img_convert/bec7d4674445f89d8e1f07d3a3c78922.png)
由于内核模块是在内核中运行的,所以获取一些只在内核而不在用户应用中的值和函数调用是可能的。比如说,Linux中的头文件定义了几个哈希函数,这个文件也定义了一个常数GOLDEN_RATIO_PRIME(这被定义为一个unsigned long)。这个值可以被如下打印:
![d9a458a3c665bc32c44efc00e02dd524.png](https://img-blog.csdnimg.cn/img_convert/d9a458a3c665bc32c44efc00e02dd524.png)
另一个例子是头文件定义了如下还是函数:
unsigned long gcd(unsigned long a, unsigned b);
这个函数会返回参数a和b的最大公约数。
下面是在simple_init()打印GOLDEN_RATION_PRIME以及在simple_exit()打印出3300和24的最大公约数的试验步骤及结果:
![8416205d18153bb03840e7fabd43f4cd.png](https://img-blog.csdnimg.cn/img_convert/8416205d18153bb03840e7fabd43f4cd.png)
![5c37f849756da41a5b767004c10f24d3.png](https://img-blog.csdnimg.cn/img_convert/5c37f849756da41a5b767004c10f24d3.png)
![571c2fcab99c59148809ea14b4bd6a0f.png](https://img-blog.csdnimg.cn/img_convert/571c2fcab99c59148809ea14b4bd6a0f.png)
在Linux中,计时器timer的滴答速度(tick rate)为中定义的HZ值。 HZ的值决定了定时器中断的频率,其值因机器类型和体系结构而异。例如,如果HZ的值为100,则定时器中断每秒发生100次,也就是每10ms一次。另外,内核跟踪全局变量jiffies,该变量维护自系统启动以来发生的计时器中断数。 jiffies变量在文件中声明。
下面是在simple_init()函数打印出jiffies和HZ以及在simple_exit()再次打印jiffies的试验步骤:
![cc15e8b9c401adc77f2b255215e196a5.png](https://img-blog.csdnimg.cn/img_convert/cc15e8b9c401adc77f2b255215e196a5.png)
![c6141aace8963960d9f3fa2ff6cf30b7.png](https://img-blog.csdnimg.cn/img_convert/c6141aace8963960d9f3fa2ff6cf30b7.png)
这里需要注意的是,使用simple_init()以及simple_exit()中的jiffies可以知道从内核模块加载到益处一共过去了多少秒。
/proc文件系统
/proc文件系统是一个只存在与内核内存中的伪文件系统,主要用来查询各种内核和单个进程的统计信息。
下面我们实现的内核模块是在/proc中创建额外的条目,其中包含内核统计和指定进程的信息。代码实现如下:
![df52ab874b6be54ff33c684bdfc61389.png](https://img-blog.csdnimg.cn/img_convert/df52ab874b6be54ff33c684bdfc61389.png)
Makefile如下:
![551d9d64ba3cd4412c4eed3db229184f.png](https://img-blog.csdnimg.cn/img_convert/551d9d64ba3cd4412c4eed3db229184f.png)
我们首先描述如何在/ proc文件系统中创建新条目。上述程序示例(名为hello.c)创建一个名为/ proc / hello的/ proc条目。如果用户输入如下命令:
![69a5b6e1c6cd29ccb083cd0be51a81e3.png](https://img-blog.csdnimg.cn/img_convert/69a5b6e1c6cd29ccb083cd0be51a81e3.png)
则会返回Hello World。
在模块进入点proc_init(),我们创建了通过使用proc_create()函数创建了/proc/hello条目。此函数通过proc_ops传递,其中包含对struct文件操作的引用。这个结构体初始化成员owner和read。read的值是函数proc_read()的函数名,当/proc_hello被读取时会被调用。
通过检视proc_read() 函数,我们可以看到字符串“Hello World”被写入变量buffer,而buffer存在于内核空间。因此/proc/hello可以用用户空间访问,我们必须通过copy_to_user函数把buffer中的内容复制到用户空间。因此,这个函数就是把内核内存buffer的内容复制到用户空间中的usr_buf。
每次/proc/hello读取时,函数proc_read会被重复调用,直到它返回0,所以必须确保这个函数在一旦收集到数据后,返回0,以便返回到/proc/hello文件中。
最后,需要注意的是/proc/hello会被proc_exit()调用后被删除,因为其中调用了remove_proc_entry()。
![f0c3fbb0ce1de3058cd586dec423fd4d.png](https://img-blog.csdnimg.cn/img_convert/f0c3fbb0ce1de3058cd586dec423fd4d.png)
下面是两个练习,这里只贴出了关键代码:
- 设计一个内核模块,它创建了一个proc文件/proc/jiffies,当/proc/jiffies文件被读取时,报告当前jiffies的值
![a04e998d14ae8a679c113e65f5990cca.png](https://img-blog.csdnimg.cn/img_convert/a04e998d14ae8a679c113e65f5990cca.png)
![6256754c4d9ad8e7013da3d5a6bc5947.png](https://img-blog.csdnimg.cn/img_convert/6256754c4d9ad8e7013da3d5a6bc5947.png)
- 设计一个内核模块,它创建了一个proc文件/proc/seconds,当/proc/seconds文件被读取时,报告自内核模块装载后过去了多少秒。
![258495959c167411800d1d03ffbec9fd.png](https://img-blog.csdnimg.cn/img_convert/258495959c167411800d1d03ffbec9fd.png)
![cb0888a2206772d8d65488cbad0f15c3.png](https://img-blog.csdnimg.cn/img_convert/cb0888a2206772d8d65488cbad0f15c3.png)
![0137c33082729ddc8c9f55adabd1288c.png](https://img-blog.csdnimg.cn/img_convert/0137c33082729ddc8c9f55adabd1288c.png)