嵌入式八股文(三)Linux驱动

系列文章目录

嵌入式八股文(一)C语言篇
嵌入式八股文(二)Linux应用篇
嵌入式八股文(三)Linux驱动篇
嵌入式八股文(四)FreeRTOS篇



前言

  本期带来Linux驱动的学习,以为后续的学习打个基础。本系列会不定期持续更新(随笔者的学习进度),大家可以收藏一下时不时看一下。


一、驱动

1. 1 驱动的分类

  常见的驱动分为三种:

  1. 字符设备驱动:用于处理顺序数据的读写操作
  2. 块设备驱动:用于处理以块为单位的读写操作
  3. 网络设备驱动:处理网络数据包的接收

1.2 常见的驱动流程

1.2.1 字符驱动

“设备号、创建类、初始化设备、添加设备、创建设备”

  1. 利用alloc_chrdev_region申请设备号
  2. 创建类class_create
  3. 初始化cdev对象:
     最重要的是设置cdev的ops字段,它指向一个包含设备操作方法的file_operations结构体。
  4. 实现文件操作接口:
     在file_operations结构体中,需要实现设备的操作方法。这些方法定义了如何打开设备、关闭设备、读取数据、写入数据等。
  5. 注册字符设备驱动:
     通过调用cdev_add函数来完成,该函数将cdev对象添加到内核的字符设备列表中,并为其分配一个inode节点。
  6. 创建设备节点:
      device_create创建一个设备节点(也称为设备文件),以便应用程序能够访问该设备。这通常是通过在/dev目录下创建一个具有特定设备号和名称的文件来完成的。可以使用mknod命令或udev系统来创建设备节点。
  7. 清理和卸载:
     当不再需要设备驱动时,需要编写清理和卸载函数来释放资源并注销设备,包括注销cdev对象、删除设备节点、释放内存等操作。

1.2.2 GPIO子系统

“设备树匹配、获取节点、获取端口号、设置方向、设置值、初始化字符设备”

  1. 注册/注销平台驱动
  2. 采用设备树匹配的方式进行匹配
  3. 获取设备树节点:
     通过 platform_device 结构体自动传递设备树节点
     使用 of_find_node_by_path() 手动查找设备树节点
  4. 获取 GPIO 描述符:
     如果通过 platform_device 自动传递设备树节点,直接使用 gpiod_get() 获取 GPIO 描述符
     如果手动查找设备树节点,使用 gpiod_get_from_of_node() 获取 GPIO 描述符
  5. 设置端口方向:
     使用 gpiod_direction_output() 或 gpiod_direction_input() 设置 GPIO 方向
  6. 设置端口值:
      使用 gpiod_set_value() 设置 GPIO 输出电平(仅限输出模式)
  7. 初始化字符设备(同上小节)
  8. 释放申请到的端口资源:
     在驱动卸载时使用 gpiod_put() 释放 GPIO 描述符
     如果手动查找设备树节点,还需使用 of_node_put() 释放设备树节点引用计数

1.2.3 中断子系统

  1. 初始化字符设备
  2. 初始化所需GPIO
  3. 获取中断号:
     使用irq_of_parse_and_map()函数
  4. 初始软中断:
     采用tasklet机制时利用tasklet_init()初始化
     采用工作队列时利用button_work()初始化
  5. 编写中断函数并在合适的位置调度软中断:
     采用tasklet机制时利用ttasklet_schedule()
     采用工作队列时利用schedule_work()初始化
  6. 编写中断下文的设计(软中断)
  7. 当结束操作之后利用free_irq进行资源释放

1.2.4 input子系统

  1. 采用设备树匹配的方式进行匹配
  2. 利用input_allocate_device申请input_dev结构体
  3. 初始化input_dev结构体,包括设置设备名称、设备ID、事件类型(如EV_KEY、EV_REL等)以及对应的事件码和值
  4. 利用input_register_device注册输入设备
  5. 当硬件产生输入事件时,驱动需要调用相应的函数(如input_event()或input_report_key()等)来上报这些事件
  6. 当不再需要使用输入设备时,应在remove中调用input_unregister_device()函数来注销该设备
  7. 注销后,还需要调用input_free_device()函数来释放之前分配的input_dev结构体和相关资源

1.2.5 iic子系统

  1. 采用设备树匹配的方式进行匹配并初始化字符设备
  2. 利用i2c_register_driver( )函数进行iic驱动的注册
  3. 将数据和设备进行关联
     利用i2c_set_clientdata()函数将私有数据(data)与I2C客户端设备关联起来
  4. 编写operations结构体相关函数
     填写i2c_msg结构体,利用i2c_transfer函数进行传输
  5. 当设备不需要的时候,使用i2c_del_driver()等函数来注销和卸载驱动

1.2.6 spi子系统

  1. 采用设备树匹配的方式进行匹配并初始化字符设备
  2. 在module_init使用spi_register_driver( )函数进行spi驱动的注册
  3. 进行获取cs片选端口和初始化
     利用of_get_named_gpio获取端口号
     使用gpio_direction_output设置默认电平为高
  4. 设置spi_device结构体的取值及初始化spi
     分别设置mode、max_speed_hz等参数;
     使用spi_setup( )进行参数保存并设置
  5. 对spi_message结构体进行操作,将信息传递至用户层
     利用spi_message_init、spi_message_add_tail初始化message和transfer结构体
     利用spi_sync,将信息传递到用户层
  6. 在module_exti中使用spi_unregister_driver( )函数进行spi驱动的注销

二、文件操作系统

  Linux的文件系统(File System)是操作系统用于明确存储设备(如磁盘或基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构。简而言之,文件系统就是在存储设备上组织文件的方法。

1. 文件类型

  描述了如何在存储设备上组织和管理数据。

  • -:常规文件,即f:
  • d:directory,目录文件
  • b:block device,块设备文件,支持以块为单位进行随机访问
  • c:character device,字符设备文件,支持以字符为单位进行访问
  • major number:主设备号,用于表示设备型号,进而确定要加载的驱动程序
  • minor number:次设备号,用于标识同类型中不同的设备
  • l:symbolic link ,符号链接文件
  • p:pipe,命名管道
  • s:socket,套接字文件

2. 文件系统类型

  描述存储在文件系统中的不同文件的性质和用途,文件系统类型决定了如何在存储设备上组织、存储和检索数据(对磁盘数据进行索引)。以下是一些常见的文件系统类型:

  • EXT4(Fourth Extended File System):
      广泛使用在Linux上的默认文件系统,支持大文件和大分区,具有高效的文件块分配和日志功能。
  • XFS:
      高性能文件系统,最初由SGI开发,适用于处理大量数据和高并发操作的环境,如数据库和大数据应用。
  • Btrfs(B-tree File System):
      新型文件系统,提供高级数据管理功能,支持快照、克隆、内置卷管理、数据校验和透明压缩。
  • NTFS(New Technology File System):
      Windows的默认文件系统,支持大文件和高级数据管理功能,Linux通过FUSE(Filesystem in Userspace)和ntfs-3g驱动程序支持NTFS。
  • FAT32(File Allocation Table 32):
      早期文件系统,广泛兼容于各种操作系统,适用于小型存储设备,但有文件和分区大小限制。

三、VFS(Virtual File System)

  VFS是Linux内核中的虚拟文件系统层,它提供了一个抽象层,使得不同的文件系统可以通过统一的接口与内核交互。VFS使得用户空间程序不需要了解底层文件系统的具体实现细节,只需要通过标准的POSIX接口进行文件操作。
主要功能:

  • 文件系统抽象:通过统一的接口屏蔽了不同文件系统的实现细节。 文件操作接口:提供open、read、write、close等文件操作接口。
  • 挂载管理:管理不同文件系统的挂载点,并将它们组织成一个统一的目录树。
  • 文件描述符管理:管理进程的文件描述符,支持多种文件类型(如普通文件、目录、设备文件等)。

关键数据结构:

  • struct file:表示一个已打开的文件实例。
  • struct inode:表示一个文件系统中的一个对象(文件、目录等)。
  • struct super_block:表示文件系统的超级块,包含了文件系统的元数据。
  • struct dentry:表示目录项,用于组织文件系统的层次结构。

四、Sysfs

  sysfs是Linux内核中专门为设备模型设计的虚拟文件系统,它以文件和目录的形式将内核对象和属性导出到用户空间,使得用户可以方便地查看和配置系统设备的属性。sysfs文件系统通常挂载在/sys目录下。
主要功能:

  • 设备信息展示:将设备驱动模型中的信息以文件形式展示,便于用户查看和管理。
  • 属性文件:提供一种机制,可以通过读写文件来获取或设置设备的属性。
  • 层次结构:以层次化的目录结构展示内核对象,如设备、类、总线等,反映内核对象的关系。
    关键数据结构:
  • struct kobject:内核对象,所有sysfs对象的基类。
  • struct attribute:sysfs中的属性,每个属性对应一个文件。
  • struct sysfs_ops:定义了属性文件的读写操作。

扩展:VFS与Sysfs的关系
  VFS和sysfs之间的关系可以从以下几个方面来理解:

  • sysfs是VFS的一种实现:
      sysfs本质上是通过VFS框架实现的一个特殊的文件系统。它利用VFS提供的抽象接口,将内核对象导出到用户空间。
  • 文件系统的统一管理:
      作为VFS的一部分,sysfs文件系统挂载在VFS的统一目录树中,通常是/sys目录。VFS管理sysfs的挂载点、目录结构和文件操作。
  • 内核对象与文件系统的结合:
      sysfs通过VFS接口,将内核中的对象(如设备、类、总线等)以文件和目录的形式呈现。这些文件和目录都是通过VFS的inode和dentry等数据结构来管理的。
  • 设备驱动与用户空间的桥梁:
      sysfs为设备驱动提供了一种标准机制,可以将驱动程序的配置和状态信息导出到用户空间,并通过VFS接口实现读写操作。这使得驱动开发者可以利用标准的文件操作接口与用户空间交互。

五、常见问题

1. Linux内核的组成部分

  Linux内核的主要组成部分可概括如下:

  1. 进程管理:内核的核心功能,负责进程的创建、调度、同步及资源分配。
  2. 内存管理:负责物理内存和虚拟内存的高效分配与回收,确保系统稳定性和性能。
  3. 文件系统:通过虚拟文件系统(VFS)抽象层统一管理多种存储介质和文件格式。
  4. 设备驱动:内核与硬件交互的桥梁,涵盖字符设备、块设备、网络接口等。
  5. 网络协议栈:实现TCP/IP等协议,支持网络通信和数据传输。
  6. 系统调用接口:用户空间与内核交互的唯一入口,提供资源访问和安全隔离。

2. insmod和modprobe的差异

  modprobe和insmod类似,都是用来动态加载驱动模块的,区别在于modprobe可以解决load module时的依赖关系。如果你确定你要加载的驱动模块不依赖其他驱动模块的话,既可以insmod也可以modprobe,当然insmod可以在任何目录下执行,更方便一些。而如果你要加载的驱动模块还依赖其他ko驱动模块的话,就只能将模块拷贝到特定目录,depmod后再modprobe。

3. 中断上下文的作用

  1. 效率:上半部只负责快速响应和准备数据,而将耗时的操作放到下半部去处理,避免中断处理程序占用过长时间。
  2. 并发性:下半部可以在系统其他部分运行时并发执行,提高了系统的并发性。
  3. 可移植性:将中断处理分解为两部分,使得内核更加模块化,提高了代码
    的可移植性。
      中断下半段处理的处理机制有软中断、tasklet和工作队列,以下是其的区别:
    中断下半部
下半部机制上下文复杂度执行性能顺序执行保障
软中断中断高(需要自己确保软中断的执行顺序及锁机制)好(全部自己实现,便于调优)没有
tasklet中断中(提供了简单的接口来使用软中断)同类型不能同时执行
工作队列进程低(在进程上下文中运行,与写用户程序差不多)没有(和进程上下文一样被调度)
### 关于Linux嵌入式内核驱动的常见面试问题及最佳实践 #### 1. 多任务处理机制的区别 在讨论Linux嵌入式系统的多任务处理时,理解不同调度单位之间的差异至关重要。FreeRTOS 和 Linux 都支持多任务并发执行,允许多个任务(或进程/线程)在CPU上交替执行[^1]。然而,在更细粒度的任务划分方面,Linux中的进程和线程具有不同的特性:进程有独立的堆区和栈区;相比之下,线程共享同一进程内的堆空间但各自保留单独的栈区域。这种设计使得在同一进程中创建多个线程能够减少资源开销并提高通信效率。 #### 2. 协程的概念及其优势 除了传统的进程和线程模型外,现代编程实践中还引入了协程这一概念。作为一种用户态下的轻量级线程形式,协程允许开发者在一个用户线程之上运行若干个协作式的子程序实例。由于其不在操作系统层面进行上下文切换,因此能显著提升单核心处理器上的并发性能,并简化异步逻辑的设计与实现[^2]。 #### 3. 堆栈的工作原理及其应用场景 对于任何涉及函数调用或者递归运算的操作而言,了解底层使用的数据结构——即所谓的“堆栈”,是非常重要的。作为典型的后进先出(LIFO)存储器抽象层,它不仅负责记录每次方法调用后的返回位置信息,还在诸如表达式解析等领域发挥着不可或缺的作用[^3]。当编写设备驱动或其他低级别代码时,熟悉这些基础知识有助于更好地管理和优化内存分配策略。 #### 4. 设备文件系统与字符设备注册流程 针对具体的硬件接口开发工作,则需掌握如何通过`register_chrdev()` API向VFS(虚拟文件系统)注册新的字符型节点对象。此过程通常涉及到定义相应的file_operations表来描述特定I/O行为模式,以及设置必要的权限位掩码以确保安全访问控制。 #### 5. 中断服务例程(ISR)的设计原则 为了响应外部事件触发信号源的变化情况,中断处理机制成为连接物理世界同软件环境之间的重要桥梁之一。一个好的ISR应该尽可能简洁高效地完成预定动作而不干扰正常业务流;同时考虑到实时性的需求,有时还需要借助下半部(deferred work)技术进一步延展复杂计算至稍后时刻再做处理。 ```c static irqreturn_t my_interrupt_handler(int irq, void *dev_id) { // 执行快速且必要的操作... return IRQ_HANDLED; } ``` #### 6. 同步原语的选择依据 面对竞争条件(race condition),死锁(deadlock)等问题频发场景下,合理选用合适的同步工具显得尤为关键。无论是自旋锁(spinlock),读写屏障(mutex),还是信号量(semaphore),每种方式都有各自的适用范围及优缺点所在,应根据实际应用背景灵活判断采用何种手段最为恰当有效。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值