# Linux-device-driver **Repository Path**: minli356/Linux-device-driver ## Basic Information - **Project Name**: Linux-device-driver - **Description**: linux内核驱动程序 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2025-09-16 - **Last Updated**: 2026-08-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # linux驱动开发学习 以下内容为我个人上课整理的笔记 - 0-9day为初级 - 10-24day为中级 - 25-32day为高级 没有提供学习参考源码和内核源码 - 内核源码版本为5.4.31 - 交叉遍历工具链为gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf # 一、准备工作 ## 编译内核步骤 环境为linux的Ubuntu2204虚拟机 编译内核源码不仅需要交叉编译工具链,还需要gcc相关编译工具(sudo apt install build-essential)和flex工具(sudo apt install flex bison) 编译生成make uImage dtbs LOADADDR=0xc2000040 -j4的时候需要(sudo apt install libssl-dev) 还需要uBoot的工具包(sudo apt install u-boot-tools) 首先在编译内核源码之前需要安装对应依赖工具 ```bash sudo apt-get update sudo apt install build-essential sudo apt install flex bison #生成.config文件需要gcc和flex工具 sudo apt install libssl-dev sudo apt install u-boot-tools #生成UImage和dtbs之前需要libssl和uboot工具包 #配置交叉编译工具链环境变量 vim ~/.bashrc export PATH=$PATH:/home/work-li/arm-linux-gnueabihf-tools/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin # 开始编译内核源码,在linux5.4.31内核源码目录下 make stm32_fsmp1a_defconfig make uImage dtbs LOADADDR=0xc2000040 -j4 ``` ## 开发板移植linux内核步骤 使用的是远程加载linux内核 - 通过tftp开发板下载uImage(内核镜像)和stm32mp157a-fsmp1a.dtb(设备树)文件 - 通过nfs开发板挂载根文件系统 ### 1.上位虚拟机配置 #### 1.1 tftp文件支持 ```shell # 安装tftp服务 sudo apt-get update sudo apt-get install tftp-hpa vim /etc/default/tftp-hpa TFTP_DIRECTORY="/home/li/hqyj_study/tftp_share" # 这个是自定义的目录 mkdir /home/li/hqyj_study/tftp_share sudo service tftp-hpa restart # 将uImage内核镜像和设备树文件放到tftp共享文件夹目录下(上一步编译的) cp arch/arm/boot/uImage ~/hqyj_study/tftp_share/ cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb ~/hqyj_study/tftp_share/ ``` #### 1.2安装nfs服务 ```shell # 安装nfs服务 sudo apt-get update sudo apt-get install nfs-kernel-server # 配置nfs服务 sudo vim /etc/exports /nfs/rootfs *(rw,sync,no_root_squash,no_subtree_check) # 这个也是可以自定义目录的 sudo mkdir -p /nfs cd /nfs cp /mnt/hgfs/LinuxShare/rootfs-ok.tar.xz . # 将根文件系统的压缩包放在nfs共享文件夹中 tar -xvf rootfs-ok.tar.xz sudo service nfs-kernel-server restart # 重启nfs服务 ``` ### 2.开发板和上位虚拟Ubuntu连接 开发板通过网线连接到电脑中的Ubuntu虚拟机,虚拟机新建一个虚拟网卡和开发板端口接模式连接 - 开发板 <-----桥接模式------> Ubuntu虚拟机 - Ubuntu <-----NAT模式------> 互联网 ![alt text](readme_image/1.png) ![alt text](readme_image/2.png) - 开发板通过SecureCRT串口连接到开发板(开发板UBoot环境) - 配置开发板uBoot环境变量 ```shell setenv ipaddr 192.168.1.6 # 设置开发板的ip地址,需要和上位虚拟机同网段192.168.1.*** setenv serverip 192.168.1.100 # 上位虚拟机设置的手动IPv4 saveenv ping 192.168.1.100 # ping通会显示host 192.168.1.100 is alive # 设置开发板下载uimage和dtb设备树的开发板内存位置 setenv bootcmd tftp 0xc2000000 uImage \; tftp 0xc4000000 stm32mp157a-fsmp1a.dtb\; bootm 0xc2000000 - 0xc4000000 # 设置开发板挂载根文件系统,其中nfsroot=需要改为上位机的nfs的rootfs文件夹 setenv bootargs root=/dev/nfs nfsroot=192.168.1.100:/nfs/rootfs,tcp,v4 rw console=ttySTM0,115200 init=/linuxrc ip=192.168.1.6 saveenv reset # 重启开发板 ``` ## 注意事项 - 检验:成功通过远程移植完成之后,首先是开发板可以正常进入到`linux`系统中(`SecureCRT串口控制`) - 检验:开发板可以正常挂载根文件系统,可以在开发板创建文件然后到上位虚拟机查看是否有相应的文件 - `make`命令执行之前需要将交叉编译工具链环境变量添加到`~/.bashrc`中 - 如果使用`vscode`的`bash`终端来`make`的话不需要改,如果使用的是`Trae`的`bash`终端来`make`的时候需要在`~/.bashrc`加入以下内容 ```bash vim ~/.bash export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- ``` - 或者修改Makefile为 ```makefile KERNEL_PATH=../../02day_env/linux-5.4.31#此处是linux内核源码目录 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- ifeq ($(KERNELRELEASE),) all: make -C $(KERNEL_PATH) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules arm-linux-gnueabihf-gcc test.c -o test -Wall clean: make -C $(KERNEL_PATH) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean rm test else obj-m += btn_drv.o endif ``` # 二、内核源码编程相关常用命令 ### 1.安装和卸载模块(开发板linux环境) ```shell insmod 模块名称.ko #安装模块 lsmod #查看模块状态 rmmod 模块名称.ko #卸载模块 ``` ------ ### 2.调整打印输出优先级(开发板linux环境) ```shell #方式一:缺点是重启系统就失效,随改随生效 cat /proc/sys/kernel/printk 4 3 1 7 4, 系统中当前使用的打印优先级阈值 3, 默认打印优先级 cat /proc/sys/kernel/printk insmod printkall.ko echo 6 >/proc/sys/kernel/printk rmmod printkall.ko #方式二: #bootargs .... loglevel=n # n,就是设定打印优先级阈值 setenv bootargs root=/dev/nfs nfsroot=192.168.1.100:/home/linux/nfs/rootfs,tcp,v4 rw console=ttySTM0,115200 init=/linuxrc ip=192.168.1.6 loglevel=3 saveenv #重启开发板 cat /proc/sys/kernel/printk #优点是一直有效,缺点是要修改环境变量,需要重启系统 ``` -------- ### 3.输出打印环形缓冲区中的内容,忽略打印优先级(开发板linux环境) ```shell dmesg ``` -------- ### 4.查看设备号(开发板linux环境) ```shell cat /proc/devices #查看主设备号 ls -l /dev/设备名称 #查看次设备号 ``` --------- ### 5.创建设备文件(开发板linux环境) ```shell mknod /dev/leds c 242 0 #mknod <路径> <设备类型> <主设备号> <次设备号> #删除设备文件正常使用rm即可 ``` ### 6.设备树编程与配置内核(上位机linux环境) ```bash make dtbs # 在linux5.4.31目录下使用 make menuconfig # 在linux5.4.31目录下使用,图形化配置需要 ncurses 库 sudo apt install libncurses-dev # 安装 ncurses 库 ``` ### 7.远程登陆开发板(...环境) ```shell vim /etc/passwd # 包含系统上所有用户的基本信息,每一行代表一个用户 vim /etc/shadow # 影子密码文件,存储加密后的密码和其他密码相关信息 ``` # 三、内核源码5.4.31常用API ### 1.程序模版 ```c module_init(<安装模块时候的函数>) module_exit(<卸载模块时候的函数>) MODULE_LICENSE("许可证协议,一般是GPL") ``` ------- ### 2.打印输出 ```c printk(打印优先级 "要输出的信息");//支持占位符 printk("要输出的信息");//使用的是默认的打印优先级 // 打印优先级的文件在源码的include/linux/kern_levels.h ``` ------- ### 3.导出符号 ```c EXPORT_SYMBOL(符号名称);// 符号指的是函数名 全局变量名 EXPORT_SYMBOL_GPL(符号名称);// 符号指的是函数名 全局变量名 ``` ------- ### 4.字符设备驱动编程的核心数据结构 ```c /*简单的说 如果要实现一个字符设备硬件的驱动程序,比如LED,其实就是在内核中实例化一个struct cdev对象就好了*/ struct cdev { /*该设备对应的操作函数*/ const struct file_operations *ops; /*设备号*/ dev_t dev; /*一个cdev可以对应多个具体硬件设备*/ unsigned int count; ... ... } /*实现一个具体硬件设备驱动程序时,要实现的驱动函数*/ struct file_operations { ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ... } ``` ------- ### 5.分配设备号 ```c /* * 作用:注册多个连续的设备号 * from , 要注册的起始设备号 * count, 要连续注册的个数 * name, 名称 * 返回值,成功返回0, 失败返回负数形式的错误编码 */ int register_chrdev_region(dev_t from, unsigned count, const char *name) /* * 作用: 注册连续多个设备号 * dev, 传出参数 用于返回分配得到的第一个设备号 * baseminor, 起始次设备号 * count, 连续注册的设备号个数 * name, 名称 */ int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) /* * 作用: 注销多个连续的设备号 * from , 要注销的起始设备号 * count, 连续注销的个数 */ void unregister_chrdev_region(dev_t from, unsigned count) ``` ------- ### 6.字符设备cdev的API ```c //初始化cdev,将字符设备cdev结构体和操作函数结构体绑定 void cdev_init(struct cdev *cdev, const struct file_operations *fops); /* * @作用:注册cdev,将字符和设备cdev和设备号绑定,可以一次绑定多个 * @参数:p, 要注册的cdev变量地址 * @参数:dev, 设备号 * @参数:count, 连续注册的个数 * @返回值:成功返回0,失败返回非0值 */ int cdev_add(struct cdev *p, dev_t dev, unsigned count) { p->dev = dev; p->count = count; ... } //注销cdev 还有可能动态释放对应的内存空间 void cdev_del(struct cdev *p); //动态分配cdev 变量空间 struct cdev *cdev_alloc(void); ``` ------- ### 7.gpio子系统API ```c /* * @作用:从设备树获取管脚编号 * @参数:np,设备树节点 * @参数:propname,属性名称 * @参数:index,第几个 * @返回值:对应的管脚编号 */ int of_get_named_gpio(struct device_node *np,const char *propname, int index); /* * @作用: 申请GPIO管脚 * @参数:gpio, 申请的管脚的编号 * @参数:label, 标签 名称 , mount -t debugfs none /mnt * @返回值:0 申请成功非0 申请失败 */ int gpio_request(unsigned gpio, const char *label); // 作用: 将指定的gpio管脚设置为输入模式 int gpio_direction_input(unsigned gpio); // 作用:将执行的管脚设置为输出模式, value为0默认输出低电平 非0输出高电平 int gpio_direction_output(unsigned gpio, int value); // 作用:获取指定管脚上的电平状态 int gpio_get_value(unsigned gpio); // 作用:设置指定管脚的电平状态 value为0输出低电平 非0输出高电平 void gpio_set_value(unsigned gpio, int value); // 作用: 释放指定GPIO管脚 void gpio_free(unsigned gpio); // Liuux4.4 之后的GPIO API (新的) /* * @作用:申请引脚,初始化引脚,返回一个可操作gpio的gpio_desc,从设备树节点获取 GPIO 描述符 * @参数:node,指向对应的设备树节点 * @参数:propname,要查找的 GPIO 属性的名称字符串 * @参数:index,第几个gpio * @参数:dflags,GPIO的标志 - GPIOD_IN: 输入 - GPIOD_OUT_LOW: 输出,初始电平为低电平 - GPIOD_OUT_HIGH: 输出,初始电平为高电平 - GPIOD_ASIS: 不初始化方向,后续需手动设置 * @参数: label,该GPIO的驱动程序或功能名称的字符串 * @返回值:成功返回GPIO描述符 */ struct gpio_desc *gpiod_get_from_of_node(struct device_node *node, const char *propname, int index, enum gpiod_flags dflags, const char *label); void gpiod_put(struct gpio_desc *desc); int gpiod_request(struct gpio_desc *desc, const char *label) int gpiod_direction_input(struct gpio_desc *desc) int gpiod_direction_output(struct gpio_desc *desc, int value) void gpiod_set_value(struct gpio_desc *desc, int value) int gpiod_get_value(const struct gpio_desc *desc) void gpiod_free(struct gpio_desc *desc) ``` ------- ### 8.内核空间和用户空间的数据交换API ```c /*一个进程有独立的0-3G的虚拟地址,所有的进程共享同一个内核,内核使用的是3G-4G的虚拟地址空间*/ /*===============<不常用>===============*/ // x, 使用内核空间的变量 // 写入的字节数为 sizeof(*ptr) put_user(x, ptr) #向用户空间写入 // x, 使用内核空间的变量 // 写入的字节数为 sizeof(*ptr) get_user(x, ptr) #从用户空间读取 /*===============<常用>===============*/ // to, 目标地址 // from , 源数据地址 // n, 期望拷贝的字节数 // 返回值,拷贝失败的字节数,如果拷贝成功返回0 int copy_to_user(void __user volatile *to, const void *from,unsigned long n); int copy_from_user(void *to, const void __user volatile *from,unsigned long n); ``` ----------- ### 9.ioctl的API ```c //ioctl实际使用时就是以下两种形式 // fd, 一个打开的设备 // request, 命令 // arg , 通常给的是用户空间的地址 既可以传入参数 也可以传出参数 ioctl(fd, request); ioctl(fd, request, arg); // 构造request的宏 _IO(type,nr) //构造无读写命令 _IOR(type,nr,size) //构造读命令 _IOW(type,nr,size) //构造写命令 _IOWR(type,nr,size) //构造读写命令 // 解析request的宏 _IOC_DIR(nr)//解析读写方向 _IOC_TYPE(nr)// 解析类型 _IOC_NR(nr) //解析命令 _IOC_SIZE(nr) //解析大小 ``` ------------ ### 10.设备文件的自动创建的API ```c class_create(owner, name); // 宏,返回值为struct class* device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...) // class, 该设备属于哪一类 // parent, 父设备 通常给NULL // devt, 该模块中申请到的设备号 // drvdata, 回调函数使用的参数, 通常给NULL // fmt, ... 决定了将来生成的设备文件名称 // 例如: "led%d", i 假如i的值为5 最终生成的设备文件名就是led5 //设备文件的自动线销毁 void class_destroy(struct class *cls) void device_destroy(struct class *class, dev_t devt) /*======================<使用示例>======================*/ static struct class *cls = NULL; // init函数 cls = class_create(THIS_MODULE, "LEDS");//在sys/class/生成一个名字为LEDS的子目录 device_create(cls, NULL, ledDevNum, NULL, "leds");//在sys/device/生成一个文件夹leds // 如果第一个参数不为NULL会在/sys/class/LEDS 产生一个指向leds文件夹的软链接产生热插拔事件 // exit函数 class_destroy(cls); device_destroy(cls, ledDevNum); ``` ---------- ### 11.paltform总线 - 内核编程中提出的总线驱动模型概念。有三要素 - 希望所有的硬件都按照总线驱动模型框架实现驱动程序,但是有些硬件没有挂在具体的总线上 - 为此linux虚拟了一条总线platform总线,没有挂在具体总线上的硬件设备可以认为是挂在了platform总线上,使得这类设备也可以按照总线驱动模型实现其驱动程序 ```c /*=======================<总线模型>=======================*/ /*===========<总线>===========*/ struct bus_type //描述总线 { const char *name; //总线的名称 ... }; int __must_check bus_register(struct bus_type *bus); void bus_unregister(struct bus_type *bus); //可以通过 ls /sys/bus/ 查看到当前运行的内核中注册了哪些总线 /*===========<设备>===========*/ struct device //描述要驱动的芯片 { struct bus_type *bus; //描述该芯片挂在了哪条总线上 void *platform_data;//设备侧用户自定义数据 ... }; int __must_check device_register(struct device *dev); void device_unregister(struct device *dev); /*===========<设备驱动>===========*/ struct device_driver //描述硬件的驱动逻辑 { const char *name; int (*probe) (struct device *dev); int (*remove) (struct device *dev); const struct of_device_id* of_match_table; ... }; struct of_device_id { char name[32]; char type[32]; char compatible[128];//通过该变量与设备树节点匹配 const void* data; } int __must_check driver_register(struct device_driver *drv); void driver_unregister(struct device_driver *drv); /*==============================================*/ /*===========<总线>===========*/ int __init platform_bus_init(void) // drivers/base/platform.c { error = bus_register(&platform_bus_type); ... } /*===========<设备>===========*/ struct platform_device // 子类 { struct device dev; // 父类 u32 num_resources; // 资源的个数 struct resource *resource; // 描述该设备使用的硬件资源 和CPU的接口信息 const char *name; // 名称 设备ID ... }; struct resource { resource_size_t start; resource_size_t end; /*用于标识资源的类型,常见类型: IORESOURCE_MEM 物理地址 IORESOURCE_IRQ 中断号 */ unsigned long flags; }; int platform_device_register(struct platform_device *); // 注册 void platform_device_unregister(struct platform_device *); // 注销 /*===========<设备驱动>===========*/ struct platform_driver // 子类 { struct device_driver driver; // 父类 int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); const struct platform_device_id *id_table; // 存储该驱动程序可以驱动的设备ID列表 ... } // 原型为int __platform_driver_register(struct platform_driver *drv,struct module *owner) // (drv,THIS_MODULE) platform_driver_register(drv); // 宏 void platform_driver_unregister(struct platform_driver *); ``` ### 12.设备树节点 ```c /*===========<节点数据结构>===========*/ struct device_node { const char *name; //节点名称 mynode const char *full_name; //节点全名 mynode@0x12345678 struct property *properties; //节点的属性 链表形式的 struct device_node *parent; //父节点 struct device_node *child; //子节点 struct device_node *sibling; //兄弟节点 ... ... }; /*===========<属性数据结构>===========*/ struct property { //对应一个属性 char *name; //键 int length; //值的长度 void *value; //值的地址 struct property *next; //下一个属性的地址 ... }; /*===========<内核中提供的找节点的API>===========*/ /* * @作用:通过路径找对应节点结构体的地址 * @参数:path, 路径 */ struct device_node *of_find_node_by_path(const char *path); /* * @作用:通过节点名称找对应节点结构体的地址 * @参数:from, 从指定节点开始向下寻找 如果取值为NULL 就是从/节点开始找 * @参数:name, 节点名称 (@符号之前的名称) */ struct device_node *of_find_node_by_name(struct device_node *from, const char *name); /* * @作用:通过属性找对应节点结构体的地址 * @参数:from, 从指定节点开始向下寻找 如果取值为NULL 就是从/节点开始找 * @参数:type, 给NULL 即可 , device_type属性 弃用 * @参数:compat, compatible属性, 该属性通常由"厂商,设备名 */ struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compat); /* * @作用:由于查找节点返回 device_node* 的这一类函数会将对应的 device_node* 节点的引用计数加1 * 所以需要使用of_node_put来释放对应的指针引用计数(引用计数减1) * @参数:node,device_node* 类型,设备节点指针 */ void of_node_put(struct device_node *node); /*===========<内核中提供的找属性的API>===========*/ /* * @作用:根据节点,属性名称找属性 * @参数:np, 节点的地址 * @参数:name, 属性中键的名称 * @参数:lenp, 返回属性值的长度 * @返回值:成功返回property结构体指针, 失败返回NULL */ struct property *of_find_property(const struct device_node *np,const char *name,int *lenp); /*===========<内核中提供的找值的API>===========*/ /* * @作用:找32bit的整型值 * @参数:np, 节点的指针 * @参数:propname, 属性名称 * @参数:index, 索引号 * @参数:out_value, 返回要读取的u32 */ int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value) /* * @作用:找8bit的1个字节的数据 * @参数:np, 节点的指针 * @参数:out_value, 返回要读取的u8数据 * @参数:sz, 要读取的个数 */ int of_property_read_u8_array(const struct device_node *np,const char *propname,u8 *out_values, size_t sz) int of_property_read_string_index(const struct device_node *np,const char *propname,int index, const char **output) /*=================<相关工具辅助函数>====================*/ __be32_to_cpup(void* ptr) // 传入一个指针,返回对应的本地字节序的数据 __be32_to_cpu(unsigned int data) // 传入一个32位大端数据,返回对应的本地字节序的数据 __be32_to_cpus(void* ptr) // 接收一个 32 位指针,并将转换结果写回该指针指向的内存 ``` ### 13.简化platform_driver注册流程的宏 ```c module_platform_driver() #define module_platform_driver(__platform_driver) \ module_driver(__platform_driver, platform_driver_register,platform_driver_unregister) #define module_driver(__driver, __register, __unregister, ...) \ static int __init __driver##_init(void) \ { \ return __register(&(__driver) , ##__VA_ARGS__); \ } \ module_init(__driver##_init); \ static void __exit __driver##_exit(void) \ { \ __unregister(&(__driver) , ##__VA_ARGS__); \ } \ module_exit(__driver##_exit); ``` ### 14.linux中断处理 ```c /* * @作用:注册中断 * @参数:irq,虚拟中断号 * @参数:handler,对应的硬件中断处理函数其类型为 irqreturn_t (*)(int, void*); * @参数:flags,常用取值: IRQF_TRIGGER_RISING , 上升沿触发中断 * IRQF_TRIGGER_FALLING, 下降沿触发中断 * IRQF_TRIGGER_HIGH, 高电平触发中断 * IRQF_TRIGGER_LOW, 低电平触发中断 * @参数:name,名称 cat /proc/interrupts 能看到 * @参数:dev,当内核调用handler时传递给handler的第二个参数值 * @返回值:返回 0 注册成功,非 0 注册失败 * @备注:__must_check是linux内核常用的编译器属性,用于强制调用者检查函数的返回值 * 如果调用这个函数时没有检查其返回值,编译器会产生一个警告 * 提醒开发者这个函数的返回值很重要,必须检查处理,否则可能导致错误或者安全隐患 */ int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev) /* * @作用:注销中断 * @参数:irq,虚拟中断号 * @参数:dev,和注册中断时最后一个参数保持一致,否则注销中断失败 * @返回值:返回request_irq的第4个参数name,ai说法是返回与中断关联的 dev_id */ const void* free_irq(unsigned int irq, void* dev); /* * @作用:解析映射得到虚拟中断号 * @参数:node,要解析映射的节点 * @参数:index,索引编号 * @返回值:返回0失败,>0 虚拟中断号 */ unsigned int irq_of_parse_and_map(struct device_node* node, int index); //中断处理函数的返回值 typedef irqreturn_t (*irq_handler_t)(int, void *); enum irqreturn { IRQ_NONE = (0 << 0), IRQ_HANDLED = (1 << 0), IRQ_WAKE_THREAD = (1 << 1), }; typedef enum irqreturn irqreturn_t; ``` ### 15.中断底半部机制 - `tasklet` 相关API ```c /*====================<核心数据结构>====================*/ struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); //记录底半部函数的地址 unsigned long data;//内核调用func函数时传递给func的数值,一般传输数据地址 }; /*====================<使用步骤>====================*/ // 定义tasklet变量 struct tasklet_struct btn_tasklet; // 初始化tasklet变量 void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data); // 以上两步可以使用以下宏来完成 等价于 定义 + 初始化 DECLARE_TASKLET(name, func, data); // 登记底半部 void tasklet_schedule(struct tasklet_struct *t); ``` - `work`工作队列 ```c /*====================<核心数据结构>====================*/ struct work_struct{ work_func_t func;//保存底半部函数的地址,void (*)(struct work_struct *work) ... }; /*====================<使用步骤>====================*/ // 定义一个 work 变量 struct work_struct btn_work; // 初始化 work 变量 INIT_WORK(_work, _func); // 以上两步可以使用如下宏替代 定义 + 初始化 DECLARE_WORK(n, f); // 登记底半部 bool schedule_work(struct work_struct* work); ``` - `delay_work`延时工作队列 ```c /*====================<核心数据结构>====================*/ struct delayed_work { struct work_struct work; struct timer_list timer; ... }; /*====================<使用步骤>====================*/ INIT_DELAYED_WORK(_work, _func) //初始化delayed_work变量 DECLARE_DELAYED_WORK(n, f) // 定义 + 初始化delayed_work变量 /* * @参数:delay,延时的时间,和【HZ】宏相关 */ bool schedule_delayed_work(struct delayed_work *dwork,unsigned long delay) //登记底半部 //阻塞,等待指定的dwork程序被执行完毕才返回 bool flush_delayed_work(struct delayed_work *dwork); //取消指定dwork的执行 bool cancel_delayed_work(struct delayed_work *dwork); ``` ### 16.内核定时器相关API ```c /*====================<核心数据结构>====================*/ struct timer_list { unsigned long expires; //超时时间 void (*function)(struct timer_list *); //定时执行的动作 ... }; /*====================<使用步骤>====================*/ /*1 定义一个定时器变量*/ struct timer_list led_timer; /*2 初始化定时器变量*/ led_timer.expires = jiffies + HZ/10; /* * @作用:初始化定时器变量 * @参数:timer, 要初始化的定时器变量地址 * @参数:callback, 回调函数 * @参数:flags, 默认给0即可 */ timer_setup(timer, callback, flags); /*3 启动定时器*/ add_timer(struct timer_list *timer); //或者 int mod_timer(struct timer_list *timer, unsigned long expires); /* * @作用:4 修改定时器超时时间 * @参数:expires, 新的超时时间 */ int mod_timer(struct timer_list *timer, unsigned long expires); /*5 停止定时器*/ int del_timer(struct timer_list *timer); ``` ### 17.内核中的延时 ```c /*==================<短延时>==================*/ ndelay(x); //纳米级延时 loops_per_jiffy udelay(x); //微秒级延时 mdelay(x); //毫秒级延时 // 它们实现的原理就是忙等待,白白消耗CPU时间获得延时的效果。 /*==================<长延时>==================*/ void msleep(unsigned int msecs) // 毫秒睡眠 ``` ### 18.反向推导包含该成员变量的整个结构体的地址(container_of) ```c container_of(已知成员变量地址, 结构体的数据类型, 已知地址成员在结构体中的名称) ``` ### 19.中断屏蔽 ```c // CPSR.I = 1/0 屏蔽中断/使能中断, local_irq_disable() // 禁止中断 local_irq_enable() // 使能中断 local_irq_save(flags);// 关闭中断,是flags,不是&flags,自定义unsigned long变量 local_irq_restore(flags);// 开中断 ``` ### 20.原子操作 ```c // 原子操作,指的整个操作过程不可再分 // 内核中有两种类型数据的原子操作,1.位原子操作 2.整型原子操作 /*--------------- <位原子操作> ---------------*/ ATOMIC_INIT(i) // 初始化原子变量 void set_bit(unsigned int nr, volatile unsigned long* p); // 将p指向的内存中的 nr bit置1 整个过程是原子性的 void clear_bit(unsigned int nr, volatile unsigned long* p); // nr位清零 void change_bit(unsigned int nr, volatile unsigned long* p);// nr位取反 /*--------------- <整型原子操作> ---------------*/ //核心数据结构 typedef struct{ int counter; }atomic_t; /*-------------- <整个过程是原子性的> --------------*/ atomic_set(atomic_t* v, int i); // *v = i; atomic_add(int i, atomic_t* v); // *v += i; atomic_add_return(int i, atomic_t* v); // *v += i; return *v; atomic_inc(atomic_t* v); // (*v) ++; atomic_dec(atomic_t* v); // (*v) --; atomic_dec_and_test(atomic_t* v); // (*v) --; 判断结果是否为0,为0返回true,反之返回false ``` ### 21.自旋锁 - 自旋锁为忙等待,非睡眠等待,可以用于中断上下文中 ```c //核心数据结构 struct spinlock; //1 定义一把自旋锁 struct spinlock btnSpinlock; //2 初始化自旋锁 spin_lock_init(&btnSpinlock); //3 获取锁 void spin_lock(spinlock_t *lock) //获取不成功 原地自旋等待 直到获取成功为止 int spin_trylock(spinlock_t *lock)//获取不成功 立即返回错误信息 //4 释放自旋锁 void spin_unlock(spinlock_t *lock) // 一个中断上下文和进程上下文中都有使用的自旋锁,在获取自旋锁的时候,推荐使用以下api void spin_lock_irq(spinlock_t *lock); //关中断 获取自旋锁 spin_lock_irqsave(lock, flags); //flags=CPSR.I CPSR.I=1 获取自旋锁(强烈推荐) //释放自旋锁 void spin_unlock_irq(spinlock_t *lock) //释放自旋锁 开中断CPSR.I=0 void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)//CPSR.I=flags, 释放自旋锁 ``` ### 22.信号量 - 允许有多个持有者,一旦获取信号不成功,睡眠等待 ```c //核心数据结构 struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; }; //使用步骤 //1 定义信号量 struct semaphore btn_sem; //2 初始化信号量 void sema_init(struct semaphore *sem, int val) //val=3 ,就最多有3个持有单元 //3 获取信号量 本质上就是计数-1 void down(struct semaphore *sem); //获取信号量成功立即返回, 不成功进入不可中断的睡眠状态 int __must_check down_interruptible(struct semaphore *sem); //成功立即返回0, 不成功进入可中断的睡眠状态,-ERESTARTSYS : 在等待信号量时被信号中断。 //4 释放信号量 计数+1 void up(struct semaphore *sem); ``` ### 23 阻塞IO ```c // 核心数据结构 struct wait_queue_head{ // 用于指向等待队列的第一个节点。 spinlock_t lock; struct list_head head; }; struct wait_queue_entry { // 等待队列中的一个节点,用于记录进程的信息 unsigned int flags; void *private; wait_queue_func_t func; struct list_head entry; }; // 初始化等待队列头,btn_wqh是一个wait_queue_head init_waitqueue_head(&btn_wqh); // 当设备IO未就绪时,让进程进入睡眠状态,信号和kill -9也打不断 // 当condition为true时不睡眠 void wait_event(wait_queue_head_t *q, int condition); // 当设备IO未就绪时,让进程进入睡眠状态,但是可以被信号中断 // 返回 0 条件满足并唤醒, -ERESTARTSYS 被信号中断 int wait_event_interruptible(wait_queue_head_t *q, int condition, long timeout); // 当设备就绪的时候,唤醒等待队列的所有处于等待状态的进程 // 无论是出于可中断睡眠状态的进程,还是出于不可中断睡眠状态的进程,都可以被唤醒 wake_up(&btn_wqh); // 唤醒等待队列的所有处于可中断睡眠状态的进程 // 使用场景:仅在确切知道只需要唤醒可中断的进程时使用,避免检查和处理不可中断睡眠的进程,但这种优化通常微不足道。 wake_up_interruptible(&btn_wqh); ``` ### IO多路复用 ```c // 内核编程中让应用层使用select、poll、epoll的话,需要实现poll函数 struct file_operations{ __poll_t (*poll) (struct file *, struct poll_table_struct *); } // 在poll函数中需要使用poll_wait,当设备当前不具备用户请求的IO条件时候,驱动程序必须将调用进程添加到等待队列中去,目的是为了当设备状态发生变化时,内核可以自动唤醒这个等待的进程 void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p); ``` # 四、随笔 ### 1.platform总线 **platform_device_register**会做两件事 - 第一件事是将`platform_device`节点注册到`device`链表中去 - 第二件事是将`device`和`driver`节点匹配 **platform_driver_register**同样也会做两件事情 - 第一件事是将`platform_driver`节点注册到`driver`链表中去 - 第二件事情是将`driver`和`decice`匹配(表示该设备驱动代码可以驱动哪些设备) ![alt text](readme_image/3.png) 其中匹配的时候是`platform_match`进行匹配,匹配成功的话就会自动调用`platform_driver`中的`probe`函数,卸载一个dev或者dri模块的时候`platform_driver`中的`remove`会且只调用一次 - 第一种匹配方式是按照name匹配(传统/遗留),`platform_device.name`和`platform_driver.driver.name`匹配。 - 第二种ID表匹配(次优先):设备侧代码不需要变化,驱动侧代码的`platform_driver.id_table`赋值为一个`{name,data}`的数组,并且最后一个元素需要是`{}`类似于字符串的`\0` - 第三种设备树匹配(最优先):设备侧代码写在设备树文件中,这个开发板对应的是`linux-5.4.31/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts`文件,驱动侧代码中`platform_driver.driver.of_match_table[i]->compatible`和设备树节点的`compatible`匹配 - 其中`platform_driver.driver.of_match_table`是一个`struct of_device_id*`,是接受一个数组 **platform_device.resource**会让驱动代码移植变到方便一些 - `cpu`和`设备`在不同开发板,相同`cpu`和`设备`的情况下接口的信息是会不相同的,如果`driver`直接使用接口信息来注册的话移植代码会很不方便,但是如果将`cpu`和`设备`的接口信息抽象到`resource`中去,`driver`从`device`的`resource`中取信息来注册的话,移植的时候只需要修改`resource`即可移植 - 如果是非标准的接口信息配置的话可以使用`platform_device.device.platform_data`来指向非标准配置(在现代设备树的情况下是在设备树自定义属性,DT API读取) ---------- ### 2.设备树 - 官网:https://www.devicetree.org/ - 设备树使用文档:https://elinux.org/Device_Tree_Usage - 当前移植的Linux内核的`stm32mp157a-fsmp1a.dts.dts`文件所在位置是`linux-5.4.31/arch/arm/boot/dts/` - `.dts`:文件类似于c文件,是设备树文件 - `.dtsi`:文件类似于h文件,是通用的硬件配置设备树文件 - `.dtc`:工具软件,将dts文件翻译为二进制文件,供内核使用 - `.dtb`:由dts文件翻译出来的二进制文件,由内核使用 ![picture](readme_image/4.png) #### 节点语法 - 设备树文件的语法: ```json /dts-v1/; dts的版本 /{ //根节点 node1{ //子节点名称 key1 = "A string"; }; }; /*同一深度的节点会合并*/ / { mynode{ p1="hello"; p2="213"; }; mynode{ p2="ttt"; p3="456"; }; } //等价于 / { mynode{ p1="hello"; p2="ttt"; p3="456"; }; }; ``` - 节点语法格式 ```json node-name@unit-address child-node1节点,它的完整路径: /node1/child-node1 ``` - 节点取别名 ```json // 通过aliases取别名 aliases{ serial0 = "/simple-bus@fe000000/serial@11c500"; // 取了个别名叫serial0 ethernet0 = "/simple-bus@fe000000/ethernet@31c000"; // 取了个别名叫ethernet0 }; // 通过label标签的形式 node:mynode@0x12345678{ }; // node就是mynode0x12345678节点的别名 ``` - 节点的引用 ```json &node就是在引用mynode@0x12345678节点 ``` - 常用的节点 - 根节点,dts文件中必须有一个根节点 - cpu节点,一般不需要开发工程师设置,在dtsi文件提前定义好了 - memory节点,标识开发板上使用的内存大小,需要移植工程师调整 - chosen节点,可以通过该节点给内核传递一些参数,这些参数就在chosen节点中设置 ```json chosen{ bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200"; }; ``` #### 属性 - 属性是由**名称**(1~31字符,有标准属性和自定义属性)和**值**组成 - 标准属性名:**compatible、model、status、#address-cells and #size-cells、reg、ranges** - 值有5种情况 - 空:如果用于传递`true/false`信息时,取值为空 - 字符串值:"" `string-property="a string"`; - 无符号32bit的整型数:<> `cell_property=<0xbeef 0x123 0xabc12345>` - 1个字节的16进制标识的数据:[] `binary-property=[01 23 45 67]` - 字符串 32bit整数 1字节数据的混合形式 `mixed-property="adc",[12 45],<0x12345678>` #### 内核对设备树的解析 - 设备树中的每个节点都会被解析为一个struct node-device类型的变量 - 内核中的全局变量of_root用于记录根节点的地址,从of_root可以找到任意一个子节点。 - 内核中会把设备树的节点进一步转换成platform_device,转换规则 - 根节点下含有compatible属性的节点会被进一步转换为platform_device结构体变量 - 含有特定的compatible属性的节点的子节点(含有compatile)也会被进一步转换为platform_device结构体变量 - `"simple_bus"` - `"simple_mfd"` - `"isa:` - `"amba_bus"` ```js / { mytest { //会转换为platform_device compatile = "mytest", "simple-bus"; mytest@0 { //会转换为platform_device compatile = "mytest_0"; }; }; i2c { //会转换为platform_device 对应的是硬件I2C控制器 compatile = "samsung,i2c"; at24c02 { //不会转换为platform_device 对应的是i2c总线上的一个从设备 会转换为i2c_client compatile = "at24c02"; }; }; spi { //会转换为platform_device 对应的是硬件SPI控制器 compatile = "samsung,spi"; flash@0 { //不会转换为platform_device compatible = "winbond,w25q32dw"; spi-max-frequency = <25000000>; reg = <0>; }; }; }; ``` - 设备树的节点信息在存储的时候是按照大端的字节序存储的,需要转换成本地字节序才能用 ------ ### 3.LED(GPIO)的编程(设备树匹配) - 使用的设备树`compatible`的方式匹配(优先级最高) #### 初始化 - 首先是正常的`platform`总线的`init`和`exit` ```c static int __init led_drv_init(void) { platform_driver_register(&ledDrv); return 0; } static void __exit led_drv_exit(void) { platform_driver_unregister(&ledDrv); } module_init(led_drv_init); module_exit(led_drv_exit); // 可以替换为module_platform_driver(led_drv); ``` #### 三种匹配方式 - 其中`ledDrv`是一个`platform_driver`的结构体变量 - `platform_driver`中目前只关心以下成员 - `probe`:函数指针,指向安装模块时候的处理函数|类型:`int(*)(struct platform_device *)`; - `remove`:函数指针,指向卸载模块时候的处理函数|类型:`int(*)(struct platform_device *)`; - `driver`:第一种name匹配和第三种设备树匹配要用到的数据结构`struct device_driver` - `id_table`:第二种方D匹配要用到的数据结构`struct platform_device_id*` ```c struct device_driver{ const char *name;//和设备侧的platform_device.name匹配,第一种名称匹配 const struct of_device_id *of_match_table;//第三种设备树匹配 } struct of_device_id{ char name[32];//和设备树的compatible的值匹配 char type[32]; char compatible[128]; const void *data; } //同样,最后一个元素也是需要{}类似于\0 struct platform_device_id { char name[PLATFORM_NAME_SIZE];//和设备侧的platform_device.name匹配,第二种ID匹配 kernel_ulong_t driver_data; }; //第二种ID匹配方式用到platform_device_id的数组,最后一个元素是{}类似于\0 ``` #### 设备树 - 根据内核帮助文档编写硬件节点 - gpio节点帮助文档:`linux-5.4.31\Documentation\devicetree\bindings\gpio\gpio.txt` - 中断帮助文档:`linux-5.4.31\Documentation\devicetree\bindings\interrupt-controller\interrupts.txt` ```js leds{ compatible = "minli,leds"; led1-gpio = <&gpioe 10 0>; led2-gpio = <&gpiof 10 0>; led3-gpio = <&gpioe 8 0>; }; //第三个cell是GPIO标志 /* 在 Linux 内核中的定义 */ #define GPIO_ACTIVE_HIGH 0 /* 高电平有效 */ #define GPIO_ACTIVE_LOW 1 /* 低电平有效 */ /* 更复杂的标志(可以组合使用) */ #define GPIO_OPEN_DRAIN (1 << 1) /* 开漏输出 */ #define GPIO_OPEN_SOURCE (1 << 2) /* 开源输出 */ #define GPIO_PULL_UP (1 << 3) /* 上拉电阻 */ #define GPIO_PULL_DOWN (1 << 4) /* 下拉电阻 */ #define GPIO_INPUT (1 << 5) /* 输入模式 */ #define GPIO_OUTPUT (1 << 6) /* 输出模式 */ ``` - `platform_driver.driver.of_device_id`赋好值(匹配)之后就是`probe`,`remove` - `remove`反向消除`probe`的影响 #### 安装模块流程 - 分配设备号`dev_t`,`alloc_chrdev_region()` - 分配`cdev`结构体内存,`cdev_alloc()` - `cedv`和操作函数绑定(为cdev.ops赋值)`cdev_init()` - 将`cdev`添加到系统中`cdev_add()` - 自动创建设备文件 - ledClass = class_create(THIS_MODULE,"LEDS"); - device_create(ledClass,NULL,ledDevNum,NULL,"leds"); - 查找设备节点`of_find_node_by_path("/leds")` - 解析节点的管脚属性 - `of_get_named_gpio(ledDTNode,"led1-gpio",0)`获取对应管脚编号 - 或者`gpiod_get_from_of_node(node, "led1-gpio", 0, GPIOD_OUT_LOW, "led1")` - 申请管脚资源`gpio_request()`,如果使用`gpiod_get_from_of_node`这一步就可以省略 - 操作函数`ioctl` - request:命令 - arg:用户空间与内核空间交换数据的参数,需要使用`copy_form_user`或者`copy_to_user`,这个参数一般是用户数据的地址,`unsigned long`是4字节数据正好对应32位操作系统的4字节的指针大小 ### 4.按键btn(irq)的编程 #### 基本中断编程流程 - 首先是正常的内核模块代码框架 ```c static int __init btn_drv_init(void){} static void __exit btn_drv_exit(void){} module_init(btn_drv_init); module_exit(btn_drv_exit); MODULE_LICENSE("GPL"); ``` - 设备树编程`linux-5.4.31/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts` ```js //帮助文档位于linux-5.4.31/Documentation/devicetree/bindings/interrupt-controller/interrupts.txt mykeys{ interrupt-parent = <&gpiof>; interrupts = <9 0>, <7 0>, <8 0> interrupt-name = "key1", "key2", "key3"; }; ``` - 虚拟中断号映射实际物理中断号,`irq_of_parse_and_map(节点,索引)`,会解析interrupts属性并映射 - 根据已有的虚拟中断号申请中断,`request_irq(中断号,中断服务函数,触发方式,名称[在/proc/interrupts看],处理函数第二个参数);` - 卸载模块的时候使用`free_irq(中断号,处理函数第二个参数)`,需要注意和申请的时候的参数一致 #### 中断处理函数特点 - 中断处理函数不要执行特别耗时的任务,中断上下文环境中不能调用引起阻塞或者睡眠的函数(不能做任务切换) - `copy_to_user`/`copy_from_user`/`request_irq`这些函数都可能引起睡眠,所以不可用于中断上下文中。 #### 中断底半步机制 - 底半部,顶半部(上半部,下半部)机制 - 中断处理函数执行速度要很快,但是又不得不做很复杂的逻辑处理,比如网卡接受数据要拆包,再将数据传输给应用层,这个过程很耗时间,处理包的时候接收到新的数据(新的中断),无法及时响应(四大三小会中断屏蔽),就会丢包 - 为了解决这个问题,linux内核将中断分为两个半部: - 顶半部(top half)完成最紧急且不耗时的工作,此过程中的CPSR.I=1(屏蔽IRQ中断)。退出顶半部时,需要想内核登记底半部 - 底半部(bottom half)完成不紧急且耗时的工作,此过程中CPSR.I=0(中断使能)。 - 内核中提供的登记底半部的方式有三种: - 软中断:最底层,最危险,内核核心使用(不常用,枚举变量,了解即可) - tasklet:基于软中断封装,根据软中断实现,底半部函数不可使用睡眠函数 - 工作队列:基于内核线程,相当于一个内核线程执行对应的任务,可以使用睡眠函数 #### tasklet任务 - 在模块初始化中的中断流程 - 查找节点 - 虚拟中断号映射 - 中断号申请`request_irq` - `tasklet_struct`定义,初始化 ```c // 定义 tasklet 变量 struct tasklet_struct btnTasklet; // 初始化 tasklet 变量 void tasklet_init(struct tasklet_struct* t, void (func*)(unsigned long), unsigned long data); //以上的两步可以使用以下宏来完成 等价于 定义 + 初始化 DECLARE_TASKLET(name, func, data); ``` - 然后是在中断号申请的api中的中断处理函数中登记底半部`void tasklet_schedule(struct tasklet_struct* t);` #### work工作队列 - 和tasklet一样的流程,定义变量,初始化变量,登记底半部函数 #### delay_work延时工作队列 - 和work一样,延时之后执行底半部函数 #### work和tasklet的区别 - 不同点:tasklet登记的底半部函数工作与中断上下文,不能调用引起阻塞或者睡眠的函数;而工作队列登记的底半步函数工作与进程上下文,其中是可以调用引起阻塞睡眠的函数。 ![alt text](readme_image/5.png) - 同等条件tasklet登记的底半部函数会被更快的执行到 ### 5.linux内核中的定时和延时 - 系统时钟中断,ARM core会收到系统时钟定时器周期性发送的中断信号,收到该信号之后会调用对应的中断处理函数:`arch_timer_handler_virt` - 中断处理函数中会完成一下工作: - 硬件相关的工作 - 通用性的工作 - 更新jiffies的值 - 更新墙上时间 - 计算当前任务的事件片消耗 - `HZ` 是一个常数,是可以通过make menuconfig配置的。这个常数决定了系统时钟中断信号产生的频率。1秒HZ次系统中断 - `系统时钟滴答tick`:tick = 1/HZ,当前配置HZ=100,tick就是10ms,时间精度也就是10ms - `jiffies`:是一个32bit的变量,上电初始值为0。用于记录自开机以来,产生的系统时钟中断的次数,用途就是用于计时。 #### 定时 - 首先是正常的module代码框架 - 使用定时器需要先定义struct timer_list timer变量 - 初始化这个timer_list变量 - 设置定时器(init):`timer_setup(&timer, timer_func, 0)` - 设置到时时间`timer.expires = jiffies + (HZ >> 1)` ,HZ是一秒多少次系统中断,`>> 1`就是除以2,也就是500ms - 启动定时器 `add_timer()`,`mod_timer()`也可以启动 - 如果不想定时器一次结束,定时器回调函数中可以再加入`mod_timer()` - 卸载模块需要使用`del_timer()`,`del_timer_sync`更安全一些 ### 6.linux内核竞态与并发 - 共享资源:多个线程都能访问,读写的资源 - 多进程有独立资源,除非使用共享内存(需要锁) - 临界资源:临界资源中需要互斥访问的资源(共享资源+会产生冲突) - 临界区:实际访问临界资源的代码片段(需要保护临界资源的代码) - 产生竞态的根本原因就是并发,具体来看就是四点原因 - SMP(对称多核处理器,多核并行,两个核同时访问counter++,结果为6,理想为7) - `任务`与`任务`之间的抢占(线程/进程之间的抢占) - `任务`与`中断`之间的抢占(任务正在修改,中断也要修改) - `中断`与`中断`之间的抢占(中断嵌套) - 内核中解决竞态的主要手段,包括一下四种 - 中断屏蔽 - 原子操作(汇编级操作变量) - 自旋锁(轮询) - 信号量 #### 简单处理竞争 - 一个static int 变量,open之后就--,release就++; - 存在bug:汇编级别代码--coutn;这句话会有3行汇编代码 - ldr,sub,str(读取,修改,写入) - 当多进程同时访问这个共享资源的时候,同时取,同时减,两个进程打开了文件(异常) - 0-3G 用户空间,3-4G 内核空间,2^32 = 4GB内存 ![alt text](/readme_image/6.png) #### 中断屏蔽 - 防止`任务`与`中断`之间的抢占 - 类似于用户编程的互斥锁的用法 ```c local_irq_disable() // 禁止中断 // 访问共享资源的代码 local_irq_enable () //使能中断 ``` - 注意事项: - 屏蔽中断的事件要非常短,一旦关闭中断之间过长,会影响系统性能,甚至有可能造成系统崩溃 - 关中断对单核cpu简单有效 - 推荐使用开关中断的函数: ```c local_irq_save(flags) // flags=CPSR.I 关中断 local_irq_restore(flags)//CPSR.I = flags /*--------- <使用场景> ---------*/ func() { unsigned long flags; local_irq_save(flags);// 关闭中断,是flags,不是&flags // 访问共享资源 local_irq_restore(flags);// 开中断 } // 上层内核代码调用func local_irq_disable() // 禁止中断 ··· func();// 如果使用local_irq_enable()会导致提前使能中断 ··· local_irq_enable () //使能中断 ``` #### 原子操作 - 原子操作是指在执行过程中不会被中断的操作,是一种不可分割的操作。 - 原子操作的主要作用是保护临界资源,防止竞态条件的发生。 - 原子操作的实现通常是通过硬件指令来完成的,例如ARM架构的LDREX/STREX指令。 - 原子操作的使用场景包括但不限于: - 计数器的增加和减少 - 标志位的设置和清除 - 链表的插入和删除 - 原子操作CAS操作通常有3个参数: - 指向要操作的变量的指针 - 期望的旧值 - 要设置的新值 - 在内核中的两种类型数据操作: - 位原子操作 - 整型原子操作 - 注意事项: - 共享资源如果是一个bit或者一个整型变量,就可以使用原子操作来解决竞态的问题,如果是一个结构体、一个字符串...就不能使用原子操作了 - 如果能使用原子操作,就尽量使用,因为他的系统消耗比中断屏蔽、锁机制要小很多 #### 自旋锁 - 自旋锁最多有一个持有单元,如果试图获取一个已经被其他执行单元持有的自旋锁,那么获取自旋锁不成功,原地自选等待,直到获取自旋锁成功为止 - 自旋锁的实现原理: - 自旋锁是一个变量,初始值为0,当一个执行单元获取自旋锁时,会将该变量设置为1,其他执行单元在获取自旋锁时会检测该变量的值,如果为1,说明自旋锁已被其他执行单元持有,当前执行单元会继续检测,直到该变量的值为0,说明自旋锁已被释放,当前执行单元才会获取到自旋锁 - 自旋锁自旋就是忙等待(不是睡眠等待),它可以用于中断上下文中 - 注意事项: - 自旋锁保护的临界区代码执行时间不宜太长,太长会影响系统性能 - 自选锁保护的临界区代码中不能出现任务调度的逻辑(不应该调用引起阻塞或者睡眠的函数) - 自旋锁的衍生锁:读写锁、顺序锁 #### 信号量 - 信号量是一种同步机制,用于保护共享资源的访问。 - 信号量的实现原理: - 信号量是一个计数器,初始值为0,当一个执行单元获取信号量时,会将该计数器减1,如果计数器的值为负,说明共享资源已被其他执行单元占用,当前执行单元会阻塞等待,直到其他执行单元释放信号量为止 - 信号量的使用场景包括但不限于: - 多个执行单元同时访问共享资源 - 多个执行单元按序访问共享资源 - 它允许有多个执行单元持有,一旦获取信号不成功,睡眠等待 - 注意事项: - 信号量保护的临界区中可以调用引起阻塞睡眠的函数,信号量机制不能用于中断上下文。 - 信号量是基于自旋锁机制实现的,它的系统消耗是大于自旋锁的,能使用自旋锁尽量选择自选锁。 - 衍生的信号量机制:读写信号量 RCU、完成量 ### 7. 阻塞IO与非阻塞IO #### 等待队列 - 内核中为了解决设备的阻塞访问,设计了等待队列机制。 - 等待队列是一种用于阻塞进程的机制,它可以用于实现同步和通信。 - 等待队列的实现原理: - 等待队列是一个双向链表,每个节点表示一个等待进程 - 当一个进程需要等待某个条件满足时,会将自己插入到等待队列中,并进入睡眠状态(内核代码实现) - 当条件满足时,会将等待队列中的进程唤醒,继续执行 - 等待队列的使用场景包括但不限于: - 多个执行单元按序访问共享资源 - 多个执行单元等待某个事件发生 - 注意事项: - 等待队列保护的临界区中可以调用引起阻塞睡眠的函数,等待队列机制不能用于中断上下文。 - 等待队列是基于自旋锁机制实现的,它的系统消耗是大于自旋锁的,能使用自选锁尽量选择自选锁。 ```c /* 原始宏 */ wait_event_interruptible() { wait_event_interruptible() { __wait_event() { init_wait_entry() { /* 记录要挂起的进程 current 指向当前正在运行的进程数据结构 */ wq_entry->private = current; } prepare_to_wait_event() { /* 将包含当前进程信息的节点添加到等待队列头指向的队列 */ __add_wait_queue(wq_head, wq_entry); /* 设置当前进程的状态为睡眠状态 */ set_current_state(state); cmd; // schedule();重新调度 } } } } /* 唤醒宏 */ wake_up_interruptible() { __wake_up() { __wake_up_common_lock() { // 执行节点中记录的函数 默认是唤醒进程 autoremove_wake_function ret = curr->func(curr, mode, wake_flags, key); } } } ``` #### 等待队列在内核代码中的使用流程 - 定义一个等待队列头变量 wait_queue_head_t btn_wqh; - init_waitqueue_head(struct wait_queue_head*) - 使用wait_event(struct wait_queue_head, int condition); // 如果condition不满足就睡眠 - 其他地方数据准备好之后使用wake_up(struct wait_queue_head*); #### 非阻塞IO写法 - 检查在btn_read中的struct file* filePtr 指向的内存中filePtr->f_flags & O_NONBLOCK - if(filePtr->f_flags & O_NONBLOCK && condition == 0) return -EAGAIN; - wait_event(btn_wqh, condition);// 如果没有O_NONBLOCK就是正常的阻塞IO,有的话就根据condition返回内容 #### 原理 - linux中每个进程会有一个文件描述符号,记录打开文件的文件描述符和对应的文件表 ```c struct task_struct { //内核中会为每个进程创建一个task_struct类型的变量 struct files_struct *files; ... } struct files_struct { struct file * fd_array[NR_OPEN_DEFAULT]; ... } open("a.txt", O_RDWR|O_NONBLOCK); -------------------------------------------- sys_open() { /*记录用户空间给定的flags 信息到 f->f_flags中去*/ do_sys_open() { struct file *f = do_filp_open(dfd, tmp, &op); //创建的struct file ,再后续调用read时,会当参数传递给read fd_install(fd, f); /*文件描述符和文件表绑定*/ } } ``` ### 8. 多路复用IO #### 编程随笔 - 在应用层的select,poll和epoll(后面统称多路IO)的使用会让应用层代码会有差别,但是在内核驱动编程中只需要实现`struct file_operations`结构体中的`.poll`函数即可 - 个人理解:应用层使用多路IO的时候都会阻塞,也就对应的了内核编程的`.poll`中的`poll_wait`,`poll_wait`会将当前的item(等待队列节点)添加到指定的等待队列上去,其他地方,比如说中断函数中生产了这个资源,同时会唤醒这个等待队列上的节点,read阻塞io也会唤醒,这样就和应用层代码编写逻辑对应上了 #### 用户空间使用select(复习) select工作流程 - 初始化集合: 使用 FD_ZERO 清空 readfds, writefds, exceptfds - 添加描述符: 使用 FD_SET 将需要监听的 FD 添加到对应的集合中。 - 设置超时: 根据需要设置 struct timeval 或将其置为 NULL。 - 调用 select: 阻塞等待(或非阻塞轮询)。 - 重要提示: select 返回后,它会修改 readfds, writefds, exceptfds,只保留那些已就绪的 FD。未就绪的 FD 会被移除。 - 检查结果: select 返回后,遍历所有之前添加的 FD,使用 FD_ISSET(fd, readfds) 等宏来检查哪个 FD 已经就绪,并进行相应的 I/O 处理。 - 循环: 重复以上步骤。 #### 用户空间使用poll(复习) #### 用户空间使用epoll(复习) ### 9. 信号驱动IO和异步IO - 由于不经常使用先跳过不学