Linux 之设备驱动

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个阶段,每个阶段需严格管理资源:

  1. 模块加载(insmod/modprobe)
    • 内核执行module_init指定的初始化函数;
    • 核心操作:申请设备号、注册字符/块设备、初始化硬件(寄存器配置)、申请中断/DMA资源。
  2. 模块运行
    • 响应用户空间的文件操作(open/read/write/ioctl);
    • 处理硬件中断、DMA传输、并发同步。
  3. 模块卸载(rmmod)
    • 内核执行module_exit指定的退出函数;
    • 核心操作:释放设备号、注销设备、关闭中断/DMA、释放内核内存、恢复硬件默认状态。
  4. 模块依赖(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)下半部
  • 核心目的:处理耗时操作(如数据解析、通知用户空间),不阻塞中断响应;

  • 实现方式:

    1. workqueue(工作队列):可阻塞,适合大耗时操作;
    2. tasklet:不可阻塞,适合小耗时操作;
    3. 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.上半部必须极简 ,执行时间控制在微秒级;

  1. 下半部可处理耗时逻辑,但需避免长时间阻塞;

  2. 中断处理函数中禁止调用可能阻塞的函数(如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,不阻塞;

  • 驱动实现:检查硬件状态,未就绪则直接返回错误,核心判断:

    c 复制代码
    if (file->f_flags & O_NONBLOCK) {
        // 非阻塞模式,硬件未就绪则返回- EAGAIN
        return -EAGAIN;
    }

3.4 IOCTL(设备控制)

针对无法通过read/write实现的硬件配置(如设置串口波特率、LED闪烁频率),驱动通过unlocked_ioctl接口提供扩展控制能力:

核心实现步骤
  1. 定义命令码(32位,含方向、大小、幻数、序号):

    c 复制代码
    // 宏格式:_IOC(方向, 幻数, 序号, 数据大小)
    #define LED_SET_BLINK _IOW('L', 1, int) // 写命令:设置闪烁频率
    #define LED_GET_STATE _IOR('L', 2, int) // 读命令:获取LED状态
  2. 实现unlocked_ioctl函数:

    c 复制代码
    long 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;
    }
  3. 用户空间调用:

    c 复制代码
    int 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");
编译&运行
  1. 编写Makefile:

    makefile 复制代码
    KERNELDIR ?= /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
  2. 编译模块:

    bash 复制代码
    make
  3. 加载模块:

    bash 复制代码
    sudo insmod led_driver.ko
  4. 验证驱动:

    bash 复制代码
    # 查看设备节点
    ls /dev/led0
    # 控制LED开启
    echo "on" | sudo tee /dev/led0
    # 控制LED关闭
    echo "off" | sudo tee /dev/led0
    # 查看内核日志
    dmesg | grep LED
  5. 卸载模块:

    bash 复制代码
    sudo 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,核心步骤:

  1. 编译内核时开启CONFIG_KGDBCONFIG_KGDB_SERIAL_CONSOLE

  2. 通过串口连接开发板和主机;

  3. 主机端使用gdb调试:

    bash 复制代码
    gdb 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的核心区别?

答:核心区别在于物理地址是否连续

  1. kmalloc:分配物理+虚拟地址均连续的内存,基于slab分配器,效率高,适合DMA操作(硬件DMA控制器要求物理连续内存),但分配大小有限(通常<128KB);
  2. vmalloc:分配虚拟地址连续、物理地址离散的内存,需构建页表映射,效率低,适合大块内存分配(>128KB),但无法用于DMA操作。

问题2:为什么中断上半部要尽量简短?

答:上半部运行时会禁用对应中断线(或本地中断),若上半部执行时间过长,会导致:

  1. 硬件中断无法及时响应,造成数据丢失(如串口数据溢出);
  2. 系统响应延迟增加,实时性下降;
  3. 严重时导致系统卡顿甚至崩溃。
    因此上半部仅做标记中断、禁用硬件中断等极简操作,耗时逻辑放到底半部处理。

问题3:如何解决驱动中的竞态条件?

答:竞态的核心原因是多执行流同时访问共享资源,解决思路:

  1. 区分执行流类型:进程上下文(可阻塞)用互斥锁/信号量,中断上下文(不可阻塞)用自旋锁;
  2. 缩小临界区范围:仅对共享资源的操作加锁,避免大段代码加锁;
  3. 优先使用轻量级机制:简单变量用原子操作,无需锁;
  4. 中断上下文与进程上下文共享资源:使用spin_lock_irqsave/spin_unlock_irqrestore,避免中断抢占导致的死锁。

问题4:字符设备驱动的核心步骤?

答:核心6步:

  1. 定义设备结构体,封装设备号、cdev、硬件资源等;

  2. 实现file_operations接口(open/release/read/write/ioctl);

  3. 初始化函数中:申请设备号→初始化cdev→注册cdev→创建设备类→创建设备节点;

  4. 实现硬件操作逻辑(寄存器读写、中断处理);

  5. 退出函数中:反向释放资源(销毁设备节点→销毁类→删除cdev→释放设备号);

  6. 编写Makefile,编译模块并测试。

8. 总结

  1. 驱动核心定位:Linux设备驱动是内核态的硬件抽象层,向上提供统一的文件类接口,向下屏蔽硬件差异,核心遵循"一切皆文件"的设计思想。
  2. 三大驱动类型:字符设备适合字节流访问(如LED、串口),块设备适合按块访问的存储设备(如磁盘),网络设备无设备文件,通过套接字交互。
  3. 核心机制:中断处理采用"上半部+下半部"分离设计,并发同步需根据场景选择自旋锁(中断上下文)或互斥锁(进程上下文),内存分配优先使用kmalloc(物理连续)。
  4. 调试与资源管理:驱动调试以printk为基础,复杂问题用KGDB/ftrace;资源申请与释放需严格对称,避免内存泄漏、设备号未释放等问题。
  5. 重点:kmalloc vs vmalloc、中断上下部分离、自旋锁与互斥锁的区别、字符设备驱动实现步骤、竞态条件解决方法。
相关推荐
进击切图仔2 小时前
基于 linux 20.04 构建 ros1 noetic 开发环境 -离线版本
linux·运维·服务器
starcat20022 小时前
ESXi安装直连显卡的KDE-NEON
linux
vortex52 小时前
Alpine Linux syslinux 启动加固(密码保护)
linux·服务器·网络
晚风吹长发2 小时前
初步了解Linux中的线程同步问题及线程安全和死锁与生产消费者模型
linux·运维·服务器·开发语言·数据结构·安全
学嵌入式的小杨同学2 小时前
【Linux 封神之路】进程进阶实战:fork/vfork/exec 函数族 + 作业实现(含僵尸进程解决方案)
linux·开发语言·vscode·嵌入式硬件·vim·软件工程·ux
mi20062 小时前
银河麒麟上tabby和electerm两款终端工具比较
linux·运维
muyan92 小时前
统信uos-server-20-1070e-arm64-20250704-1310 安装mysql-5.7.44
linux·mysql·yum·rpm·uos·统信
muyan92 小时前
浅吐槽一下统信uos linux
linux·运维·国产化·uos·统信·去ioe
LaoWaiHang2 小时前
Linux基础知识14:文件使用权限信息
linux