3-内核开发-第一个字符设备模块开发案例
目录
3-内核开发-第一个字符设备模块开发案例
(1) 字符设备背景介绍
(2) 简单版本字符设备模块
(3) 继续丰富我们的字符驱动模块,增加write,read 功能
(4) 编译执行验证
(5)总结
(6)后记
(7)参考
课程简介:
Linux内核开发入门是一门旨在帮助学习者从最基本的知识开始学习Linux内核开发的入门课程。该课程旨在为对Linux内核开发感兴趣的初学者提供一个扎实的基础,让他们能够理解和参与到Linux内核的开发过程中。
课程特点:
1. 入门级别:该课程专注于为初学者提供Linux内核开发的入门知识。无论你是否具有编程或操作系统的背景,该课程都将从最基本的概念和技术开始,逐步引导学习者深入了解Linux内核开发的核心原理。
2. 系统化学习:课程内容经过系统化的安排,涵盖了Linux内核的基础知识、内核模块编程、设备驱动程序开发等关键主题。学习者将逐步了解Linux内核的结构、功能和工作原理,并学习如何编写和调试内核模块和设备驱动程序。
3. 实践导向:该课程强调实践,通过丰富的实例和编程练习,帮助学习者将理论知识应用到实际的Linux内核开发中。学习者将有机会编写简单的内核模块和设备驱动程序,并通过实际的测试和调试来加深对Linux内核开发的理解。
4. 配套资源:为了帮助学习者更好地掌握课程内容,该课程提供了丰富的配套资源,包括教学文档、示例代码、实验指导和参考资料等。学习者可以根据自己的学习进度和需求,灵活地利用这些资源进行学习和实践。
无论你是计算机科学专业的学生、软件工程师还是对Linux内核开发感兴趣的爱好者,Linux内核开发入门课程都将为你提供一个扎实的学习平台,帮助你掌握Linux内核开发的基础知识,为进一步深入研究和应用Linux内核打下坚实的基础。
这一讲主要讲述如何开发第一个Linux字符设备驱动程序模块,动手开发代码,运行加载卸载模块
(1) 字符设备背景介绍
Linux 字符设备驱动程序是一种内核模块,它允许用户空间程序与字符设备进行交互。
字符设备是按字节而不是按块访问的设备。这意味着您可以将它们视为一组连续的字节。
字符设备驱动程序对于 Linux 系统的正常运行至关重要。如果没有字符设备驱动程序,系统将无法与各种设备进行通信。例如,以下是一些字符设备驱动程序的示例:
- 串口驱动程序(
/dev/ttyS*
) - 并口驱动程序(
/dev/lp*
) - 打印机驱动程序(
/dev/lp*
) - 键盘驱动程序(
/dev/input/event*
) - 鼠标驱动程序(
/dev/input/mouse*
) - LCD 显示器驱动程序(
/dev/fb*
) - LED 驱动程序(
/dev/led*
) - 文件系统驱动程序(
/dev/sda*
) - 网络设备驱动程序(
/dev/eth*
) - 块设备驱动程序(
/dev/sda*
有了前面两节的学习,当前我们的实验环境有了,也知道了模块内核加载的流程。所以可以开始实现各种类型的设备模块开发。
在开始前,这里先我们在一起回顾下模块加载和卸载的流程。
Linux 内核模块加载过程
1. 用户空间程序调用 insmod 命令。 此命令将模块文件(以 .ko 结尾)传递给内核。
2. 内核检查模块文件。 内核检查模块文件的语法和签名。
如果模块文件无效或未签名,则内核将拒绝加载它。
3. 内核分配内存并加载模块。
内核分配内存并将模块代码加载到内存中。
4. 内核解析模块符号。
内核解析模块符号并将其添加到内核符号表中。
内核可以在运行时引用模块符号。
5. 内核初始化模块。
内核调用模块的 init() 函数来初始化模块。
init() 函数负责执行模块的任何初始化任务,例如注册设备或创建内核线程。
6. 模块已加载。
一旦模块的 init() 函数返回,模块即已加载。
加载后,内核可以使用模块提供的功能。
卸载内核模块的过程
1. 用户空间程序调用 rmmod 命令。 此命令将模块名称传递给内核。
2. 内核检查模块。 内核检查模块是否正在使用。如果模块正在使用,则内核将拒绝卸载它。
3. 内核调用模块的 exit() 函数。 exit() 函数负责执行模块的任何清理任务,例如注销设备或销毁内核线程。
4. 内核释放模块内存。 内核释放模块占用的内存。
5. 模块已卸载。 一旦模块的 exit() 函数返回,模块即已卸载。
卸载模块之前,内核将等待所有使用该模块的进程完成。这意味着如果某个进程正在使用模块提供的功能,则您可能无法立即卸载该模块。
接下来我们进行开发,我们基于上一节的简单的HelloModule 继续开发。
首先我们在干一件事情前,先思考下,干这件事情,我们有什么收获,这个知识点可以做什么,未来能做什么开发?
字符设备驱动程序通常用于与串口、并口和打印机等设备进行交互。
要编写字符设备驱动程序,您需要遵循以下步骤:
1. 创建一个新的内核模块项目。
2. 在模块中定义字符设备结构。
3. 注册字符设备(open,release)。
4. 实现字符设备操作函数(read,write)。
5. 构建并加载模块。
以下是一个编写字符设备驱动程序的示例(本课程的内核模块名称时hello,字符设备名称为mydev,执行命令时请务必不要混淆)
(2) 简单版本字符设备模块
(a)简单版本,先增加模块的open 与release 方法。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define MAJOR_NUM 90
#define MINOR_NUM 1
#define DEVICE_NAME "mydev"
static struct cdev cdev;
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kk hu");
MODULE_DESCRIPTION("A simple hello world char dev module");
MODULE_VERSION("1.0.0");
static int helloworld_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Hello world!\n");
return 0;
}
static int helloworld_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Goodbye world!\n");
return 0;
}
static struct file_operations helloworld_fops = {
.owner = THIS_MODULE,
.open = helloworld_open,
.release = helloworld_release,
};
static int __init hellowrld_init(void)
{
printk(KERN_INFO "hello world \n");
int ret;
// 注册字符设备
ret = register_chrdev(MAJOR_NUM, DEVICE_NAME, &helloworld_fops);
if (ret < 0) {
printk(KERN_ERR "Failed to register chrdev\n");
return ret;
}
// 初始化字符设备结构
cdev_init(&cdev, &helloworld_fops);
// 添加字符设备到系统
ret = cdev_add(&cdev, MKDEV(MAJOR_NUM, MINOR_NUM), 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add chrdev\n");
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
return ret;
}
printk(KERN_INFO "Hello world character device driver loaded\n");
return 0;
}
static void __exit helloworld_exit(void)
{
// 从系统中移除字符设备
cdev_del(&cdev);
// 注销字符设备
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
printk(KERN_INFO "Hello world character device driver unloaded\n");
}
module_init(hellowrld_init);
module_exit(helloworld_exit)
里面两个核心函数register_chrdev,unregister_chrdev 这里详细说明下:
register_chrdev 函数用于向内核注册字符设备。必须在使用字符设备之前完成。
register_chrdev 函数的原型如下:
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
其中:
- major:字符设备的主设备号。
- name:字符设备的名称。
- fops:指向文件操作结构的指针。文件操作结构包含用于字符设备的各种操作的函数指针。
unregister_chrdev 函数用于从内核注销字符设备。必须在不再使用字符设备后完成。
unregister_chrdev 函数的原型如下:
int unregister_chrdev(unsigned int major, const char *name);
其中:
- major:字符设备的主设备号。
- name:字符设备的名称。
我们的Makefile文件不需要修改,直接复制上一节课的内容。编译及测试上述代码。
(a)简单版编译,加载,测试
peach@peach-VirtualBox:~/HelloModule$ make
make -C /lib/modules/5.15.0-105-generic/build M=/home/peach/HelloModule modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-105-generic'
CC [M] /home/peach/HelloModule/hello.o
/home/peach/HelloModule/hello.c: In function ‘hellowrld_init’:
/home/peach/HelloModule/hello.c:45:13: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
45 | int ret;
| ^~~
MODPOST /home/peach/HelloModule/Module.symvers
CC [M] /home/peach/HelloModule/hello.mod.o
LD [M] /home/peach/HelloModule/hello.ko
BTF [M] /home/peach/HelloModule/hello.ko
Skipping BTF generation for /home/peach/HelloModule/hello.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-105-generic'
(b)通过设备名称检查设备是否安装
peach@peach-VirtualBox:~/HelloModule$ cat /proc/devices |grep mydev
90 mydev
从输出,我们也看到,已经加载成功,主设备号为 90
(c)dmesg 查看内核日志信息
peach@peach-VirtualBox:~/HelloModule$ dmesg
[79204.243936] Hello world character device driver unloaded
(3) 继续丰富我们的字符驱动模块,增加write,read 功能
helloworld_fops 增加write,read ops ,后面我们需要实现这两个函数
.read = helloworld_read,
.write = helloworld_write,
定义一个全局的缓冲区 static char message[256]; 后面操作我们会用到这个缓冲区,实现 read,write 方法,这两个方法都需要返回读写了多少个字节。
static ssize_t helloworld_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
int len = strlen(message);
if (*pos >= len) {
return 0;
}
if (copy_to_user(buf, message + *pos, len - *pos)) {
return -EFAULT;
}
*pos += len;
return len;
}
static ssize_t helloworld_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
int len = count;
if (*pos + len > sizeof(message)) {
return -ENOSPC;
}
if (copy_from_user(message + *pos, buf, len)) {
return -EFAULT;
}
*pos += len;
return len;
}
(4) 编译执行验证
(a)make
$make
(b)insmod ko 文件
$sudo insmod hello.ko
(c)mknod
使用 mknod 命令创建一个字符设备文件 /dev/mydev,命令将创建字符设备文件 /dev/mydev,其主设备号为 90,次设备号为 1。为我们后面进行实验做好准备工作。
$sudo mknod /dev/mydev c 90 1
(d)测试echo 数据,这个时候,需要切换到root
$sudo su root #进入root 用户
$echo "12345" > /dev/mydev
验证写入的内容:
root@peach-VirtualBox:/home/peach/HelloModule# cat /dev/mydev
12345
root@peach-VirtualBox:/home/peach/HelloModule#
测试:echo "hello" > /dev/mydev
验证成功。
dmesg 看内核消息, 显示了模块加载卸载流程。
[80080.128565] Hello world character device driver loaded
[81006.718723] Hello world!
[81006.718748] Goodbye world!
(5)总结
到此,我们实现了字符设备,本章课程所有代码(简单版本hello.v1.c,丰富版本 hello.c )
Makefile 文件
obj-m += hello.o
CFLAGS := -Wall -O2
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
简单版本 hello.v1.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define MAJOR_NUM 90
#define MINOR_NUM 1
#define DEVICE_NAME "mydev"
static struct cdev cdev;
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kk hu");
MODULE_DESCRIPTION("A simple hello world char dev module");
MODULE_VERSION("1.0.0");
static int helloworld_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Hello world!\n");
return 0;
}
static int helloworld_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Goodbye world!\n");
return 0;
}
static struct file_operations helloworld_fops = {
.owner = THIS_MODULE,
.open = helloworld_open,
.release = helloworld_release,
};
static int __init hellowrld_init(void)
{
printk(KERN_INFO "hello world \n");
int ret;
// 注册字符设备
ret = register_chrdev(MAJOR_NUM, DEVICE_NAME, &helloworld_fops);
if (ret < 0) {
printk(KERN_ERR "Failed to register chrdev\n");
return ret;
}
// 初始化字符设备结构
cdev_init(&cdev, &helloworld_fops);
// 添加字符设备到系统
ret = cdev_add(&cdev, MKDEV(MAJOR_NUM, MINOR_NUM), 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add chrdev\n");
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
return ret;
}
printk(KERN_INFO "Hello world character device driver loaded\n");
return 0;
}
static void __exit helloworld_exit(void)
{
// 从系统中移除字符设备
cdev_del(&cdev);
// 注销字符设备
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
printk(KERN_INFO "Hello world character device driver unloaded\n");
}
module_init(hellowrld_init);
module_exit(helloworld_exit)
丰富版本代码-hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define MAJOR_NUM 90
#define MINOR_NUM 1
#define DEVICE_NAME "mydev"
static struct cdev cdev;
static char message[256];
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kk hu");
MODULE_DESCRIPTION("A simple hello world char dev module");
MODULE_VERSION("1.0.0");
static int helloworld_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Hello world open !\n");
return 0;
}
static int helloworld_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Goodbye world! release!\n");
return 0;
}
static ssize_t helloworld_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
int len = strlen(message);
if (*pos >= len) {
return 0;
}
if (copy_to_user(buf, message + *pos, len - *pos)) {
return -EFAULT;
}
*pos += len;
return len;
}
static ssize_t helloworld_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
int len = count;
if (*pos + len > sizeof(message)) {
return -ENOSPC;
}
if (copy_from_user(message + *pos, buf, len)) {
return -EFAULT;
}
*pos += len;
return len;
}
static struct file_operations helloworld_fops = {
.owner = THIS_MODULE,
.open = helloworld_open,
.release = helloworld_release,
.read = helloworld_read,
.write = helloworld_write,
};
static int __init hellowrld_init(void)
{
printk(KERN_INFO "hello world \n");
int ret;
// 注册字符设备
ret = register_chrdev(MAJOR_NUM, DEVICE_NAME, &helloworld_fops);
if (ret < 0) {
printk(KERN_ERR "Failed to register chrdev\n");
return ret;
}
// 初始化字符设备结构
cdev_init(&cdev, &helloworld_fops);
// 添加字符设备到系统
ret = cdev_add(&cdev, MKDEV(MAJOR_NUM, MINOR_NUM), 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add chrdev\n");
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
return ret;
}
printk(KERN_INFO "Hello world character device driver loaded\n");
return 0;
}
static void __exit helloworld_exit(void)
{
// 从系统中移除字符设备
cdev_del(&cdev);
// 注销字符设备
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
printk(KERN_INFO "Hello world character device driver unloaded\n");
}
module_init(hellowrld_init);
module_exit(helloworld_exit)
(6)后记
当前linux 内核变化大,可能这边文章的代码,可能无法在你的环境里面执行,这时候,你就需要进行修改了。那么如何让你的代码具有兼容各个版本内核,就是一项工程化工作及问题。
(7)参考
有关字符设备驱动程序的更多信息,请参阅 Linux 内核文档:https://www.kernel.org/doc/html/v4.11/driver-api/index.html
有关 cdev 结构的更多信息,请参阅 Linux 内核 API 文档:https://www.kernel.org/doc/html/latest/search.html?q=cdev