最全Linux驱动开发八股文(二)

你好,我是拉依达。

这是我的Linux驱动开发八股文详细解析系列

本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。

全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料

现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。

4.3 内核模块

Linux 驱动有两种运行方式

  • 驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序。
  • 驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用相应命令加载驱动模块。
    • 内核模块是Linux内核向外部提供的一个插口
    • 内核模块是具有独立功能的程序,他可以被单独编译,但不能单独运行。他在运行时被链接到内核作为内核的一部分在内核空间运行
    • 内核模块便于驱动、文件系统等的二次开发

内核模块组成

  1. 模块加载函数

    module_init(xxx_init);
    
    • module_init 函数用来向 Linux 内核注册一个模块加载函数,
    • 参数 xxx_init 就是需要注册的具体函数(理解是模块的构造函数)
    • 当加载驱动的时, xxx_init 这个函数就会被调用
  2. 模块卸载函数

    module_exit(xxx_exit);
    
    • module_exit函数用来向 Linux 内核注册一个模块卸载函数,
    • 参数 xxx_exit 就是需要注册的具体函数(理解是模块的析构函数)
    • 当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用
  3. 模块许可证明

    MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议
    
  4. 模块参数(可选) 模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 --> 内核空间单向的,他对应模块内部的全局变量

  5. 模块信息(可选)

    MODULE_AUTHOR("songwei") //添加模块作者信息
    
  6. 模块打印 printk printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。 printk函数主要做两件事情:①将信息记录到log中 ②调用控制台驱动来将信息输出

  • printk 可以根据日志级别对消息进行分类,一共有 8 个日志级别

    #define KERN_SOH  "\001" 
    #define KERN_EMERG KERN_SOH "0"  /* 紧急事件,一般是内核崩溃 */
    #define KERN_ALERT KERN_SOH "1"  /* 必须立即采取行动 */
    #define KERN_CRIT  KERN_SOH "2"  /* 临界条件,比如严重的软件或硬件错误*/
    #define KERN_ERR  KERN_SOH "3"  /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
    #define KERN_WARNING KERN_SOH "4"  /* 警告信息,不会对系统造成严重影响 */
    #define KERN_NOTICE  KERN_SOH "5"  /* 有必要进行提示的一些信息 */
    #define KERN_INFO  KERN_SOH "6"  /* 提示性的信息 */
    #define KERN_DEBUG KERN_SOH "7"  /* 调试信息 */
    
  • 以下代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。

    printk(KERN_DEBUG"gsmi: Log Shutdown Reason\n");
    

    如果使用 printk 的时候不显式的设置消息级别,那 么printk 将会采用默认级别MESSAGE_LOGLEVEL_DEFAULT,默认为 4

  • 在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:

    #define CONSOLE_LOGLEVEL_DEFAULT 7
    

    CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。

    这个就是 printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为 4,4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制台上的。

模块操作命令

  1. 加载模块
    • insmod XXX.ko
      • 为模块分配内核内存、将模块代码和数据装入内存、通过内核符号表解析模块中的内核引用、调用模块初始化函数(module_init)
      • insmod要加载的模块有依赖模块,且其依赖的模块尚未加载,那么该insmod操作将失败
    • modprobe XXX.ko
      • 加载模块时会同时加载该模块所依赖的其他模块,提供了模块的依赖性分析、错误检查、错误报告
      • modprobe 提示无法打开“modules.dep”这个文件 ,输入 depmod 命令即可自动生成 modules.dep
  2. 卸载模块
    • rmmod XXX.ko
  3. 查看模块信息
    • lsmod
      • 查看系统中加载的所有模块及模块间的依赖关系
    • modinfo (模块路径)
      • 查看详细信息,内核模块描述信息,编译系统信息

4.4 设备号

  • Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成
  • 主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
  • Linux 提供了一个名为 dev_t 的数据类型表示设备号其中高 12 位为主设备号, 低 20 位为次设备
  • 使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号(主)
MAJOR // 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
MINOR //用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
MKDEV //用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。

4.5 地址映射

MMU(Memory Manage Unit)内存管理单元

  1. 完成虚拟空间到物理空间的映射
  2. 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性
  3. 对于 32 位的处理器来说,虚拟地址(VA,Virtual Address)范围是 2^32=4GB

alt

内存映射函数

CPU只能访问虚拟地址,不能直接向寄存器地址写入数据,必须得到寄存器物理地址在Linux系统中对应的虚拟地址

物理内存和虚拟内存之间的转换,需要用到: ioremap 和 iounmap两个函数

  • ioremap,用于获取指定物理地址空间对应的虚拟地址空间

    /*
    phys_addr:要映射给的物理起始地址(cookie)
    size:要映射的内存空间大小
    mtype: ioremap 的类型,可以选择 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC, 
    ioremap 函数选择 MT_DEVICE
    返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址
    */
    #define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)
     
    void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
    {
        return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
    }
    

    例:获取某个寄存器对应的虚拟地址

    #define addr (0X020E0068)  // 物理地址
    static void __iomem*  va; //指向映射后的虚拟空间首地址的指针
    va=ioremap(addr, 4);   // 得到虚拟地址首地址
    
  • iounmap,卸载驱动使用 iounmap 函数释放掉 ioremap 函数所做的映射。 参数 addr:要取消映射的虚拟地址空间首地址

    iounmap(va);
    

I/O内存访问函数

外部寄存器外部内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。

使用 ioremap 函数将寄存器的物理地址映射到虚拟地址后,可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作

  • 读操作函数

    u8 readb(const volatile void __iomem *addr)
    u16 readw(const volatile void __iomem *addr)
    u32 readl(const volatile void __iomem *addr)
    

    readb、 readw 和 readl 分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值是读取到的数据

  • 写操作函数

    void writeb(u8 value, volatile void __iomem *addr)
    void writew(u16 value, volatile void __iomem *addr)
    void writel(u32 value, volatile void __iomem *addr)
    

    writeb、 writew 和 writel分别对应 8bit、 16bit 和 32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址。

#嵌入式##校招##linux##linux驱动##八股文#
最全Linux驱动开发八股文 文章被收录于专栏

你好,我是拉依达。 这是我的Linux驱动开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。 全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。 现在我重新对内容进行整理,已专栏的形式发布在牛客上,希望可以帮助到更多学习嵌入式的同学。

全部评论

相关推荐

1 2 评论
分享
牛客网
牛客企业服务