注:本文为 “Linux 字符设备驱动” 相关文章合辑。
英文引文,机翻未校。
中文引文,略作重排。
未整理去重,如有内容异常,请看原文。
Simple Linux character device driver
简单 Linux 字符设备驱动程序
Oleg Kutkov / March 14, 2018
A character device is one of the simplest ways to communicate with a module in the Linux kernel.
字符设备是与 Linux 内核模块通信的最简单方式之一。
These devices are presented as special files in a /dev
directory and support direct reading and writing of any data, byte by byte, like a stream. Actually, most of the pseudo-devices in /dev
are character devices: serial ports, modems, sound, and video adapters, keyboards, some custom I/O interfaces. Userspace programs can easily open, read, write, and custom control requests with such device files.
这些设备以 /dev
目录中的特殊文件形式呈现,并支持逐字节读取和写入任何数据,就像一个数据流一样。实际上,/dev
中的大多数伪设备都是字符设备:串行端口、调制解调器、音频设备、视频适配器、键盘以及一些自定义的 I/O 接口。用户空间程序可以轻松地打开、读取、写入以及对这些设备文件进行自定义控制请求。
Here I am describing how to write a simple Linux kernel module which can create one or multiple character device.
本文将介绍如何编写一个简单的 Linux 内核模块,以创建一个或多个字符设备。
Introducing to character devices.
介绍字符设备。
Detection of the device type in /dev
directory is pretty simple.
在 /dev
目录中检测设备类型非常简单。
$ ls -l /dev/ttyS0
crw-rw---- 1 root dialout 4, 64 Mar 11 16:52 /dev/ttyS0
Symbol C , in the beginning, means that this device is a character device. Also, you can find here two strange numbers: 4 and 64. This is a Major and Minor number of this device. Inside the Linux kernel, every device is identified not by a symbolic name but by a unique number – the device’s major number. This number is assigned by the kernel during device registration. Every device driver can support multiple “sub-devices”. For example, a serial port adapter may contain two hardware ports. Both of these ports are handled by the same driver, and they share one Major number. But inside this driver, each of these ports is also identified by the unique number, and this is a device Minor number.
开头的符号 C 表示这是一个字符设备。此外,这里还有两个奇怪的数字:4 和 64。这是设备的主设备号和次设备号。在 Linux 内核中,每个设备不是通过符号名称而是通过一个唯一的数字来识别的——设备的主设备号。这个数字是在设备注册时由内核分配的。每个设备驱动程序可以支持多个“子设备”。例如,一个串行端口适配器可能包含两个硬件端口。这两个端口由同一个驱动程序处理,并且它们共享一个主设备号。但在该驱动程序内部,每个端口也通过一个唯一的数字来识别,这就是设备的次设备号。
crw-rw---- 1 root dialout 4, 64 Mar 11 16:52 /dev/ttyS0
crw-rw---- 1 root dialout 4, 65 Mar 11 16:52 /dev/ttyS1
crw-rw---- 1 root dialout 4, 66 Mar 11 16:52 /dev/ttyS2
One Major number 4 for every ttySX device and different (64–65) Minor numbers. The driver’s code assigns minor numbers, and the developer of this driver may select any suitable values.
每个 ttySX 设备有一个主设备号 4 和不同的次设备号(64–65)。驱动程序代码分配次设备号,驱动程序开发者可以选择任何合适的值。
As this device acts like a file – programs can do almost everything except seeking. Every file operation on this object commands the driver to do something inside the Linux kernel and start reading some data from the hardware.
由于该设备表现得像一个文件,程序几乎可以执行任何操作,除了定位(seeking)。对这个对象的每一个文件操作都会命令驱动程序在 Linux 内核中执行某些操作,并开始从硬件读取一些数据。
At the end of this article, you can find a complete example of the character device driver, but first, let’s discuss how it works.
在本文的最后,您会找到一个完整的字符设备驱动程序示例,但在那之前,让我们先讨论它是如何工作的。
The diagram below shows how the userspace program interacts with the IBM PC serial port using the character device.
下图展示了用户空间程序如何通过字符设备与 IBM PC 串行端口进行交互。
The virtual filesystem is an abstraction layer on top of a more concrete filesystem. A VFS aims to allow client applications to access different types of concrete filesystems uniformly.
虚拟文件系统是更具体的文件系统之上的抽象层。VFS 的目标是允许客户端应用程序以统一的方式访问不同类型的文件系统。
File operations
文件操作
In special device files, VFS is responsible for calling I/O functions set by the device driver. To set this function special kernel structure is used.
在特殊设备文件中,VFS 负责调用设备驱动程序设置的 I/O 函数。为此,使用了一个特殊的内核结构。
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char *, size_t, loff_t *);ssize_t (*write) (struct file *, const char *, size_t, loff_t *);int (*readdir) (struct file *, void *, filldir_t);unsigned int (*poll) (struct file *, struct poll_table_struct *);int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, struct dentry *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*readv) (struct file *, const struct iovec *, unsigned long,loff_t *);ssize_t (*writev) (struct file *, const struct iovec *, unsigned long,loff_t *);};
Some operations are not implemented by a driver. For example, a driver that handles a video card won’t need to read from a directory structure. The corresponding entries in the file_operations structure should be set to NULL
.
某些操作可能不会由驱动程序实现。例如,处理显卡的驱动程序不需要从目录结构中读取。在这种情况下,file_operations 结构中对应的条目应设置为 NULL
。
In a C99 way, initialization is simple.
以 C99 的方式,初始化非常简单。
struct file_operations fops = {.read = device_read,.write = device_write,.open = device_open,.release = device_release};
Initialized file_operations can be assigned to the character device during device registration.
初始化后的 file_operation 可以在设备注册期间分配给字符设备。
Registration of the character device
字符设备的注册
The registration procedure consists of several simple steps.
注册过程包括几个简单的步骤。
First, you need to decide how many minor devices you need. This is a constant which typically depends on your hardware (if you are writing a driver for real hardware).
首先,您需要确定需要多少个次设备。这是一个常量,通常取决于您的硬件(如果您正在为真实硬件编写驱动程序)。
Minor numbers are convenient to use as part of the device name. For example, /dev/mychardev0
with a Minor 0, /dev/mychardev2
with a Minor 2.
次设备号可以方便地用作设备名称的一部分。例如,/dev/mychardev0
的次设备号为 0,/dev/mychardev2
的次设备号为 2。
The first step is an allocation and registration of the range of char device numbers using alloc_chrdev_region.
第一步是使用 alloc_chrdev_region 分配和注册字符设备号范围。
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
Where dev is output parameter for first assigned number, firstminor is first of the requested range of minor numbers (e.g., 0), count is a number of minor numbers required, and name – the associated device’s name driver.
其中,dev 是输出参数,用于第一个分配的设备号;firstminor 是请求范围内的第一个次设备号(例如,0);count 是所需的次设备号数量;name 是与设备驱动程序关联的名称。
The major number will be chosen dynamically and returned (along with the first minor number) in dev. The function returns zero or a negative error code.
主设备号将动态选择并返回到 dev 中(连同第一个次设备号)。该函数返回零或一个负的错误代码。
To get generated Major number, we can use MAJOR() macros.
要获取生成的主设备号,可以使用 MAJOR() 宏。
int dev_major = MAJOR(dev);
Now it’s time to initialize a new character device and set file_operations with cdev_init.
现在是时候初始化一个新的字符设备,并使用 cdev_init 设置文件操作了。
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
struct cdev represents a character device and is allocated by this function.
struct cdev 表示一个字符设备,由该函数分配。
Now add the device to the system.
现在将设备添加到系统中。
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
Finally – create a device file node and register it with sysfs.
最后,创建设备文件节点并将其注册到 sysfs 中。
struct device * device_create(struct class *class, struct device *parent, dev_t devt, const char *fmt, ...);
Now all together. This code creates 2 character devices with names /dev/mychardev0
and /dev/mychardev1
.
现在将所有内容放在一起。这段代码创建了两个字符设备,名称分别为 /dev/mychardev0
和 /dev/mychardev1
。
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/fs.h>// max Minor devices
#define MAX_DEV 2// initialize file_operations
static const struct file_operations mychardev_fops = {.owner = THIS_MODULE,.open = mychardev_open,.release = mychardev_release,.unlocked_ioctl = mychardev_ioctl,.read = mychardev_read,.write = mychardev_write
};// device data holder, this structure may be extended to hold additional data
struct mychar_device_data {struct cdev cdev;
};// global storage for device Major number
static int dev_major = 0;// sysfs class structure
static struct class *mychardev_class = NULL;// array of mychar_device_data for
static struct mychar_device_data mychardev_data[MAX_DEV];void mychardev_init(void)
{int err, i;dev_t dev;// allocate chardev region and assign Major numbererr = alloc_chrdev_region(&dev, 0, MAX_DEV, "mychardev");dev_major = MAJOR(dev);// create sysfs classmychardev_class = class_create(THIS_MODULE, "mychardev");// Create necessary number of the devicesfor (i = 0; i < MAX_DEV; i++) {// init new devicecdev_init(&mychardev_data[i].cdev, &mychardev_fops);mychardev_data[i].cdev.owner = THIS_MODULE;// add device to the system where "i" is a Minor number of the new devicecdev_add(&mychardev_data[i].cdev, MKDEV(dev_major, i), 1);// create device node /dev/mychardev-x where "x" is "i", equal to the Minor numberdevice_create(mychardev_class, NULL, MKDEV(dev_major, i), NULL, "mychardev-%d", i);}
}
You can find a few new things in this example. The creation of the sysfs class is a necessary part of the device node creation.
在这个示例中,您会发现一些新的内容。创建 sysfs 类是设备节点创建的必要部分。
Function class_create(THIS_MODULE, “mychardev”)
creates sysfs class with paths for each character devices:
函数 class_create(THIS_MODULE, “mychardev”)
为每个字符设备创建 sysfs 类,并为其生成路径:
$ tree /sys/devices/virtual/mychardev/
/sys/devices/virtual/mychardev/
├── mychardev-0
│ ├── dev
│ ├── power
│ │ ├── async
│ │ ├── autosuspend_delay_ms
│ │ ├── control
│ │ ├── runtime_active_kids
│ │ ├── runtime_active_time
│ │ ├── runtime_enabled
│ │ ├── runtime_status
│ │ ├── runtime_suspended_time
│ │ └── runtime_usage
│ ├── subsystem -> ../../../../class/mychardev
│ └── uevent
└── mychardev-1├── dev├── power│ ├── async│ ├── autosuspend_delay_ms│ ├── control│ ├── runtime_active_kids│ ├── runtime_active_time│ ├── runtime_enabled│ ├── runtime_status│ ├── runtime_suspended_time│ └── runtime_usage├── subsystem -> ../../../../class/mychardev└── uevent
Sysfs can be used as an additional way to interact with userspace. Setting up some driver params, for example.
Sysfs 可以作为一种与用户空间交互的额外方式。例如,设置一些驱动程序参数。
Another useful thing – configure UDEV variables to set up correct permissions to the character device.
另一个有用的功能是配置 UDEV 变量,以设置字符设备的正确权限。
This can be done by setting uevent callback to sysfs class.
可以通过为 sysfs 类设置 uevent 回调来实现。
static int mychardev_uevent(struct device *dev, struct kobj_uevent_env *env)
{add_uevent_var(env, "DEVMODE=%#o", 0666);return 0;
}...mychardev_class = class_create(THIS_MODULE, "mychardev");
mychardev_class->dev_uevent = mychardev_uevent;
Now we got “rw-rw-rw-
” permissions on each mychardev.
现在每个 mychardev 的权限都设置为“rw-rw-rw-
”。
$ ls -l /dev/mychardev-*
crw-rw-rw- 1 root root 246, 0 Mar 14 12:24 /dev/mychardev-0
crw-rw-rw- 1 root root 246, 1 Mar 14 12:24 /dev/mychardev-1
Every user can read and write.
每个用户都可以读取和写入。
When a character device is no longer required it must be properly destroyed.
当不再需要字符设备时,必须正确地销毁它。
void mychardev_destroy(void)
{int i;for (i = 0; i < MAX_DEV; i++) {device_destroy(mychardev_class, MKDEV(dev_major, i));}class_unregister(mychardev_class);class_destroy(mychardev_class);unregister_chrdev_region(MKDEV(dev_major, 0), MINORMASK);
}
Device I/O functions
设备 I/O 函数
To interact with your device file, we need to set a few functions to the struct file_operations.
为了与设备文件进行交互,我们需要为 struct file_operations 设置一些函数。
static int mychardev_open(struct inode *inode, struct file *file)
{printk("MYCHARDEV: Device open\n");return 0;
}static int mychardev_release(struct inode *inode, struct file *file)
{printk("MYCHARDEV: Device close\n");return 0;
}static long mychardev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{printk("MYCHARDEV: Device ioctl\n");return 0;
}static ssize_t mychardev_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{printk("MYCHARDEV: Device read\n");return 0;
}static ssize_t mychardev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{printk("MYCHARDEV: Device write\n");return 0;
}
Now we can handle I/O requests. If build and load the kernel module with this code and then run “cat /dev/mychardev-0
” these messages will be printed in dmesg:
现在我们可以处理 I/O 请求。如果构建并加载包含此代码的内核模块,然后运行“cat /dev/mychardev-0
”,这些消息将打印在 dmesg 中:
$ cat /dev/mychardev-0$ sudo tail -n3 /var/log/messages
Mar 14 12:52:46 oleg-lab kernel: [244801.849652] MYCHARDEV: Device open
Mar 14 12:52:46 oleg-lab kernel: [244801.849665] MYCHARDEV: Device read
Mar 14 12:52:46 oleg-lab kernel: [244801.849672] MYCHARDEV: Device close
It’s working.
它正常工作了。
To transfer some real data within read
/write
requests, we need to use special kernel functionality. It’s very dangerous or even impossible to do simple memory copying using *buf pointers. Safe way is to use copy_to_user() and copy_from_user()
要在 read
/write
请求中传输一些真实数据,我们需要使用特殊的内核功能。使用 *buf 指针进行简单的内存复制是非常危险的,甚至是不可能的。安全的方式是使用 copy_to_user() 和 copy_from_user()。
#include <linux/uaccess.h>unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
These functions perform additional checks of the permissions and memory regions before actual data access.
这些函数在实际数据访问之前会进行额外的权限和内存区域检查。
Let’s modify our mychardev_read().
让我们修改 mychardev_read()。
static ssize_t mychardev_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{uint8_t *data = "Hello from the kernel world!\n";size_t datalen = strlen(data);if (count > datalen) {count = datalen;}if (copy_to_user(buf, data, count)) {return -EFAULT;}return count;
}
It’s always important to check how many bytes users want to read. If this size exceeds the prepared data’s actual size – the user can read the kernel stack which can be a hole in the system security.
始终重要的是要检查用户想要读取的字节数。如果这个大小超过了准备好的数据的实际大小——用户可能会读取内核栈,这可能会成为系统安全的一个漏洞。
Now let’s try to read 29 bytes from our character device.
现在让我们尝试从我们的字符设备中读取 29 个字节。
$ head -c29 /dev/mychardev-1
Hello from the kernel world!
Of course, we can send to the user space not only strings but any other raw data structures.
当然,我们不仅可以向用户空间发送字符串,还可以发送任何其他原始数据结构。
Now mychardev_write().
现在是 mychardev_write()。
static ssize_t mychardev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{size_t maxdatalen = 30, ncopied;uint8_t databuf[maxdatalen];if (count < maxdatalen) {maxdatalen = count;}ncopied = copy_from_user(databuf, buf, maxdatalen);if (ncopied == 0) {printk("Copied %zd bytes from the user\n", maxdatalen);} else {printk("Could't copy %zd bytes from the user\n", ncopied);}databuf[maxdatalen] = 0;printk("Data from the user: %s\n", databuf);return count;
}
It’s also very important to verify how many bytes sending users and how many bytes we can accept.
验证发送的字节数以及我们可以接受的字节数也非常重要。
Function copy_from_user returns the number of bytes that could not be copied. On success, this will be zero.
函数 copy_from_user 返回未能复制的字节数。成功时,这个值为零。
If some data could not be copied, this function will pad the copied data to the requested size using zero bytes.
如果某些数据未能复制,该函数将使用零字节将已复制的数据填充到请求的大小。
Test:
测试:
$ echo "Hello from the user" > /dev/mychardev-1$ sudo tail -n5 /var/log/messages
Mar 14 15:57:14 oleg-lab kernel: [255870.547447] MYCHARDEV: Device open
Mar 14 15:57:14 oleg-lab kernel: [255870.547466] Copied 20 bytes from the user
Mar 14 15:57:14 oleg-lab kernel: [255870.547468] Data from the user: Hello from the user
Mar 14 15:57:14 oleg-lab kernel: [255870.547468]
Mar 14 15:57:14 oleg-lab kernel: [255870.547472] MYCHARDEV: Device close
You may ask how to identify which device (mychardev-0 or mychardev-1) is used in a specific I/O process? Since our Minor numbers are the same as device names we can get Minor number
from the file inode using struct file.
您可能会问,如何识别在特定 I/O 过程中使用的是哪个设备(mychardev-0 还是 mychardev-1)?由于我们的次设备号与设备名称相同,因此可以通过 struct file 从文件 inode 中获取 次设备号
。
MINOR(file->f_path.dentry->d_inode->i_rdev)
Let’s print this value in the read and write functions and see what happens.
让我们在 read 和 write 函数中打印这个值,看看会发生什么。
...
printk("Reading device: %d\n", MINOR(file->f_path.dentry->d_inode->i_rdev));...
printk("Writing device: %d\n", MINOR(file->f_path.dentry->d_inode->i_rdev));
Result:
结果:
$ echo "Hello from the user" > /dev/mychardev-0dmesg
Mar 14 16:02:08 oleg-lab kernel: [256164.495609] Writing device: 0$ echo "Hello from the user" > /dev/mychardev-1dmesg
Mar 14 16:02:08 oleg-lab kernel: [256164.495609] Writing device: 1
Few notes about ioctl.
关于 ioctl 的一些说明。
static long mychardev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
This utility function is used to pass some CMD as the number and some optional data as ARG.
这个工具函数用于传递一些作为数字的 CMD 和一些可选数据作为 ARG。
You need to define some magic numbers used as CMD (and probably as ARG) somewhere in a separate header file, shared between driver code and user application code.
您需要在某个单独的头文件中定义一些用作 CMD(以及可能用作 ARG)的魔术数字,该头文件在驱动程序代码和用户应用程序代码之间共享。
All implementation of the ioctl function is a simple switch case routine where you do something depending on the sent CMD.
ioctl 函数的所有实现都是一个简单的 switch case 例程,您根据发送的 CMD 执行某些操作。
Now a complete example of the Linux kernel module, which implements everything that we were discussed here.
现在是一个完整的 Linux 内核模块示例,它实现了我们在这里讨论的所有内容。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/uaccess.h>
#include <linux/fs.h>#define MAX_DEV 2static int mychardev_open(struct inode *inode, struct file *file);
static int mychardev_release(struct inode *inode, struct file *file);
static long mychardev_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
static ssize_t mychardev_read(struct file *file, char __user *buf, size_t count, loff_t *offset);
static ssize_t mychardev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset);static const struct file_operations mychardev_fops = {.owner = THIS_MODULE,.open = mychardev_open,.release = mychardev_release,.unlocked_ioctl = mychardev_ioctl,.read = mychardev_read,.write = mychardev_write
};struct mychar_device_data {struct cdev cdev;
};static int dev_major = 0;
static struct class *mychardev_class = NULL;
static struct mychar_device_data mychardev_data[MAX_DEV];static int mychardev_uevent(struct device *dev, struct kobj_uevent_env *env)
{add_uevent_var(env, "DEVMODE=%#o", 0666);return 0;
}static int __init mychardev_init(void)
{int err, i;dev_t dev;err = alloc_chrdev_region(&dev, 0, MAX_DEV, "mychardev");dev_major = MAJOR(dev);mychardev_class = class_create(THIS_MODULE, "mychardev");mychardev_class->dev_uevent = mychardev_uevent;for (i = 0; i < MAX_DEV; i++) {cdev_init(&mychardev_data[i].cdev, &mychardev_fops);mychardev_data[i].cdev.owner = THIS_MODULE;cdev_add(&mychardev_data[i].cdev, MKDEV(dev_major, i), 1);device_create(mychardev_class, NULL, MKDEV(dev_major, i), NULL, "mychardev-%d", i);}return 0;
}static void __exit mychardev_exit(void)
{int i;for (i = 0; i < MAX_DEV; i++) {device_destroy(mychardev_class, MKDEV(dev_major, i));}class_unregister(mychardev_class);class_destroy(mychardev_class);unregister_chrdev_region(MKDEV(dev_major, 0), MINORMASK);
}static int mychardev_open(struct inode *inode, struct file *file)
{printk("MYCHARDEV: Device open\n");return 0;
}static int mychardev_release(struct inode *inode, struct file *file)
{printk("MYCHARDEV: Device close\n");return 0;
}static long mychardev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{printk("MYCHARDEV: Device ioctl\n");return 0;
}static ssize_t mychardev_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{uint8_t *data = "Hello from the kernel world!\n";size_t datalen = strlen(data);printk("Reading device: %d\n", MINOR(file->f_path.dentry->d_inode->i_rdev));if (count > datalen) {count = datalen;}if (copy_to_user(buf, data, count)) {return -EFAULT;}return count;
}static ssize_t mychardev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{size_t maxdatalen = 30, ncopied;uint8_t databuf[maxdatalen];printk("Writing device: %d\n", MINOR(file->f_path.dentry->d_inode->i_rdev));if (count < maxdatalen) {maxdatalen = count;}ncopied = copy_from_user(databuf, buf, maxdatalen);if (ncopied == 0) {printk("Copied %zd bytes from the user\n", maxdatalen);} else {printk("Could't copy %zd bytes from the user\n", ncopied);}databuf[maxdatalen] = 0;printk("Data from the user: %s\n", databuf);return count;
}MODULE_LICENSE("GPL");
MODULE_AUTHOR("Oleg Kutkov <elenbert@gmail.com>");module_init(mychardev_init);
module_exit(mychardev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Oleg Kutkov <elenbert@gmail.com>");module_init(mychardev_init);
module_exit(mychardev_exit);
And Makefile to build this code.
以及用于构建此代码的 Makefile。
BINARY := mychardev
KERNEL := /lib/modules/$(shell uname -r)/build
ARCH := x86
C_FLAGS := -Wall
KMOD_DIR := $(shell pwd)
TARGET_PATH := /lib/modules/$(shell uname -r)/kernel/drivers/charOBJECTS := main.occflags-y += $(C_FLAGS)obj-m += $(BINARY).o$(BINARY)-y := $(OBJECTS)$(BINARY).ko:make -C $(KERNEL) M=$(KMOD_DIR) modulesinstall:cp $(BINARY).ko $(TARGET_PATH)depmod -auninstall:rm $(TARGET_PATH)/$(BINARY).kodepmod -aclean:make -C $(KERNEL) M=$(KMOD_DIR) clean
Module building and loading
模块的构建和加载
make && sudo insmod mychardev.ko
You should find two new devices: /dev/mychardev-0
and /dev/mychardev-1
, and repeat all experiments from this article.
您应该会找到两个新的设备:/dev/mychardev-0
和 /dev/mychardev-1
,并重复本文中的所有实验。
I hope this material will be helpful. This code can be used as a basic pattern in some more complex driver project.
希望这些材料能有所帮助。此代码可以用作更复杂驱动程序项目的基本模板。
Writing a Simple Character Device driver in Linux
在 Linux 中编写一个简单的字符设备驱动程序
March 25, 2010
Basic Concepts of Character Device Drivers
字符设备驱动程序的基本概念
A Character device driver needs a major number and a minor number. The devices are registered in the Kernel and it lies either in the /dev/ or in the /proc folder.
字符设备驱动程序需要一个主设备号和一个次设备号。设备在内核中注册,并且位于 /dev/ 或 /proc 文件夹中。
Example Device Driver
示例设备驱动程序
The following example uses a char device driver with major number 222 and a minor number 0. The name of the device driver namely “new_device”
以下示例使用了一个主设备号为 222、次设备号为 0 的字符设备驱动程序,其设备驱动程序的名称为 “new_device”。
Functions Used by the Device Driver
设备驱动程序的功能
It uses the following things:
它使用了以下功能:
-
Open or register a device
打开或注册设备 -
Close or unregister the device
关闭或注销设备 -
Reading from the device (Kernel to the userspace)
从设备读取数据(从内核到用户空间) -
Writing to the device (userlevel to the kernel space)
向设备写入数据(从用户级别到内核空间)
Files Included
文件组成
There are three files, Copy the following or download all the three files here
共有三个文件,可以复制以下内容或从 这里下载所有三个文件
Source Code
源代码
/* new_dev.c */
#include<linux/module.h>
#include<linux/init.h>
#include "new_dev.h"MODULE_AUTHOR("PRADEEPKUMAR");
MODULE_DESCRIPTION("A simple char device");static int r_init(void);
static void r_cleanup(void);module_init(r_init);
module_exit(r_cleanup);static int r_init(void)
{
printk("<1>hi\n");
if(register_chrdev(222,"new_device",&my_fops)){
printk("<1>failed to register");
}
return 0;
}
static void r_cleanup(void)
{
printk("<1>bye\n");
unregister_chrdev(222,"new_device");
return ;
}
/* new_dev.h */
/*
\* my device header file
*/
#ifndef _NEW_DEVICE_H
#define _NEW_DEVICE_H#include <linux/fs.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <asm/current.h>
#include <asm/segment.h>
#include <asm/uaccess.h>char my_data[80]="hi from kernel"; /* our device */int my_open(struct inode *inode,struct file *filep);
int my_release(struct inode *inode,struct file *filep);
ssize_t my_read(struct file *filep,char *buff,size_t count,loff_t *offp );
ssize_t my_write(struct file *filep,const char *buff,size_t count,loff_t *offp );
struct file_operations my_fops={
open: my_open,
read: my_read,
write: my_write,
release:my_release,
};int my_open(struct inode *inode,struct file *filep)
{
/*MOD_INC_USE_COUNT;*/ /* increments usage count of module */
return 0;
}int my_release(struct inode *inode,struct file *filep)
{
/*MOD_DEC_USE_COUNT;*/ /* decrements usage count of module */
return 0;
}
ssize_t my_read(struct file *filep,char *buff,size_t count,loff_t *offp )
{
/* function to copy kernel space buffer to user space*/
if ( copy_to_user(buff,my_data,strlen(my_data)) != 0 )
printk( "Kernel -> userspace copy failed!\n" );
return strlen(my_data);}
ssize_t my_write(struct file *filep,const char *buff,size_t count,loff_t *offp )
{
/* function to copy user space buffer to kernel space*/
if ( copy_from_user(my_data,buff,count) != 0 )
printk( "Userspace -> kernel copy failed!\n" );
return 0;
}
#endif
Makefile
obj-m += new_dev.oall:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Compilation and Usage
编译和使用
How to Compile
如何编译
Put all the three files in the same folder and execute the following commands
将所有三个文件放在同一个文件夹中,并执行以下命令
-
make (to compile the module)
make(用于编译模块) -
insmod new_dev.ko (To insert the module)
insmod new_dev.ko(用于插入模块)
After Inserting the Module
插入模块后
Once the module is inserted, do the following
在插入模块后,执行以下操作:
mknod /dev/new_device c 222 0 (Command to make an entry in the /dev/, once the device is created, go and see the /dev/ folder for the entry new_device)
mknod /dev/new_device c 222 0(用于在 /dev/ 中创建一个条目,设备创建后,可以在 /dev/ 文件夹中查看名为 new_device 的条目)
cat /dev/new_device (The message will be printed which is from the kernel, that is read operation)
cat /dev/new_device(将打印来自内核的消息,即 读取操作)
echo “This is a write information to the kernel” > /dev/new_device (This command is to perform the write operation)
echo “This is a write information to the kernel” > /dev/new_device(用于执行 写入操作)
After Checking the Read and Write Operation
检查读取和写入操作后
After checking the read and write operation, just remove the module
在检查读取和写入操作后,移除模块:
rmmod new_dev.ko
(Source: Implementing a Simple Char Device in Linux LG #125)(下文)
Implementing a Simple Char Device in Linux
在 Linux 中实现一个简单的字符设备
By Ranjeet Mishra
Device
For the purpose of this article, let’s consider a device to be a virtual representation, within Linux, of hardware that one would like to drive by using a piece of software. In the Linux world, devices are implemented in the form of modules. By using modules, we can provide device functionality that can be accessed from userspace.
在本文中,我们将“设备”视为 Linux 中硬件的虚拟表示,这些硬件可以通过软件进行驱动。在 Linux 中,设备以模块的形式实现。通过使用模块,我们可以提供可以从用户空间访问的设备功能。
A userspace entry point to a device is provided by a file node in the /dev directory. As we know, most of the things in Linux world are represented in the form of files. We can do [ls -l] on any device file, which will report the device type - character or block device, as well as its major number and minor number.
设备的用户空间入口由 /dev 目录中的文件节点提供。正如我们所知,Linux 中的大多数事物都以文件的形式表示。我们可以在任何设备文件上执行 [ls -l],它将报告设备类型——字符设备或块设备,以及其主设备号和次设备号。
The type of device indicates the way data is written to a device. For a character device, it’s done serially, byte by byte, and for a block device (e.g., hard disk) in the form of chunks of bytes - just as the name suggests.
设备的类型表明数据写入设备的方式。对于字符设备,数据是逐字节顺序写入的,而对于块设备(例如硬盘),则是以字节块的形式写入——正如其名称所暗示的那样。
The major number is assigned at the time of registering the device (using some module) and the kernel uses it to differentiate between various devices. The minor number is used by the device driver programmer to access different functions in the same device.
主设备号是在注册设备时(使用某个模块)分配的,内核使用它来区分不同的设备。次设备号由设备驱动程序编写者用来访问同一设备中的不同功能。
Looking at the number of files in the /dev directory, one might think that a very large number of devices are up and running in the system, but only few might be actually present and running. This can be seen by executing [cat /proc/devices]. (One can then see the major numbers and names of devices that are passed at the time of registering.)
查看 /dev 目录中文件的数量,人们可能会以为系统中有大量设备正在运行,但实际上可能只有少数设备真正存在并运行。通过执行 [cat /proc/devices] 可以看到这一点。(然后可以看到注册时传递的主设备号和设备名称。)
Modules
Every device requires a module. Information about the currently loaded modules can be extracted from the kernel through [cat /proc/modules]. A module is nothing more than an object file that can be linked into a running kernel; to accomplish this, Linux provides the [insmod] utility. As an example, let’s say that my module’s object file is called my_dev.o; we can link it to the kernel using [insmod my_dev.o]. If [insmod] is successful we can see our module’s entry using [cat /proc/modules], or [lsmod]. We can remove the module using the rmmod utility, which takes the object file name as an argument.
每个设备都需要一个模块。可以通过 [cat /proc/modules] 从内核中获取当前已加载模块的信息。模块不过是一个可以链接到运行中的内核的对象文件;为此,Linux 提供了 [insmod] 工具。例如,假设我的模块对象文件名为 my_dev.o,我们可以使用 [insmod my_dev.o] 将其链接到内核。如果 [insmod] 成功,我们可以通过 [cat /proc/modules] 或 [lsmod] 查看我们模块的条目。我们可以使用 rmmod 工具移除模块,它以对象文件名作为参数。
Writing a Module to register a Char device
First of all, we should know the basics of generating a module object file. The module uses kernel space functions and since the whole kernel code is written inside the KERNEL directive we need to define it at time of compiling, or in our source code. We need to define the MODULE directive before anything else because Module functions are defined inside it. In order to link our module with the kernel, the version of the running kernel should match the version which the module is compiled with, or [insmod] will reject the request. This means that we must include the [include] directory present in the Linux source code of the appropriate version. Again, if my module file is called my_dev.c, a sample compiler instruction could be [gcc -D__KERNEL__ -I/usr/src/linux.2.6.7/linux/include -c my_dev.c]. A -D is used to define any directive symbol. Here we need to define KERNEL, since without this kernel-specific content won’t be available to us.
首先,我们需要了解生成模块对象文件的基础知识。模块使用内核空间函数,由于整个内核代码都写在 KERNEL 指令中,因此我们需要在编译时或在源代码中定义它。我们需要在其他任何内容之前定义 MODULE 指令,因为模块函数是在其中定义的。为了将我们的模块与内核链接,运行中的内核版本必须与模块编译时的版本匹配,否则 [insmod] 将拒绝请求。这意味着我们必须包含适当版本的 Linux 源代码中的 [include] 目录。再次说明,如果我的模块文件名为 my_dev.c,一个示例编译指令可以是 [gcc -D__KERNEL__ -I/usr/src/linux.2.6.7/linux/include -c my_dev.c]。-D 用于定义任何指令符号。在这里我们需要定义 KERNEL,因为没有它,内核特定的内容将无法供我们使用。
The two basic functions for module operations are module_init and module_exit. The insmod utility loads the module and calls the function passed to module_init, and rmmod removes the module and calls function passed to module_exit. So inside module_init, we can do whatever we wish using our kernel API. For registering the char device, the kernel provides register_chrdev which takes three arguments, namely: the major number, the char string (which gives a tag name to the device), and the file operations struct address which defines all the stuff we would like to do with our char device. struct file_operations is defined in $(KERNELDIR)/linux/include/fs.h which declares the function pointers for basic operations like open, read, write, release, etc. One needs to implement whatever functions are necessary for the device. Finally, inside the function passed to module_exit, we should free the resources using unregister_chrdev which will be called when we do rmmod.
模块操作的两个基本函数是 module_init 和 module_exit。insmod 工具加载模块并调用传递给 module_init 的函数,而 rmmod 移除模块并调用传递给 module_exit 的函数。因此,在 module_init 中,我们可以使用内核 API 来做任何我们想做的事情。为了注册字符设备,内核提供了 register_chrdev,它接受三个参数,分别是:主设备号、字符字符串(为设备提供一个标签名称)以及定义了我们希望对字符设备进行的所有操作的文件操作结构体地址。struct file_operations 在 $(KERNELDIR)/linux/include/fs.h 中定义,它声明了基本操作(如打开、读取、写入、释放等)的函数指针。需要为设备实现必要的函数。最后,在传递给 module_exit 的函数中,我们应该使用 unregister_chrdev 释放资源,这将在我们执行 rmmod 时被调用。
Below is the code listing where the device is nothing but an 80 byte chunk of memory.
以下是代码清单,其中设备只是一个 80 字节的内存块。
Program Listing
程序列表
- Makefile
- my_dev.c
- my_dev.h
Playing with the char device
Load the device using [insmod my_dev.o]. Look for the entry through /proc/modules and /proc/devices. Create a file node in /dev directory using [mknod /dev/my_device c 222 0]. Look inside the code, we have given the major number as 222. You might think that this number may clash with some other device - well, that’s correct, but I have checked whether this number is already occupied by some other device. One could use dynamic allocation of the major number; for that we have to pass 0 as the argument.
使用 [insmod my_dev.o] 加载设备。通过 /proc/modules 和 /proc/devices 查看条目。使用 [mknod /dev/my_device c 222 0] 在 /dev 目录中创建一个文件节点。查看代码,我们指定的主设备号为 222。你可能会担心这个号码可能会与其他设备冲突——没错,但我已经检查过这个号码是否已被其他设备占用。也可以使用主设备号的动态分配;为此我们需要将 0 作为参数传递。
Now we can read the data in the device using [cat /dev/my_device] and can write to our device using [echo “something” > /dev/my_device]. We can also write full-fledged userspace code to access our device using standard system calls of open, read, write, close, etc. Sample code is presented below.
现在,我们可以使用 [cat /dev/my_device] 读取设备中的数据,并使用 [echo “something” > /dev/my_device] 向设备写入数据。我们还可以编写完整的用户空间代码,使用标准的打开、读取、写入、关闭等系统调用来访问我们的设备。以下是示例代码。
-------------------------------------------
/* Sample code to access our char device */#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{int fd=0,ret=0;char buff[80]="";fd=open("/dev/my_device",O_RDONLY);printf("fd :%d\n",fd);ret=read(fd,buff,10);buff[ret]='\0';printf("buff: %s ;length: %d bytes\n",buff,ret);close(fd);
}-------------------------------------------
Output
fd: 3
buff: hi from kernel ;length: 14 bytes
-------------------------------------------
Conclusion
[ Note: a tarball containing all the code in this article can be downloaded here. ]
In this article I have tried to show how to use the kernel functions to register a character device, and how to invoke it from userspace. There are many issues that have not been touched upon here, such as the concurrency problem where we need to provide a semaphore for the device to do mutual exclusion as more than one process may try to access it. I will try to cover these issues in my future articles.
[注意:包含本文所有代码的 tarball 可以从 这里 下载。]
在本文中,我尝试展示了如何使用内核函数注册字符设备,以及如何从用户空间调用它。这里没有涉及许多问题,例如并发问题,我们需要为设备提供一个信号量以实现互斥,因为可能会有多个进程尝试访问它。我将在未来的文章中尝试涵盖这些问题。
Linux —— 字符设备驱动程序设计
1900_ 于 2020-06-23 11:16:20 发布
编写一个虚拟字符设备驱动程序
char.c
。
- 以内核模块的形式插入内核,编译方法与内核编译方法一致。
- 创建设备节点,然后通过编写一个测试程序。
- 功能:首先向设备中写入数据,再从设备中读出数据,并把数据显示在屏幕上。
- 要求:设备名为
demo
,主设备号为 250,次设备号为 0。
首先,要确保环境已经安装好了。
开始编写三个文件:
char.c
- Makefile (ps:M 必须大写)
test.c
用于测试
vi char.c
vi Makefile
vi test.c
写好之后执行 make
命令:
make
然后加载驱动程序:
sudo insmod char.ko
lsmod # 查看是否加载成功
必须使用管理员权限。
在 /dev
目录下创建设备文件:
sudo mknod /dev/mychar c 250 0
# 要使用管理员权限
# 1. 命令中的数字要和驱动程序定义的 major,minor 保持一致。
# 2. Mychar 文件名与测试程序中的名字一致
编译运行测试程序:
gcc test.c -o test
sudo ./test
char.c
代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/ioctl.h>
#include <asm/io.h>
#include <asm/uaccess.h>MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Bob Geng");
MODULE_DESCRIPTION("a simple driver");#define N 128
int major = 250;
int minor = 0;
struct cdev mycdev; // 字符型结构体
char buf[N] = {"hello world !!"};int char_open(struct inode* myinode, struct file* fp)
{printk("char is opened\n");return 0;
}int char_release(struct inode* myinode, struct file* fp)
{printk("char is closeed\n");return 0;
}static ssize_t char_read(struct file* filep, char __user* user_buf, size_t count, loff_t* off)
{// 1. ssize_t :ssize_t 是 signed size_t;size_t: unsigned int// 2. Off:当前文件的偏移量ssize_t ret = 0;long num = 0;printk("char_read is called\n");printk("count is %d\n", count);num = copy_to_user(user_buf, buf, count);if (num < 0){printk("copy_to_user is failed\n");return ret;}return ret;
}ssize_t char_write(struct file* filep, const char __user* from, size_t count, loff_t* off)
{ssize_t ret = 0;long num = 0;printk("char_write is called \n");printk("count is %d\n", count);// if(count > N ) return -ENOMEM;if (count > N)count = N;num = copy_from_user(buf, from, count);if (num < 0){printk("copy_to_user is failed\n");return ret;}printk("from user is %s\n", buf);return ret;
}struct file_operations fops = {.owner = THIS_MODULE,.open = char_open,.release = char_release,.read = char_read,.write = char_write,
};static int __init char_init(void)
{int ret;dev_t devno = MKDEV(major, minor);ret = register_chrdev_region(devno, 1, "char"); // 静态申请设备号if (ret < 0){printk("fail to get devno\n");return ret;}mycdev.owner = THIS_MODULE;cdev_init(&mycdev, &fops);ret = cdev_add(&mycdev, devno, 1);if (ret < 0){printk("cdev_add fail to system\n");return ret;}printk("init_module\n");return 0;
}static void __exit char_exit(void)
{dev_t devno = MKDEV(major, minor);cdev_del(&mycdev);unregister_chrdev_region(devno, 1);printk("cleanup_module\n");
}module_init(char_init);
module_exit(char_exit);
Makefile 代码
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# KERNELDIR ?= /home/linux/workdir/source-pack/linux-3.2-net/ (交叉编译)
M=$(PWD) modules
PWD := $(shell pwd)modules:$(MAKE) -C $(KERNELDIR) M=$(PWD) modulesmodules_install:$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_installclean:rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions Module* modules*.PHONY: modules modules_install clean
else
obj-m := char.o
endif
test.c
代码
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>#define N 128
char buf[N];int main()
{int fd;if ((fd = open("/dev/mychar", O_RDWR)) < 0){perror("open");exit(-1);}if (read(fd, buf, N) < 0){perror("read");exit(-1);}printf("read from mychar is %s\n", buf);// memset(buf,0,sizeof(buf));// strcpy(buf,"goddbye\0");printf("please input second buf:\n");scanf("%s", buf);if (write(fd, buf, N + 1) < 0){perror("write");exit(-1);}if (read(fd, buf, N) < 0){perror("read");exit(-1);}printf("second read from mychar is %s\n", buf);getchar();printf("mychar is opened\n");close(fd);
}
设备驱动:Linux 系统下的字符设备驱动程序编程
丶di 于 2021-11-01 14:32:37 发布
一、实验目的
通过一个简单的设备驱动的实现过程,学会 Linux 中设备驱动程序的编写。
二、实验环境
Ubuntu 20.04 TSL, Linux 5.10.0
三、实验内容
- 编写一个字符设备驱动程序,并在设备的打开操作中打印主次设备号。
- 编写一个用户测试程序,实现设备的读操作。
四、实验原理
实验中用到的系统调用函数(包括实验原理中介绍的和自己采用的),实验步骤。
字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和 LED 设备等。每一个字符设备都在 /dev
目录下对应一个设备文件。Linux 用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备。一个字符设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
描述字符设备的数据结构
在 Linux 2.6 内核中的字符设备用 cdev
结构来描述,其定义如下:
struct cdev
{struct kobject kobj; // 类似对象类,驱动模块的基础对象struct module* owner; // 所属内核模块,一般为 THIS_MODULEconst struct file_operations* ops; // 文件操作结构struct list_head list;dev_t dev; // 设备号,int 类型,高 12 位为主设备号,低 20 位为次设备号unsigned int count;
};
字符设备驱动模块的编写
实现一个基本的字符驱动设备需要以下几个部分:字符设备驱动模块的加载、卸载函数和 file_operations
结构中的成员函数。具体步骤如下:
-
分配和释放设备号
- 在设备驱动程序中,注册设备前首先要向系统申请设备号。
- 分配设备号有静态和动态的两种方法:
- 静态分配(
register_chrdev_region()
函数) - 动态分配(
alloc_chrdev_region()
)
- 静态分配(
- 通过
unregister_chrdev_region()
函数释放已分配的(无论是静态的还是动态的)设备号。
-
定义并初始化一个
struct file_operations
结构,并实现其中的操作函数static struct file_operations cdrv_fops = {.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的 __this_module 变量 */.open = cdrv_open,.read = cdrv_read,.write = cdrv_write, }; static int cdrv_open(struct inode* inode, struct file* filp) static ssize_t cdrv_read(struct file* filp, char __user* buf, size_t size, loff_t* ppos) static ssize_t cdrv_write(struct file* filp, const char __user* buf, size_t size, loff_t* ppos)
-
字符设备的注册
-
删除字符设备
-
注销设备号
-
模块声明
MODULE_LICENSE("GPL");
-
加载模块
module_init(cdrv_init);
-
卸载模块
module_exit(cdrv_exit);
编译模块 Makefile 文件
利用 mknod
命令在 /dev
目录下为字符设备生成对应的节点
五、实验结果分析
(截屏的实验结果,与实验结果对应的实验分析)
字符设备驱动模块 device_driver.c
用户测试程序 test.c
编写 Makefile 文件
编译字符驱动内核模块
……# make
执行发现编译错误,对函数被拼写错误,分号的中英文格式等问题进行修改
根据提示成功修改后,再次执行 make
指令,则会产生一些系列模块文件
……# sudo insmod device_driver.ko
采用高级管理员权限插入模块到内核
查看系统信息
……# dmesg
采用 |
管道配合 grep
过滤不需要的文件,筛选出刚出入的 devic_driver
模块的进程号为 16384
插入的设备需要在 /dev
目录下生成对应的结点
……# mknod /dev/demo_drv c 主设备号 0
使用 cat /proc/devices
指令,查看当前系统设备上已经存在的设备号,查看到动态申请 237 号作为本次实验的主设备号
执行指令:sudo mknod /dev/demo_drv c 237 0
查看 /dev
目录情况
……# ls /dev
在目录 /dev
下黄色标明文件名的文件,都是当前系统的设备;在这里也可以查看到刚刚插入的 demo_drv
字符设备
编译用户测试程序
……# gcc test.c -o test
……# ./test
……# dmesg
使用 dmesg | tail
查看最近的日记信息
Makefile
ifneq ($(KERNELRELEASE),)
obj-m += device_driver.o
else
KERNELDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:rm -rf *.o *.mod.c *.ko *.order *.symvers .*.cmd .tmp_versions
endif
Test.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>#define DEMO_DEV_NAME "/dev/demo_drv"int main()
{char buffer[64];int fd;fd = open(DEMO_DEV_NAME, O_RDONLY);if (fd < 0){printf("open device %s failed\n", DEMO_DEV_NAME);return -1;}read(fd, buffer, 64);printf("%s\n", buffer);close(fd);return 0;
}
Device_driver.c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>#define DEMO_NAME "my_demo_dev" // 设备名static dev_t dev;
static struct cdev* demo_cdev;
static signed count = 1;static int demodrv_open(struct inode* inode, struct file* file)
{int major = MAJOR(inode->i_rdev);int minor = MINOR(inode->i_rdev);printk("%s: major=%d, minor=%d\n", __func__, major, minor);return 0;
}static ssize_t demodrv_read(struct file* file, char __user* buf, size_t lbuf, loff_t* ppos)
{printk("%s enter\n", __func__);return 0;
}static ssize_t demodrv_write(struct file* file, const char __user* buf, size_t count, loff_t* f_pos)
{printk("%s enter\n", __func__);return 0;
}static const struct file_operations demodrv_fops = {.owner = THIS_MODULE,.open = demodrv_open,.read = demodrv_read,.write = demodrv_write,
};static int __init simple_char_init(void)
{int ret;ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME); // 动态申请设备号if (ret){printk("failed to allocate char device region\n");return ret;}demo_cdev = cdev_alloc(); // 分配设备空间if (!demo_cdev){printk("cedv_alloc failed\n");goto unregister_chrdev;}cdev_init(demo_cdev, &demodrv_fops); // 设备空间进行初始化和赋值操作ret = cdev_add(demo_cdev, dev, count); // 设备添加进系统函数if (ret){printk("cdev_add failed\n");goto cdev_fail;}printk("successed register char device: %s\n", DEMO_NAME);printk("Major number=%d, Minor number=%d\n", MAJOR(dev), MINOR(dev));return 0;cdev_fail:cdev_del(demo_cdev); // 删除字符设备
unregister_chrdev:unregister_chrdev_region(dev, count); // 注销设备号return ret;
}static void __exit simple_char_exit(void)
{printk("removing device\n");if (demo_cdev)cdev_del(demo_cdev);unregister_chrdev_region(dev, count); // 注销字符设备return;
}MODULE_LICENSE("GPL"); // 模块声明
module_init(simple_char_init); // 加载模块
module_exit(simple_char_exit); // 卸载模块
Linux 驱动入门 —— 最简单字符设备驱动(基于 pc ubuntu)
Wireless_Link 于 2022-12-31 15:07:48 发布
一、字符设备驱动概念
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI、LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
在详细的学习字符设备驱动架构之前,我们先来简单的了解一下 Linux 下的应用程序是如何调用驱动程序的,Linux 应用程序对驱动程序的调用如图
在 Linux 中一切皆为文件,驱动加载成功以后会在 /dev
目录下生成一个相应的文件,应用程序通过对这个名为 /dev/xxx
(xxx
是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。比如现在有个叫做 /dev/led
的驱动文件,此文件是 led 灯的驱动文件。应用程序使用 open
函数来打开文件 /dev/led
,使用完成以后使用 close
函数关闭 /dev/led
这个文件。open
和 close
就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write
函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取 led 灯的状态,就用 read
函数从驱动中读取相应的状态。
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open
函数打开 /dev/led
这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open
、close
、write
和 read
等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open
函数的时候流程如图
其中关于 C 库以及如何通过系统调用“陷入”到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open
这个函数,那么在驱动程序中也得有一个名为 open
的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h
中有个叫做 file_operations
的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所示:
struct file_operations {struct module* owner;loff_t (*llseek)(struct file*, loff_t, int);ssize_t (*read)(struct file*, char __user*, size_t, loff_t*);ssize_t (*write)(struct file*, const char __user*, size_t, loff_t*);ssize_t (*read_iter)(struct kiocb*, struct iov_iter*);ssize_t (*write_iter)(struct kiocb*, struct iov_iter*);int (*iterate)(struct file*, struct dir_context*);unsigned int (*poll)(struct file*, struct poll_table_struct*);long (*unlocked_ioctl)(struct file*, unsigned int, unsigned long);long (*compat_ioctl)(struct file*, unsigned int, unsigned long);int (*mmap)(struct file*, struct vm_area_struct*);int (*mremap)(struct file*, struct vm_area_struct*);int (*open)(struct inode*, struct file*);int (*flush)(struct file*, fl_owner_t id);int (*release)(struct inode*, struct file*);int (*fsync)(struct file*, loff_t, loff_t, int datasync);int (*aio_fsync)(struct kiocb*, int datasync);int (*fasync)(int, struct file*, int);int (*lock)(struct file*, int, struct file_lock*);ssize_t (*sendpage)(struct file*, struct page*, int, size_t, loff_t*, int);unsigned long (*get_unmapped_area)(struct file*, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock)(struct file*, int, struct file_lock*);ssize_t (*splice_write)(struct pipe_inode_info*, struct file*, loff_t*, size_t, unsigned int);ssize_t (*splice_read)(struct file*, loff_t*, struct pipe_inode_info*, size_t, unsigned int);int (*setlease)(struct file*, long, struct file_lock**, void**);long (*fallocate)(struct file* file, int mode, loff_t offset, loff_t len);void (*show_fdinfo)(struct seq_file* m, struct file* f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file*);
#endif
};
简单介绍一下 file_operation
结构体中比较重要的、常用的函数:
- owner:拥有该结构体的模块的指针,一般设置为
THIS_MODULE
。 - llseek:函数用于修改文件当前的读写位置。
- read:函数用于读取设备文件。
- write:函数用于向设备文件写入(发送)数据。
- poll:是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
- unlocked_ioctl:函数提供对于设备的控制功能,与应用程序中的
ioctl
函数对应。 - compat_ioctl:函数与
unlocked_ioctl
函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl
。 - mmap:函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
- open:函数用于打开设备文件。
- release:函数用于释放(关闭)设备文件,与应用程序中的
close
函数对应。 - fasync:函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
- aio_fsync:函数与
fasync
函数的功能类似,只是aio_fsync
是异步刷新待处理的数据。
在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像 open
、release
、write
、read
等都是需要实现的,当然了,具体需要实现哪些函数还是要看具体的驱动要求。
二、字符设备驱动开发步骤
我们简单的介绍了一下字符设备驱动,那么字符设备驱动开发都有哪些步骤呢?我们在学习裸机或者 STM32 的时候关于驱动的开发就是初始化相应的外设寄存器,在 Linux 驱动开发中肯定也是要初始化相应的外设寄存器,这个是毫无疑问的。只是在 Linux 驱动开发中我们需要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是学习其驱动框架。
1. 驱动模块的加载和卸载
Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为 .ko
),在 Linux 内核启动以后使用“insmod
”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进 Linux 内核中,当然也可以不编译进 Linux 内核中,具体看自己的需求。
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init); // 注册模块加载函数
module_exit(xxx_exit); // 注册模块卸载函数
module_init
函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init
就是需要注册的具体函数,当使用“insmod
”命令加载驱动的时候,xxx_init
这个函数就会被调用。module_exit()
函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit
就是需要注册的具体函数,当使用“rmmod
”命令卸载具体驱动的时候 xxx_exit
函数就会被调用。字符设备驱动模块加载和卸载模板如下所示:
/* 驱动入口函数 */
static int __init xxx_init(void)
{/* 入口函数的具体内容 */return 0;
}/* 驱动出口函数 */
static void __exit xxx_deinit(void)
{/* 出口函数的具体内容 */
}module_init(xxx_init);
module_exit(xxx_deinit);
驱动编译完成以后扩展名为 .ko
,有两种命令可以加载驱动模块:insmod
和 modprobe
,insmod
是最简单的模块加载命令,此命令用于加载指定的 .ko
模块,比如加载 drv.ko
这个驱动模块,命令如下:
insmod drv.ko
insmod
命令不能解决模块的依赖关系,比如 drv.ko
依赖 first.ko
这个模块,就必须先使用 insmod
命令加载 first.ko
这个模块,然后再加载 drv.ko
这个模块。但是 modprobe
就不会存在这个问题,modprobe
会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此 modprobe
命令相比 insmod
要智能一些。modprobe
命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe
命令来加载驱动。modprobe
命令默认会去 /lib/modules/<kernel-version>
目录中查找模块,比如本书使用的 Linux kernel 的版本号为 4.1.15,因此 modprobe
命令默认会到 /lib/modules/4.1.15
这个目录中查找相应的驱动模块,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建。
驱动模块的卸载使用命令“rmmod
”即可,比如要卸载 drv.ko
,使用如下命令即可:
rmmod drv.ko
也可以使用“modprobe -r
”命令卸载驱动,比如要卸载 drv.ko
,命令如下:
modprobe -r drv.ko
使用 modprobe
命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe
来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod
命令。
2. 添加 LICENSE 以及作者信息
写完基本的框架后,我们要加上 LICENSE 信息以及作者信息,LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息不是必选,LICENSE 和作者信息的添加使用如下两个函数:
MODULE_LICENSE(); // 添加模块 LICENSE 信息
MODULE_AUTHOR(); // 添加模块作者信息
其中 LICENSE 填写 GPL,因为 Linux 本身就是 GPL 协议的,实例如下:
MODULE_LICENSE("GPL");
3. 在 Ubuntu 巩固小节 1 - 2 内容
学习完以上的内容,我们就可以写程序,光说不练假把式,所以我们直接来实践。我们在 Ubuntu 来写一个程序来体验下(由于最开始的简单的字符设备驱动,不需要操作特定的板子,所以我们在 Ubuntu 直接写程序比较简单验证)。
3.1 写一个 hello_driver.c
#include <linux/module.h>
static int __init hello_driver_init(void)
{printk("hello_driver_init\r\n");return 0;
}
static void __exit hello_driver_cleanup(void)
{printk("hello_driver_cleanup\r\n");
}
module_init(hello_driver_init);
module_exit(hello_driver_cleanup);
MODULE_LICENSE("GPL");
就是这么简单,内容我们之前都见过,只有 printk
我们没有见过,printk
是内核打印 log 的机制,类似于 app 层面的 printf
。
3.2 写 Makefile
写完 hello_driver.c
,我们要写一个 Makefile 来编译以下文件,Makefile 内容如下:
KERNELDIR := /lib/modules/$(shell uname -r)/build
CURRENT_PATH := $(shell pwd)
obj-m := hello_driver.o
build: kernel_modules
kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
其中 KERNELDIR
就是 Ubuntu build 内核的路径,至于什么作用下,先不用管,先暂时照着写,后续再介绍。
3.3 编译
make
3.4 加载
sudo insmod hello_driver.ko
可以看到并没有我们 printk
的 log 输出,那么怎么能确定我们加载成功了呢?lsmod | grep hello_driver
可以看到我们加载成功了,那么 printk
的打印内容呢?那是因为你如果没有配置 printk
的等级,他不会打印到 terminal 上,查看用 dmesg
来查看就可以看到了。
另外注意的一点在 Ubuntu 需要用 sudo
来加载。
3.5 卸载
sudo rmmod hello_driver
好了,简单吧,我们来继续学下字符设备注册跟注销用上 file_operations
。
4. 字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
static inline int register_chrdev(unsigned int major, const char* name, const struct file_operations* fops);
static inline void unregister_chrdev(unsigned int major, const char* name);
register_chrdev
函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
- major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
- name:设备名字,指向一串字符串。
- fops:结构体
file_operations
类型指针,指向设备的操作函数集合变量。
unregister_chrdev
函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
- major:要注销的设备对应的主设备号。
- name:要注销的设备对应的设备名。
一般字符设备的注册在驱动模块的入口函数 xxx_init
中进行,字符设备的注销在驱动模块的出口函数 xxx_exit
中进行。
4.1 设备号的组成
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t
的数据类型表示设备号,dev_t
定义在文件 include/linux/types.h
里面,定义如下:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
可以看出 dev_t
是 __u32
类型的,而 __u32
定义在文件 include/uapi/asm-generic/int-ll64.h
里面,定义如下:
typedef unsigned int __u32;
综上所述,dev_t
其实就是 unsigned int
类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux 系统中主设备号范围为 0 - 4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文件 include/linux/kdev_t.h
中提供了几个关于设备号的操作函数(本质是宏),如下所示:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int)((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int)((dev) & MINORMASK))
#define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi))
- 宏
MINORBITS
表示次设备号位数,一共是 20 位。 - 宏
MINORMASK
表示次设备号掩码。 - 宏
MAJOR
用于从dev_t
中获取主设备号,将dev_t
右移 20 位即可。 - 宏
MINOR
用于从dev_t
中获取次设备号,取dev_t
的低 20 位的值即可。 - 宏
MKDEV
用于将给定的主设备号和次设备号的值组合成dev_t
类型的设备号。
5. 内核空间跟用户空间交互数据
我们有了 file_operations
的概念后,但是有两个函数指针 read
/write
,那么用户空间可以直接用内核空间的 buffer 指针吗?内核空间可以直接使用用户空间的 buffer 指针吗?答案是不能,所以要有两个函数来做转换,函数分别如下:
unsigned long copy_to_user(void* dst, const void* src, unsigned long len);
unsigned long copy_from_user(void* to, const void* from, unsigned long n);
copy_to_user
函数来完成内核空间的数据到用户空间的复制,参数to
表示目的,参数from
表示源,参数n
表示要复制的数据长度。copy_from_user
函数来完成用户空间的数据到内核空间的复制,参数to
表示目的,参数from
表示源,参数n
表示要复制的数据长度。
6. 在 Ubuntu 巩固小节 4 - 5 内容
6.1 hello_driver.c
的内容
#include <linux/types.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/module.h>
#define CHRDEVBASE_MAJOR 200
uint8_t kernel_buffer[1024] = {0};static int hello_world_open(struct inode* inode, struct file* file)
{printk("hello_world_open\r\n");return 0;
}static int hello_world_release(struct inode* inode, struct file* file)
{printk("hello_world_release\r\n");return 0;
}static ssize_t hello_world_read(struct file* file, char __user* buffer, size_t size, loff_t* ppos)
{printk("hello_world_read size:%ld\r\n", size);copy_to_user(buffer, kernel_buffer, size);return size;
}static ssize_t hello_world_write(struct file* file, const char __user* buffer, size_t size, loff_t* ppos)
{printk("hello_world_write size:%ld\r\n", size);copy_from_user(kernel_buffer, buffer, size);return size;
}static const struct file_operations hello_world_fops = {.owner = THIS_MODULE,.open = hello_world_open,.release = hello_world_release,.read = hello_world_read,.write = hello_world_write,
};static int __init hello_driver_init(void)
{int ret;printk("hello_driver_init\r\n");ret = register_chrdev(CHRDEVBASE_MAJOR, "hello_driver", &hello_world_fops);return 0;
}static void __exit hello_driver_cleanup(void)
{unregister_chrdev(CHRDEVBASE_MAJOR, "hello_driver");printk("hello_driver_cleanup\r\n");
}module_init(hello_driver_init);
module_exit(hello_driver_cleanup);
MODULE_LICENSE("GPL");
hello_driver.c
是驱动,用于编译出来 .ko
,以上的内容就是我们 4 - 5 小节学习的内容,如果看不懂移步前面巩固内容。
6.2 test_app.c
内容
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>/* mknod /dev/hello c 200 0 */
uint8_t buffer[512] = {0};int main(int argc, char* argv[])
{int fd;int ret;fd = open(argv[1], O_RDWR);if (!strcmp("read", argv[2])){printf("read data from kernel\r\n");ret = read(fd, buffer, sizeof(buffer));printf("ret len:%d data:%s\r\n", ret, buffer);}if (!strcmp("write", argv[2])){printf("write data to kernel %s len:%d\r\n", argv[3], strlen(argv[3]));ret = write(fd, argv[3], strlen(argv[3]));printf("ret len:%d\r\n", ret);}close(fd);
}
test_app.c
的内容是用户态的 app,用于跟驱动的节点去交互。此程序的用法是 test_app /dev/xxx write/read 数据
。
6.3 Makefile
KERNELDIR := /lib/modules/$(shell uname -r)/build
CURRENT_PATH := $(shell pwd)
obj-m := hello_driver.o
build: kernel_modules
kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules$(CROSS_COMPILE)gcc -o test_app test_app.c
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) cleanrm -rf test_app
此 Makefile 跟之前的差不多,只不过多了一个编译 test_app.c
的动作,应用程序生成的名称为 test_app
。
6.4 验证程序
- 加载
.ko
:sudo insmod hello_driver.ko
- 命令行生成节点名称:
sudo mknod /dev/hello c 200 0
,其中c 200 0
代表是字符设备驱动,主设备号是 200,次设备号是 0,200 这个是驱动中我们写的#define CHRDEVBASE_MAJOR 200
,也可以通过cat /proc/devices
命令行来确认。 - 应用程序写数据:
sudo ./test_app /dev/hello write 1234567890
- 应用程序读取数据:
sudo ./test_app /dev/hello read
- 卸载驱动:
sudo rmmod hello_driver
- 删除节点:
sudo rm -rf /dev/hello
7. 自动创建节点
在前面的 Linux 驱动实验中,当我们使用 modprobe
加载驱动程序以后还需要使用命令“mknod
”手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用 modprobe
加载驱动模块成功的话就会自动在 /dev
目录下创建对应的设备文件。
7.1 创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add
函数后面添加自动创建设备节点相关代码。首先要创建一个 class
类,class
是个结构体,定义在文件 include/linux/device.h
里面。class_create
是类创建函数,class_create
是个宏定义,内容如下:
#define class_create(owner, name) \
({ \static struct lock_class_key __key; \__class_create(owner, name, &__key); \
})
struct class* __class_create(struct module* owner, const char* name, struct lock_class_key* key);
根据上述代码,将宏 class_create
展开以后内容如下:
struct class* class_create(struct module* owner, const char* name);
class_create
一共有两个参数,参数 owner
一般为 THIS_MODULE
,参数 name
是类名字。返回值是个指向结构体 class
的指针,也就是创建的类。卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy
,函数原型如下:
void class_destroy(struct class* cls);
参数 cls
就是要删除的类。
7.2 创建设备
上一小节创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用 device_create
函数在类下面创建设备,device_create
函数原型如下:
struct device* device_create(struct class* class, struct device* parent, dev_t devt, void* drvdata, const char* fmt, ...);
device_create
是个可变参数函数,参数 class
就是设备要创建哪个类下面;参数 parent
是父设备,一般为 NULL
,也就是没有父设备;参数 devt
是设备号;参数 drvdata
是设备可能会使用的一些数据,一般为 NULL
;参数 fmt
是设备名字,如果设置 fmt=xxx
的话,就会生成 /dev/xxx
这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy
,函数原型如下:
void device_destroy(struct class* class, dev_t devt);
参数 class
是要删除的设备所处的类,参数 devt
是要删除的设备号。
8. 巩固小节 7 的内容
我们就来修改下小节 6 的 hello_driver.c
就好了,其他都跟小节 6 一样(包括 test_app.c
和 Makefile)。
hello_driver.c
的内容如下:
#include <linux/types.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/module.h>
#include <linux/device.h>#define CHRDEVBASE_MAJOR 200
uint8_t kernel_buffer[1024] = {0};
static struct class* hello_class;static int hello_world_open(struct inode* inode, struct file* file)
{printk("hello_world_open\r\n");return 0;
}static int hello_world_release(struct inode* inode, struct file* file)
{printk("hello_world_release\r\n");return 0;
}static ssize_t hello_world_read(struct file* file, char __user* buffer, size_t size, loff_t* ppos)
{printk("hello_world_read size:%ld\r\n", size);copy_to_user(buffer, kernel_buffer, size);return size;
}static ssize_t hello_world_write(struct file* file, const char __user* buffer, size_t size, loff_t* ppos)
{printk("hello_world_write size:%ld\r\n", size);copy_from_user(kernel_buffer, buffer, size);return size;
}static const struct file_operations hello_world_fops = {.owner = THIS_MODULE,.open = hello_world_open,.release = hello_world_release,.read = hello_world_read,.write = hello_world_write,
};static int __init hello_driver_init(void)
{int ret;printk("hello_driver_init\r\n");ret = register_chrdev(CHRDEVBASE_MAJOR, "hello_driver", &hello_world_fops);hello_class = class_create(THIS_MODULE, "hello_class");device_create(hello_class, NULL, MKDEV(CHRDEVBASE_MAJOR, 0), NULL, "hello"); /* /dev/hello */return 0;
}static void __exit hello_driver_cleanup(void)
{printk("hello_driver_cleanup\r\n");device_destroy(hello_class, MKDEV(CHRDEVBASE_MAJOR, 0));class_destroy(hello_class);unregister_chrdev(CHRDEVBASE_MAJOR, "hello_driver");
}module_init(hello_driver_init);
module_exit(hello_driver_cleanup);
MODULE_LICENSE("GPL");
做完这个步骤后就可以 insmod
后自动生成 /dev/hello
的节点,直接可以用 test_app
做测试了。
9. 新的字符设备驱动
9.1 设备号的分配
使用 register_chrdev
函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:
- 需要我们事先确定好哪些主设备号没有使用。
- 会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为 200,那么 0 - 1048575((2^{20} - 1))这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
分配的方式有以下两种,但是我们推荐尽量用动态分配设备号的方式。
9.1.1 静态分配设备号
本小节讲的设备号分配主要是主设备号的分配。前面讲解字符设备驱动的时候说过了,注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个设备号,比如选择 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt
。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个设备号,使用“cat /proc/devices
”命令即可查看当前系统中所有已经使用了的设备号。
9.1.2 动态分配设备号
静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题,Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:
int alloc_chrdev_region(dev_t* dev, unsigned baseminor, unsigned count, const char* name);
函数 alloc_chrdev_region
用于申请设备号,此函数有 4 个参数:
- dev:保存申请到的设备号。
- baseminor:次设备号起始地址,
alloc_chrdev_region
可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以baseminor
为起始地址地址开始递增。一般baseminor
为 0,也就是说次设备号从 0 开始。 - count:要申请的设备号数量。
- name:设备名字。
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count);
此函数有两个参数:
- from:要释放的设备号。
- count:表示从
from
开始,要释放的设备号数量。
9.2 新的字符设备注册方法
9.2.1 cdev
结构体
在 Linux 中使用 cdev
结构体表示一个字符设备,cdev
结构体在 include/linux/cdev.h
文件中,定义如下:
struct cdev {struct kobject kobj;struct module* owner;const struct file_operations* ops;struct list_head list;dev_t dev;unsigned int count;
};
在 cdev
中有两个重要的成员变量:ops
和 dev
,这两个就是字符设备文件操作函数集合 file_operations
以及设备号 dev_t
,其中 file_operations
和 dev_t
我们在前面已经介绍了!
9.2.2 cdev_init
函数
定义好 cdev
变量以后就要使用 cdev_init
函数对其进行初始化,cdev_init
函数原型如下:
void cdev_init(struct cdev* cdev, const struct file_operations* fops);
参数 cdev
就是要初始化的 cdev
结构体变量,参数 fops
就是字符设备文件操作函数集合。
9.2.3 cdev_add
函数
cdev_add
函数用于向 Linux 系统添加字符设备(cdev
结构体变量),首先使用 cdev_init
函数完成对 cdev
结构体变量的初始化,然后使用 cdev_add
函数向 Linux 系统添加这个字符设备。cdev_add
函数原型如下:
int cdev_add(struct cdev* p, dev_t dev, unsigned count);
参数 p
指向要添加的字符设备(cdev
结构体变量),参数 dev
就是设备所使用的设备号,参数 count
是要添加的设备数量。
9.2.4 cdev_del
函数
卸载驱动的时候一定要使用 cdev_del
函数从 Linux 内核中删除相应的字符设备,cdev_del
函数原型如下:
void cdev_del(struct cdev* p);
参数 p
就是要删除的字符设备。
下面我们写个代码来巩固下这个小节的内容,主要跟前面程序的差异还是 hello_driver.c
。
#include <linux/types.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/cdev.h>dev_t hello_devid;
struct cdev hello_cdev;
int hello_major = 0;
int hello_minor;
uint8_t kernel_buffer[1024] = {0};
static struct class* hello_class;static int hello_world_open(struct inode* inode, struct file* file)
{printk("hello_world_open\r\n");return 0;
}static int hello_world_release(struct inode* inode, struct file* file)
{printk("hello_world_release\r\n");return 0;
}static ssize_t hello_world_read(struct file* file, char __user* buffer, size_t size, loff_t* ppos)
{printk("hello_world_read size:%ld\r\n", size);copy_to_user(buffer, kernel_buffer, size);return size;
}static ssize_t hello_world_write(struct file* file, const char __user* buffer, size_t size, loff_t* ppos)
{printk("hello_world_write size:%ld\r\n", size);copy_from_user(kernel_buffer, buffer, size);return size;
}static const struct file_operations hello_world_fops = {.owner = THIS_MODULE,.open = hello_world_open,.release = hello_world_release,.read = hello_world_read,.write = hello_world_write,
};static int __init hello_driver_init(void)
{int ret;printk("hello_driver_init\r\n");alloc_chrdev_region(&hello_devid, 0, 1, "hello");hello_major = MAJOR(hello_devid);hello_minor = MINOR(hello_devid);printk("hello driver major=%d,minor=%d\r\n", hello_major, hello_minor);hello_cdev.owner = THIS_MODULE;cdev_init(&hello_cdev, &hello_world_fops);cdev_add(&hello_cdev, hello_devid, 1);hello_class = class_create(THIS_MODULE, "hello_class");device_create(hello_class, NULL, hello_devid, NULL, "hello"); /* /dev/hello */return 0;
}static void __exit hello_driver_cleanup(void)
{printk("hello_driver_cleanup\r\n");cdev_del(&hello_cdev);unregister_chrdev_region(hello_devid, 1);device_destroy(hello_class, hello_devid);class_destroy(hello_class);
}module_init(hello_driver_init);
module_exit(hello_driver_cleanup);
MODULE_LICENSE("GPL");
三. 总结
在网上找了一个非常不错的图片,基本涵盖了很多内容,在这里贴出来:
参考内容:
- 正点原子 imx6ull 嵌入式 Linux 驱动开发指南.pdf
- 韦东山视频:01_Hello 驱动(不涉及硬件操作)
via:
-
Simple Linux character device driver – Oleg Kutkov personal blog
https://olegkutkov.me/2018/03/14/simple-linux-character-device-driver/ -
Writing a Simple Character Device driver in Linux
https://www.pradeepkumar.org/2010/03/writing-a-simple-character-device-driver-in-linux.html -
Implementing a Simple Char Device in Linux LG #125
http://linuxgazette.net/125/mishra.html -
Linux— 字符设备驱动程序设计_虚拟字符设备驱动程序设计 - CSDN 博客
https://blog.csdn.net/holly_Z_P_F/article/details/106918733 -
设备驱动: Linux 系统下的字符设备驱动程序编程_字符设备驱动程序实验平台运行结果分析 - CSDN 博客
https://blog.csdn.net/hrd535523596/article/details/121079620 -
Linux 驱动入门 - 最简单字符设备驱动 (基于 pc ubuntu)_ubuntu 应用程序访问字符驱动程序 - CSDN 博客
https://blog.csdn.net/XiaoXiaoPengBo/article/details/128505550 -
Linux 设备驱动之字符设备驱动(超级详细~) - 知乎
https://zhuanlan.zhihu.com/p/506834783 -
字符设备驱动 — [野火] 嵌入式 Linux 驱动开发实战指南 —— 基于 LubanCat - 全志系列板卡 文档
https://doc.embedfire.com/linux/h618/driver/zh/latest/linux_driver/base/character_device/character_device.html
—-
Developing a Simple Device Driver as a Linux Loadable Kernel Module | CISC 7310X Operating Systems I
https://huichen-cs.github.io/course/CISC7310X/25SP/tutorial/lnxkernelmodule.html -
GitHub - renanleonellocastro/char_driver: A simple char driver that has some basic functions. This driver was create for learning purposes.
https://github.com/renanleonellocastro/char_driver -
GitHub - alessandro-scalambrino/simple-device-driver: This project implements a simple character device driver for Linux, allowing interaction with the kernel through basic read and write operations. It demonstrates the fundamental principles of Linux device driver development, including dynamic device number allocation, handling of character device structures, and sysfs integration.
https://github.com/alessandro-scalambrino/simple-device-driver
-