Linux 之设备驱动
1. 宏观理解
Linux设备驱动的核心是内核层对硬件设备的抽象与管控,是连接用户空间程序和底层硬件的桥梁。
-
向下(硬件管控):驱动运行在内核态,拥有最高权限,直接操作硬件寄存器、中断控制器和DMA。
-
向上(接口抽象):遵循"一切皆文件"的设计理念,将硬件抽象为 /dev 下的设备节点,为用户空间提供统一的标准文件接口(open/read/write/ioctl),从而屏蔽底层硬件差异。
-
中间(核心机制):处理并发竞争(锁)、中断响应(上下半部)、内存映射等内核核心逻辑。

2. 设备驱动基础组成
2.1 驱动核心架构分层
Linux设备驱动从上到下分为三层,层间职责清晰,解耦硬件与应用:
| 层级 | 核心职责 | 运行空间 | 关键接口/机制 |
|---|---|---|---|
| 用户层 | 通过设备文件访问硬件,无需关注硬件细节 | 用户态 | open/read/write/ioctl/close(标准文件接口) |
| 驱动层 | 硬件抽象、寄存器操作、中断处理、内核接口实现 | 内核态 | file_operations、中断API、内存分配API |
| 硬件层 | 物理硬件的寄存器、中断线、DMA通道等物理资源 | 硬件电路 | 寄存器, 中断线, DMA控制器等 |
2.2 设备标识体系
Linux通过设备号 和设备文件唯一标识硬件设备,内核通过设备号找到驱动,用户通过设备文件找到设备。
(1)设备号(dev_t): 32位整数
-
主设备号(Major):高12位,标识设备所属的驱动类型(如字符设备主号1对应mem,主号8对应sd块设备),同一类设备共享主设备号。
-
次设备号(Minor):低20位,标识同一驱动下的不同物理设备(如主号8的sd驱动,次号0对应sda,次号1对应sda1)。
-
设备号组合:内核通过
dev_t类型(32位整数,高12位为主设备号,低20位为次设备号)存储设备号,核心转换宏:c// 主/次设备号合成dev_t MKDEV(major, minor) // 从dev_t提取主设备号 MAJOR(dev_t dev) // 从dev_t提取次设备号 MINOR(dev_t dev)
(2)设备文件(设备节点)
- 存储路径:
/dev目录下的特殊文件,由mknod命令或udev/mdev自动创建。 - 文件类型:
- 字符设备:
ls -l显示c标识(如crw-rw-rw- 1 root root 1, 3 2月 2 10:00 null); - 块设备:
ls -l显示b标识(如brw-rw---- 1 root disk 8, 0 2月 2 10:00 sda);
- 字符设备:
- 核心作用:用户空间通过操作设备文件(如
open("/dev/led0", O_WRONLY)),内核自动映射到对应驱动的file_operations接口。
2.3 驱动核心数据结构
(1)file_operations(文件操作结构体)
驱动的核心接口集合,定义了用户空间调用open/read/write等函数时的内核处理逻辑,是驱动与用户空间的契约:
c
struct file_operations {
// 打开设备文件(必选,基础接口)
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);
// 异步IO通知
unsigned int (*poll) (struct file *, struct poll_table_struct *);
// 内存映射(如帧缓冲设备)
int (*mmap) (struct file *, struct vm_area_struct *);
};
(2)内核模块核心结构
驱动以内核模块(Kernel Module) 为载体,核心通过module_init/module_exit定义生命周期:
c
// 模块加载时执行(驱动初始化)
module_init(xxx_init);
// 模块卸载时执行(驱动资源释放)
module_exit(xxx_exit);
// 模块许可证(必须,否则加载报错)
MODULE_LICENSE("GPL");
2.4 三大类设备驱动对比
Linux将硬件分为字符设备、块设备、网络设备三类,驱动设计差异显著:
| 对比维度 | 字符设备驱动 | 块设备驱动 | 网络设备驱动 |
|---|---|---|---|
| 数据访问方式 | 字节流方式,按字符/字节顺序访问(如串口、键盘、LED) | 按固定大小"块"访问(如磁盘、U盘,块大小通常512B/4KB) | 按"数据包"访问,无设备文件,通过套接字(socket)交互 |
| 核心标识 | 主/次设备号 + /dev字符设备节点(c) | 主/次设备号 + /dev块设备节点(b) | 无设备号/设备文件,通过网络接口名(如eth0)标识 |
| 核心接口 | file_operations(read/write为主) | request_queue(请求队列)+ bio(块I/O) | net_device(网络设备结构体)+ 数据包收发函数 |
| 缓存机制 | 无内置缓存,需驱动自行实现 | 内核页缓存(page cache)+ 块缓存,大幅提升读写效率 | 网络协议栈缓存(sk_buff),处理数据包分片/重组 |
| 典型设备 | 串口(tty)、键盘(input)、LED、ADC/DAC | 硬盘(sd)、U盘(usb-storage)、固态硬盘(nvme) | 网卡(eth)、WiFi模块(wlan)、蓝牙 |
| 核心特点 | 简单直接,适合低速、顺序访问的设备 | 高IOPS优化,适合高速、随机访问的存储设备 | 面向网络协议,异步收发,高并发处理 |

2.5 内核态内存分配对比
驱动运行在核心态,需使用内核专属的内存分配接口,核心接口对比如下:
| 接口 | 分配区域 | 内存特性 | 分配效率 | 适用场景 |
|---|---|---|---|---|
| kmalloc(size, GFP_KERNEL) | 物理连续内存(DMA区) | 物理/虚拟地址均连续 | 高(基于slab分配器) | 驱动中小块内存(<128KB)、DMA操作(需物理连续) |
| vmalloc(size) | 虚拟连续内存 | 虚拟地址连续,物理地址离散 | 低(需页表映射) | 大块内存(>128KB)、非DMA操作(如驱动缓冲区) |
| dma_alloc_coherent(dev, size, dma_addr, GFP_KERNEL) | DMA专用连续内存 | 物理连续,CPU和DMA控制器均可访问 | 中 | 硬件DMA传输(如网卡、磁盘控制器) |
| get_free_pages(gfp_mask, order) | 物理连续的整页内存 | 按2^order页分配(order=0对应4KB) | 高 | 大块连续物理内存,需手动管理页 |
3. 驱动核心机制
3.1 内核模块生命周期
驱动以模块形式加载/卸载,核心生命周期分为4个阶段,每个阶段需严格管理资源:
- 模块加载(insmod/modprobe) :
- 内核执行
module_init指定的初始化函数; - 核心操作:申请设备号、注册字符/块设备、初始化硬件(寄存器配置)、申请中断/DMA资源。
- 内核执行
- 模块运行 :
- 响应用户空间的文件操作(open/read/write/ioctl);
- 处理硬件中断、DMA传输、并发同步。
- 模块卸载(rmmod) :
- 内核执行
module_exit指定的退出函数; - 核心操作:释放设备号、注销设备、关闭中断/DMA、释放内核内存、恢复硬件默认状态。
- 内核执行
- 模块依赖(modprobe) :
modprobe自动加载依赖模块(如USB驱动依赖usbcore),insmod需手动加载依赖。
3.2 中断处理机制
硬件通过中断 主动通知内核事件(如按键按下、数据接收完成),驱动需实现中断处理逻辑,Linux采用"上半部+下半部"分离设计,平衡响应速度和处理效率:

(1)上半部
-
核心函数:
request_irq(irq, handler, flags, name, dev_id)(注册中断); -
特点:快速执行、不可阻塞、禁止抢占,仅做最核心操作(如标记中断发生、禁用硬件中断);
-
示例:
c// 中断上半部处理函数 irqreturn_t key_irq_handler(int irq, void *dev_id) { // 仅标记中断发生,不做耗时处理 struct key_dev *dev = (struct key_dev *)dev_id; dev->irq_flag = 1; // 触发底半部处理 schedule_work(&dev->work); return IRQ_HANDLED; } // 注册中断(假设按键中断号为IRQ_KEY) request_irq(IRQ_KEY, key_irq_handler, IRQF_TRIGGER_FALLING, "key_irq", dev);
(2)下半部
-
核心目的:处理耗时操作(如数据解析、通知用户空间),不阻塞中断响应;
-
实现方式:
- workqueue(工作队列):可阻塞,适合大耗时操作;
- tasklet:不可阻塞,适合小耗时操作;
- softirq(软中断):内核高频使用,驱动慎用;
-
示例(workqueue):
c// 底半部处理函数 void key_work_handler(struct work_struct *work) { struct key_dev *dev = container_of(work, struct key_dev, work); // 耗时操作:读取按键值并通知用户空间 dev->key_val = read_key_value(); wake_up_interruptible(&dev->wait_queue); // 唤醒阻塞的读进程 } // 初始化工作队列 INIT_WORK(&dev->work, key_work_handler);
中断处理核心规则
1.上半部必须极简 ,执行时间控制在微秒级;
-
下半部可处理耗时逻辑,但需避免长时间阻塞;
-
中断处理函数中禁止调用可能阻塞的函数(如kmalloc(GFP_KERNEL)、sleep)。
3.3 阻塞与非阻塞IO
驱动需支持用户空间的阻塞/非阻塞访问,核心通过等待队列(wait queue)实现:
(1)阻塞IO
-
特点:用户进程调用read/write后,若硬件未准备好数据/资源,进程进入睡眠(TASK_INTERRUPTIBLE),直到硬件就绪被唤醒;
-
核心接口:
c// 定义等待队列头 wait_queue_head_t wait_queue; // 初始化等待队列头 init_waitqueue_head(&wait_queue); // 阻塞等待条件满足(条件为假则睡眠) wait_event_interruptible(wait_queue, condition); // 唤醒等待队列中的进程 wake_up_interruptible(&wait_queue);
(2)非阻塞IO
-
特点:用户进程调用read/write后,若硬件未就绪,立即返回- EAGAIN,不阻塞;
-
驱动实现:检查硬件状态,未就绪则直接返回错误,核心判断:
cif (file->f_flags & O_NONBLOCK) { // 非阻塞模式,硬件未就绪则返回- EAGAIN return -EAGAIN; }
3.4 IOCTL(设备控制)
针对无法通过read/write实现的硬件配置(如设置串口波特率、LED闪烁频率),驱动通过unlocked_ioctl接口提供扩展控制能力:
核心实现步骤
-
定义命令码(32位,含方向、大小、幻数、序号):
c// 宏格式:_IOC(方向, 幻数, 序号, 数据大小) #define LED_SET_BLINK _IOW('L', 1, int) // 写命令:设置闪烁频率 #define LED_GET_STATE _IOR('L', 2, int) // 读命令:获取LED状态 -
实现unlocked_ioctl函数:
clong led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct led_dev *dev = filp->private_data; int val; // 校验命令码合法性 if (_IOC_TYPE(cmd) != 'L') return -ENOTTY; if (_IOC_NR(cmd) > 2) return -ENOTTY; switch (cmd) { case LED_SET_BLINK: // 从用户空间拷贝参数(copy_from_user:内核<-(用户) if (copy_from_user(&val, (int *)arg, sizeof(int))) { return -EFAULT; } dev->blink_ms = val; break; case LED_GET_STATE: // 向用户空间拷贝数据(copy_to_user:内核->用户) val = dev->state; if (copy_to_user((int *)arg, &val, sizeof(int))) { return -EFAULT; } break; default: return -ENOTTY; } return 0; } -
用户空间调用:
cint fd = open("/dev/led0", O_RDWR); int blink_ms = 500; // 设置闪烁频率 ioctl(fd, LED_SET_BLINK, &blink_ms);
4. 典型设备驱动实现
4.1 字符设备驱动(LED驱动)
字符设备是最基础的驱动类型,以下是完整的LED驱动实现:
完整代码
c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>
// 设备名称
#define LED_DEV_NAME "led_dev"
// 设备数量
#define LED_DEV_CNT 1
// 虚拟LED寄存器地址(真实硬件需替换为物理地址)
#define LED_CTRL_REG 0x12345678
// LED设备结构体(封装设备资源)
struct led_dev {
dev_t dev_num; // 设备号
struct cdev cdev; // 字符设备结构体
struct class *class; // 设备类(用于自动创建设备节点)
struct device *device;// 设备实例
int state; // LED状态:0-关闭,1-开启
int blink_ms; // 闪烁频率(ms)
wait_queue_head_t wq; // 等待队列
};
struct led_dev led_dev; // 全局LED设备实例
// 硬件操作:设置LED状态
static void led_set_state(int state) {
// 真实硬件需通过iowrite32等接口操作寄存器
if (state) {
printk(KERN_INFO "LED: ON\n");
led_dev.state = 1;
} else {
printk(KERN_INFO "LED: OFF\n");
led_dev.state = 0;
}
}
// 打开设备
static int led_open(struct inode *inode, struct file *filp) {
// 将设备实例绑定到file结构体,方便后续操作
filp->private_data = &led_dev;
printk(KERN_INFO "LED device opened\n");
return 0;
}
// 关闭设备
static int led_release(struct inode *inode, struct file *filp) {
printk(KERN_INFO "LED device closed\n");
return 0;
}
// 写设备(控制LED开关)
static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) {
char kbuf[16];
int state;
// 拷贝用户空间数据到内核空间
if (copy_from_user(kbuf, buf, count)) {
return -EFAULT;
}
kbuf[count] = '\0';
// 解析用户指令
if (strcmp(kbuf, "on") == 0) {
state = 1;
} else if (strcmp(kbuf, "off") == 0) {
state = 0;
} else {
return -EINVAL;
}
// 设置LED状态
led_set_state(state);
return count;
}
// IOCTL控制(设置闪烁频率)
static long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
struct led_dev *dev = filp->private_data;
int val;
#define LED_SET_BLINK _IOW('L', 1, int)
if (_IOC_TYPE(cmd) != 'L') return -ENOTTY;
switch (cmd) {
case LED_SET_BLINK:
if (copy_from_user(&val, (int *)arg, sizeof(int))) {
return -EFAULT;
}
dev->blink_ms = val;
printk(KERN_INFO "LED blink set to %d ms\n", val);
break;
default:
return -ENOTTY;
}
return 0;
}
// 文件操作结构体
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write,
.unlocked_ioctl = led_ioctl,
};
// 驱动初始化函数
static int __init led_init(void) {
int ret;
// 1. 申请设备号(动态分配,避免主设备号冲突)
ret = alloc_chrdev_region(&led_dev.dev_num, 0, LED_DEV_CNT, LED_DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "alloc chrdev region failed\n");
return ret;
}
// 2. 初始化字符设备
cdev_init(&led_dev.cdev, &led_fops);
led_dev.cdev.owner = THIS_MODULE;
// 注册字符设备到内核
ret = cdev_add(&led_dev.cdev, led_dev.dev_num, LED_DEV_CNT);
if (ret < 0) {
unregister_chrdev_region(led_dev.dev_num, LED_DEV_CNT);
printk(KERN_ERR "cdev add failed\n");
return ret;
}
// 3. 创建设备类(/sys/class/led_dev)
led_dev.class = class_create(THIS_MODULE, LED_DEV_NAME);
if (IS_ERR(led_dev.class)) {
cdev_del(&led_dev.cdev);
unregister_chrdev_region(led_dev.dev_num, LED_DEV_CNT);
printk(KERN_ERR "class create failed\n");
return PTR_ERR(led_dev.class);
}
// 4. 创建设备节点(/dev/led0)
led_dev.device = device_create(led_dev.class, NULL, led_dev.dev_num, NULL, "led0");
if (IS_ERR(led_dev.device)) {
class_destroy(led_dev.class);
cdev_del(&led_dev.cdev);
unregister_chrdev_region(led_dev.dev_num, LED_DEV_CNT);
printk(KERN_ERR "device create failed\n");
return PTR_ERR(led_dev.device);
}
// 5. 初始化等待队列
init_waitqueue_head(&led_dev.wq);
led_dev.state = 0;
led_dev.blink_ms = 1000;
printk(KERN_INFO "LED driver init success\n");
return 0;
}
// 驱动退出函数
static void __exit led_exit(void) {
// 反向释放资源
device_destroy(led_dev.class, led_dev.dev_num);
class_destroy(led_dev.class);
cdev_del(&led_dev.cdev);
unregister_chrdev_region(led_dev.dev_num, LED_DEV_CNT);
printk(KERN_INFO "LED driver exit success\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("LED Character Device Driver");
MODULE_AUTHOR("Driver Dev");
编译&运行
-
编写Makefile:
makefileKERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) obj-m += led_driver.o all: make -C $(KERNELDIR) M=$(PWD) modules clean: make -C $(KERNELDIR) M=$(PWD) clean -
编译模块:
bashmake -
加载模块:
bashsudo insmod led_driver.ko -
验证驱动:
bash# 查看设备节点 ls /dev/led0 # 控制LED开启 echo "on" | sudo tee /dev/led0 # 控制LED关闭 echo "off" | sudo tee /dev/led0 # 查看内核日志 dmesg | grep LED -
卸载模块:
bashsudo rmmod led_driver
4.2 块设备驱动(核心框架)
块设备驱动核心围绕"请求队列"和"bio"(块I/O)实现,以下是简化框架:
c
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/bio.h>
#define BLOCK_DEV_NAME "myblk"
#define BLOCK_DEV_SIZE (1024 * 1024) // 1MB虚拟块设备
#define BLOCK_DEV_MAJOR 250
static struct request_queue *blk_queue; // 请求队列
static struct gendisk *blk_disk; // 通用磁盘结构体
static unsigned char *blk_data; // 虚拟存储数据
// 处理块I/O请求
static void blk_request_fn(struct request_queue *q) {
struct request *req;
// 遍历请求队列中的所有请求
while ((req = blk_fetch_request(q)) != NULL) {
sector_t sector = blk_rq_pos(req); // 请求的起始扇区
unsigned int len = blk_rq_bytes(req); // 请求的字节数
void *buf = page_address(req->bio->bi_io_vec->bv_page); // 数据缓冲区
// 处理读请求
if (rq_data_dir(req) == READ) {
memcpy(buf, blk_data + sector * 512, len);
}
// 处理写请求
else if (rq_data_dir(req) == WRITE) {
memcpy(blk_data + sector * 512, buf, len);
}
// 标记请求完成
__blk_end_request_all(req, 0);
}
}
// 驱动初始化
static int __init blk_init(void) {
// 1. 分配虚拟存储内存
blk_data = vmalloc(BLOCK_DEV_SIZE);
if (!blk_data) return -ENOMEM;
// 2. 创建请求队列
blk_queue = blk_init_queue(blk_request_fn, NULL);
if (!blk_queue) {
vfree(blk_data);
return -ENOMEM;
}
// 3. 分配通用磁盘结构体
blk_disk = alloc_disk(1); // 1个分区
if (!blk_disk) {
blk_cleanup_queue(blk_queue);
vfree(blk_data);
return -ENOMEM;
}
// 4. 配置磁盘参数
blk_disk->major = BLOCK_DEV_MAJOR;
blk_disk->first_minor = 0;
blk_disk->fops = &blk_fops; // 块设备操作结构体
blk_disk->queue = blk_queue;
strcpy(blk_disk->disk_name, BLOCK_DEV_NAME);
set_capacity(blk_disk, BLOCK_DEV_SIZE / 512); // 设置磁盘容量(扇区数)
// 5. 注册磁盘
add_disk(blk_disk);
printk(KERN_INFO "Block driver init success\n");
return 0;
}
// 驱动退出
static void __exit blk_exit(void) {
del_gendisk(blk_disk);
put_disk(blk_disk);
blk_cleanup_queue(blk_queue);
vfree(blk_data);
printk(KERN_INFO "Block driver exit success\n");
}
module_init(blk_init);
module_exit(blk_exit);
MODULE_LICENSE("GPL");
4.3 网络设备驱动(核心框架)
网络设备驱动核心围绕net_device结构体和数据包收发实现,简化框架:
c
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#define NET_DEV_NAME "myeth0"
// 网络设备结构体
static struct net_device *net_dev;
// 数据包发送函数
static netdev_tx_t net_start_xmit(struct sk_buff *skb, struct net_device *dev) {
// 模拟硬件发送:打印数据包信息
printk(KERN_INFO "Send packet: len=%d\n", skb->len);
// 释放skb缓冲区
dev_kfree_skb(skb);
// 标记发送完成
dev->trans_start = jiffies;
return NETDEV_TX_OK;
}
// 网络设备初始化函数
static int net_dev_init(struct net_device *dev) {
// 初始化以太网设备
ether_setup(dev);
// 设置MAC地址
dev->dev_addr[0] = 0x00;
dev->dev_addr[1] = 0x11;
dev->dev_addr[2] = 0x22;
dev->dev_addr[3] = 0x33;
dev->dev_addr[4] = 0x44;
dev->dev_addr[5] = 0x55;
// 设置发送函数
dev->netdev_ops->ndo_start_xmit = net_start_xmit;
return 0;
}
// 驱动初始化
static int __init net_init(void) {
// 分配网络设备结构体
net_dev = alloc_netdev(0, NET_DEV_NAME, NET_NAME_UNKNOWN, net_dev_init);
if (!net_dev) return -ENOMEM;
// 注册网络设备
register_netdev(net_dev);
printk(KERN_INFO "Net driver init success: %s\n", NET_DEV_NAME);
return 0;
}
// 驱动退出
static void __exit net_exit(void) {
unregister_netdev(net_dev);
free_netdev(net_dev);
printk(KERN_INFO "Net driver exit success\n");
}
module_init(net_init);
module_exit(net_exit);
MODULE_LICENSE("GPL");
5. 驱动并发与同步
驱动会被多个进程/线程同时访问(如多个进程读写同一个串口),需通过同步机制避免竞态条件,核心同步机制对比如下:
| 同步机制 | 核心原理 | 阻塞特性 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| 自旋锁(spinlock) | 忙等待,循环检测锁状态,不释放CPU | 非阻塞(忙等) | 临界区极短(<10us)、中断上下文 | 禁止在自旋锁内调用阻塞函数(如kmalloc(GFP_KERNEL)) |
| 互斥锁(mutex) | 睡眠等待,获取不到锁则进入TASK_INTERRUPTIBLE | 阻塞 | 临界区较长(>10us)、进程上下文 | 只能在进程上下文使用,不可中断上下文 |
| 信号量(semaphore) | 计数型互斥锁,支持多进程同时访问 | 阻塞 | 需限制访问数量的场景(如最多3个进程访问) | 避免死锁(PV操作顺序) |
| 原子操作(atomic_t) | 硬件级别的原子指令,无锁操作 | 无阻塞 | 简单变量操作(如计数器、状态标记) | 仅支持整数操作,无法处理复杂逻辑 |
| 完成量(completion) | 等待某个事件完成,事件完成后唤醒等待进程 | 阻塞 | 驱动初始化完成、硬件操作完成通知 | 一对一等待,适合异步操作同步 |
典型使用示例(自旋锁+互斥锁)
c
// 自旋锁(中断上下文使用)
static spinlock_t irq_lock;
// 互斥锁(进程上下文使用)
static struct mutex dev_mutex;
// 中断处理函数(上半部)
irqreturn_t dev_irq_handler(int irq, void *dev_id) {
unsigned long flags;
// 保存中断状态并禁用本地中断
spin_lock_irqsave(&irq_lock, flags);
// 临界区(极短):修改共享变量
dev->count++;
// 恢复中断状态并释放自旋锁
spin_unlock_irqrestore(&irq_lock, flags);
return IRQ_HANDLED;
}
// 读函数(进程上下文)
ssize_t dev_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) {
// 获取互斥锁,阻塞等待
mutex_lock(&dev_mutex);
// 临界区(较长):读取数据并拷贝到用户空间
memcpy_to_user(buf, dev->buf, count);
// 释放互斥锁
mutex_unlock(&dev_mutex);
return count;
}
// 初始化同步机制
static int dev_init(void) {
spin_lock_init(&irq_lock);
mutex_init(&dev_mutex);
return 0;
}
6. 驱动调试方法
驱动开发中调试是核心环节,Linux提供多种调试手段,核心方法如下:
6.1 内核打印(printk)
最基础的调试方式,通过printk输出内核日志,核心优先级:
c
// 优先级从高到低(数值越小优先级越高)
#define KERN_EMERG 0 // 紧急错误,系统崩溃
#define KERN_ALERT 1 // 必须立即处理
#define KERN_CRIT 2 // 严重错误
#define KERN_ERR 3 // 普通错误
#define KERN_WARNING 4 // 警告
#define KERN_NOTICE 5 // 注意(默认级别)
#define KERN_INFO 6 // 信息
#define KERN_DEBUG 7 // 调试(默认不输出)
// 使用示例
printk(KERN_ERR "LED driver: register failed\n");
printk(KERN_INFO "LED state: %d\n", dev->state);
查看日志:
bash
dmesg # 查看内核日志
dmesg -w # 实时监控内核日志
grep "LED" /var/log/kern.log # 过滤特定日志
6.2 内核调试器(KGDB)
支持远程调试内核模块,需配置内核开启KGDB,核心步骤:
-
编译内核时开启
CONFIG_KGDB和CONFIG_KGDB_SERIAL_CONSOLE; -
通过串口连接开发板和主机;
-
主机端使用gdb调试:
bashgdb vmlinux (gdb) target remote /dev/ttyUSB0:115200 (gdb) break led_init # 设置断点 (gdb) c # 继续执行
6.3 其他调试工具
| 工具 | 核心作用 | 使用场景 |
|---|---|---|
| ftrace | 跟踪内核函数调用、中断延迟、调度延迟 | 性能瓶颈分析、函数调用流程追踪 |
| perf | 性能分析,统计函数执行时间、CPU占用 | 驱动性能优化 |
| crash | 分析内核崩溃转储文件(core dump) | 内核Oops/Panic问题定位 |
| devmem | 直接读写物理内存(寄存器) | 硬件寄存器调试 |
7. 常见问题
问题1:kmalloc和vmalloc的核心区别?
答:核心区别在于物理地址是否连续:
- kmalloc:分配物理+虚拟地址均连续的内存,基于slab分配器,效率高,适合DMA操作(硬件DMA控制器要求物理连续内存),但分配大小有限(通常<128KB);
- vmalloc:分配虚拟地址连续、物理地址离散的内存,需构建页表映射,效率低,适合大块内存分配(>128KB),但无法用于DMA操作。
问题2:为什么中断上半部要尽量简短?
答:上半部运行时会禁用对应中断线(或本地中断),若上半部执行时间过长,会导致:
- 硬件中断无法及时响应,造成数据丢失(如串口数据溢出);
- 系统响应延迟增加,实时性下降;
- 严重时导致系统卡顿甚至崩溃。
因此上半部仅做标记中断、禁用硬件中断等极简操作,耗时逻辑放到底半部处理。
问题3:如何解决驱动中的竞态条件?
答:竞态的核心原因是多执行流同时访问共享资源,解决思路:
- 区分执行流类型:进程上下文(可阻塞)用互斥锁/信号量,中断上下文(不可阻塞)用自旋锁;
- 缩小临界区范围:仅对共享资源的操作加锁,避免大段代码加锁;
- 优先使用轻量级机制:简单变量用原子操作,无需锁;
- 中断上下文与进程上下文共享资源:使用
spin_lock_irqsave/spin_unlock_irqrestore,避免中断抢占导致的死锁。
问题4:字符设备驱动的核心步骤?
答:核心6步:
-
定义设备结构体,封装设备号、cdev、硬件资源等;
-
实现file_operations接口(open/release/read/write/ioctl);
-
初始化函数中:申请设备号→初始化cdev→注册cdev→创建设备类→创建设备节点;

-
实现硬件操作逻辑(寄存器读写、中断处理);
-
退出函数中:反向释放资源(销毁设备节点→销毁类→删除cdev→释放设备号);
-
编写Makefile,编译模块并测试。
8. 总结
- 驱动核心定位:Linux设备驱动是内核态的硬件抽象层,向上提供统一的文件类接口,向下屏蔽硬件差异,核心遵循"一切皆文件"的设计思想。
- 三大驱动类型:字符设备适合字节流访问(如LED、串口),块设备适合按块访问的存储设备(如磁盘),网络设备无设备文件,通过套接字交互。
- 核心机制:中断处理采用"上半部+下半部"分离设计,并发同步需根据场景选择自旋锁(中断上下文)或互斥锁(进程上下文),内存分配优先使用kmalloc(物理连续)。
- 调试与资源管理:驱动调试以printk为基础,复杂问题用KGDB/ftrace;资源申请与释放需严格对称,避免内存泄漏、设备号未释放等问题。
- 重点:kmalloc vs vmalloc、中断上下部分离、自旋锁与互斥锁的区别、字符设备驱动实现步骤、竞态条件解决方法。