字符设备是 Linux 三大设备类型之一(字符设备、块设备、网络设备),按字节流顺序访问,不支持随机访问。典型例子:串口、键盘、鼠标、LED、虚拟设备。
访问方式:用户空间通过 /dev/ 下的设备文件节点(如 /dev/myled)进行 read/write/ioctl 操作。
用户空间 内核空间
───────── ─────────
open("/dev/myled") → file_operations.open()
read(fd, buf, n) → file_operations.read()
write(fd, buf, n) → file_operations.write()
ioctl(fd, cmd, arg) → file_operations.ioctl()
close(fd) → file_operations.release()
1. alloc_chrdev_region() → 动态分配主/次设备号
2. cdev_init() → 初始化 cdev,绑定 file_operations
3. cdev_add() → 将 cdev 注册到内核
4. class_create() → 创建设备类(/sys/class/xxx)
5. device_create() → 创建设备节点(/dev/xxx)
卸载时反向执行:
5. device_destroy()
4. class_destroy()
3. cdev_del()
2. (cdev 无需显式销毁)
1. unregister_chrdev_region()
struct file_operations {
struct module *owner;
int (*open)(struct inode *, struct file *);
int (*release)(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 *);
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
int (*mmap)(struct file *, struct vm_area_struct *);
loff_t (*llseek)(struct file *, loff_t, int);
__poll_t (*poll)(struct file *, struct poll_table_struct *);
// ... 更多操作
};
#include <linux/ioctl.h>
// 宏定义(自动编码方向、大小、magic、序号)
_IO(type, nr) // 无数据传输
_IOR(type, nr, datatype) // 从内核读数据
_IOW(type, nr, datatype) // 向内核写数据
_IOWR(type, nr, datatype) // 双向传输
// 示例
#define LED_MAGIC 'L'
#define LED_ON _IO(LED_MAGIC, 0)
#define LED_OFF _IO(LED_MAGIC, 1)
#define LED_SET _IOW(LED_MAGIC, 2, int)
#define LED_GET _IOR(LED_MAGIC, 3, int)
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
// 设备号相关
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
void unregister_chrdev_region(dev_t dev, unsigned count);
// cdev 操作
void cdev_init(struct cdev *cdev, struct file_operations *fops);
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);
void cdev_del(struct cdev *cdev);
// 设备类和设备节点
struct class *class_create(const char *name);
void class_destroy(struct class *cls);
struct device *device_create(struct class *cls, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...);
void device_destroy(struct class *cls, dev_t devt);
#include <linux/uaccess.h>
unsigned long copy_to_user(void __user *dst, const void *src, unsigned long size);
unsigned long copy_from_user(void *dst, const void __user *src, unsigned long size);
// 返回0成功,非0失败
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/ioctl.h>
#define DEVICE_NAME "myled"
#define CLASS_NAME "led_class"
#define LED_MAGIC 'L'
#define LED_ON _IO(LED_MAGIC, 0)
#define LED_OFF _IO(LED_MAGIC, 1)
#define LED_SET _IOW(LED_MAGIC, 2, int)
static dev_t dev_num;
static struct cdev led_cdev;
static struct class *led_class;
static struct device *led_device;
static int led_state = 0;
static int led_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "LED: open\n");
return 0;
}
static int led_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "LED: release\n");
return 0;
}
static ssize_t led_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
int ret;
if (*ppos > 0)
return 0;
ret = copy_to_user(buf, &led_state, sizeof(led_state));
if (ret)
return -EFAULT;
*ppos += sizeof(led_state);
return sizeof(led_state);
}
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
int ret;
if (count < sizeof(led_state))
return -EINVAL;
ret = copy_from_user(&led_state, buf, sizeof(led_state));
if (ret)
return -EFAULT;
printk(KERN_INFO "LED: write state=%d\n", led_state);
return sizeof(led_state);
}
static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int val;
switch (cmd) {
case LED_ON:
led_state = 1;
printk(KERN_INFO "LED: ON\n");
break;
case LED_OFF:
led_state = 0;
printk(KERN_INFO "LED: OFF\n");
break;
case LED_SET:
if (copy_from_user(&val, (int __user *)arg, sizeof(val)))
return -EFAULT;
led_state = val;
printk(KERN_INFO "LED: SET state=%d\n", led_state);
break;
default:
return -ENOTTY;
}
return 0;
}
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.read = led_read,
.write = led_write,
.unlocked_ioctl = led_ioctl,
};
static int __init led_init(void)
{
int ret;
/* 1. 动态分配设备号 */
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "LED: alloc_chrdev_region failed\n");
return ret;
}
printk(KERN_INFO "LED: major=%d minor=%d\n", MAJOR(dev_num), MINOR(dev_num));
/* 2. 初始化并添加 cdev */
cdev_init(&led_cdev, &led_fops);
led_cdev.owner = THIS_MODULE;
ret = cdev_add(&led_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "LED: cdev_add failed\n");
goto err_unregister;
}
/* 3. 创建设备类 */
led_class = class_create(CLASS_NAME);
if (IS_ERR(led_class)) {
printk(KERN_ERR "LED: class_create failed\n");
ret = PTR_ERR(led_class);
goto err_cdev;
}
/* 4. 创建设备节点 /dev/myled */
led_device = device_create(led_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(led_device)) {
printk(KERN_ERR "LED: device_create failed\n");
ret = PTR_ERR(led_device);
goto err_class;
}
printk(KERN_INFO "LED: driver loaded, device /dev/%s created\n", DEVICE_NAME);
return 0;
err_class:
class_destroy(led_class);
err_cdev:
cdev_del(&led_cdev);
err_unregister:
unregister_chrdev_region(dev_num, 1);
return ret;
}
static void __exit led_exit(void)
{
device_destroy(led_class, dev_num);
class_destroy(led_class);
cdev_del(&led_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "LED: driver unloaded\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example");
MODULE_DESCRIPTION("Simple LED character device driver");
MODULE_VERSION("1.0");
obj-m := led_chrdev.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
# 编译
make
# 加载驱动
sudo insmod led_chrdev.ko
# 查看设备号
dmesg | tail
# [xxxxx] LED: major=240 minor=0
# 查看设备节点
ls -l /dev/myled
# crw------- 1 root root 240, 0 ... /dev/myled
# 用户空间测试
# 写入状态
echo -n -e '\x01\x00\x00\x00' > /dev/myled
# 读取状态
cat /dev/myled
# 卸载驱动
sudo rmmod led_chrdev
make clean
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define LED_MAGIC 'L'
#define LED_ON _IO(LED_MAGIC, 0)
#define LED_OFF _IO(LED_MAGIC, 1)
#define LED_SET _IOW(LED_MAGIC, 2, int)
int main(void)
{
int fd = open("/dev/myled", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
ioctl(fd, LED_ON);
printf("LED ON\n");
int val = 0;
ioctl(fd, LED_GET, &val);
printf("LED state: %d\n", val);
ioctl(fd, LED_OFF);
printf("LED OFF\n");
close(fd);
return 0;
}
// 编译:gcc test_led.c -o test_led
// 运行:sudo ./test_led
register_chrdev_region)可能与其他驱动冲突,推荐使用 alloc_chrdev_region 动态分配IS_ERR 检查:class_create 和 device_create 返回 ERR_PTR 而不是 NULL,必须用 IS_ERR 检查copy_to_user / copy_from_user 不可省略:用户空间指针可能非法,直接解引用会导致内核 oops_IO/_IOR/_IOW/_IOWR 宏自动生成命令码,避免手动编码冲突module 的 .owner 字段:file_operations 中的 .owner = THIS_MODULE 确保模块在使用中不能被卸载cdev_del 后不能再调用文件操作:cdev 删除后,用户空间的 open 调用会返回 -ENXIOQ1:字符设备驱动的注册流程是什么?
A:① alloc_chrdev_region 动态分配设备号 → ② cdev_init 初始化 cdev 并绑定 file_operations → ③ cdev_add 注册到内核 → ④ class_create 创建设备类 → ⑤ device_create 创建 /dev/ 设备节点。卸载时反向销毁。
Q2:file_operations 结构体的作用是什么?它有哪些常用成员?
A:file_operations 是字符设备驱动的核心,定义了用户空间操作(open/read/write/ioctl/close)对应的内核函数指针。常用成员:open(打开设备)、release(关闭设备)、read/write(数据读写)、unlocked_ioctl(控制命令)。
Q3:copy_to_user 和 copy_from_user 为什么要检查返回值?
A:它们返回未拷贝的字节数,非零表示部分或完全失败(用户空间指针非法、进程被杀死等)。不检查返回值可能导致内核使用未初始化数据,产生安全漏洞或数据不一致。
Q4:ioctl 命令码 _IOW(LED_MAGIC, 2, int) 是怎么编码的?
A:内核宏 _IOW 将方向(写)、数据大小(sizeof(int))、类型码('L')和序号(2)编码为 32 位整数。_IO 无数据传输,_IOR 读,_IOW 写,_IOWR 双向。这样不同的 ioctl 命令不会冲突。
Q5:为什么设备类和设备节点要分开创建?
A:class_create 在 /sys/class/ 下创建类,device_create 在类下创建具体设备节点(/dev/xxx)。分离设计允许 udev/mdev 根据规则自动创建/命名设备节点,支持设备热插拔事件通知。