驱动复习用笔记

Last edited
Last updated July 21, 2023
Pages
Tags
简述 IO 模型有哪些?各自的特点?以及文字描述实现原理
  1. 非阻塞 IO:使用 O_NONBLOCK 标志打开驱动文件,不管硬件数据是否准备,直接读取数据,因此读取的数据不一定准确。
  1. 阻塞 IO:以阻塞的形式打开驱动文件,若硬件数据为准备就绪,程序切换休眠状态,待数据准备完毕对进程进行唤醒并读取
  1. IO 多路复用:有 select,poll,epoll 等方式实现,运行一个进程监听多个文件描述符,当其中一个准备好,就返回其文件描述符
  1. 信号驱动 IO:当文件描述符准备就绪,内核发出信号通知应用程序进行 IO 操作
什么是并发?驱动里面为什么要有并发互斥的控制?如何实现?举例说明
💡
对同一资源访问。多线程,抢占。自旋锁:等待,运行态,CPU占用大,支持中断和线程,死锁;信号量:休眠态,CPU占用小,只支持线程,无死锁;互斥体:休眠态,CPU占用小,只支持线程,无死锁,不会立即休眠
并发:两个进程对同一个资源同时进行访问
产生竞态的原因:表面原因是两个进程同时访问同一片临界资源,进行了资源的争抢
根本原因:对于单核处理器,如果支持进程抢占,就会出现竞态;对于多核处理器,竞态现象本来就会出现;对于中断和进程,会出现竞态。
自旋锁:自旋状态需要消耗CPU的资源,处于自旋状态的进程处于运行态。适用于临界资源比较小的场景。可以用于进程中也可以用于中断中,会产生死锁。
信号量:信号量休眠状态不消耗CPU资源,但是状态的切换需要消耗CPU的资源。可以用于临界区较大的场景。只能用于进程上下文中,不能用于中断。不会产生死锁。
互斥体:互斥体休眠状态不消耗CPU资源,但是状态的切换需要消耗CPU的资源。可以用于临界区较大的场景。只能用于进程上下文中,不能用于中断。不会产生死锁。如果进程获取不到互斥体,不会立即进入休眠,而是稍微等一下再进入休眠。
请简述Linux内核中断处理分成上半部分和下半部分的原因,如何实现,底半部的实现机制?各自的特点
💡
中断中不允许有耗时操作,在底半部实现耗时任务。软中断:数量少,32;tasklet:基于软中断,不能延时和睡眠,单线程最多5个任务;工作队列:kthread 完成,可用于进程上下文,可用于中断中。可进行延时和耗时操作。
内核不允许在中断处理过程中有延时、耗时的操作,但在某些场景必须要在中断处理过程中有延时、耗时的操作,与内核的原则有了冲突,为了解决该冲突,将中断执行过程分为上半部和下半部
  • 顶半部:一般去处理一些比较紧急且不耗时的任务
  • 底半部:处理不紧急但是耗时的任务
软中断:存在个数限制(32),一般内核开发者会使用进行相关测试
Tasklet:基于软中断实现底半部的一种机制,用于中断处理过程中,是中断的一部分,不能处理延时、睡眠任务。默认同时处理最多5个底半部,若超出则需唤醒相关内核线程去处理其他底半部。 工作队列:将底半部任务添加到工作队列中,由内核线程完成处理。工作队列可以用于进程的上下文,也可以用于中断中。这个机制开启底半部后可以进行延时、耗时等操作。
💀请列举三个以上的内核中申请内存的函数,并说明他们之间的区别
1.void *kmalloc(size_t s, gfp_t gfp)
  • 功能:分配对应的虚拟内存 (物理内存映射区)
  • 特点:最大128k , 分配虚拟地址,其虚拟地址空间连续, 物理地址空间也是连续,分配的内存必须是2的次幂的形式
  • 类似函数:kzalloc = kmalloc+memset(,0,):分配虚拟内存区并清零
2. void *vmalloc(unsigned long size)
  • 功能:分配对应的虚拟内存
  • 特点:分配虚拟地址,其虚拟地址空间连续, 但是物理地址空间不一定连续
3. unsigned long __get_free_page(gfp_t gfp)
  • 功能:分配一个页的内存 大小为4K
4. unsigned long __get_free_pages(gfp_t gfp_mask, get_order(57600))
  • 功能:分配多个页的内存 57600-->2^n :第二个参数填写的是n
内核模块的三要素
入口:安装内核模块时需要执行的代码,用于资源的申请和初始化 出口:卸载内核模块时需要执行的代码,用于资源的释放和注销 许可证:声明当前我们编写的内核模块遵循GPL协议
请简述通过udev机制实现设备节点创建的过程
Udev是在用户空间创建设备节点。 在设备驱动中向上提交目录信息,在内核会申请一个 struct class 类型的空间,内部存放了目录信息,内核会在 /sys/class/ 下创建一个目录。 在设备驱动中向上提交节点信息,在内核中会申请一个 struct device 类型的空间,内部存放了设备相关节点信息,内核会在 /sys/class/目录/ 下创建一个存放节点信息的文件 完成上面两步后Hotplug会通知udev根据sys/class/目录/存放节点信息的文件中的信息,在/dev下创建一个设备文件
IO 多路复用方式有几种?各自的特点?epoll原理
Select:1、存在最大文件描述符的限制(1024个);2、需要用户和内核之间进行表的数据拷贝;3、需要轮询,数据准备好之后需要再次遍历表,找出准备好的描述符,效率低
Poll:1、不存在最大文件描述符的限制;2、需要用户和内核之间进行表的数据拷贝;3、需要轮询,数据准备好之后需要再次遍历表,找出准备好的描述符,效率低
Epoll:1、不存在最大文件描述符的限制;2、epoll不需要重复构造表,也不需要反复拷贝表,效率比较高;3、数据准备好后epoll能够直接找到准备好的文件描述符调用回调,不需要遍历,效率高
Epoll工作原理: 通过epoll_create创建epoll对象,返回一个该对象的文件描述符。 使用epoll_ctl将需要监听的文件描述符加入到内核的epoll红黑树中。 使用epoll_wait阻塞等待IO事件发生,将发生事件的文件描述符添加到内核的链表中 epoll_wait返回就绪事件(存放在数组中)给用户应用程序,应用程序可以进行相应的事件处理
💀请写出以下代码:1、通过platform总驱动框架gpio子系统编写led的驱动;2、应用程序里面通过ioctl实现灯亮灭的控制
💡
file_operation.unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg) 方法注册 ioctl 的控制逻辑,用户态直接通过 <sys/ioctl.h> 中的 ioctl 函数调用对应逻辑
#define LED_ON _IO('l',1)  //开灯 #define LED_OFF _IO('l',0)//关灯 //设备树节点 /*myplatform{         compatible = "hqyj,mypfled";         led1=<&gpioe 10 0>;        };*/ int mycdev_open(struct inode *inode, struct file *file) {} long mycdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {     switch(cmd) {     case LED_ON://开灯     gpiod_set_value(gpiono,1);       break;     case LED_OFF://关灯       gpiod_set_value(gpiono,0);       break;     }     return 0; } struct file_operations fops={     .open=mycdev_open,     .unlocked_ioctl=mycdev_ioctl,   }; //probe函数,匹配设备成功执行 int pdrv_probe(struct platform_device *pdev) {     //1.注册字符设备驱动     //2.创建设备节点     //3.获取gpio信息 } //remove 设备和驱动分离时执行 int pdrv_remove(struct platform_device *pdev) { // 销毁设备节点 // 注销设备驱动     // 释放gpio信息 } //构建设备树匹配的表 struct of_device_id oftable[]={    {.compatible="hqyj,mypfled",},    {}, }; struct platform_driver pdrv={     .probe=pdrv_probe,     .remove=pdrv_remove,     .driver={         .name="aaaaa",         .of_match_table=oftable,//设置设备树匹配     },     }; //一键注册宏 module_platform_driver(pdrv);
#include<stdlib.h> #include<stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include<unistd.h> #include<string.h> #include <sys/ioctl.h> #define LED_ON _IO('l',1)  //开灯 #define LED_OFF _IO('l',0)//关灯 int main(int argc, char const *argv[]) {     int a;     char buf[128]={0};     int fd=open("/dev/myled",O_RDWR);     if(fd<0) {         printf("打开设备文件失败\n");         exit(-1);     }     while(1)     {         //从终端读取         printf("请选择要实现的功能\n");         printf("0(关灯) 1(开灯)>");         scanf("%d",&a);         if(a==1)         {             ioctl(fd,LED_ON);         }         else if(a==0)//关灯         {             ioctl(fd,LED_OFF);         }             }         close(fd);     return 0; }
请简述platform架构分成哪几个部分?编程框架
💡
总线驱动 + 设备信息 + 设备驱动 platform_driver 结构体: 1. probe 方法;2. remove 方法;3. driver 属性 name/of_match_table(匹配表);4. 注册宏
当前总线成总线驱动,当前设备的设备信息,当前设备的设备驱动。
int pdrv_probe(struct platform_device *pdev) int pdrv_remove(struct platform_device *pdev) struct of_device_id oftable[] = {} struct platform_driver pdrv={ .probe=pdrv_probe, .remove=pdrv_remove, .driver={ .name="aaaaa", .of_match_table=oftable, }, }; module_platform_driver(pdrv);
请简述如何通过SPI总线控制数码管显示指定的数字(画图也可以)
💡
封装 spi_driver 对象,实现其初始化相关代码,使用 module_spi_driver 一件注册,调用 spi_writespi 对象写入数据
notion image
GPIO子系统的编程框架
解析设备树节点 → of_find_node_by_name/path/compatible 根据设备树节点解析GPIO编号 → of_get_named_gpio 向内核申请要使用的GPIO编号 → gpio_request 设置管脚输入输出 → gpio_direction_output 设置管脚默认值 → gpio_set_value 卸载时释放GPIO编号 → gpio_free
 
SPI 和 IIC 的特点和区别?
💡
相同点:串行同步主从架构 不同点:半双工/全双工;先高后低/不限制;高低电平/边沿触发;应答机制/无需应答;速率低/速率兆级;七位寻址/片选线
相同点:
  1. 都是串行总线
  1. 都是同步总线,都有自己的SCL时钟信号线
  1. 都采用主从模式架构
不同点:
  • IIC总线为半双工总线,只有一根双向的SDA数据线
  • SPI总线为全双工总线,由两根单向的数据线(MISO/MOSI)
  • IIC总线传输时,需要先传送高位,再传送低位
  • SPI总线传输时,需要先传送高位,再传送低位,也可以先传送低位再传送高位(看芯片的设计)
  • IIC总线是通过时钟线高低电平变化,进行传输数据
  • SPI总线是边沿进行触发,边沿进行采样(上升沿还是下降沿看芯片)
  • IIC总线有应答机制,SPI总线没有应答机制,所以SPI总线传输数据时,数据容易丢失
  • IIC总线传输速率比较低,但是SPI总线传输速率达到几兆水平
  • IIC总线通过7位从机地址进行寻址,但是SPI总线通过片选线使能决定和哪一个从机进行通信
 
内核中 jiffies 的作用是什么?
记录内核从启动到当前时刻从 0 开始不断增加的一个变量,记录内核的节拍数,可以用来实现定时器功能。
简述字符设备驱动实现的流程
💡
初始化 → 实现 file_operations 中的方法 → 注册内核 → 创建文件
准备工作:分配一个字符设备对象空间。字符设备驱动成员的初始化。将字符设备驱动对象注册进内核。此时得到一个字符设备驱动。
当字符设备驱动注册进内核后,会得到一个设备号,基于这个设备号会在文件系统中创建一个设备文件,这样就完成了设备驱动和设备文件的关联。
由于该文件存在于文件系统中,所以内核中一定存在一个 struct inode 结构体,这个结构体中存放了文件的相关信息。
用户层中进行打开文件操作open(),虚拟文件系统层调用sys_open() open()中传递的文件路径,根据找到文件的inode结构体。 根据inode结构体找到文件对应的驱动对象结构体指针 根据驱动对象结构体找到操作方法结构体指针 调用操作方法结构体中对应的函数指针
open() --> sys_open() --> struct inode结构体 --> struct cdev结构体 --> struct file_operations结构体 --> mycdev_open