第三章 驱动file_operations
3.1 字符设备驱动
在讨论file_operations的作用之前, 我们先讨论Linux字符设备驱动。 Linux 字符驱动(Character Driver)允许用户通过字符设备文件与硬件进行交互。字符设备是一种按字节读写数据的设备,像键盘、串口、终端等,都属于字符设备。与之对应的是块设备(Block Device),如硬盘,它们按块(通常是512字节或更大)进行读写。
字符驱动就是告诉 Linux 内核,如何跟某个字符设备(比如键盘或串口)沟通。当应用程序需要与设备通信时,它会通过字符驱动来进行。例如,你输入一个字符,驱动程序会将这个信息转发给相应的设备,然后设备会执行相应的操作。
3.2 如何注册字符设备驱动
创建一个字符驱动代码文件fileop.c, 内容如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linx Zhang");
MODULE_DESCRIPTION("A simple LKM without file operations");
#define DEVICE_NAME "fopdev"
#define CLASS_NAME "demo_fops_class"
static int major; // major device number
static struct class* demo_fops_class = NULL; // device class structure
static struct cdev* demo_fops_device = NULL; // character device structure
// Initialize the driver
static int __init fops_init(void) {
dev_t dev_num; // device number (major and minor)
// 1. Allocate a device number
if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
printk(KERN_ALERT "Failed to allocate major number\n");
return -1;
}
major = MAJOR(dev_num); // Get the major device number
// 2. Create the device class
demo_fops_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(demo_fops_class)) {
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(demo_fops_class);
}
// 3. Allocate memory for the cdev structure and initialize it
demo_fops_device = cdev_alloc(); // Allocate memory for the cdev structure
if (!demo_fops_device) {
class_destroy(demo_fops_class);
unregister_chrdev_region(dev_num, 1);
return -1;
}
cdev_init(demo_fops_device, NULL); // Not using file_operations
demo_fops_device->owner = THIS_MODULE;
// 4. Add the character device to the system
if (cdev_add(demo_fops_device, dev_num, 1) < 0) {
kfree(demo_fops_device); // Free allocated memory for cdev
class_destroy(demo_fops_class);
unregister_chrdev_region(dev_num, 1);
return -1;
}
// 5. Create a device node in /dev
if (device_create(demo_fops_class, NULL, dev_num, NULL, DEVICE_NAME) == NULL) {
cdev_del(demo_fops_device);
class_destroy(demo_fops_class);
unregister_chrdev_region(dev_num, 1);
return -1;
}
printk(KERN_INFO "Device registered with major number %d\n", major);
return 0;
}
// Exit function to clean up the driver
static void __exit fops_exit(void) {
dev_t dev_num = MKDEV(major, 0);
device_destroy(demo_fops_class, dev_num); // Remove the device node
class_destroy(demo_fops_class); // Remove the device class
cdev_del(demo_fops_device); // Unregister the character device
unregister_chrdev_region(dev_num, 1); // Release the device number
kfree(demo_fops_device); // Free allocated memory for the cdev structure
printk(KERN_INFO "Goodbye!\n");
}
module_init(fops_init);
module_exit(fops_exit);
1.) 设备号分配
- 使用
alloc_chrdev_region()
分配一个动态的主设备号,并将其保存在 major 变量中。
2.) 设备节点的创建
(1). 使用 class_create()
创建一个设备类。
class_create 函数用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加到 Linux 内核系统中。这个函数的执行效果是在 /sys/class 目录下创建一个新的文件夹,文件夹的名字为函数的第二个输入参数。
struct class *class_create(struct module *owner, const char *name);
• owner: 指向模块的指针,通常为 THIS_MODULE
。
• name: 类的名称,用于在 /sys/class 下创建相应的目录。
上面驱动编译并加载驱动后, 有如下:
neardi@LPA3588:~/fileop$ ls /sys/class/demo_fops_class/
fopdev
(2). device_create
device_create 函数用于动态创建逻辑设备,并对新的逻辑设备类进行相应的初始化,将其与指定的逻辑类关联起来,然后将此逻辑设备添加到 Linux 内核系统的设备驱动程序模型中。这个函数能够自动在 /sys/devices/virtual 目录下创建新的逻辑设备目录,并在 /dev 目录下创建与逻辑类对应的设备文件。
加载驱动后, 在/dev有生成如下节点:
neardi@LPA3588:~/fileop$ ls /dev/fopdev -l
crw------- 1 root root 234, 0 Oct 17 14:01 /dev/fopdev
上面的234, 0
对应:
3.) 字符设备的初始化和注册
4.) 模块加载与卸载
3.2.1 测试驱动
编写一个test.cpp测试文件, 内容与 3.7.1一致。 编译及运行有如下结果:
neardi@LPA3588:~/fileop$ sudo ./test
Failed to open the device
这里可以看出, 打开/dev/fopdev设备失败, 就是因为fopdev.c驱动没有对file_operations
的支持。
3.3 file_operations
在Linux设备驱动程序中,file_operations结构体是非常重要的,因为它定义了驱动程序如何处理文件操作。这些操作包括应用层打开、读取、写入和关闭设备文件。
file_operations结构体定义在kernel的linux/fs.h头文件里, 主要内容如下:
static struct file_operations fops = {
.open = device_open,
.read = device_read,
.write = device_write,
.release = device_release,
};
• open: 打开设备文件时调用。
• read: 从设备文件读取数据。
• write: 向设备文件写入数据。
• release: 关闭设备文件时调用。
3.4 为什么需要file_operations?
• 提供统一的接口
file_operations结构体为用户空间应用程序提供了一个统一的接口,使得它们可以通过标准的文件操作函数(如open、read、write和close)与设备进行交互。这种统一的接口简化了应用程序的开发,因为开发者不需要了解设备的具体实现细节。
• 管理设备访问
通过实现file_operations中的函数,驱动程序可以控制设备的访问权限。例如,可以在open函数中检查设备是否已经被其他进程打开,从而防止多个进程同时访问同一个设备,导致数据冲突或设备损坏。
• 数据传输
read和write函数允许用户空间应用程序与设备进行数据传输。read函数从设备读取数据并传输到用户空间,而write函数则将用户空间的数据写入设备。这些函数确保数据在用户空间和内核空间之间安全传输。
• 处理设备特定操作
不同的设备可能需要特定的操作,例如配置设备参数或处理中断。通过实现file_operations中的函数,驱动程序可以处理这些特定操作。例如,可以在ioctl函数中实现设备的配置操作。
• 资源管理
file_operations中的函数还可以用于管理设备的资源。例如,在release函数中,可以释放设备在open函数中分配的资源,确保系统资源得到有效管理,避免资源泄漏。
• 提高系统稳定性
通过实现file_operations中的函数,驱动程序可以处理错误情况并提供适当的错误信息。这有助于提高系统的稳定性和可靠性。例如,如果设备在读取过程中出现错误,read函数可以返回适当的错误代码,通知用户空间应用程序处理错误。
3.5 file_operations驱动代码
在第二章的基础上, 添加file_operations的函数, 如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h> // for copy_to_user and copy_from_user
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linx Zhang");
MODULE_DESCRIPTION("A simple LKM with file operations");
#define DEVICE_NAME "fopdev"
#define CLASS_NAME "demo_fops_class"
#define BUFFER_SIZE 1024 // Buffer size for read/write
static int major; // major device number
static struct class* demo_fops_class = NULL; // device class structure
static struct cdev demo_fops_device; // character device structure
static char device_buffer[BUFFER_SIZE] = {0}; // Buffer for data storage
static int open_count = 0; // Counter for how many times the device has been opened
// Prototype functions for file operations
static int dev_open(struct inode *, struct file *);
static int dev_release(struct inode *, struct file *);
static ssize_t dev_read(struct file *, char *, size_t, loff_t *);
static ssize_t dev_write(struct file *, const char *, size_t, loff_t *);
// Define file_operations structure
static struct file_operations fops = {
.open = dev_open,
.read = dev_read,
.write = dev_write,
.release = dev_release,
};
// Open function
static int dev_open(struct inode *inodep, struct file *filep) {
open_count++;
printk(KERN_INFO "fopdev: Device has been opened %d times\n", open_count);
return 0;
}
// Release (close) function
static int dev_release(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "fopdev: Device successfully closed\n");
return 0;
}
// Read function
static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
int bytes_read = len < BUFFER_SIZE ? len : BUFFER_SIZE;
if (copy_to_user(buffer, device_buffer, bytes_read) != 0) {
return -EFAULT;
}
printk(KERN_INFO "fopdev: Sent %d characters to the user\n", bytes_read);
return bytes_read;
}
// Write function
static ssize_t dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
int bytes_to_write = len < BUFFER_SIZE ? len : BUFFER_SIZE;
if (copy_from_user(device_buffer, buffer, bytes_to_write) != 0) {
return -EFAULT;
}
printk(KERN_INFO "fopdev: Received %zu characters from the user\n", len);
return bytes_to_write;
}
// Initialize the driver
static int __init fops_init(void) {
dev_t dev_num; // device number (major and minor)
// 1. Allocate a device number
if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
printk(KERN_ALERT "Failed to allocate major number\n");
return -1;
}
major = MAJOR(dev_num); // Get the major device number
// 2. Create the device class
demo_fops_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(demo_fops_class)) {
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(demo_fops_class);
}
// 3. Initialize and register the character device with file_operations
cdev_init(&demo_fops_device, &fops);
demo_fops_device.owner = THIS_MODULE;
// 4. Add the character device to the system
if (cdev_add(&demo_fops_device, dev_num, 1) < 0) {
class_destroy(demo_fops_class);
unregister_chrdev_region(dev_num, 1);
return -1;
}
// 5. Create a device node in /dev
if (device_create(demo_fops_class, NULL, dev_num, NULL, DEVICE_NAME) == NULL) {
cdev_del(&demo_fops_device);
class_destroy(demo_fops_class);
unregister_chrdev_region(dev_num, 1);
return -1;
}
printk(KERN_INFO "fopdev: Device registered with major number %d\n", major);
return 0;
}
// Exit function to clean up the driver
static void __exit fops_exit(void) {
dev_t dev_num = MKDEV(major, 0);
device_destroy(demo_fops_class, dev_num); // Remove the device node
class_destroy(demo_fops_class); // Remove the device class
cdev_del(&demo_fops_device); // Unregister the character device
unregister_chrdev_region(dev_num, 1); // Release the device number
printk(KERN_INFO "fopdev: Goodbye!\n");
}
module_init(fops_init);
module_exit(fops_exit);
3.6 编译及加载驱动
3.6.1 Makefile
Makefile与第二章里的2.4节一样, 在此不再赘述。
3.6.2 编译
neardi@LPA3588:~/fileop$ make
make -C /lib/modules/5.10.110/build M=/home/neardi/fileop modules
make[1]: Entering directory '/usr/src/linux-headers-5.10.110'
CC [M] /home/neardi/fileop/fileop.o
MODPOST /home/neardi/fileop/Module.symvers
CC [M] /home/neardi/fileop/fileop.mod.o
LD [M] /home/neardi/fileop/fileop.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.10.110'
3.6.3 加载驱动
neardi@LPA3588:~/fileop$ sudo insmod fileop.ko
neardi@LPA3588:~/fileop$ dmesg
[20686.519675] fopdev: Device registered with major number 234
neardi@LPA3588:~/fileop$
驱动加载成功后, 在/dev/下面有如下节点:

可以看出, 此设备的主设备号及次设备号分别是: 234和0.
3.7 测试驱动
接下来设计一个应用程序来测试驱动file_operations里的函数: open/read/write/relase。
3.7.1 APP测试代码
新建一个test.cpp文件, 内容如下:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define DEVICE_PATH "/dev/fopdev"
#define BUFFER_SIZE 1024
int main() {
int fd;
char write_buf[BUFFER_SIZE] = "Hello, Kernel!";
char read_buf[BUFFER_SIZE];
// open this device
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
std::cout << "Failed to open the device" << std::endl;
return EXIT_FAILURE;
}
// write data to device
if (write(fd, write_buf, strlen(write_buf)) < 0) {
std::cout << "Failed to write to the device" << std::endl;
close(fd);
return EXIT_FAILURE;
}
// read data from device
if (read(fd, read_buf, BUFFER_SIZE) < 0) {
std::cout << "Failed to read from the device" << std::endl;
close(fd);
return EXIT_FAILURE;
}
std::cout << "Read from device: " << read_buf << std::endl;
// close the device
close(fd);
return EXIT_SUCCESS;
}
此处open、read、write及close都是Linux系统里标准的API。
3.7.2 编译APP
在test.cpp相同的路径, 确保安装了gcc(sudo apt install build-essential), 执行如下命令:
neardi@LPA3588:~/fileop$ aarch64-linux-gnu-g++ -o test test.cpp
neardi@LPA3588:~/fileop$ ls test -lh
-rwxrwxr-x 1 neardi neardi 15K Oct 17 15:03 test
上面也就编译成功, 并生成了test测试程序。
3.7.3 运行APP
直接在命令行执行即可, 如下:
neardi@LPA3588:~/fileop$ sudo ./test
Read from device: Hello, Kernel!
neardi@LPA3588:~/fileop$ dmesg
[20686.519675] fopdev: Device registered with major number 234
[21942.935538] fopdev: Device has been opened 1 times
[21942.935561] fopdev: Received 14 characters from the user
[21942.935572] fopdev: Sent 1024 characters to the user
[21942.935728] fopdev: Device successfully closed
3.8 卸载驱动
执行sudo rmmod fileop 即可卸载驱动, 如下:
neardi@LPA3588:~/fileop$ lsmod
Module Size Used by
fileop 16384 0
neardi@LPA3588:~/fileop$ sudo rmmod fileop
neardi@LPA3588:~/fileop$ dmesg
[20686.519675] fopdev: Device registered with major number 234
[21942.935538] fopdev: Device has been opened 1 times
[21942.935561] fopdev: Received 14 characters from the user
[21942.935572] fopdev: Sent 1024 characters to the user
[21942.935728] fopdev: Device successfully closed
[21998.556572] fopdev: Goodbye!
3.9 APP调用驱动的流程

3.9.1 应用层调用系统调用
在用户空间,应用程序通过系统调用与设备文件进行交互。最常用的系统调用包括:
open("/dev/xxx", O_RDWR):打开设备文件。
read(fd, buf, count):从设备读取数据。
write(fd, buf, count):向设备写入数据。
ioctl(fd, command, arg):对设备进行控制操作。
这些系统调用通过系统调用接口进入内核空间。
3.9.2 系统调用syscall进入内核
当应用程序调用 open, read, write 等系统调用时,控制权从用户态切换到内核态,进入 Linux 内核。具体步骤如下:
1.) 中断(软件中断陷入)
每个系统调用都有一个系统调用号(syscall number)。当应用程序调用系统调用时,会通过 syscall 指令或 SVC 指令(在 ARM64 上)触发中断,并将控制权交给内核。
2.) 系统调用入口函数
内核根据系统调用号查找系统调用表( sys_call_table[],在kernel代码arch/arm64/kernel/syscall.c)。例如,如果是 open 系统调用,它会调用内核中的 sys_open 函数。
3.) VFS(虚拟文件系统)层
sys_open 函数进入虚拟文件系统(VFS)层,VFS 是内核中的一个抽象层,用于处理不同文件系统的调用。VFS 查找与设备文件 /dev/xxx 对应的 inode 节点。
4.) 设备文件和驱动程序绑定
VFS 会根据设备文件的主设备号(major number)找到与设备相关的驱动程序。主设备号由设备驱动注册时决定,使用函数如 register_chrdev() 来注册字符设备驱动(内核有设备与驱动对应表来保存)。找到相应驱动后,VFS 会调用与设备关联的 file_operations 结构体
3.9.3 驱动的注册与查找
在编写驱动时,通常需要通过 register_chrdev() 或 misc_register() 函数注册设备驱动,并指定主设备号。如果是动态分配主设备号,驱动程序会使用 alloc_chrdev_region() 来分配。
3.9.4 驱动的 file_operations 结构体
设备驱动通过实现 file_operations 结构体中的函数来响应应用层的调用。
3.9.5 驱动返回调用结果
驱动程序处理完请求后,将结果返回给内核,内核再将结果返回给用户空间的应用程序。
3.10 小结
在本章节中,我们扩展了Hello World驱动程序,添加了基本的文件操作功能。通过这些操作,应用程序可以与驱动程序进行交互,读取和写入数据。