Linux面试高频(Linux驱动)

Linux驱动

1 什么是模块?⭐⭐⭐

在 Linux 驱动中,模块是一种具有特定功能的可动态加载和卸载的代码单元。它能在不重新编译内核的情况下,为内核添加新功能或驱动新硬件。比如网卡驱动模块,可使内核支持特定型号的网卡。模块有独立的代码和数据空间,通过特定的接口与内核其他部分交互,如初始化函数用于在加载时进行资源分配等操作,清理函数用于在卸载时释放资源。常见的模块包括设备驱动模块、文件系统模块、网络协议模块等。

2 驱动类型有几种⭐⭐⭐⭐⭐

Linux 驱动类型主要有以下几种:

字符设备驱动:以字符为单位进行数据传输,像键盘、鼠标、串口设备等,应用程序可通过系统调用对其进行读写等操作,通常按字节流方式处理数据。

块设备驱动:以块为单位传输数据,常见的如硬盘、U 盘等存储设备,数据读写以固定大小的块为单位,支持随机访问,能提高数据传输效率。

网络设备驱动:负责网络设备与内核网络子系统间的通信,如网卡驱动,实现数据包的发送和接收,使设备能接入网络进行数据通信。

3 字符设备驱动框架编程流程?⭐⭐⭐⭐⭐

在 Linux 内核中进行字符设备驱动框架编程,一般可以按照以下流程进行:

1. 准备工作

包含必要的头文件:在驱动代码文件开头,包含 Linux 内核中与字符设备驱动开发相关的头文件,例如 <linux/init.h>、<linux/module.h>、<linux/fs.h> 等,这些头文件提供了驱动开发所需的各种数据结构和函数声明

定义必要的全局变量:定义字符设备的主设备号、次设备号、设备类指针、设备指针等全局变量,方便后续在不同函数中使用。

2. 实现文件操作结构体

文件操作结构体 struct file_operations 定义了用户空间对设备文件进行各种操作时对应的内核函数,是字符设备驱动的核心部分。需要实现以下常见的操作函数:

open 函数:当用户空间程序调用 open() 系统调用打开设备文件时,内核会调用该函数,一般用于对设备进行初始化、检查设备状态等操作。

read 函数:对应 read() 系统调用,用于从设备读取数据到用户空间缓冲区。

write 函数:对应 write() 系统调用,用于将用户空间缓冲区的数据写入设备。

release 函数:当用户空间程序调用 close() 系统调用关闭设备文件时,内核会调用该函数,通常用于释放设备占用的资源。

以下是一个简单的文件操作结构体示例:

static struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
};


3.分配和注册设备号

设备号用于唯一标识一个字符设备,由主设备号和次设备号组成。可以通过以下两种方式分配设备号:

静态分配:手动指定一个主设备号,但需要确保该主设备号未被其他设备使用,使用 register_chrdev_region() 函数进行注册。

动态分配:由内核自动分配一个可用的主设备号,使用 alloc_chrdev_region() 函数进行分配和注册。

示例代码如下:

static dev_t dev;
static int major;

// 动态分配设备号
static int __init my_init(void) {
    if (alloc_chrdev_region(&dev, 0, 1, "my_device") < 0) {
        printk(KERN_ERR "Failed to allocate device number\n");
        return -1;
    }
    major = MAJOR(dev);
    printk(KERN_INFO "Allocated major number: %d\n", major);
    return 0;
}


4.创建设备类和设备节点

创建设备类:使用 class_create() 函数创建一个设备类,设备类是一种抽象的表示,用于将相关的设备组织在一起。

创建设备节点:使用 device_create() 函数在 /dev 目录下创建设备节点,用户空间程序可以通过该设备节点来访问字符设备。

示例代码如下:

static struct class *my_class;
static struct device *my_device;

static int __init my_init(void) {
    // 分配设备号代码...

    // 创建设备类
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        printk(KERN_ERR "Failed to create device class\n");
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(my_class);
    }

    // 创建设备节点
    my_device = device_create(my_class, NULL, dev, NULL, "my_device");
    if (IS_ERR(my_device)) {
        printk(KERN_ERR "Failed to create device node\n");
        class_destroy(my_class);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(my_device);
    }

    return 0;
}


5.注册字符设备

使用 cdev_init() 函数将文件操作结构体与字符设备对象关联起来,然后使用 cdev_add() 函数将字符设备对象注册到内核中。

示例代码如下:

static struct cdev my_cdev;

static int __init my_init(void) {
    // 分配设备号、创建设备类和设备节点代码...

    // 初始化字符设备对象
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    // 注册字符设备
    if (cdev_add(&my_cdev, dev, 1) < 0) {
        printk(KERN_ERR "Failed to add character device\n");
        device_destroy(my_class, dev);
        class_destroy(my_class);
        unregister_chrdev_region(dev, 1);
        return -1;
    }

    return 0;
}


6.实现初始化和退出函数

初始化函数:在驱动模块加载时,内核会调用初始化函数,在该函数中完成设备号分配、设备类和设备节点创建、字符设备注册等操作。

退出函数:在驱动模块卸载时,内核会调用退出函数,在该函数中完成字符设备注销、设备节点和设备类销毁、设备号释放等操作。

示例代码如下:

static void __exit my_exit(void) {
    cdev_del(&my_cdev);
    device_destroy(my_class, dev);
    class_destroy(my_class);
    unregister_chrdev_region(dev, 1);
    printk(KERN_INFO "Character device module unloaded\n");
}

module_init(my_init);
module_exit(my_exit);


7.编译和加载驱动模块

编写 Makefile:编写 Makefile 文件,指定编译驱动模块所需的内核源码路径和编译规则。

编译驱动模块:在终端中执行 make 命令,编译生成 .ko 格式的驱动模块文件

加载和卸载驱动模块:使用 insmod 命令加载驱动模块,使用 rmmod 命令卸载驱动模块。

通过以上步骤,就可以完成一个基本的字符设备驱动框架的编程。不同的字符设备可能还需要根据具体需求实现更多的功能,如中断处理、设备资源管理等。

4 什么是并发,驱动中产生竞态的原因有哪些?⭐⭐⭐⭐

并发是指在同一时间段内,有多个任务或操作在同时进行或交替执行,这些任务可以是不同的进程、线程或内核中的不同执行路径等。

在驱动中产生竞态的原因主要有以下几点:

多进程或多线程访问:多个进程或线程可能同时访问驱动中的共享资源,如全局变量、设备寄存器等,如果没有适当的同步机制,就会导致数据不一致等竞态问题。

中断处理:中断可能在任意时刻发生,若中断处理程序和其他正常执行路径同时访问同一资源,比如中断处理程序修改了某个设备状态变量,而主程序也在对该变量进行操作,就容易引发竞态。

内核抢占:在支持内核抢占的系统中,一个正在执行的驱动程序可能被更高优先级的任务抢占,当它再次恢复执行时,可能与抢占期间执行的其他代码产生对共享资源的竞态。

5 解决竞态的途径有哪些?分别有什么特点?⭐⭐⭐⭐⭐

在驱动开发中,解决竞态的途径及特点如下:

自旋锁

特点:当一个进程获取自旋锁时,如果锁已被占用,它会持续循环等待,直到锁被释放。优点是开销小、响应快,适用于锁被持有的时间短、不希望进程睡眠的场景。缺点是会浪费 CPU 资源,且不能在持有自旋锁时进行可能导致睡眠的操作。

互斥锁

特点:获取互斥锁的进程若发现锁已被占用,会进入睡眠状态,直到锁被释放。它能避免自旋锁的忙等待问题,适合锁被持有的时间较长的情况。不过,由于涉及进程状态切换,开销相对较大,且不能在中断上下文中使用。

信号量

特点:可

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

嵌入式/C++面试八股文 文章被收录于专栏

该专栏面向嵌入式开发工程师、C++开发工程师,包括C语言、C++,操作系统,ARM架构、RTOS、Linux基础、Linux驱动、Linux系统移植、计算机网络、数据结构与算法、数电基础、模电基础、5篇面试题目、HR面试常见问题汇总和嵌入式面试简历模板等文章。超全的嵌入式软件工程师面试题目和高频知识点总结! 另外,专栏分为两个部分,大家可以各取所好,为了有更好的阅读体验,后面会持续更新!!!

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务