嵌入式驱动学习第五周——驱动模块

前言

   Linux驱动有两种运行方式,第一种是将驱动编译进Linux内核中,另一种是编译成模块,本篇博客来介绍一下驱动模块。

   嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!

1. 驱动模块介绍

   Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。

   将驱动编译成模块最大的好处就是方便开发,当驱动开发完成,确定没有问题后就可以选择性的将驱动编译进Linux内核中。

   整体驱动如下所示,应用软件根据主次设备号查看设备节点,调用相应的底层驱动。

在这里插入图片描述

2. 模块加载与卸载

   模块有加载和卸载两种操作,在编写驱动时需要注册这两种操作函数,即入口和出口:

module_init(xxx_init);		// 注册模块加载函数
module_exit(xxx_exit);		// 注册模块卸载函数

   module_init是用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当在终端使用insmod命令加载驱动时,xxx_init函数就会被调用。同样的道理,module_exit是用来向Linux内核注册一个模块卸载函数,参数xxx_exit是需要注册的具体函数,当在终端使用rmmod时,xxx_exit函数就会被调用。驱动模块加载和卸载模板如下:

// 驱动入口函数
static int __init xxx_init(void) {
	// 入口函数的具体内容
	return 0;
}

// 驱动出口函数
static void __exit xxx_exit(void) {
	// 入口函数的具体内容
	return 0;
}

module_init(xxx_init);		// 注册模块加载函数
module_exit(xxx_exit);		// 注册模块卸载函数

   __init 标记的函数会在模块被加载时执行,用于初始化驱动所需的数据结构、注册设备、申请资源等操作。这些函数只会在模块加载时执行一次,之后就不再需要,因此被标记为 __init,以表示它们只在初始化时执行。__exit同理。

   驱动编译完成后生成一个扩展名为.ko的模块驱动文件,ko文件在数据组织形式上是ELF(Excutable And Linking Format)格式,是一种普通的可重定位目标文件。 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。

在这里插入图片描述

   加载模块驱动有两种命令:insmodmodprobe,区别是insmod不能解决模块依赖关系,比如led.ko依赖于gpioled.ko,就必须先试用insmod加载gpioled.ko,再加载led.ko。但是modprobe就会分析模块依赖关系,然后将所有依赖模块加载到内核中。推荐使用modprobe

   modprobe查找模块的目录为:/lib/modules/<kernel-version>,如果没有该目录,需要像我上面一样新建一个目录。

   整体模块驱动结构如下:

在这里插入图片描述

3. 用户空间和内核空间

   为了彻底解决一个应用程序出错不影响系统和其它app的运行,操作系统给每个app一个独立的假想的地址空间,这个假想的地址空间被称为虚拟地址空间(也叫逻辑地址),操作系统也占用其中固定的一部分,32位Linux的虚拟地址空间大小为4G,并将其划分两部分:

0~3G 用户空间 :每个应用程序只能使用自己的这份虚拟地址空间
3G~4G 内核空间:内核使用的虚拟地址空间,应用程序不能直接使用这份地址空间,但可以通过一些系统调用函数与其中的某些空间进行数据通信

在这里插入图片描述

   所有系统资源的管理都是在内存空间进行的,也就是在内核态去做的,那我们应用程序需要访问磁盘,读取网卡的数据,新建一个线程都需要通过系统调用接口,完成从用户态到内存态的切换。

   但是内核态和用户态不能相互访问,因此需要使用copy_from_usercopy_to_user将数据拷贝到所需要的地方。

 /*
  * @description: 将数据从用户空间复制到内核空间
  * @param-to   : 指向内核空间的指针
  * @param-from : 用户空间的指针
  * @param-n	: 复制的字节数
  * @return     : 该函数返回未能复制的字节数,如果返回值为0,则表示全部复制成功
  */
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
 /*
  * @description: 将数据从内核空间复制到用户空间
  * @param-to   : 指向用户空间
  * @param-from : 内核空间的指针
  * @param-n	: 复制的字节数
  * @return     : 该函数返回未能复制的字节数,如果返回值为0,则表示全部复制成功
  */
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

   需要注意的是,由于用户空间和内核空间是分离的,因此在进行数据传输时需要进行安全检查,以防止非法访问。在使用copy_from_usercopy_to_user函数时,需要使用access_ok函数进行检查,以确保指针指向的内存区域是合法的。例如:

if (access_ok(VERIFY_READ, from, cnt)) {
    if (copy_from_user(to, from, cnt)) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }
} else {
    printk("Illegal memory");
}

参考资料

[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第四十章
[2] 内核空间与用户空间
[3] 用户空间和内核空间的区别

  • 16
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值