驱动开发--汇总

一,【驱动相关概念】

1,什么是驱动

能够驱使硬件实现特定功能的软件代码
根据驱动程序是否依赖于系统内核将驱动分为裸机驱动和系统驱动

2,逻辑驱动和系统驱动的区别

裸机驱动:编写的驱动代码中没有进行任何内核相关API的调用,开发者自己配置寄存器完成了相关硬件控制的代码编写。
裸机驱动不依赖于系统内核,由开发者独立即可完成,但是裸机驱动实现的硬件控制工作相对而言比较简单

系统驱动:系统驱动指的是编写的驱动代码中需要调用系统内核中提供到的各种API,驱动最终也会加载到系统内核生效。
系统驱动开发者无法独立完成,需要依赖于系统内核,基于系统驱动实现的硬件功能也更加复杂

3,系统驱动在系统中的层次

1,操作系统的功能

向下管理硬件,向上提供接口

接口类型:
文件管理
内存管理
进程管理
网络管理
设备管理 (设备驱动的管理):linux设备驱动是属于设备管理功能的一部分,它的作用是丰富系统内核的设备管理功能

2,进程上下文的切换

当进程进行系统调用时,进程访问到的资源从用户空间切换到了内核空间,叫做上下文的切换

文件IO通过系统调用实现; 
标准IO通过库函数实现,标准IO = 缓冲区 + 系统调用,当缓冲区刷新时会进行系统调用
缓冲区刷新:
    行缓存(终端相关的stdin,stdout)6种:遇到换行符,关闭文件指针,程序结束,手动调用fflush函数,缓冲区满,输入输出切换
    全缓存(自定义文件指针) 5种:关闭文件指针,程序结束,手动调用fflush函数,缓冲区满,输入输出切换
    不缓存(终端相关的stderr) 无:

3,linux设备驱动的分类

字符设备:能够以字节流的形式进行顺序访问的设备叫做字符设备(90%)ex:鼠标、键盘、lcd...
块设备:能够以块(512字节)为单位进行随机访问的设备叫做块设备。(磁盘)
网卡设备:进行网络通信时使用网卡设备实现。网卡设备数据的读取要基于套接字来实现

二,【linux内核模块编程】

1,内核模块的意义

不同于应用程序,驱动是加载到内核空间中的,所以需要按照内核模块的编程框架编写驱动代码

2,内核模块三要素

入口:安装内核模块时执行,主要负责资源的申请工作
出口:卸载内核模块时执行,主要负责资源的释放工作
许可证:声明内核模块遵循GPL协议

3,内核模块的编译

命令: make modules
方式:
    内部编译(静态编译):需要依赖于内核源码树进行编译
        将编写的内核模块源码存放到linux内核指定目录下
        修改该目录下的kconfig文件,添加当前模块文件的选配项
        执行make menuconfig,将当前内核模块源码的选配项选配为【M】
        执行make menuconfig进行模块化编译
    外部编译(动态编译):不需要依赖于内核源码树,在编译时只需要编译当前内核模块文件即可,外部编译需要自己手写当前内核模块编译的Makefile

4,操作内核模块的安装,卸载,查看命令

安装                  insmod ***.ko
查看已经安装的内核模块  lsmod
卸载内核模块            rrmod ***
查看内核模块相关信息    modinfo ***.ko

三,【打印函数printk】

1,使用格式

printk("格式控制符",输出列表);//按照默认的输出级别输出内容
或者
printk(消息输出级别 "格式控制符",输出列表);//让消息按照指定的级别进行输出

2,消息输出级别相关

printk输出的内容属于内核的消息,一般内核的消息有重要的,也有相对不重要的,我们现在想要将比较重要的消息输出到终端,不重要的消息不在终端进行输出。做法是将输出的消息设置为不同的输出级别,终端会有一个默认的级别,只有输出消息的级别高于终端的默认级别,消息才可以在终端输出。printk消息级别分为0-7级共8级,其中数字越小表示级别越高,常用的消息级别是3-7级。

#define KERN_EMERG    KERN_SOH "0"    /* system is unusable */
#define KERN_ALERT    KERN_SOH "1"    /* action must be taken immediately */
#define KERN_CRIT    KERN_SOH "2"    /* critical conditions */
#define KERN_ERR    KERN_SOH "3"    /* error conditions */
#define KERN_WARNING    KERN_SOH "4"    /* warning conditions */
#define KERN_NOTICE    KERN_SOH "5"    /* normal but significant condition */
#define KERN_INFO    KERN_SOH "6"    /* informational */
#define KERN_DEBUG    KERN_SOH "7"    /* debug-level messages */

查看消息默认级别
    终端输入 cat /proc/sys/kernel/printk查看
    4            4              1                      7
    终端默认级别  消息默认级别   终端支持的消息最高级别   终端支持消息的最低级别

修改消息默认级别 
注意:一旦重启,消息默认级别会被重置为修改之前的数值
Ubuntu:
    sudo su//切换到管理员模式
    echo 4 3 1 7 > /proc/sys/kernel/printk
开发板
    修改 ~/nfs/rootfs/etc/init.d/rcS
    在这个文件最后添加一行:echo 4 3 1 7 > /proc/sys/kernel/printk,加上这行不用每次都改了

3,ubuntu虚拟终端

ubuntu由于官方的限制,无论内核消息级别有多高,消息都无法在终端正常显示,此时可以切换到虚拟终端进行消息的显示

切换到虚拟终端方式
    ctrl+alt+[f2-f6](fn)
退出虚拟终端
    ctrl+alt+f1(fn)

4,dmesg命令

功能:输出从内核启动到当前时刻开始所有的打印消息
dmesg -c/dmesg -C:清除当前dmesg的buf中保存的所有打印消息

四,【linux内核模块传参】

什么是内核模块传参
    内核模块传参指的是在安装内核模块时在命令行给内核模块中的变量传递数值
    ex:  insmod demo.ko  a=100  //在安装内核模块的时候给变量a进程传递数值
内核模块传参的意义
    通过内核模块传参的使用,我们可以对内核模块中的一些属性进行修改,让当前的内核模块向下适配多种不同的硬件,向上也可以兼容各自复杂的应用程序

API

module_param(name, type, perm)
功能:声明可以进行命令行传参的变量信息
参数:
    name:要进行命令行传参的变量名
    type:要进行命令行传参的变量类型
           / * Standard types are:
           byte(单字节类型), hexint, short, ushort, int, uint, long, ulong
           charp:     a character pointer(char *)
           bool:      a bool, values 0/1, y/n, Y/N.
           invbool:   the above, only sense-reversed (N = true).
           */
    perm:文件权限,当使用module_param函数声明要传参的变量时,会在/sys/module/当前内核模块名/parameters/目录下生成一个以当前变量名为名的文件,文件的权限就是perm和文件权限掩码运算得到,文件的数值时变量的值

MODULE_PARM_DESC(变量名, 对变量的描述)
功能:添加对要传参的变量的描述,这个描述可以通过modinfo ***.ko查看到

注意:
    1.如果给char类型的变量进行传参的话,要传递字符的十进制形式
    2.如果传参的类型是一个char *类型,传递的字符串中间不要有空格

五,【内核的导出符号表】

内核导出符号表的意义
    实现不同模块之间资源的相互访问,构建模块之间的依赖关系
    内核模块都是加载到同一个内核空间,所以模块2想要访问模块1里的资源,只需要模块1将自己资源的符号表导出,模块2借助模块1的符号表即可以访问模块1的资源

API
    EXPORT_SYMBOL_GPL(变量名|函数名) ,模块2中调用改函数即可

编译模块
    先编译模块1,将模块1编译生成的符号表文件Module.symvers拷贝到模块2的目录下,再编译模块2

注意:
    在新版本内核中不支持符号表文件的复制了,如果模块2想要访问模块1,将模块1的符号表文件直接复制到模块2的路径下,编译模块2,会报未定义错误,
    解决方法:在模块2的Makefile中指定模块1的符号表文件路径
        KBUILD_EXTRA_STMBOLS += /home/ubuntu/23051班驱动/day2/1/Module.symvers

安装&卸载
    因为模块2和模块1构成依赖关系,所以先安装模块1,再安装模块2,先卸载模块2,再卸载模块1

六,【字符设备驱动】

框架图

1,字符设备驱动的注册和注销相关API

注册:
   int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
功能:实现字符设备驱动的注册(申请了一定数量(256)的设备资源)
参数:
    major:驱动的主设备号
        ==0:动态申请主设备号
        >0:静态指定一个主设备号
            //次设备号有256个,范围是(0-255)
    name:驱动名字
    fops:操作方法结构体指针
        struct file_operations {
            int (*open) (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 *);
            int (*release) (struct inode *, struct file *);
            };
返回值:失败返回错误码
    成功:
        major==0,返回申请得到的主设备号
        major>0:返回0
        //可以通过 cat /proc/devices查看已经注册成功的驱动的名字以及主设备号

注销 
    void unregister_chrdev(unsigned int major, const char *name)
功能:注销字符设备驱动
参数:
    major:注册时申请的主设备号
    name:注册时填写的名字
返回值:无

2,copy_to_user & copy_from_user 用户和内核之间数据拷贝API

1.long copy_to_user(void __user *to, const void *from, unsigned long n)
    功能:实现内核空间数据向用户空间拷贝
    参数:
        to:用户空间存放拷贝来的数据的buf首地址
        from:内核空间存放要拷贝的数据的buf首地址
        n:要拷贝的数据大小
    返回值:成功返回0失败返回未拷贝的字节数

2.long copy_from_user(void *to, const void __user *from, unsigned long n)
    功能:实现用户空间数据向内核空间拷贝
    参数:
        to:内核空间存放拷贝来的数据的buf首地址
        from:用户空间存放要拷贝的数据的buf首地址
        n:要拷贝的数据大小
    返回值:成功返回0失败返回未拷贝的字节数

3,ioremap物理内存映射虚拟内存API

想要实现硬件的控制,需要对硬件相关的寄存器进行控制,而寄存器对应的内存属于物理内存,驱动是加载虚拟内存上的,想要在驱动中操作硬件寄存器,需要将寄存器对应的物理内存映射为虚拟内存,操作对应的虚拟内存即可控制硬件。
1.void  *ioremap(unsigned long port, unsigned long size)
    功能:映射指定大小的物理内存为虚拟内存
    参数:
        port:要映射的物理内存首地址
        size:映射的物理内存大小
    返回值:成功返回映射得到的虚拟内存首地址,失败返回NULL

2.void iounmap(volatile void __iomem *addr)
    功能:取消物理内存映射
    参数:
        addr:虚拟内存首地址
    返回值:无

七,【手动 / 自动创建设备节点(设备文件)】

1,创建设备文件的机制

mknod命令:手动创建设备节点的命令:
    mknod /dev/mychrdev c 241 0
        解释:
        mknod:创建设备文件的命令码
        /dev/mychrdev:创建的设备文件的名字以及路径
        c:设备文件类型为字符设备文件  b表示块设备文件
        241:主设备号
        0:次设备号(0-255)

devfs:可以用于创建设备节点,创建设备节点的逻辑在内核空间(内核2.4版本之前使用)
udev:自动创建设备节点的机制,创建设备节点的逻辑在用户空间(从内核2.6版本一直使用至今)
mdev:是一种轻量级的udev机制,用于一些嵌入式操作系统中

2,udev自动创建节点过程分析

1,注册驱动,register_chrdev()函数
2,获取设备信息(设备树相关文件,目前为指定寄存器地址)
3,创建一个设备类(向上提交目录信息),会在内核中申请一个struct class对象,并且初始化,此时会在/sys/class/目录下创建一个以类名为名的目录
4,创建一个设备对象(向上提交设备节点信息),会在内核中申请一个struct device对象,并且初始化,此时会在上一步创建好的目录下创建存放设备节点信息的文件
5,当创建好存放设备节点信息的文件后,内核会发起hotplug event事件,激活用户空间的hotplug进程
6,hotplug进程激活后,会通知udev进程在刚创建的存放设备节点信息的文件中查询设备节点相关信息
7,udev查询设备节点相关信息后,会在/dev目录下创建设备节点

3,udev创建设备节点时使用的API

1.向上提交目录信息
struct class * class_create(struct module *owner,const char *name );
    功能:申请一个设备类并初始化,向上提交目录信息
    参数:
        owner:指向当前内核模块自身的一个模块指针,填写THIS_MODULE
        name:向上提交的目录名
    返回值:成功返回申请的struct class对象空间首地址,失败返回错误码指针
  
2.销毁目录
void class_destroy(struct class *cls)
    功能:销毁目录信息
    参数:cls:指向class对象的指针
    返回值:无

3.向上提交节点信息
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
    功能:创建一个设备对象,向上提交设备节点信息
    参数:
        cls:向上提交目录时的到的类对象指针
        parent:当前申请的对象前一个节点的地址,不知道就填 NULL
        devt:设备号    主设备号<<20|次设备号
        dridata:申请的device对象的私有数据,填写NULL
        fmt:向上提交的设备节点名
        ...:不定长参数   
    返回值:成功返回申请到的device对象首地址,失败返回错误码指针,指向4K预留空间

4.销毁设备节点信息
void device_destroy(struct class *class, dev_t devt)
    功能:销毁设备节点信息
    参数:
        class:向上提交目录时得到的类对象指针
        devt:向上提交设备节点信息时提交的设备号
    返回值:无

错误相关
    在内核空间最顶层预留4K空间,当struct class函数调用失败后函数会返回一个指向这4K空间的指针
bool __must_check IS_ERR(__force const void *ptr)
    功能:判断指针是否指向4K预留空间
    参数:要判断的指针
    返回值:如果指着指向4K预留空间返回逻辑真,否则返回逻辑假

long __must_check PTR_ERR(__force const void *ptr)
     功能:通过错误码指针得到错误码
         ex:struct class_create *cls=struct class_create(THIS_MODULE,"mycdev");
         if(IS_ERR(cls))
         {
             printk("向上提交目录失败\n");
             return -PRT_ERR(cls);     
         }

获取设备号相关
        MKDEV(主设备号,次设备号):根据主设备号和次设备号得到设备号
        MAJOR(dev):根据设备号获取主设备号
        MINOR(dev):根据设备号获取次设备号

八,【ioctl硬件控制函数】

使用ioctl函数的意义
    linux有意将对硬件的控制划分到不同的系统调用来实现,让read()/write()函数专注于数据的读写,至于对于硬件不同控制功能的选择我们交给ioctl函数来实现。比如在串口通信时让read()/write()进行正常数据读写,至于设置波特率和数据位宽等交给ioctl进行选择控制

ioctl函数分析
*********系统调用函数分析***********

int ioctl(int fd, unsigned long request, ...);
    功能:进行IO选择控制
    参数:
        fd:文件描述符
        request:要进行的功能控制的功能码
        ...:可以写也可以不写,如果写的话传递一个整型变量或者一个地址
    返回值:成功返回0,失败返回错误码

*********驱动中的ioctl操作方法************

    当应用程序中调用ioctl函数时,驱动中的ioctl操作方法被回调
long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg)
{
    
    参数分析:
        file:文件指针
        cmd:功能码,由ioctl第二个参数传递得到
        arg:由ioctl第三个参数传递得到 
}


功能码解析:
    一个ioctl的功能码是一个32位的数值,尽量保证每一个硬件不同功能的功能码都不一样,所以我们需要对功能码进行编码
    查询内核帮助手册:~/linux-5.10.61/Documentation/userspace-api/ioctl
    vi ioctl-decoding.rst
 ====== ==================================
 31-30    00 - no parameters: uses _IO macro
    10 - read: _IOR
    01 - write: _IOW
    11 - read/write: _IOWR

 29-16    size of arguments

 15-8    ascii character supposedly
    unique to each driver

 7-0    function #
 ====== ==================================
31-30:读写方向位
29-16:ioctl第三个参数的大小
15-8:设备的标识,通常用'a'-'z'的表示
7-0:功能位,自己设定

构建功能码的API
#define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)    _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size)    _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size)
例:
//构建LED开关的功能码,不添加ioctl第三个参数
#define  LED_ON _IO('l',1)
#define  LED_OF _IO('l',0)
//构建LED开关的功能码,添加ioctl第三个参数int
#define LED_ON _IOW('l',1,int)
#define LED_OFF _IOW('l',0,int) 
第三个参数通常填指针类型

九,【字符设备驱动的内部实现】

1,字符设备驱动内部注册过程

通过对register_chrdev内部的实现过程进行分析,其实注册字符设备驱动的过程就是下面几步:
    1.分配struct cdev对象空间
    2.初始化struct cdev对象
    3.注册cdev对象
完成上面的三步,就完成了字符设备驱动的注册。

2,注册字符设备驱动分步实现相关API分析

*************注册过程**********
1.分配 字符设备驱动对象
    a.struct cdev cdev;
    b.struct cdev *cdev = cdev_alloc();
    /*
        struct cdev *cdev_alloc(void)
        功能:申请一个字符设备驱动对象空间
        参数:无
        返回值:成功返回申请的空间首地址
        失败返回NULL
    */
 2.字符设备驱动对象初始化
     void cdev_init(struct cdev *cdev, const struct file_operations *fops)
     功能:实现字符设备驱动的部分初始化
     参数:
         cdev:字符设备驱动对象指针
         fops:操作方法结构体指针
    返回值:无
3.设备号的申请
    3.1 静态指定设备号
    int register_chrdev_region(dev_t from, unsigned count, const char *name)
        功能:静态申请设备号并注册一定数量的设备资源
        参数:
            from:静态指定的设备号(第一个设备的设备号)
            count:申请的设备数量
            name:设备名或者驱动名
        返回值:成功返回0,失败返回错误码
    3.2 动态申请设备号
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
        功能:动态申请设备号并注册一定数量的设备资源
        参数:
            dev:存放申请的到的设备号的空间首地址
            baseminor:次设备号的起始值
            count:申请的设备资源数量
            name:设备名或者驱动名
         返回值:成功返回0,失败返回错误码   
 4.根据申请的设备号和驱动对象注册驱动
     int cdev_add(struct cdev *p, dev_t dev, unsigned count)
     功能:注册字符设备驱动对象
     参数:
         cdev:字符设备驱动对象指针
         dev:申请的设备号的第一个值
         count:申请的设备资源的数量
    返回值:成功返回0,失败返回错误码

***********注销过程*****************
1.注销驱动对象
void cdev_del(struct cdev *p)
    参数:
        p:要注销的对象空间指针
    返回值:无

2.释放申请的设备号和设备资源
    void unregister_chrdev_region(dev_t from, unsigned count)
    参数:
        from:申请的第一个设备号
        count:申请的设备资源的数量
    返回值:无

3.释放字符设备驱动对象空间
    void kfree(void *addr)
    功能:释放申请的内核空间
    参数:要释放的空间首地址
    返回值:无

3,struct cdev 驱动描述相关信息结构体

只要一个驱动存在于系统内核中,就会存在一个struct cdev对象,对象中是关于当前驱动的相关描述信息

struct cdev {
    struct kobject kobj;//基类对象
    struct module *owner;//模块对象指针  THIS_MODULE
    const struct file_operations *ops;//操作方法结构体指针
    struct list_head list;//用于构成链表的成员
    dev_t dev;//第一个设备号  
    unsigned int count;//设备资源数量
    ...
};

4,struct inode 操作系统中文件相关信息结构体

只要文件存在于操作系统上,那么在系统内核中就一定会存在一个struct inode结构体对象用来描述当前文件的相关信息

struct inode {
    umode_t            i_mode;//文件的权限
    unsigned short        i_opflags;
    kuid_t            i_uid;//文件的用户ID
    kgid_t            i_gid;//组ID
    unsigned int        i_flags;
    dev_t            i_rdev;//设备号
    union {
        
        struct block_device    *i_bdev;//块设备
        struct cdev        *i_cdev;//字符设备
        char            *i_link;
        unsigned        i_dir_seq;
    };

5,open函数回调驱动中操作方法open的路线

6,struct file 进程中打开的文件相关信息结构体

    open函数的第一个参数是文件路径,可以进而找到inode对象,从而回调到驱动的方法,但是read()\write()这些函数操作对象不是文件的路径,而是文件描述符,那么如何通过文件描述符回调到驱动的操作方法?
文件描述符是什么?
    文件描述符是在一个进程里面打开文件时得到的一个非负整数,一个进程里最多可以有1024个文件描述符。不同的进程的文件描述符独立的。文件描述符依赖于进程存在。想要探究文件描述符的本质,就要知道文件描述符在进程中的作用,
    通过分析struct task_struct结构体,fd_array是一个指针数组,数组中每一个成员都指向一个struct file类型的对象,而数组的下标就是我们常说的 文件描述符

struct file结构体分析
只要在一个进程里面打开一个文件,在内核中就会存在一个struct file对象,用来描述打开的文件相关的信息
struct file {
  struct path        f_path;//文件路径
    struct inode        *f_inode;    /* cached value */
    const struct file_operations    *f_op;//操作方法结构体
    unsigned int         f_flags;//open函数的第二个参数赋值给f_flags
    fmode_t            f_mode;//打开的文件的权限
    void            *private_data;//私有数据,可以实现函数件数据的传递

        };

7,struct task_sturct进程相关信息结构体

只要一个进程存在于操作系统上,在系统内核中一定会存在一个struct task_struct结构体对应保存进程的相关信息
struct task_struct {
    volatile long            state;//进程状态
    int             on_cpu;//表示进程在哪个CPU上执行
    int                prio;//进程优先级
    pid_t                pid;//进程号
    struct task_struct __rcu    *real_parent;//父进程
    struct files_struct        *files;//打开的文件相关结构体

};

struct files_struct {
     
        struct file __rcu * fd_array[NR_OPEN_DEFAULT];//结构体指针数组
        };
  fd_array是一个指针数组,数组中每一个成员都指向一个struct file类型的对象,而数组的下标就是我们常说的 文件描述符

8,通过文件描述符回调驱动操作方法的路线

9,设备文件和设备的绑定

int mycdev_open(struct inode *inode, struct file *file)
{
    int min=MINOR(inode->i_rdev);  //根据打开的文件对应的设备号获取次设备号
    file->private_data=(void *)min; //将次设备号传递给file的私有数据
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}

long mycdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
   int min=(int)file->private_data; //将file的私有数据保存的私有数据取出
    switch (min){
        case 0://控制LED1
            switch(cmd){
                case LED_ON:
                    //开灯
                    break;
                case LED_OFF:
                    //关灯
                    break;                                
            }
            break;
    case 1://控制LED2
    case 2://控制LED3
    }
    return 0;
}

十,【linux内核中的并发和竞态】

1,linux内核中产生的原因

表面原因
    多个进程同时访问同一个驱动资源,就会出现对资源争抢的情况
本质原因
    单核处理器,如果支持资源抢占,就会出现竞态
    对于多核处理,核与核之间本身就会出现资源争抢的情况
    对于中断和进程,会出现竞态
    对于中断和中断之间,如果中断控制器支持中断嵌套,则会出现竞态,否则不会。ARM芯片使用的中断控    制器是GIC,gic不支持中断嵌套

2,竞态解决方法

1,中断屏蔽(了解)

    中断屏蔽是针对于单核处理器实现的竞态解决方案,如果进程想要访问临界资源,可以在访问资源之前先将中断屏蔽掉,当进程访问临界资源结束后在恢复中断的使能。一般屏蔽中断的时间要尽可能短,长时间屏蔽中断可能会导致用户数据的丢失甚至内核的崩溃。一般中断屏蔽仅仅留给内核开发者测试使用。
    local_irq_disable()//中断屏蔽
    临界资源
    local_irq_enable()//取消中断屏蔽

2,自旋锁

    一个进程想要访问临界资源,首先要获取自旋锁,如果获取自旋锁成功,就访问临界资源,如果获取自旋锁失败,进程会进入自旋状态,自旋锁又被成为盲等锁
特点
    自旋状态下的进程处于运行态,时刻需要消耗CPU的资源
    自旋锁保护的临界资源尽可能的小,临界区中不能有延时、耗时甚至休眠的操作,也不可以有copy_to_user和copy_from_user
    自旋锁会出现死锁现象
    自旋锁既可以用于进程的上下文,也可以用于中断的上下文
    自旋锁使用时会关闭抢占//尽量保证上锁的时间尽可能的短
API
1.定义自旋锁
    spinlock_t lock;
2.初始化自旋锁
    spin_lock_init(&lock);
3.上锁(获取锁)
    void spin_lock(spinlock_t *lock)
4.解锁(释放锁)
    void spin_unlock(spinlock_t *lock)

3,信号量

    一个进程想要访问临界资源,先要获取信号量,如果获取不到,进程就切换到休眠状态
特点
    获取不到信号量的进程会切换到休眠状态休眠状态下的进程不消耗CPU的资源,进程状态的切换需要消耗CPU资源
    信号量保护的临界区可以很大,也可以有延时、耗时、休眠的操作
    信号量不会出现死锁
    信号量只能用于进程上下文
    信号量不会关闭抢占
API
1.定义一个信号量
    struct semaphore sema;
2.初始化信号量
    void sema_init(struct semaphore *sem, int val)
    参数:
        sem:信号量指针
        val:给信号量的初始值
3.获取信号量(上锁)
    void down(struct semaphore *sem)//信号量数值-1
4.释放信号量(解锁)
    void up(struct semaphore *sem);

4,互斥体

    一个进程想要访问临界资源需要先获取互斥体,如果获取不到,进程会切换到休眠状态
特点
    获取不到互斥体的进程会切换到休眠状态休眠状态下的进程不消耗CPU的资源,进程状态的切换需要消耗CPU资源
    互斥体保护的临界区可以很大,也可以有延时、耗时、休眠的操作
    互斥体不会出现死锁
    互斥体只能用于进程上下文
    互斥体不会关闭抢占
    获取不到互斥体的进程不会立即进入休眠状态,而是稍微等一会儿,互斥体的效率要比信号量更高
API
1.定义互斥体
    struct mutex mutex;
2.初始化互斥体
    mutex_init(&mutex);
3.上锁
    void  mutex_lock(struct mutex *lock)
4.解锁
    void  mutex_unlock(struct mutex *lock)

5,原子操作

    将进程访问临界资源的过程看作一个不可分割的原子状态。原子状态的实现通过修改原子变量额数值来实现,而原子变量数值的修改再内核里面是通过内联汇编来完成的。

API
1.定义原子变量并且初始化
    atomic_t atm=ATOMIC_INIT(1);//将原子变量的数值初始化为1
2.int atomic_dec_and_test(atomic_t *v)
    功能:将原子变量的数值-1并且和0比较
    参数:
        v:原子变量的指针
    返回值:如果原子变量-1后结果为0,则返回真,否则返回假
3.void atomic_inc(atomic_t *v)
    功能:原子变量的数值+1
***********************************
或者相反的-1
1.定义原子变量并且初始化
    atomic_t atm=ATOMIC_INIT(-1);//将原子变量的数值初始化为-1
2.int atomic_inc_and_test(atomic_t *v)
    功能:将原子变量的数值+1并且和0比较
    参数:
        v:原子变量的指针
    返回值:如果原子变量-1后结果为0,则返回真,否则返回假
3.void atomic_dec(atomic_t *v)
    功能:原子变量的数值+1

十一,【IO模型】

什么是IO模型?为什么要设计不同的IO模型
    IO模型就是对文件的不同读写方式。
    在驱动中对硬件数据的读写需要通过读写设备文件来实现,而读取设备文件根据需求也有不同的方式,所以在这里我们要研究不同的IO模型的实现。
    IO模型分为非阻塞IO、阻塞IO、IO多路复用、信号驱动IO。

    read/write 是否阻塞跟 open的打开方式有关,通常为阻塞方式打开,打开文件时添加O_NONBLOCK可以实现非阻塞方式
所以在驱动程序中可以通过标志位判断是否为阻塞方式

1,非阻塞IO

非阻塞IO
    当进程通过read()读取硬件数据时,不管数据是否准备好,read函数立即返回。通过非阻塞IO,read函数有可能读到的数据不是本次准备好的数据。在打开文件时可以添加O_NONBLOCK flag来实现文件的非阻打开

***********应用程序************
int fd=open("/dev/mycdev",O_RDWR|O_NONBLOCK);//以非阻塞的模式打开文件
read(fd,buf,sizeof(buf));
*********驱动程序**************
ssize_t mycdev_read(struct file *file, char *ubuf, size_t size, loff_t *lof)
{
    int ret;
      if(file->f_flags&O_NONBLOCK)
      {
          1.读取硬件的数据
          2.copy_to_user将硬件数据传递到用户空间                  
      }
    return 0;
}

2,阻塞IO

    当进程在读取硬件的数据时,如果此时硬件数据准备就绪就读取,没有准备就绪则进程阻塞在read函数位置一直等到数据就绪。当硬件数据准备就绪后,硬件会发起硬件中断将休眠的进程唤醒,被唤醒后的进程停止阻塞,将准备好的硬件数据读走。阻塞状态下的进程处于休眠态,休眠态分为可中断休眠态和不可中断休眠态:
    S    interruptible sleep (waiting for an event to complete)//可中断休眠态,可以被外部信号打断
    D    uninterruptible sleep (usually IO)

    实现过程
***********应用程序************
    int fd=open("/dev/mycdev",O_RDWR);//以阻塞的模式打开文件
    read(fd,buf,sizeof(buf));
*********驱动程序**************
ssize_t mycdev_read(struct file *file, char *ubuf, size_t size, loff_t *lof)
{
    int ret;
      if(file->f_flags&O_NONBLOCK)
      {
          1.读取硬件的数据
          2.copy_to_user将硬件数据传递到用户空间                
      }
      else//阻塞IO
      {
          1.判断硬件数据是否准备好
          2.如果数据没有准备好,将进程切换为休眠状态
          3.读取硬件数据
          4.copy_to_user      
      }
  
    return 0;
}

//硬件的中断处理程序
irq_handler()
{
    1.确定硬件数据准备就绪
    2.唤醒休眠的进程
}

阻塞IO实现相关的API

1.定义一个等待队列头
    wait_queue_head_t wq_head;
2.初始化等待队列
    init_waitqueue_head(&wq_head);
3.wait_event(wq_head, condition)
    功能: 将进程切换为不可中断的休眠态
    参数:
        wq_head:等待队列头
        condition:标识硬件数据是否就绪的标志变量
    返回值:无
4.wait_event_interruptible(wq_head, condition)
    功能:将进程切换为可中断的休眠态
    参数:
        wq_head:等待队列头
        condition:标识硬件数据是否就绪的标志变量
    返回值:当硬件数据准备好后进程正常被唤醒返回0,如果进程被外部信号中断休眠则返回错误码 -ERESTARTSYS

5.wake_up(&wq_head)
    功能:唤醒不可中断休眠态的进程,如果在condition为假的情况下调用此函数,休眠的进程被唤醒后会马上再次休眠
    参数:
    等待队列头指针
    返回值:无

6.wake_up_interruptible(&wq_head)
    功能:唤醒可中断休眠态的进程,如果在condition为假的情况下调用此函数,休眠的进程被唤醒后会马上再次休眠
    参数:
    等待队列头指针
    返回值:无

3,IO多路复用

    当在应用程序中同时实现对多个硬件数据读取时就需要用到IO多路复用。io多路复用有select/poll/epoll三种实现方式。如果进程同时监听的多个硬件数据都没有准备好,进程切换进入休眠状态,当一个或者多个硬件数据准备就绪后,休眠的进程被唤醒,读取准备好的硬件数据。

***********************VFS(虚拟文件系统层)*********
sys_select()
{
    1.在内核申请一片内存用于保存从用户空间的文件描述符集合中拷贝的文件描述符,拷贝完毕后用户的事件集合被清空
    2.根据文件描述符集合中的每一个文件描述符按照fd->fd_array[fd]->struct file对象->操作方法对象->poll方法  ,按照这个路线回调每个fd对应的驱动中的poll方法
    3.判断每个文件描述符的poll方法的返回值,如果所有的poll方法的返回值都为0,表示没有任何硬件数据准备就绪,此时将进程切换为休眠态(可中断休眠态)
    4.当休眠的进程收到一个或者多个事件就绪的唤醒提示后,在这里根据事件集合中的每一个文件描述符再次回调poll方法,找出发生事件的文件描述符
    5.将发生事件的文件描述符重新拷贝回用户空间的事件集合
}
*************************驱动程序****************
//所有的io复用方式在驱动中对应的操作方法都是poll方法
    __poll_t (*poll) (struct file *file, struct poll_table_struct *wait)
    {
              //向上提交等待队列头
       void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
       /*功能:将等待队列头向上层提交
       参数:
       filp:文件指针,将poll方法第一个参数填进去
       wait_address:要向上提交的等待队列头地址
       p:设备驱动和上层关联的通道,将poll方法的第二个参数填进去
       返回值:无*/
            //判断condition的值,根据事件是否发生给一个合适的返回值
            if(condition){
                return POLLIN;//POLLIN表示读   POLLLOUT表示写            
            }else{
                return 0;            
            }
    }

epoll的实现

核心操作:一棵树(红黑树),一张表,三个接口

API:

int epoll_create(int size);
功能:创建一个epoll句柄//创建红黑树根节点
epoll把要监测的事件文件描述符挂载到红黑树上
参数:size 没有意义,但是必须>0
返回值:成功返回根节点对应的文件描述符,失败返回-1

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:实现对于epoll的控制
参数:
epfd:epoll_create创建的句柄
op:控制方式
     EPOLL_CTL_ADD:添加要监测的事件文件描述符
     EPOLL_CTL_MOD:修改epoll检测的事件类型
     EPOLL_CTL_DEL:将文件描述符从epoll删除
fd:要操作的文件描述符
event:事件结构体
typedef union epoll_data {
               void        *ptr;
               int          fd;//使用这个
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

 struct epoll_event {
               uint32_t     events; //EPOLLIN(读) EPOLLOUT(写)
               epoll_data_t data;        /* User data variable */
           };
返回值:成功返回0,失败返回-1

int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
功能:阻塞等待准备好的文件描述符
参数:
epfd:epoll句柄
events:存放就绪事件描述符的结构体数组首地址
maxevents:监听的事件最大个数
timeout:超时检测
    >0:毫秒级检测
    ==0:立即返回
    -1:不关心是否超时

返回值:
>0:准备好的文件描述符的个数
==0:超时
<0:失败

4,信号驱动IO

概述:
    信号驱动IO是一种异步IO方式,linux预留了一个信号SIGIO用于进行信号驱动IO,进程主程序注册一个SIGIO信号的信号处理函数,当硬件数据准备就绪后会发起一个硬件中断,在中断的处理函数中向当前进程发送一个SIGIO信号,进程收到SIGIO信号执行信号处理函数,在信号处理函数中读走即可

实现过程:

应用程序:
    1,打开设备文件:
    2,注册信号的信号处理函数signal(SIGIO,信号处理函数名)
    3,回调驱动中的fasync方法,完成发送信号之前的准备工作
        获取文件描述符属性int flags = fcntl(fd, F_GETFL);
        在文件描述符表的flags中添加FASYNC异步处理方式,就可以回调fasync方法
    4,设置当前fd对应的驱动程序接收SIGIO信号 fcntl(fd,F_SETOWN,getpid());
    5,不让主程序结束,等待中断信号

驱动程序:
    1,定义一个异步对象指针
    2,封装异步操作方法的函数,完成异步空间对象的空间分配和初始化
    3,封装处理函数

API:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
功能:完成异步对象的空间分配和初始化

void kill_fasync(struct fasync_struct **fp, int sig, int band)
    功能:向进程发送信号
    参数:
    fp:异步对象的二级指针
    sig:要发生的信号  SIGIO
    band:发送信号时添加的事件标志   POLL_IN表述读数据操作

十二,【设备树】

什么是设备树    
    设备树(DeviceTree/DT/of)是用来保存设备信息的一种树形结构。设备树的源码是独立于linux内核源码存在的。设备树上的设备信息在内核启动后被内核解析,加载到内核空间。以树形结构包窜在内核空间中。设备树上的每一个节点都是用来保存一个设备的设备信息。一个设备的信息由多种树形共同描述。一个设备的多个属性在内核空间中是以链表的形式存在,链表的每一个节点都表示这个设备的一个属性

为什么引入设备树
    按照之前驱动的编写方式,在驱动中直接固化了硬件的设备信息,这种形式编写的驱动只适用于特定的硬件,一旦硬件环境更换,驱动就无法正常使用了。现在为了让驱动可以兼容更多硬件,我们不在驱动中指定设备信息,而是引入了设备树。驱动中获取设备树上的设备信息,基于这些设备信息完成硬件的控制

设备树的文件格式
    设备树源码路径/linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts
    ***.dts --设备树的源码文件
    ***.dtsi --设备树的补充文件,类似于c语言的h文件
    |
    |通过DTC编译工具 执行 make dtbs 编译设备树文件的命令生成 ***.dtb设备树的镜像文件

如何启用设备树和设备树的编译工具DTC:
    打开内核的顶层目录下的.config文件,在文件中有如下两个配置则说明当前内核已经启用的设备树和DTC工具: 
       CONFIG_DTC = y
       CONFIG_OF  = y

1,设备树语法

设备树linux官方手册:Device Tree Usage - eLinux.org

基本语法格式:
    设备树是节点和属性的简单树结构。属性是键值对,节点可以同时 包含属性和子节点

例:
/dts-v1/;//设备树的版本号

/ {  // '/'表示根节点
    node1 {  //node1是根节点的子节点
        a-string-property = "A string";//node1节点的属性键值对
        a-string-list-property = "first string", "second string";
        // hex is implied in byte arrays. no '0x' prefix is required
        a-byte-data-property = [01 23 34 56];
        child-node1 {  //child-node1是node1节点的子节点
            first-child-property;//child-node1节点的额属性键值对,空属性
            second-child-property = <1>;
            a-string-property = "Hello, world";
        };
        child-node2 {
        };
    };
    node2 {// 根节点的子节点
        an-empty-property;
        a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
        child-node1 {
        };
    };
};

设备树节点的命名格式: <name>@<unit-address>
    <name>是一个简单的 ASCII 字符串,长度最多为 31 个字符。通常,节点是根据它所代表的设备类型来命名的。
    如果节点使用地址描述设备,则包含单元地址。通常,单元地址是用于访问设备的主要地址,列在节点的                                属性中。
ex:
    1.gpio@50006000{};//gpioe控制器节点的名字,gpio支持寻址,所以要在名字里加上地址
    2.LED1{};

关于设备树节点的别名和合并问题
1.
aliases {
        serial0 = &uart4;
        serial5 = &usart3;
    };
解释:上面节点中serial0就是给uart4起了一个别名
2.
gpioe: gpio@50006000 {
                gpio-controller;
                #gpio-cells = <2>;
                ...
            };
解释:gpioe是gpio@50006000节点的标签,在别的位置操作gpioe相当于操作到了gpio@50006000节点
3.
    两个文件中有同名节点,按照设备树的编译规则,同级目录下有相同名字的节点,节点会合并,如果相同节点中属性名相同,后一次的值会覆盖前一次的值,如果属性名不同,直接合并

属性键值对的数据类型
    属性是简单的键值对,其中值可以为空或包含任意字节流。虽然数据类型未编码到数据结构中,但可以在设备树源文件中表示一些基本数据表示形式
    文本字符串(以null结尾)用双引号表示:string-property = "a string";
    "cell"是32位无符号整数,由尖括号分割:cell-property = <0xbeef 123 0xabcd1234>;
    单字节数据用方括号分割:binary-property = [0x01 0x23 0x45 0x67];
    不同表示形式的数据可以用逗号链接在一起: mixed-property = "a string", [0x01 0x02 0x03 0x04], <0x12345678>;
    逗号也用于创建字符串列表:string-list = "red fish","blue fish";

常用标准化的属性键值对
    在设备树中有一些特定的键值对用来表示特定的含义:
compatible = "芯片厂商,芯片型号";//描述当前设备的厂商信息
device_type:用于描述当前设备的设备类型
reg=<地址,内存大小>:用于描述当前节点对应设备的寻址内存首地址和大小
#address-cells=<n>:用于指定子节点中reg属性用来描述地址的u32的个数
#size-cells=<n>:用于指定子节点中reg属性用来描述地址对应内存大小的u32的个数

2,添加一个自定义的设备树节点到设备树源码中被内核解析

1,添加设备树节点
    在stm32mp157a-fsmp1a.dts文件的根节点内部添加如下内容:
    //自定义设备树
    mynode@0x12345678{
        compatible = "hqyj,mynode";
        astring="hello 23051";
        uint  =<0xaabbccdd 0x11223344>;
        binarry=[00 0c 29 7b f9 be];
        mixed ="hello",[11 22],<0x12345678>;
     };
2,编译设备树
    返回到内核顶层目录下执行编译设备树的命令make dtbs
3,将镜像复制到~/tftpboot中,重启开发板
4,查看自己添加的节点是否被成功解析
    开发板系统目录:/proc/device-tree/目录下是否有以节点名为名的文件夹生成

3,在驱动程序中获取设备树中指定的设备信息

设备树节点结构体struct device_node
    当设备树中的信息加载到内核空间后,每一个节点都是一个struct device_node类型
struct device_node {
    const char *name;//设备树节点的名字mynode
    phandle phandle;//节点标识
    const char *full_name;//全名  mynode@0x12345678
    struct  property *properties;//属性链表首地址
    struct  device_node *parent;//父节点指针
    struct  device_node *child;//子节点指针
    struct  device_node *sibling;//兄弟节点指针
};

属性结构体 struct propety
    一个设备树节点中存在多个属性,组成了一个链表,链表中每一个节点保存了设备的一个信息,链表节点的类型是struct propety类型
struct property {
    char    *name;//键名
    int length;//数值的大小
    void    *value;//数值首地址
    struct property *next;//下一个属性对象指针
};

4,设备树节点解析API & 属性解析API

设备树节点解析:

struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
    功能:根据设备树节点的名字解析指定的设备树节点信息
    参数:
        from:要解析的节点所在子树的根节点,填NULL,默认从根节点解析
        name:要解析的设备树节点的名字
    返回值:成功返回目标节点首地址,失败返回NULL

struct device_node *of_find_node_by_path(const char *path);
    功能:根据设备树节点路径解析设备树节点信息
    参数:
        path:设备树所在的节点路径,非文件路径 例:/mynode@0x12345678 
    返回值:成功返回目标节点首地址,失败返回NULL

struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat);
    功能:根据节点的厂商信息解析指定的节点
    参数:
        from:要解析的节点所在子树的根节点,填NULL,默认从根节点解析
        type:设备类型,填NULL
        compat:compatible值
    返回值:成功返回目标节点首地址,失败返回NULL

__u32 __be32_to_cpup(const __be32 *p)
    功能:将大端字节序32位的数据转换为主机字节序
    参数:要转换的数据指针
    返回值:转换后的值


设备树属性解析:
struct propety *of_find_propety(const struct device_node *np, const char *name, int *lenp)
    功能:解析指定键名的属性信息
    参数:
        np:设备树节点对象指针
        name:要解析的属性键名
        lemp:解析到的属性的值的长度
    返回值:成功返回属性对象指针,失败返回NULL
相关推荐
JunLan~2 小时前
Rocky Linux 系统安装/部署 Docker
linux·docker·容器
方竞3 小时前
Linux空口抓包方法
linux·空口抓包
sun0077004 小时前
ubuntu dpkg 删除安装包
运维·服务器·ubuntu
海岛日记4 小时前
centos一键卸载docker脚本
linux·docker·centos
oi775 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
AttackingLin5 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
shuangrenlong6 小时前
slice介绍slice查看器
java·ubuntu
学Linux的语莫6 小时前
Ansible使用简介和基础使用
linux·运维·服务器·nginx·云计算·ansible