前言
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)格式,是一种普通的可重定位目标文件。 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。
加载模块驱动有两种命令:insmod
和modprobe
,区别是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_user
和copy_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_user
和copy_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] 用户空间和内核空间的区别