目录
一、字符设备的基本概念
二、字符设备驱动的核心结构
1. struct cdev结构体的主要成员
2. 设备号的组成
3. 怎样分配设备号
三、字符设备驱动的注册与注销
四、设备操作函数集
五、实际开发产品实例
六、总结
字符设备是Linux设备驱动中的一种类型,通常用于处理以字节流形式进行数据传输的设备。与块设备不同,字符设备不支持随机访问,数据只能按顺序读取或写入。常见的字符设备包括键盘、鼠标、串口等。
字符设备驱动的主要任务是为用户空间提供访问硬件设备的接口。这些接口通常通过文件系统暴露给用户,用户可以通过标准的文件操作函数(如open
、read
、write
、close
等)来与设备进行交互。
在Linux内核中,字符设备驱动的核心结构是struct cdev
,它代表一个字符设备。struct cdev
结构体包含了设备号、设备操作函数集等信息。设备号由主设备号和次设备号组成,主设备号用于标识设备类型,次设备号用于标识具体的设备实例。
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
1. struct cdev
结构体的主要成员dev_t dev
:设备号,由主设备号和次设备号组成。主设备号用于标识设备类型,次设备号用于标识具体的设备实例。设备号可以通过MKDEV(major, minor)
宏来生成,其中major
是主设备号,minor
是次设备号。
struct file_operations *ops
:指向设备操作函数集的指针。file_operations
结构体定义了设备的各种操作函数,如open
、read
、write
、release
等。这些函数是用户空间与内核空间交互的接口。
主设备号(Major Number):用于标识设备类型。例如,所有硬盘设备可能共享同一个主设备号,而所有串口设备可能共享另一个主设备号。主设备号的范围通常是0到255。
次设备号(Minor Number):用于标识同一类型设备中的具体实例。例如,如果有多个串口设备,每个串口设备将有一个唯一的次设备号。次设备号的范围通常是0到255。
在Linux设备驱动开发中,分配设备号是一个关键步骤,它用于在内核中唯一标识一个字符设备。设备号由主设备号和次设备号组成,主设备号用于标识设备类型,次设备号用于标识具体的设备实例。
分配设备号可以通过以下两种方式实现:
动态分配:使用alloc_chrdev_region
函数
该函数会自动分配一个可用的主设备号,并返回给调用者。这种方式适用于开发者不确定使用哪个主设备号,或者希望避免与其他设备冲突的情况。
函数原型:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
参数说明:
dev
:用于存储分配的设备号。
baseminor
:请求的起始次设备号。
count
:请求的次设备号数量。
name
:设备名称,通常出现在/proc/devices
中。
dev_t dev;
int ret = alloc_chrdev_region(&dev, 0, 1, "my_device");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
静态分配:使用register_chrdev_region
函数
该函数允许开发者指定一个特定的主设备号。这种方式适用于开发者已经知道要使用的主设备号,并且确保该设备号未被其他设备占用。
函数原型:int register_chrdev_region(dev_t from, unsigned count, const char *name);
参数说明:
from
:指定的设备号。
count
:请求的次设备号数量。
name
:设备名称,通常出现在/proc/devices
中。
dev_t dev = MKDEV(240, 0); // 主设备号为240,次设备号为0
int ret = register_chrdev_region(dev, 1, "my_device");
if (ret < 0) {
printk(KERN_ERR "Failed to register device number\n");
return ret;
}
在实际开发中,通常建议使用alloc_chrdev_region
进行动态分配,以避免设备号冲突。分配成功后,设备号可以通过MAJOR
和MINOR
宏来提取主设备号和次设备号。设备号分配完成后,还需要通过cdev_init
和cdev_add
等函数将设备与文件操作结构体关联,并注册到内核中。在设备驱动卸载时,应使用unregister_chrdev_region
函数释放已分配的设备号,以避免资源泄漏。
字符设备驱动的注册通常通过cdev_init
和cdev_add
函数完成。cdev_init
函数用于初始化struct cdev
结构体,并关联设备操作函数集。cdev_add
函数将字符设备添加到内核中,使其对用户空间可见。
struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops);
cdev_add(&my_cdev, dev, 1);
字符设备的注销通过cdev_del
函数完成,通常在模块卸载时调用。
cdev_del(&my_cdev);
在Linux内核中,字符设备驱动的注册与注销通常通过以下步骤完成:
分配设备号:使用alloc_chrdev_region
或register_chrdev_region
函数分配设备号。
初始化cdev
结构体:使用cdev_init
函数初始化cdev
结构体,并设置file_operations
。
添加cdev
到系统:使用cdev_add
函数将cdev
添加到系统中,使其对用户空间可见。
注销设备:使用cdev_del
函数从系统中删除cdev
,并使用unregister_chrdev_region
函数释放设备号。
设备操作函数集struct file_operations
定义了用户空间与设备交互的接口。常见的操作函数包括open
、release
、read
、write
、ioctl
等。
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.ioctl = my_ioctl,
};
file_operations
结构体定义了设备的各种操作函数,这些函数是用户空间与内核空间交互的接口。常见的操作函数包括:
int (*open)(struct inode *, struct file *)
:打开设备时调用的函数。
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *)
:从设备读取数据时调用的函数。
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *)
:向设备写入数据时调用的函数。
int (*release)(struct inode *, struct file *)
:关闭设备时调用的函数。
假设开发一个简单的LED控制设备驱动,该设备通过字符设备接口控制LED的开关。首先,定义设备操作函数集。
static int led_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "LED device opened\n");
return 0;
}
static int led_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "LED device closed\n");
return 0;
}
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
char val;
if (copy_from_user(&val, buf, 1))
return -EFAULT;
if (val == '1')
printk(KERN_INFO "LED ON\n");
else if (val == '0')
printk(KERN_INFO "LED OFF\n");
return count;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write,
};
接下来,初始化并注册字符设备。
static dev_t dev;
static struct cdev led_cdev;
static int __init led_init(void) {
int ret;
dev = MKDEV(240, 0);
ret = register_chrdev_region(dev, 1, "led");
if (ret < 0) {
printk(KERN_ERR "Failed to register device\n");
return ret;
}
cdev_init(&led_cdev, &led_fops);
ret = cdev_add(&led_cdev, dev, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
unregister_chrdev_region(dev, 1);
return ret;
}
printk(KERN_INFO "LED device registered\n");
return 0;
}
static void __exit led_exit(void) {
cdev_del(&led_cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_INFO "LED device unregistered\n");
}
module_init(led_init);
module_exit(led_exit);
用户空间可以通过以下命令控制LED设备。
echo 1 > /dev/led # 打开LED
echo 0 > /dev/led # 关闭LED
字符设备驱动是Linux设备驱动中的重要组成部分,通过文件系统接口为用户空间提供访问硬件设备的能力。开发字符设备驱动需要理解struct cdev
、struct file_operations
等核心结构,并掌握设备的注册、注销以及操作函数的实现。通过实际开发产品实例,可以更好地理解字符设备驱动的具体技术要点和细节。