【主线剧情07.3】Linux驱动编程-内核态API总结备查
驱动程序 中的 设备相关 和 常用内核态 API 总结备查
多处网搜和引用,做了良好的整理,侵删。
本文对应的驱动源代码在 github/gitee 仓库里:并且 在 Github 上的原版文章日后可能会更新,但这里不会跟进。文章的 Gitee 仓库地址,Gitee 访问更流畅。
驱动模块编译和插入与系统版本一致性的重要说明
编译驱动程序:
首先编译一次内核(只一次),再编译驱动程序,因为编译后者需要用到前者编译后产生的一些文件,二者要使用同一套编译器工具链。
即使是不同的编译器,编译后的固件、模块的编排格式都有差异!
插入驱动模块:
编译驱动时用到的内核和编译器,与要插入的系统的内核的编译器尽量一致,即 内核版本号一致 和 编译器工具链一致,最好 内核源码、编译器 这些 始终 都是同一套东西!
如果 SoC 板子上 运行的 内核 和 编译驱动时候用到的内核源码的版本不一致,应尽量一致,这种情况也可以插入模块,但是会提示可能会有不兼容的不可预知状况!
如果 SoC 板子上 运行的 内核 和 编译驱动时候用到的内核源码的版本一致,但是编译器不同!这种情况模块是不能插入的,因为不同编译器的编排固件的格式会有差别,这时应该重新编译内核源码,把得到的 内核固件 zImage 、设备树 和 所有模块 都替换 SoC 板子上的。,就可以解决问题。
驱动程序和应用程序开源协议说明
驱动必须得采用和 Linux 内核一样的协议 GPL,
因此驱动程序必须随 Linux 源码一样开源,
好多商家为规避开源自己的核心代码,就将核心代码写在应用程序里面,应用程序不用开源,
由而 应用程序写很复杂 而 驱动写的较简单,由此避开自己的核心代码 带上 GPL 协议。
内核 API 查询
- Linux内核API|极客笔记 (deepinout.com) 极其好!!!
- .etc(用到时候慢慢补充)
驱动程序内的
主次设备号相关
在内核中,用dev_t类型(其实就是一个32位的无符号整数)的变量来保存设备的主次设备号,其中高12位表示主设备号,低20位表示次设备号。
设备获得主次设备号有两种方式:一种是手动给定一个32位数,并将它与设备联系起来(即用某个函数注册);另一种是调用系统函数给设备动态分配一个主次设备号。
与主次设备号相关的3个宏:
|
|
- MAJOR(dev_t dev):根据设备号 dev 获得主设备号。
- MINOR(dev_t dev):根据设备号 dev 获得次设备号。
- MKDEV(int major, int minor):根据主设备号 major 和次设备号 minor 构建 dev_t 类型设备号。
register_chrdev / unregister_chrdev
Linux内核API register_chrdev|极客笔记 (deepinout.com)。
|
|
- 其中参数major如果等于0,则表示采用系统动态分配的主设备号;不为0,则表示静态注册,范围为 1~255。
- name 是注册驱动的名子(出现在
/proc/devices
),fops 是 file_operations 结构。 - 函数register_chrdev()返回int型的结果,表示设备添加是否成功。如果成功返回0,如果失败返回-ENOMEM, ENOMEM的定义值为12。
Linux内核API unregister_chrdev|极客笔记 (deepinout.com)。
|
|
- 第一个输入参数代表即将被删除的字符设备区及字符设备的主设备号,函数将根据此参数查找内核中的字符设备。
- 第二个输入参数代表设备名,但在函数的实现源码中没有用到,没有什么意义。
动态字符设备创建
参考 字符设备驱动编写流程以及大概框架_辣眼睛的Developer的博客-CSDN博客。
这里面讲另外两种创建字符设备方式:cdev 方式 和混杂方式。详情看上面这个链接。
register_chrdev_region:对于 手动/静态 给定一个主次设备号(不推荐),使用以下函数:int register_chrdev_region(dev_t first, unsigned int count, char *name);
。其中first是我们手动给定的设备号,count是所请求的连续设备号的个数,而name是和该设备号范围关联的设备名称,它将出现在/proc/devices和sysfs中。比如,若first为0x3FFFF0,count为0x5,那么该函数就会为5个设备注册设备号,分别是0x3FFFF0、 0x3FFFF1、 0x3FFFF2、 0x3FFFF3、 0x3FFFF4。用这种方法注册设备号有一个缺点,那就是若该驱动module被其他人广泛使用,那么无法保证注册的设备号是其他人的Linux系统中未分配使用的设备号。
alloc_chrdev_region:对于动态分配设备号,使用以下函数:int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
。该函数需要传递给它指定的第一个次设备号firstminor(一般为0)和要分配的设备数count,以及设备名,调用该函数后自动分配得到的设备号保存在dev中。**次设备号可以指定,主设备号不能指定只能内核动态分配。**动态分配设备号可以避免手动指定设备号时带来的缺点,但是它却也有自己的缺点,那就是无法预知在/dev下创建设备节点是什么名字,因为动态分配设备号不能保证在每次加载驱动module时始终一致,这个缺点可以避免,因为在加载驱动module后,我们可以读取/proc/devices文件以获得Linux内核分配给该设备的主设备号。
|
|
100ask 的例子,01b_hello_drv 里面的:
|
|
更简明的教程 对 linux驱动 及 字符型设备驱动 的理解_艾特号的博客-CSDN博客。
更多例程:字符设备驱动框架3:深入探讨—完整的驱动代码工程_欧阳海宾的博客-CSDN博客,看看理解就好,这个例子并不通用。
class_create / class_destroy
Linux内核API class_create|极客笔记 (deepinout.com)。Linux内核API class_destroy|极客笔记 (deepinout.com)。
|
|
class_create()
用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进Linux内核系统中。此函数的执行效果就是在目录/sys/class
下创建一个新的文件夹,此文件夹的名字为此函数的第二个输入参数 name。owner 一般赋值为 THIS_MODULE。
|
|
函数
class_destroy()
用于删除设备的逻辑类。不返回任何值。
device_create / device_destroy
Linux内核API device_create|极客笔记 (deepinout.com)。Linux内核API device_destroy|极客笔记 (deepinout.com)。
|
|
函数
device_create()
用于动态地创建逻辑设备,并对新的逻辑设备类进行相应的初始化,将其与此函数的第一个参数所代表的逻辑类关联起来,然后将此逻辑设备加到Linux内核系统的设备驱动程序模型中。函数能够自动地在/sys/devices/virtual
目录下创建新的逻辑设备目录,在/dev
目录下创建与逻辑类对应的设备文件。函数
device_create()
的第一个输入参数代表与即将创建的逻辑设备相关的逻辑类。即class_create()
的返回值。第二个输入参数代表即将创建的逻辑设备的父设备的指针,子设备与父设备的关系是:当父设备不可用时,子设备不可用,子设备依赖父设备,父设备不依赖子设备。不用时可填入 NULL。
第三个输入参数是逻辑设备的设备号。可填入 MKDEV(major, minor)。
第四个输入参数是void类型的指针,代表回调函数的输入参数。不用时可填入 NULL。
第五个输入参数是逻辑设备的设备名,即在目录
/sys/devices/virtual
创建的逻辑设备目录的目录名。可以用 printf 的格式写,比如"drv_%d",drv_num
。函数
device_create()
的返回值是struct device结构体类型的指针,指向新创建的逻辑设备。
|
|
函数device_destroy():用于从Linux内核系统设备驱动程序模型中移除一个设备,并删除
/sys/devices/virtual
目录下对应的设备目录及/dev目录下对应的设备文件。函数device_destroy()第一个输入参数是struct class类型的变量,代表与待注销的逻辑设备相关的逻辑类,用于Linux内核系统逻辑设备的查找。即
class_create()
的返回值。第二个参数是逻辑设备的设备号,与第一个参数共同确定一个逻辑设备。可填入 MKDEV(major, minor)。
module_init / module_exit
修饰本模块的 加载 和 卸载 时候 调用的函数。
struct file_operations
参考:
- file_operations结构体详细解释 - 百度文库 (baidu.com)。
- linux内核中struct file_operations 结构体介绍_鱼思故渊的博客-CSDN博客_file_operations结构体。
- struct file_operations_zlcchina的博客-CSDN博客。
- Linux设备驱动的struct file_operations结构体中unlocked_ioctl和compat_ioctl的区别 - 简书 (jianshu.com)。
- Linux中的File_operations结构体-pudn.com。
|
|
重要的成员释义:
loff_t (*llseek) (struct file *, loff_t, int);
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL(即填入 struct file_operations 结构体这个函数指针为 NULL), seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL(“Invalid argument”) 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 “signed size” 类型, 常常是目标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
read_iter 和 write_iter
异步读 和 异步写,即完成操作之前就返回。而从4.1版本开始,关于异步读写的函数已经被read_iter和write_iter取代了。
Linux内核4.1在file_operations的read_iter和write_iter_潜行金枪鱼的博客-CSDN博客。
unsigned int (*poll) (struct file *, struct poll_table_struct *);
poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
ioctl 系统调用提供了发出设备特定命令的方法. 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, “设备无这样的 ioctl”), 系统调用返回一个错误.
int (*mmap) (struct file *, struct vm_area_struct *);
mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
int (*open) (struct inode *, struct file *);
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
int (*flush) (struct file *); 很少用
flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.
int (*release) (struct inode *, struct file *);
在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.
int (*fsync) (struct file *, struct dentry *, int);
这个方法是 fsync 系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.
int (*aio_fsync)(struct kiocb *, int);
这是 fsync 方法的异步版本.
int (*fasync) (int, struct file *, int);
这个操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题. 这个成员可以是NULL 如果驱动不支持异步通知.
总线平台驱动相关
参考 Linux Platform驱动模型(一) _设备信息_Neilo_chen的博客-CSDN博客,关于platform_device一些讲解_Leo丶Fun的博客-CSDN博客_platform_device。
详细用例 Linux 设备驱动开发 —— platform设备驱动应用实例解析_zqixiao_09的博客-CSDN博客_linux设备驱动开发。 设备树——platform_driver_7个棋的博客-CSDN博客_platform_driver。
dts 和 device 和 driver 文件位置
dts:
可以在 shell 中查看当前已经装载的设备树:
/sys/firmware/devicetree
目录下是以目录结构程现的dtb文件, 根节点对应base目录, 每一个节点对应一个目录, 每一个属性对应一个文件。这些属性的值如果是字符串,可以使用cat命令把它打印出来;对于数值,可以用hexdump把它打印出来。(一个单板启动时,u-boot先运行,它的作用是启动内核。U-boot会把内核和设备树文件都读入内存,然后启动内核。在启动内核时会把设备树在内存中的地址告诉内核。)
-
driver :/sys/bus/platform/drivers,platform 总线下注册的驱动都在这了。
-
device:/sys/devices/platform。
-
platform_device 的信息:
/sys/devices/platform
目录含有注册进内核的所有 platform_device。一个设备对应一个目录,进入某个目录后,如果它有 “driver” 子目录,就表示这个platform_device跟某个platform_driver配对了。设备树被系统解析后生成的 platform_device 可以在这里面找到。platform_driver 的信息:
/sys/bus/platform/drivers
目录含有注册进内核的所有 platform_driver。一个driver对应一个目录,进入某个目录后,如果它有配对的设备,可以直接看到(一个平台设备只能配对一个平台驱动,一个平台驱动可以配对多个平台设备)。在装载 驱动程序中的 driver 的模块 之后就可以在 这个目录看到对应的 driver。
结构体成员只取一部分进行展示。
platform_driver_register/unregister
platform_device 详细
|
|
|
|
platform_driver 详细
|
|
platform_get_xxx 获取资源
可参考 linux (platform_driver)平台设备驱动常用API函数 (icode9.com)。
|
|
|
|
|
|
|
|
ioctl
可以参考:
- ioctl函数详解(参数详解,驱动unlocked_ioctl使用、命令码如何封装)_相望@于江湖的博客-CSDN博客_ioctl函数参数。
- Linux驱动学习6(ioctl的实现) - 灰信网(软件开发博客聚合) (freesion.com)。
- (八)linux驱动之ioctl的使用 - 灰信网(软件开发博客聚合) (freesion.com)。
- linux驱动开发(四):ioctl()函数_精致的螺旋线的博客-CSDN博客_ioctl函数linux。
等待队列 wait_queue
可参考:
- 小白学Linux——等待队列(waitqueue)_蚝油生菜的博客-CSDN博客_linux等待队列。源码分析。
基本字符设备驱动程序-输入
文件夹内基本字符设备驱动程序获取数据的说明.md
中的休眠-唤醒 机制
一节。里面有说明都有什么 API,并且有程序例子。
初始化
|
|
wait_event(wq, condition):调用wait_event宏定义后进程进入睡眠状态直到传入的条件为真。该进程进入睡眠状态(TASK_UNINTERRUPTIBLE),直到条件为真。每次唤醒等待队列wq时都会检查条件。休眠,直到condition为真; 退出的唯一条件是condition为真,信号也不能打断。
wait_event_interruptible(wq, condition):调用wait_event_interruptible宏定义后进程进入睡眠状态直到传入的条件为真。该进程进入睡眠状态(TASK_INTERRUPTIBLE),直到条件为真或者收到信号。每次唤醒等待队列wq时都会检查条件。如果睡眠期间被信号中断,该函数将返回 -ERESTARTSYS,如果条件为真,则返回0。休眠,直到condition为真; 休眠期间是可被打断的,可以被信号打断。
wake_up(x):从处于不可中断睡眠状态的等待队列中唤醒一个进程。 唤醒x队列中状态为 “TASK_INTERRUPTIBLE” 或 “TASK_UNINTERRUPTIBLE” 的线程,只唤醒其中的一个线程。
wake_up_interruptible(x):从处于可中断睡眠状态的等待队列中唤醒一个进程。唤醒x队列中状态为 “TASK_INTERRUPTIBLE” 的线程,只唤醒其中的一个线程。
其它用到的 设备相关API
copy_from_user / copy_to_user
内核空间 的数据与 应用/用户进程 的数据相互之间的拷贝。
- 初步解析内核函数copy_to_user和copy_from_user_江东风又起的博客-CSDN博客_copy_to_user。
- linux系统中copy_to_user()函数和copy_from_user()函数的用法_fxfreefly的博客-CSDN博客_copytouser函数。
|
|
ioremap / iounmap
用来将物理地址映射到一个虚拟地址,内核进程通过该虚拟地址访问到实际物理地址,安全。
把物理地址phys_addr开始的一段空间(大小为size),映射为虚拟地址;返回值是该段虚拟地址的首地址。
virt_addr = ioremap(phys_addr, size);
实际上,它是按页(4096字节)进行映射的,是整页整页地映射的。
假设phys_addr = 0x10002,size=4,ioremap的内部实现是:
a. phys_addr按页取整,得到地址0x10000
b. size按页取整,得到4096
c. 把起始地址0x10000,大小为4096的这一块物理地址空间,映射到虚拟地址空间,
假设得到的虚拟空间起始地址为0xf0010000
d. 那么phys_addr = 0x10002对应的virt_addr = 0xf0010002
EXPORT_SYMBOL
变量或函数的导出,表示这些变量对内核公开,其它模块可以访问到,否则访问是 NULL。
使用方法:
- 就在 驱动程序 .h 文件里面 声明所有要 导出的 函数、变量 和结构体结构(不是结构体变量的定义,而是结构体本身定义放到 驱动程序的 .h 文件里)等,并且都加上 extern 修饰,函数除外。
- 在驱动程序里面 定义和初始化这些函数、变量和结构体等。
- 在 其它要用到 这些 函数和变量的 模块 的驱动文件里面 include 前面的驱动程序 .h 文件,然后就可以直接调用了。
a.c编译为a.ko,里面定义了func_a;如果它想让b.ko使用该函数,那么a.c里需要导出此函数。即 如果 a.c, b.c 分别编译出两个 .ko,即 a.ko 和 b.ko,则需使用这个来导出。并且,使用时要先加载a.ko。如果先加载b.ko,会有类似如下“Unknown symbol”的提示。
如果 a.c, b.c 编译在一起,编译出一个 .ko,则无需使用这个来导出。
- EXPORT_SYMBOL的作用是什么 (eepw.com.cn)。
- linux export_symbol 变量,Linux的EXPORT_SYMBOL和EXPORT_SYMBOL_GPL的使用和区别_App小公主的博客-CSDN博客。
file_inode / iminor
参考 字符设备驱动框架2:设备文件(设备节点)如何和驱动建立联系-Linux字符设备中的两个重要结构体(file、inode)_欧阳海宾的博客-CSDN博客 就比较清楚了。
一般而言在驱动程序的设计中,会关系 struct file 和 struct inode 这两个结构体。
用户空间使用open()系统调用函数打开一个字符设备时( int fd = open(“dev/demo”, O_RDWR) )大致有以下过程:
- 在虚拟文件系统VFS中的查找对应与字符设备对应 struct inode节点
- 遍历字符设备列表(chardevs数组),根据inod节点中的 cdev_t设备号找到cdev对象
- 创建struct file对象(系统采用一个数组来管理一个进程中的多个被打开的设备,每个文件秒速符作为数组下标标识了一个设备对象)
- 初始化struct file对象,将 struct file对象中的 file_operations成员指向 struct cdev对象中的 file_operations成员(file->fops = cdev->fops)
- 回调file->fops->open函数
inode 结构体
VFS inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。
内核使用inode结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体(即file 文件结构)是不同的,我们可以使用多个file 文件结构表示同一个文件的多个文件描述符,但此时,所有的这些file文件结构全部都必须只能指向一个inode结构体。
inode结构体包含了一大堆文件相关的信息,但是就针对驱动代码来说,我们只要关心其中的两个域即可:
- dev_t i_rdev; 表示设备文件的结点,这个域实际上包含了设备号。
- struct cdev *i_cdev; struct cdev是内核的一个内部结构,它是用来表示字符设备的,当inode结点指向一个字符设备文件时,此域为一个指向inode结构的指针。
file 文件结构体
在设备驱动中,这也是个非常重要的数据结构,必须要注意一点,这里的file与用户空间程序中的FILE指针是不同的,用户空间FILE是定义在C库中,从来不会出现在内核中。而struct file,却是内核当中的数据结构,因此,它也不会出现在用户层程序中。
file结构体指示一个已经打开的文件(设备对应于设备文件),其实系统中的每个打开的文件在内核空间都有一个相应的struct file结构体,它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数,直至文件被关闭。如果文件被关闭,内核就会释放相应的数据结构。
在内核源码中,struct file要么表示为file,或者为filp(意指“file pointer”), 注意区分一点,file指的是struct file本身,而filp是指向这个结构体的指针。
参考 Linux中的File_operations结构体-pudn.com。
struct inode被内核用来代表一个文件,注意和struct file的区别,struct inode一个是代表文件,struct file一个是代表打开的文件
struct inode包括很重要的二个成员:
- dev_t i_rdev 设备文件的设备号
- struct cdev *i_cdev 代表字符设备的数据结构
struct inode结构是用来在内核内部表示文件的.同一个文件可以被打开好多次,所以可以对应很多struct file,但是只对应一个struct inode.
-
在 xxx_write() 和 xxx_read() 函数里面,实际控制一个设备类下面的哪一个设备,根据子设备号,获取通过 file_inode() 根据 file 得到文件的 inode,再用 iminor() 根据 inode 得到子/次设备号。
1 2 3 4 5 6 7 8 9 10
/* 提取主设备号 */ static inline unsigned imajor(const struct inode *inode) { return MAJOR(inode->i_rdev); } /* 提取次设备号 */ static inline unsigned iminor(const struct inode *inode) { return MINOR(inode->i_rdev); }
-
在 xxx_open() 和 xxx_close() 里面 可以根据 int minor = iminor(node); 直接获得次设备号(来或者这一个外设的哪一个具体资源)。
devm_kzalloc / devm_kfree
这个功能分配的内存会在驱动卸载时自动释放。参考 linux内核中的devm_kzalloc_不止冬雷和夏雪的博客-CSDN博客_devm_kzalloc。
|
|
参数 dev 是 申请内存的目标设备 device,其它参数与 kzalloc一致。
以下为 request/region/release 相关 API,不常用。
参考 linux (platform_driver)平台设备驱动常用API函数 (icode9.com)。
申请内存资源函数
- request_region
- request_mem_region
- devm_request_region
- devm_request_mem_region
释放内存资源
- release_region
- release_ mem_region
- devm_release_region
- devm_release_mem_region
常用内核态 API
内存申请
一文说明清楚:Linux内核空间内存申请函数kmalloc、kzalloc、vmalloc的区别【转】_danxibaoxxx的博客-CSDN博客。
更多 API Linux内核API 内存管理|极客笔记 (deepinout.com)。 linux中kmalloc函数详解_fulinux的博客-CSDN博客_kmalloc linux。
kmalloc()
|
|
kmalloc() 申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。
较常用的 flags(分配内存的方法):
- GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;
- GFP_KERNEL —— 正常分配内存;
- GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)。
- 更多 标志位 的列举 linux中kmalloc函数详解_fulinux的博客-CSDN博客_kmalloc函数。
下文引自 linux 字符驱动 申请内存最大,Linux驱动技术(一) _内存申请_一只小短腿的博客-CSDN博客。
GFP_KERNEL是最常用的flag,注意,使用这个flag来申请内存时,如果暂时不能满足,会引起进程阻塞,So,一定不要在中断处理函数、tasklet和内核定时器等非进程上下文中使用GFP_KERNEL!
kzalloc()
|
|
kzalloc() 函数与 kmalloc() 非常相似,参数及返回值是一样的,可以说是前者是后者的一个变种,因为 kzalloc() 实际上只是额外附加了 __GFP_ZERO 标志。所以它除了申请内核内存外,还会对申请到的内存内容清零。
kzalloc() 对应的内存释放函数也是 kfree()。
vmalloc()
|
|
vmalloc() 函数则会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存在物理内存中并不一定连续。由于 vmalloc() 没有保证申请到的是连续的物理内存(所以不能用来做DMA之类的操作),对申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。
注意:vmalloc() 和 vfree() 可以睡眠,因此不能从中断上下文调用。
vmalloc() 还会调用使用GFP_KERN的kmalloc,一定不要在中断处理函数、tasklet和内核定时器等非进程上下文中使用 vmalloc!
总结
kmalloc()、kzalloc()、vmalloc() 的共同特点是:
- 用于申请内核空间的内存;
- 内存以字节为单位进行分配;
- 所分配的内存虚拟地址上连续;
kmalloc()、kzalloc()、vmalloc() 的区别是:
- kzalloc 是强制清零的 kmalloc 操作;(以下描述不区分 kmalloc 和 kzalloc)
- kmalloc 分配的内存大小有限制(128KB),而 vmalloc 没有限制;
- kmalloc 可以保证分配的内存物理地址是连续的,但是 vmalloc 不能保证;
- kmalloc 分配内存的过程可以是原子过程(使用 GFP_ATOMIC),而 vmalloc 分配内存时则可能产生阻塞;
- kmalloc 分配内存的开销小,因此 kmalloc 比 vmalloc 要快;
一般情况下,内存只有在要被 DMA 访问的时候才需要物理上连续,但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用 vmalloc()。例如,当模块被动态加载到内核当中时,就把模块装载到由 vmalloc() 分配的内存上。
引自 100ask 手册
kmalloc 分配到的内存物理地址是连续的
kzalloc 分配到的内存物理地址是连续的,内容清 0
vmalloc 分配到的内存物理地址不保证是连续的
vzalloc 分配到的内存物理地址不保证是连续的,内容清 0
内核驱动中的内存用于mmap
引自 100ask 手册
我们应该使用 kmalloc 或 kzalloc,这样得到的内存物理地址是连续的,在 mmap 时后 APP 才可以使用同一个基地址去访问这块内存。(如果物理地址不连续,就要执行多次 mmap了)。
引自 mmap函数_vmalloc与mmap_weixin_39611161的博客-CSDN博客。
需要映射到用户空间的内存段,不能直接利用vmalloc()分配,而应该使用**vmalloc_user()**函数。
Linux内核API vmalloc_user|极客笔记 (deepinout.com)。
vmalloc_user() 的测试:Linux内核 vmalloc_user()|酷客网 (coolcou.com)。
likely 与 unlikely
引自 linux内核中likely与unlikely_夜风~的博客-CSDN博客_linux unlikely。
简单从表面上看
if( likely(value) ){ }
和if(unlikely(value)){ }else{ }
。 也就是likely和unlikely是一样的,但是实际上执行是不同的,加likely的意思是value的值为真的可能性更大一些,那么执行if的机会大,而unlikely表示value的值为假的可能性大一些,执行else机会大一些。加上这种修饰,编译成二进制代码时likely使得if后面的执行语句紧跟着前面的程序,unlikely使得else后面的语句紧跟着前面的程序,这样就会被cache预读取,增加程序的执行速度。
用来引导gcc进行条件分支预测。在一条指令执行时,由于流水线的作用,CPU可以同时完成下一条指令的取指,这样可以提高CPU的利用率。在执行条件分支指令时,CPU也会预取下一条执行,但是如果条件分支的结果为跳转到了其他指令,那CPU预取的下一条指令就没用了,这样就降低了流水线的效率。
简单理解:
- likely(x) 代表 x 是 逻辑真 的可能性比较大。
- unlikely(x) 代表 x 是 逻辑假 的可能性比较大。
内核中错误处理
参考:
- linux中ERR_PTR、PTR_ERR、IS_ERR和IS_ERR_OR_NULL_夜风~的博客-CSDN博客_linux ptr。
- Linux内核使用ERR_PTR和PTR_ERR等函数来实现指针函数返回错误码_tanglinux的博客-CSDN博客。
- Linux 内核IS_ERR函数 - 简书 (jianshu.com)。
- 【Linux内核】Linux的errno和ERR_PTR、PTR_ERR简介_gccwdn的博客-CSDN博客。
linux内核中判断返回指针是否错误的内联函数主要有:ERR_PTR、PTR_ERR、IS_ERR 和 IS_ERR_OR_NULL等。
在写设备驱动程序的过程中,涉及到的任何一个指针,必然有三种情况:
- 有效指针;
- NULL,空指针;
- 错误指针,或者说无效指针。
内核中对字符串的操作
具体 API 用法看 linux内核驱动中对字符串的操作【转】 - 走看看 (zoukankan.com)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
#include <linux/string.h> int strnicmp(const char *s1, const char *s2, size_t len) int strcasecmp(const char *s1, const char *s2) int strncasecmp(const char *s1, const char *s2, size_t n) char *strcpy(char *dest, const char *src) char *strncpy(char *dest, const char *src, size_t count) size_t strlcpy(char *dest, const char *src, size_t size) char *strcat(char *dest, const char *src) char *strncat(char *dest, const char *src, size_t count) size_t strlcat(char *dest, const char *src, size_t count) int strcmp(const char *cs, const char *ct) int strncmp(const char *cs, const char *ct, size_t count) char *strchr(const char *s, int c) char *strrchr(const char *s, int c) char *strnchr(const char *s, size_t count, int c) char *skip_spaces(const char *str) char *strim(char *s) size_t strlen(const char *s) size_t strnlen(const char *s, size_t count) char *strpbrk(const char *cs, const char *ct) char *strsep(char **s, const char *ct) bool sysfs_streq(const char *s1, const char *s2) void *memset(void *s, int c, size_t count) void *memcpy(void *dest, const void *src, size_t count) void *memmove(void *dest, const void *src, size_t count) int memcmp(const void *cs, const void *ct, size_t count) void *memscan(void *addr, int c, size_t size) char *strstr(const char *s1, const char *s2) char *strnstr(const char *s1, const char *s2, size_t len) void *memchr(const void *s, int c, size_t n)