赞
踩
在简单结束应用层的开发学习后,本系列将开启驱动层的学习,本文作为该系列第一期旨在归纳前期需要准备的知识。
FPGA的编程更多地依赖于硬件描述语言(HDL),如VHDL或Verilog,用于描述电路的结构和行为。这种编程方法将硬件电路抽象为逻辑门、寄存器等基本元素,并通过编写代码来描述它们之间的连接关系和功能。相较于其他两种编程方式,FPGA编程需要频繁抓取时钟,这是因为FPGA的并行性、硬件描述方式、精确的时序控制需求以及性能优化等方面的要求。这种时钟抓取机制使得FPGA能够高效地处理复杂的数字逻辑任务,并在各种应用中实现高性能和灵活性。
相比之下,MCU的编程通常更侧重于顺序执行和寄存器的直接操作。因为MCU的操作是顺序执行的,开发者可以通过直接读写寄存器来管理内部状态和操作。这种编程方式相对简单直观,但可能不如FPGA在并行处理和复杂逻辑实现上灵活。
至于Linux框架下的驱动编程,它与FPGA和单片机的编程方式有显著的不同。Linux驱动编程主要关注于设备与操作系统之间的交互,包括设备节点的创建、设备驱动程序的编写和与操作系统的接口实现等。驱动程序需要处理设备的硬件特性,并将其抽象为操作系统可以理解和操作的接口。这种编程方式更注重于软件的架构和接口设计,与硬件的交互通常是通过特定的接口和协议来实现的。
在Linux系统中,设备驱动会以内核模块的形式出现,学习Linux内核模块编程是驱动开发的先决条件。 第一次接触Linux内核模块,我们将围绕着“Linux内核模块是什么”,“Linux内核模块的工作原理”以及 “我们该怎么使用Linux内核模块”这样的思路一起走进Linux内核世界。
Linux内核模块是Linux内核向外部提供的一个插口,也被称为动态可加载内核模块(Loadable Kernel Module,LKM)。它是一个具有独立功能的程序,可以被单独编译,但不能独立运行。在运行时,内核模块被链接到内核作为内核的一部分在内核空间运行。
内核模块的主要作用是扩展内核的功能,而无需重新编译整个内核。例如,内核模块通常用于添加新的设备驱动程序、文件系统或其他功能到内核中。通过内核模块,Linux内核能够实现内核功能扩展,提供新的系统调用或特性,甚至实现内核的安全增强以增加系统的安全性。
这里展示一张图片可以让大家直观地感受一下Linux的内核体系(Monolithic Kernel)。
可以看到Linux所使用的宏内核架构是将包括微内核(Microkernel)以及微内核之外的应用层IPC、文件系统功能、设备驱动模块都编译成一个整体。 其优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。 Linux操作系统正是采用了宏内核结构。为了解决这一缺点,linux中引入了内核模块这一机制。
Linux内核模块的代码框架通常由下面几个部分组成:
参数 | 功能 |
---|---|
MODULE_LICENSE() | 表示模块代码接受的软件许可协议,Linux内核遵循GPL V2开源协议,内核模块与linux内核保持一致即可 |
MODULE_AUTHOR() | 描述模块的作者信息 |
MODULE_DESCRIPTION() | 对模块的简单介绍 |
MODULE_ALIAS() | 给模块设置一个别名 |
这里给出一个简单的示例(注:仅作介绍使用):
#include <linux/module.h> // 包含内核模块所需的头文件 #include <linux/kernel.h> // 模块许可证声明,这是必须的,且必须是这种形式的宏定义 MODULE_LICENSE("Dual BSD/GPL"); // 模块初始化函数,当模块被加载时调用 static int __init my_module_init(void) { // 初始化代码,比如申请资源、注册设备驱动等 printk(KERN_INFO "My module has been loaded.\n"); // 返回0表示初始化成功,非0值表示失败 return 0; } // 模块清理函数,当模块被卸载时调用 static void __exit my_module_exit(void) { // 清理代码,比如释放资源、注销设备驱动等 printk(KERN_INFO "My module has been unloaded.\n"); } // 使用module_init和module_exit宏来指定初始化函数和清理函数 module_init(my_module_init); module_exit(my_module_exit);
前面我们已经接触过了Linux的应用编程,了解到Linux的头文件都存放在/usr/include中。 编写内核模块所需要的头文件,并不在上述说到的目录,而是在Linux内核源码中的include文件夹。编写内核模块中经常要使用到的头文件有以下两个:<linux/init.h>和<linux/module.h>。
#include <linux/module.h>//包含内核模块信息声明的相关函数
#include <linux/init.h>//包含了 module_init()和 module_exit()函数的声明
#include <linux/kernel.h>//包含内核提供的各种函数,如printk
模块参数允许用户在加载模块时通过命令行指定参数值。这些参数在模块的加载过程中被获取,并转换成相应类型的值,然后赋值给对应的变量,这个过程常常发生在函数调用之前。
module_param(name, type, perm)
/*
name: 我们定义的变量名
type: 参数的类型,目前内核支持的参数类型有byte,short,ushort,int,uint,long,ulong,charp,bool,
invbool。其中charp表示的是字符指针,bool是布尔类型,其值只能为0或者是1;invbool是反布尔类型,其值
也是只能取0或者是1,但是true值表示0,false表示1。变量是char类型时,传参只能是byte,char * 时只能是
charp
perm: 表示的是该文件的权限,详细参考下表
*/
用户 | 参数 | 功能 |
---|---|---|
当前用户 | S_IRUSR | 用户拥有读权限 |
S_IWUSR | 用户拥有写权限 | |
当前用户组 | S_IRGRP | 当前用户组的其他用户拥有读权限 |
S_IWUSR | 当前用户组的其他用户拥有写权限 | |
其他用户 | S_IROTH | 其他用户拥有读权限 |
S_IWOTH | 其他用户拥有写权限 |
模块参数的使用通常涉及以下步骤:
对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。 为此,我们在编译时需要到内核源码目录下进行编译。 编译内核模块使用的Makefile文件,和我们前面编译C代码使用的Makefile大致相同, 这得益于编译Linux内核所采用的Kbuild系统,因此在编译内核模块时,我们也需要指定环境变量ARCH和CROSS_COMPILE的值。
KERNEL_DIR=../../../kernel/
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export ARCH CROSS_COMPILE
obj-m := XXX.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
以上代码中提供了一个关于编译内核模块的Makefile。
第1行:该Makefile定义了变量KERNEL_DIR,来保存内核源码的目录,需要指定到内核编译输出目录下。
第3-5行: 指定了工具链并导出环境变量
第6行:变量obj-m保存着需要编译成模块的目标文件名。
第8行:$ (MAKE)的MAKE是Makefile中的宏变量,要引用宏变量要使用符号。这里实际上就是指向make程序,所以这里也可以把$ (MAKE)换成make,通过选项’-C’,可以让make工具跳转到源码目录下读取顶层Makefile。 ‘M=$(CURDIR)’表明返回到当前目录,读取并执行当前目录的Makefile,开始编译内核模块。CURDIR是make的内嵌变量,自动设置为当前目录。
在之前的应用开发中,我们多次使用到open、write及read函数等进行数据的传输,设备节点的链接等等操作。在驱动设计时,我们其实也是同样的套路,不同之处在于我们需要写出自己驱动的open等函数。
在内核中,dev_t用来表示设备编号,dev_t是一个32位的数,其中,高12位表示主设备号,低20位表示次设备号。 也就是理论上主设备号取值范围:0-212 ,次设备号0-220。 实际上在内核源码中__register_chrdev_region(…)函数中,major被限定在0-CHRDEV_MAJOR_MAX,CHRDEV_MAJOR_MAX是一个宏,值是512。 在kdev_t中,设备编号通过移位操作最终得到主/次设备号码,同样主/次设备号也可以通过位运算变成dev_t类型的设备编号。
在驱动开发过程中,不可避免要涉及到三个重要的的内核数据结构分别包括文件操作方式(file_operations), 文件描述结构体(struct file)以及inode结构体,在我们开始阅读编写驱动程序的代码之前,有必要先了解这三个结构体。
file_operations是 Linux 内核中的一个重要的数据结构,用于表示内核中的一个文件所支持的操作集合。这个结构体定义了一系列的文件操作函数,如打开文件、读取文件、写入文件、关闭文件等。这些函数被内核用于处理与文件相关的各种请求。
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); };
这里是一些主要成员的解释:
file是 Linux 内核中用于描述打开的文件或设备的一个核心数据结构。每当一个文件或设备被用户空间进程打开时,内核都会为其创建一个 struct file 结构体实例,并关联到该进程的文件描述符上。这个结构体包含了与该文件或设备相关的各种信息和操作。
struct file 的定义通常包括文件的偏移量、文件操作函数集(通过 f_op 指向 file_operations 结构体)、文件所有者、文件类型等信息。以下是一个简化的 struct file 的定义示例:
struct file {
struct list_head f_u;
struct dentry *f_dentry;
struct vfsmount *f_vfsmnt;
const struct file_operations *f_op;
mode_t f_mode;
loff_t f_pos;
unsigned int f_flags;
fmode_t f_mode_orig;
struct file_lock *f_lock;
struct address_space *f_mapping;
/* ... 其他成员 ... */
};
这里是一些主要成员的解释:
inode是 Linux 内核中用于表示文件系统中一个具体文件或目录的元数据的数据结构。它包含了与文件或目录相关的各种信息,如权限、所有者、大小、时间戳等。inode 的存在使得文件系统能够高效地管理文件和目录。每个文件和目录在文件系统中都对应一个唯一的 inode。这些 inode 通常存储在磁盘的特定区域,称为 inode 表。通过 inode 的索引,文件系统能够快速地定位到文件或目录的数据块,并进行读写操作。
struct inode { dev_t i_rdev; {......} union { /* linux内核管道 */ struct pipe_inode_info *i_pipe; /* 如果这是块设备,则设置并使用 */ struct block_device *i_bdev; /* 如果这是字符设备,则设置并使用 */ struct cdev *i_cdev; char *i_link; unsigned i_dir_seq; }; {......} };
struct inode 结构体包含了很多字段,以下是一些重要的字段:
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫作散列函数,存放记录的数组叫作散列表。哈希表有以下几个特点:
哈希表在实际应用中非常广泛,例如在数据库索引、缓存系统、数据结构中的关联数组等地方都有使用。然而,哈希表并不适用于所有情况,它对于非均匀分布的数据具有较好的性能,但对于均匀分布或具有特定模式的数据,性能可能较差。此外,哈希表也不支持范围查询。
内核通过一个散列表(哈希表)来记录设备编号。 哈希表由数组和链表组成,吸收数组查找快,链表增删效率高,容易拓展等优点。
以主设备号为cdev_map编号,使用哈希函数f(major)=major%255来计算组数下标(使用哈希函数是为了链表节点尽量平均分布在各个数组元素中,提高查询效率); 主设备号冲突,则以次设备号为比较值来排序链表节点。 如下图所示,内核用struct cdev结构体来描述一个字符设备,并通过struct kobj_map类型的散列表cdev_map来管理当前系统中的所有字符设备。
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
在Linux内核中,struct kobj_map是一个用于管理kobject(内核对象)映射的数据结构,其与上文提到的cdev结构体共同组成了存放设备号的哈希表结构,这有助于高效地管理大量的设备号及其对应的设备。通过哈希表,内核可以快速定位到特定的设备号,从而实现高效的设备号查找和管理。
struct kobj_map { struct probe { //指向下一节点 struct probe *next; //设备号 dev_t dev; //次设备号数量 unsigned long range; struct module *owner; kobj_probe_t *get; int (*lock)(dev_t, void *); //空指针 void *data; } *probes[255]; struct mutex *lock; };
*data用于保存cdev结构体中的指针。
在file_operations结构体中,我们提到read和write函数时,需要使用copy_to_user函数以及copy_from_user函数来进行数据访问,写入/读取成 功函数返回0,失败则会返回未被拷贝的字节数。
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。