Linux 驱动笔记
目录
链接
1. Linux 驱动框架
2. Linux 驱动加载逻辑
3. 字符设备基础
4. 并发与竞争
5. 高级字符设备进阶
6. 中断
7. 平台总线
8. 设备树
9. 设备模型
10. 热插拔
11. pinctrl 子系统
12. gpio 子系统
13. 输入子系统
14. 单总线
15. I2C
16. SPI
17. UART
18. PWM
19. RTC
20. Watchdog
21. CAN
22. 网络设备
23. ADC
24. IIO
25. USB
26. LCD
内核工具和辅助函数
container_of 宏
1 2 3 4 5 6 7 #define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \ !__same_type(*(ptr), void), \ "pointer type mismatch in container_of()" ); \ ((type *)(__mptr - offsetof(type, member))); })
ptr 指向结构体中某个成员的指针。
type 目标结构体类型。
member 成员在结构体中的名字。
BUILD_BUG_ON_MSG(...) 编译期类型检查,保证 ptr 和结构体成员类型一致,避免类型错误。
__mptr - offsetof(type, member) 核心公式:用成员指针减去该成员在结构体中的偏移,得到结构体起始地址。
offsetof(type, member) 标准宏,计算成员在结构体内的字节偏移。
container_of 考虑 name 从该结构开始处的偏移量,进而获得正确的指针位置。从指针 __mptr中减去字段 name 的偏移量,即可得到正确的位置。
链表
一个驱动程序管理多个设备,假设有 5个设备,可能需要在驱动程序中跟踪每个设备,这就需要链表。
内核开发者只实现了循环双链表,因为这个结构能够实现FIFO和LIFO,并且内核开发者要保持最少代码。为了支持链表,代码中要添加的头文件是<linux/list.h>。内核中链表实现核心部分的数据结构是struct list_head,其定义如下:
1 2 3 struct list_head { struct list_head *next , *prev ; };
struct list_head 用在链表头和每个节点中。在内核中,将数据结构表示为链表之前,该结构必须嵌入struct list_head字段。
例子
我们来创建汽车链表:
1 2 3 4 5 struct car { int door_number; char *color; char *model; };
在创建汽车链表之前,必须修改其结构,嵌入struct list_head字段。结构变成如下形式:
1 2 3 4 5 6 struct car { int door_number; char *color; char *model; struct list_head list ; }
创建struct list_head变量,该变量总是指向链表的头部(第一个元素)。list_head的这个实例与任何汽车都无关,而是一个特殊实例:
1 static LIST_HEAD (carlist) ;
现在可以创建汽车并将其添加到链表carlist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <linux/list.h> struct car *redcar = kmalloc(sizeof (*car), GFP_KERNEL);struct car *bluecar = kmalloc(sizeof (*car), GFP_KERNEL);INIT_LIST_HEAD(&bluecar->list ); INIT_LIST_HEAD(&redcar->list ); [...] list_add(&redcar->list , &carlist) ; list_add(&bluecar->list , &carlist) ;
创建和初始化链表
有两种方法创建和初始化链表。
动态方法
动态方法由 struct list_head 组成,用 INIT_LIST_HEAD宏初始化:·
1 2 struct list_head mylist ;INIT_LIST_HEAD(&mylist);
INIT_LIST_HEAD 定义如下
1 2 3 4 5 6 7 8 9 10 11 12 static inline void INIT_LIST_HEAD (struct list_head *list ) { WRITE_ONCE(list ->next, list ); list ->prev = list ; }
静态方法
静态分配通过LIST_HEAD宏完成:
LIST_HEAD 定义如下
1 2 3 4 #define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name)
这为name字段内的每个指针(prev和next)赋值,使其指向name自身(就像INIT_LIST_HEAD做的那样)
创建链表节点
要创建新节点,只需创建数据结构实例,初始化嵌入在其中的list_head字段。以汽车为例,其代码如下:
1 2 3 4 struct car *blackcar = kzalloc(sizeof (struct car), GFP_KERNEL);INIT_LIST_HEAD(&blackcar->list );
添加链表节点
内核提供的list_add()用于向链表添加新项,它是内部函数__list_add的包装:
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 static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { if (!__list_add_valid(new, prev, next)) return ; next->prev = new; new->next = next; new->prev = prev; WRITE_ONCE(prev->next, new); } static inline void list_add (struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); }
下面的例子在链表中添加两辆车:
1 2 list_add(&redcar->list , &carlist); list_add(&blue->list , &carlist);
这种模式可以用来实现堆栈。将节点添加到链表的另一个函数,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 static inline void list_add_tail (struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); }
这将把指定新项插入链表的末尾。对于之前的例子,可以使用以下代码:
1 2 list_add_tail(&redcar->list , &carlist); list_add_tail(&blue->list , &carlist);
这种模式可以用来实现队列
删除链表节点
内核代码中的链表处理是一项简单的任务。删除节点很简单:
1 2 3 4 5 6 7 8 9 10 11 12 static inline void list_del (struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; }
删除红色车:
1 list_del(&redcar->list );
list_del 断开指定节点的 prev 和 next 指针,移除该节点。分配给该节点的内存需要使用 kfree 手动释放。
链表遍历
使用宏 list_for_each_entry(pos, head, member) 进行链表遍历。
1 2 3 4 5 6 7 8 9 struct car *acar ; int blue_car_num = 0 ;list_for_each_entry(acar, carlist, list ){ if (acar->color == "blue" ) blue_car_num++; }
list_for_each_entry 定义如下:
1 2 3 4 5 6 7 8 9 10 11 #define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ !list_entry_is_head(pos, head, member); \ pos = list_next_entry(pos, member))
内核的睡眠机制
进程通过睡眠机制释放处理器,使其能够处理其他进程。处理器睡眠的原因可能在于感知数据的可用性,或等待资源释放。
内核调度器管理要运行的任务列表,这被称作运行队列。睡眠进程不再被调度,因为已将它们从运行队列中移除。除非其状态改变(唤醒),否则睡眠进程将永远不会被执行。进程一旦进入等待状态,就可以释放处理器,一定要确保有条件或其他进程会唤醒它。
Linux 内核通过提供一组函数和数据结构来简化睡眠机制的实现。
等待队列
等待队列实际上用于处理被阻塞的I/O,以等待特定条件成立,并感知数据或资源可用性。
等待队列是实现阻塞和唤醒的内核机制,等待队列以双循环链表为基础结构 ,其中链表头和链表项两部分别表示等待队列头和等待队列元素。
定义
include/linux/wait.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef struct wait_queue_entry wait_queue_entry_t ;struct wait_queue_entry { unsigned int flags; void *private; wait_queue_func_t func; struct list_head entry ; }; struct wait_queue_head { spinlock_t lock; struct list_head head ; }; typedef struct wait_queue_head wait_queue_head_t ;struct task_struct ;
初始化等待队列
API
用途
说明 / 参数
注意事项
DECLARE_WAIT_QUEUE_HEAD(name)
静态定义等待队列头
生成一个名为 name 的全局/静态等待队列头
最常用方式
init_waitqueue_head(wq)
动态初始化等待队列头
适用于动态分配结构体中的等待队列头
需要先分配内存
等待队列项API
这类API大部分情况下不用用到,task一般是current
API
用途
说明 / 参数
注意事项
DECLARE_WAITQUEUE(name, task)
静态创建一个等待队列项
task 通常填 current
全局或静态场景
init_waitqueue_entry(&entry, task)
动态初始化队列项
task 通常为 current
动态创建项
add_wait_queue(head, entry)
将等待项加入等待队列
非排他 wait
多用于底层调用(不建议直接用)
add_wait_queue_exclusive(head, entry)
排他 wait
唤醒时只唤醒 1 个 exclusive 进程
用于写操作等阻塞队列
remove_wait_queue(head, entry)
从等待队列移除项
和 add_wait_queue 配对
睡眠等待API
API
用途
说明
返回值 / 注意事项
wait_event(wq, condition)
条件不满足 → 睡眠(不可中断)
condition 为真返回
不能被信号打断
wait_event_timeout(wq, condition, timeout)
不可中断睡眠 + 超时
timeout 为 jiffies
返回剩余 jiffies 或 0 超时
wait_event_interruptible(wq, condition)
可中断睡眠
可被信号打断
被打断时返回 -ERESTARTSYS
wait_event_interruptible_timeout(wq, condition, timeout)
可中断 + 超时
返回:>0 剩余时间,0 超时,<0 被信号打断
wait_event_killable(wq, condition)
仅 fatal signal 打断
比 interruptible 更安全
被杀死信号打断
wait_event_killable_timeout(wq, condition, timeout)
killable + timeout
同上
wait_event_interruptible 不会持续轮询,而只是在被调用时评估条件。如果条件为假,则进程将进入TASK_INTERRUPTIBLE状态并从运行队列中删除。
之后,当每次在等待队列中调用wake_up_interruptible时,都会重新检查条件。如果wake_up_interruptible运行时发现条件为真,则等待队列中的进程将被唤醒,并将其状态设置为TASK_RUNNING。进程按照它们进入睡眠的顺序唤醒。
要唤醒在队列中等待的所有进程,应该使用wake_up_interruptible_all
如果调用了wake_up 或 wake_up_interruptible,并且条件仍然是FALSE,则什么都不会发生。如果没有调用wake_up(或wake_up_interuptible),进程将永远不会被唤醒。
wait_event、wake_up 和 wake_up_all。它们以独占(不可中断)等待的方式处理队列中的进程,因为它们不能被信号中断。它们只能用于关键任务。
可中断函数只是可选的(但推荐使用),由于它们可以被信号中断,所以应该检查它们的返回值。非零值意味着睡眠被某种信号中断,驱动程序应该返回 ERESTARTSYS。
唤醒API
API
用途
说明 / 参数
唤醒规则
wake_up(&wq)
唤醒 queue 中所有非 exclusive waiter
不修改任务状态
适合读者多、唤醒不需要限制
wake_up_all(&wq)
唤醒所有 waiter
包含 exclusive
强制唤醒所有任务
wake_up_interruptible(&wq)
唤醒 TASK_INTERRUPTIBLE 状态任务
适配 interruptible wait
wake_up_interruptible_all(&wq)
唤醒所有可中断等待者
wake_up_nr(&wq, nr)
唤醒 nr 个 exclusive waiter
最常用:wake_up(&wq)
exclusive 只唤醒一个
等待队列里有两类等待者:
类型
怎么来的
典型场景
非 exclusive
add_wait_queue()
多读者(read)
exclusive
add_wait_queue_exclusive()
写者、资源竞争
exclusive 的设计目标是:
避免“惊群(thundering herd)”
延迟和定时器管理
时间是继内存之后常用的资源之一。它用于执行几乎所有的事情:延迟工作、睡眠、调度、超时以及许多其他任务。
时间有两类。内核使用绝对时间来了解具体时间,也就是一天的日期和时间,而相对时间则被内核调度程序使用。
对于绝对时间,有一个称为实时时钟(RTC)的硬件芯片。
为了处理相对时间,内核依赖于被称作定时器的CPU功能(外设),从内核的角度来看,它被称为内核定时器。
内核定时器分为两个不同的部分。
Linux 内核标准定时器
标准定时器是内核定时器,它以Jiffy为粒度运行。
硬件为内核提供了一个系统定时器来计算流逝的时间(即基于未来时间点的计时方式,以当前时刻为计时开始的起点,以未来的某一时刻为计时的终点),内核只有在系统定时器的帮助下才能计算和管理时间 ,但是内核定时器的精度并不高 ,所以不能作为高精度定时器使用。
并且内核定时器的运行没有周期性,到达计时终点后会自动关闭。如果要实现周期性定时,就要在定时处理函数中重新开启定时器。
定义
Linux 内核中使用 timer_list 结构体表示内核定时器
include/linux/timer.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct timer_list { struct hlist_node entry ; unsigned long expires; void (*function)(struct timer_list *); u32 flags; #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map ; #endif };
DEFINE_TIMER
1 2 3 4 5 6 7 8 9 10 11 12 #define __TIMER_INITIALIZER(_function, _flags) { \ .entry = { .next = TIMER_ENTRY_STATIC }, \ .function = (_function), \ .flags = (_flags), \ __TIMER_LOCKDEP_MAP_INITIALIZER( \ __FILE__ ":" __stringify(__LINE__)) \ } #define DEFINE_TIMER(_name, _function) \ struct timer_list _name = \ __TIMER_INITIALIZER(_function, 0)
可以使用以下代码对定时器和相应的定时处理函数进行定义
1 DEFINE_TIMER(timer_test,function_test);
timer_setup()
项目
说明
函数定义
void timer_setup(struct timer_list *timer, void (*callback)(struct timer_list *), unsigned int flags);
头文件
#include <linux/timer.h>
参数 timer
要初始化的 struct timer_list 定时器对象
参数 callback
定时器到期后执行的回调函数,类型为 void (*)(struct timer_list *)
参数 flags
定时器标志位(如 TIMER_DEFERRABLE、TIMER_PINNED 等)
功能
初始化一个定时器对象,为第一次使用做准备,设置回调函数和标志位
返回值
无返回值(void)
说明
仅完成初始化,不会启动定时器;需配合 add_timer() 或 mod_timer() 才会开始计时
timer_setup_on_stack()
项目
说明
函数定义
void timer_setup_on_stack(struct timer_list *timer, void (*callback)(struct timer_list *), unsigned int flags);
头文件
#include <linux/timer.h>
参数 timer
位于栈上的 struct timer_list 定时器对象
参数 callback
定时器到期后执行的回调函数
参数 flags
定时器标志位
功能
初始化一个栈上分配的定时器对象
返回值
无返回值(void)
注意事项
必须与 destroy_timer_on_stack() 成对使用,否则会导致内核调试警告或资源问题
destroy_timer_on_stack()
项目
说明
函数定义
static inline void destroy_timer_on_stack(struct timer_list *timer);
头文件
#include <linux/timer.h>
参数 timer
位于栈上的 struct timer_list 定时器对象
功能
销毁一个通过 timer_setup_on_stack() 初始化的栈上定时器
返回值
无返回值(void)
使用场景
仅用于栈上分配的定时器(非堆/全局变量)
注意事项
必须与 timer_setup_on_stack() 成对使用
add_timer()
项目
说明
函数定义
void add_timer(struct timer_list *timer);
头文件
#include <linux/timer.h>
参数 timer
一个已初始化好的定时器对象,必须设置 expires(到期时间) 和 function(回调函数)
功能
将定时器注册到内核,使其开始计时。当 expires 到达时触发回调函数
返回值
无返回值(void)
del_timer()
项目
说明
函数定义
int del_timer(struct timer_list *timer);
头文件
#include <linux/timer.h>
参数 timer
需要删除的定时器对象
功能
从内核中删除定时器,使其不再触发。不会等待回调执行结束(如需等待请使用 del_timer_sync())
返回值
成功删除:1 (定时器处于激活状态)已经不在激活队列:0
注意:从内核中删除定时器,使其不再触发。如果要等待回调执行结束要使用 del_timer_sync(),等待处理程序(即使是在另一个CPU上执行)执行完成。不应该持有阻止处理程序完成的锁,这样将导致死锁。
应该在模块清理例程中释放定时器。可以调用timer_pending()函数独立检查定时器是否正在运行
timer_pending()
这个函数检查是否有触发的定时器回调函数挂起。
项目
说明
函数定义
static inline int timer_pending(const struct timer_list *timer);
头文件
#include <linux/timer.h>
参数 timer
要检查状态的 struct timer_list 定时器对象
功能
判断该定时器当前是否处于“等待触发(pending)”状态
返回值
若定时器已加入内核定时器队列(正在计时)返回 1 ;否则返回 0
注意事项
调用者必须保证对该定时器的访问已做并发保护(如加锁或在同一上下文中)
mod_timer()
项目
说明
函数定义
int mod_timer(struct timer_list *timer, unsigned long expires);
头文件
#include <linux/timer.h>
参数 timer
需要修改的定时器对象
参数 expires
新的到期时间(单位:jiffies)
功能
修改定时器的到期时间。如果定时器未激活,会自动激活 。如果已激活,会重新开始计时
返回值
定时器之前已激活:1 定时器之前未激活:0
在使用 add_timer()函数向 Linux 内核注册定时器之前,还需要设置定时时间,定时时间由 timer_list 结构体中的 expires 参数所确定,单位为节拍数,可以通过Linux编译时图形化界面menuconfig设置系统节拍的频率,具体路径如下所示:
1 2 -> Kernel Features -> Timer frequency (<choice> [=y])
当前版本内核代码可选的系统节拍率为 100Hz、250Hz、300Hz 和 1000Hz,默认情况下选择300Hz。
全局变量 jiffies
通过全局变量 jiffies 来记录自系统启动以来产生节拍的总数。jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。启动时,内核将该变量初始化为 0,此后,每次时钟中断处理程序都会增加该变量的值,一秒内 jiffes 增加的值为设置的系统节拍数,该变量定义在include/linux/jiffies.h文件中(timer.h 文件中已经包含,不需要重复引用),具体定义如下:
1 2 extern u64 __cacheline_aligned_in_smp jiffies_64;extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;
jiffies 与时间单位转换函数
函数定义
作用
int jiffies_to_msecs(const unsigned long j)
将 jiffies 类型的参数 j 转换为对应的毫秒
int jiffies_to_usecs(const unsigned long j)
将 jiffies 类型的参数 j 转换为对应的微秒
u64 jiffies_to_nsecs(const unsigned long j)
将 jiffies 类型的参数 j 转换为对应的纳秒
long msecs_to_jiffies(const unsigned int m)
将毫秒转换为 jiffies 类型
long usecs_to_jiffies(const unsigned int u)
将微秒转换为 jiffies 类型
unsigned long nsecs_to_jiffies(u64 n)
将纳秒转换为 jiffies 类型
示例
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 #include <linux/module.h> #include <linux/init.h> #include <linux/cdev.h> #include <linux/slab.h> #include <linux/fs.h> #include <linux/sched.h> #include <linux/timer.h> #include <linux/uaccess.h> struct drv_data { dev_t dev_num; struct cdev cdev ; struct class *class ; struct device *dev ; }; static struct drv_data *drv_dat ;static void timer_irq_func (struct timer_list *t) ;DEFINE_TIMER(timer_test, timer_irq_func); int timer_mod_test_open (struct inode *inode, struct file *file) { file->private_data = drv_dat; pr_info("open is called by pid: %d\n" , task_pid_nr(current)); return 0 ; } ssize_t timer_mod_test_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { size_t len = min(sizeof (u64) + 1 , size); char kbuf[72 ]; snprintf (kbuf, sizeof (kbuf), "%llu" , jiffies64_to_msecs(jiffies_64)); if (copy_to_user(buf, kbuf, len) != 0 ) return -EFAULT; pr_info("read is called by pid: %d\n" , task_pid_nr(current)); return len; } int timer_mod_test_release (struct inode *inode, struct file *file) { pr_info("release is called by pid: %d\n" , task_pid_nr(current)); return 0 ; } struct file_operations fops = { .owner = THIS_MODULE, .open = timer_mod_test_open, .read = timer_mod_test_read, .release = timer_mod_test_release, }; static void timer_irq_func (struct timer_list *t) { pr_info("timer_irq_func is called\n" ); mod_timer(&timer_test, jiffies_64 + msecs_to_jiffies(3000 )); } static int __init timer_mod_test_init (void ) { int ret; drv_dat = kzalloc(sizeof (struct drv_data), GFP_KERNEL); if (drv_dat == NULL ) { ret = -ENOMEM; goto kzalloc_fail; } ret = alloc_chrdev_region(&drv_dat->dev_num, 1 , 0 , "timer_mod_test_chrdev_region" ); if (ret < 0 ) goto alloc_chrdev_region_fail; cdev_init(&drv_dat->cdev, &fops); drv_dat->cdev.owner = THIS_MODULE; ret = cdev_add(&drv_dat->cdev, drv_dat->dev_num, 1 ); if (ret < 0 ) goto cdev_add_fail; drv_dat->class = class_create(THIS_MODULE, "chrdev" ); if (IS_ERR(drv_dat->class)) { ret = PTR_ERR(drv_dat->class); goto class_create_fail; } drv_dat->dev = device_create(drv_dat->class, NULL , drv_dat->dev_num, NULL , "timer_test%d" , 0 ); if (IS_ERR(drv_dat->dev)) { ret = PTR_ERR(drv_dat->dev); goto device_create_fail; } timer_test.expires = jiffies_64 + msecs_to_jiffies(3000 ); add_timer(&timer_test); return 0 ; device_create_fail: class_destroy(drv_dat->class); class_create_fail: cdev_del(&drv_dat->cdev); cdev_add_fail: unregister_chrdev_region(drv_dat->dev_num, 1 ); alloc_chrdev_region_fail: kfree(drv_dat); kzalloc_fail: return ret; } static void __exit timer_mod_test_exit (void ) { del_timer(&timer_test); device_destroy(drv_dat->class, drv_dat->dev_num); class_destroy(drv_dat->class); cdev_del(&drv_dat->cdev); unregister_chrdev_region(drv_dat->dev_num, 1 ); kfree(drv_dat); } module_init(timer_mod_test_init); module_exit(timer_mod_test_exit); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("even629<asqwgo@outlook.com>" ); MODULE_DESCRIPTION("This is a test sample for linux timer" );
可以利用原子变量和timer做一个定时器,示例如下:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 #include <linux/module.h> #include <linux/init.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/timer.h> #include <linux/sched.h> #include <linux/slab.h> #include <linux/atomic.h> #include <linux/string.h> struct timer_drv_data { dev_t dev_num; struct cdev cdev ; struct class *class ; struct device *dev ; atomic64_t sec; }; static struct timer_drv_data *timer_drv_data ;static void timer_sec_func (struct timer_list *t) ;DEFINE_TIMER(timer_sec, timer_sec_func); int timer_sec_test_open (struct inode *inode, struct file *file) { file->private_data = timer_drv_data; atomic64_set(&timer_drv_data->sec, 0 ); add_timer(&timer_sec); pr_info("open is called by pid: %d\n" , task_pid_nr(current)); return 0 ; } ssize_t timer_sec_test_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { struct timer_drv_data *dat = file->private_data; size_t len; char kbuf[128 ]; snprintf (kbuf, sizeof (kbuf), "current sec: %llu\n" , atomic64_read(&dat->sec)); len = min(size, strlen (kbuf) + 1 ); if (copy_to_user(buf, kbuf, len) != 0 ) return -EFAULT; return len; } int timer_sec_test_release (struct inode *inode, struct file *file) { del_timer(&timer_sec); pr_info("release is called by pid: %d\n" , task_pid_nr(current)); return 0 ; } struct file_operations fops = { .owner = THIS_MODULE, .open = timer_sec_test_open, .read = timer_sec_test_read, .release = timer_sec_test_release, }; static void timer_sec_func (struct timer_list *t) { atomic64_inc(&timer_drv_data->sec); mod_timer(&timer_sec, get_jiffies_64() + msecs_to_jiffies(1000 )); } static int __init timer_sec_init (void ) { int ret; timer_drv_data = kzalloc(sizeof (struct timer_drv_data), GFP_KERNEL); if (timer_drv_data == NULL ) { ret = -ENOMEM; goto kzalloc_fail; } ret = alloc_chrdev_region(&timer_drv_data->dev_num, 0 , 1 , "test_chrdev_region" ); if (ret < 0 ) goto alloc_chrdev_region_fail; cdev_init(&timer_drv_data->cdev, &fops); timer_drv_data->cdev.owner = THIS_MODULE; ret = cdev_add(&timer_drv_data->cdev, timer_drv_data->dev_num, 1 ); if (ret < 0 ) goto cdev_add_fail; timer_drv_data->class = class_create(THIS_MODULE, "chrdev" ); if (IS_ERR(timer_drv_data->class)) { ret = PTR_ERR(timer_drv_data->class); goto class_create_fail; } timer_drv_data->dev = device_create(timer_drv_data->class, NULL , timer_drv_data->dev_num, NULL , "timer_sec%d" , 0 ); if (IS_ERR(timer_drv_data->dev)) { ret = PTR_ERR(timer_drv_data->dev); goto device_create_fail; } return 0 ; device_create_fail: class_destroy(timer_drv_data->class); class_create_fail: cdev_del(&timer_drv_data->cdev); cdev_add_fail: unregister_chrdev_region(timer_drv_data->dev_num, 1 ); alloc_chrdev_region_fail: kfree(timer_drv_data); kzalloc_fail: return ret; } static void __exit timer_sec_exit (void ) { class_destroy(timer_drv_data->class); cdev_del(&timer_drv_data->cdev); unregister_chrdev_region(timer_drv_data->dev_num, 1 ); kfree(timer_drv_data); } module_init(timer_sec_init); module_exit(timer_sec_exit); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("even629<asqwgo@outlook.com>" ); MODULE_DESCRIPTION("This is a sec timer" );
Linux 高精度定时器 HRT
hrtimer = **High Resolution Timer(高精度定时器)**由内核配置中的CONFIG_HIGH_RES_TIMERS 选项启用,特点如下:
纳秒级精度(基于 ktime)
适用于高精度周期任务
运行在软中断上下文
可周期性或单次触发
与传统 timer_list 区别:
项目
timer_list
hrtimer
精度
jiffies 级别
纳秒级
精度依赖
HZ
硬件高精度时钟
适合
普通延时
高精度周期任务
单位
符号
等于多少秒
应用场景
毫秒
ms
10⁻³ 秒 = 0.001 s
计算机响应、音频
微秒
μs
10⁻⁶ 秒
电子、网络延迟
纳秒
ns
10⁻⁹ 秒
CPU 时钟周期、光传播
皮秒
ps
10⁻¹² 秒
激光、量子物理
飞秒
fs
10⁻¹⁵ 秒
化学反应动力学
阿秒
as
10⁻¹⁸ 秒
原子内部电子运动(2023 年诺贝尔物理学奖相关)
头文件:
1 #include <linux/hrtimer.h>
检查系统上是否可用HRT。
查看内核配置文件,其中应该包含像这样的内容:CONFIG_ HIGH_RES_TIMERS=y
查看 cat /proc/timer_list 或 cat /proc/timer_list | grep resolution的结果。.resolution项必须显示1 nsecs,事件处理程序必须显示hrtimer_interrupts。
使用 clock_getres 系统调用。
在内核代码中,使用#ifdef CONFIG_HIGH_RES_TIMERS。
在系统启用HRT的情况下,睡眠和定时器系统调用的精度不再依赖于jiffies,但它们会像HRT一样精确。这就是有些系统不支持nanosleep()之类的原因。
struct hrtimer
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 struct hrtimer { struct timerqueue_node node ; ktime_t _softexpires; enum hrtimer_restart (*function) (struct hrtimer *) ; struct hrtimer_clock_base *base ; u8 state; u8 is_rel; u8 is_soft; u8 is_hard; };
hrtimer_init()
初始化hrtimer。hrtimer初始化之前,需要设置ktime,它代表持续时间。
项目
说明
原型
void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode);
功能
初始化 hrtimer
参数
timer:定时器 which_clock:时钟源 mode:模式
返回值
无
常用 clock:
CLOCK_MONOTONIC
CLOCK_REALTIME
常用 mode:
HRTIMER_MODE_REL(相对时间,对于现在的时间值)
HRTIMER_MODE_ABS(绝对时间)
hrtimer_start()
项目
说明
原型
int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode);
功能
启动定时器
参数
timer:定时器 tim:时间 mode:模式
返回值
0 或 1
hrtimer_cancel()
项目
说明
原型
int hrtimer_cancel(struct hrtimer *timer);
功能
取消定时器
返回值
1=成功取消; 0=未运行
hrtimer_try_to_cancel()
项目
说明
函数定义
extern int hrtimer_try_to_cancel(struct hrtimer *timer);
头文件
#include <linux/hrtimer.h>
参数 timer
要取消的 struct hrtimer 高精度定时器对象
功能
尝试取消一个高精度定时器(非阻塞方式)
返回值
>0 :成功取消(定时器处于激活状态)0 :定时器未激活(已经过期或未启动)-1 :定时器回调函数正在执行,无法取消
是否阻塞
❌ 不会等待回调执行完成(非阻塞),可在中断上下文调用
hrtimer_try_to_cancel 内部会调用 hrtimer_callback_running
hrtimer_callback_running()
独立检查hrtimer的回调函数是否仍在运行:
项目
说明
函数定义
static inline int hrtimer_callback_running(struct hrtimer *timer);
头文件
#include <linux/hrtimer.h>
参数 timer
要检查状态的 struct hrtimer 高精度定时器对象
功能
判断该 hrtimer 的回调函数当前是否正在执行
返回值
若回调正在执行返回 非 0(真) ;否则返回 0(假)
是否阻塞
❌ 不阻塞,仅做状态检查
使用场景
用于检测回调执行状态,常配合 hrtimer_try_to_cancel() 使用
1 2 3 4 5 6 7 8 static inline int hrtimer_callback_running (struct hrtimer *timer) { return timer->base->running == timer; }
hrtimer_forward_now()
项目
说明
原型
u64 hrtimer_forward_now(struct hrtimer *timer, ktime_t interval);
功能
用于周期定时
返回值
转发次数
回调返回值
1 2 3 4 enum hrtimer_restart { HRTIMER_NORESTART, HRTIMER_RESTART, };
为了防止定时器自动重启,hrtimer回调函数必须返回HRTIMER_ NORESTART
示例
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 32 33 34 35 36 37 38 39 40 #include <linux/module.h> #include <linux/hrtimer.h> #include <linux/ktime.h> static struct hrtimer my_timer ;static ktime_t period;static enum hrtimer_restart my_timer_callback (struct hrtimer *timer) { printk("hrtimer fired\n" ); hrtimer_forward_now(timer, period); return HRTIMER_RESTART; } static int __init my_init (void ) { printk("hrtimer init\n" ); period = ktime_set(1 , 0 ); hrtimer_init(&my_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); my_timer.function = my_timer_callback; hrtimer_start(&my_timer, period, HRTIMER_MODE_REL); return 0 ; } static void __exit my_exit (void ) { hrtimer_cancel(&my_timer); printk("hrtimer exit\n" ); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL" );
动态Tick/Tickless内核
使用之前的HZ选项,即使处于空闲状态,内核也会每秒中断HZ次以再次调度任务。如果将HZ设置为1000,则每秒会有1000次内核中断,阻止CPU长时间处于空闲状态,因此影响CPU的功耗。
现在来看一看无固定或预定义Tick的内核 ,这些内核中,在需要执行某些任务之前禁用Tick 。这样的内核称为Tickless(无Tick)内核。
实际上,Tick 激活是依据下一个操作安排的,其正确的名字应该是动态Tick内核。内核负责系统中的任务调度,并维护可运行任务列表(运行队列)。当没有任务需要调度时,调度器切换到空闲线程,它启用动态Tick的方法是,在下一个定时器过期前(新任务排队等待处理),禁用周期性Tick 。
内核内部还维护一个任务超时列表(它知道什么时候要睡眠以及睡眠多久)。
在空闲状态下,如果下一个Tick比任务列表超时中的最小超时更远,内核则使用该超时值对定时器进行编程。
当定时器到期时,内核重新启用周期Tick并调用调度器,它调度与超时相关的任务。
通过以上方式,Tickless内核移除周期性Tick,节省电量。
内核中的延迟和睡眠
延迟有两种类型,取决于代码运行的上下文:原子的或非原子的 。处理内核延迟需要包含的头文件是#include <linux/delay.h>。
原子上下文
原子上下文中的任务(如ISR)不能进入睡眠状态 ,无法进行调度。这就是原子上下文延迟必须使用繁忙—等待循环 的原因。内核提供Xdelay系列函数,在繁忙循环中消耗足够长的时间(基于jiffies),得到所需的延迟。
ndelay(unsigned long nsecs)。
udelay(unsigned long usecs)。
mdelay(unsigned long msecs)。
应该始终使用udelay(),因为ndelay()的精度取决于硬件定时器的精度(嵌入式SOC不一定如此)。不建议使用mdelay()。
定时器处理程序(回调)在原子上下文中执行,这意味着根本不允许进入睡眠。这指的是可能导致调用程序进入睡眠的所有功能,如分配内存、锁定互斥锁、显式调用sleep()函数等
非原子上下文
在非原子上下文中,内核提供sleep[_range]系列函数,使用哪个函数取决于需要延迟多长时间。
udelay(unsigned long usecs):基于繁忙—等待循环。如果需要睡眠数微秒(小于等于10 us左右),则应该使用该函数。
usleep_range(unsigned long min, unsigned long max):依赖于hrtimer,睡眠数微秒到数毫秒(10 us~20
ms)时建议使用它,避免使用udelay()的繁忙—等待循环。
msleep(unsigned long msecs):由jiffies/传统定时器支持。对于数毫秒以上的长睡眠(10 ms+)请使用该函数。
内核源文档Documentation/timers/timers-howto.rst中详细解释了睡眠和延迟相关的主题。
从内核中调用用户空间中的程序
来看下面的例子:
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 32 33 34 35 36 37 38 39 40 41 42 43 #include <linux/init.h> #include <linux/module.h> #include <linux/workqueue.h> #include <linux/kmod.h> static struct delayed_work initiate_shutdown_work ;static void delayed_shutdown ( void ) { char *cmd = "/sbin/shutdown" ; char *argv[] = { cmd, "-h" , "now" , NULL , }; char *envp[] = { "HOME=/" , "PATH=/sbin:/bin:/usr/sbin:/usr/bin" , NULL , }; call_usermodehelper(cmd, argv, envp, 0 ); } static int __init my_shutdown_init ( void ) { schedule_delayed_work(&delayed_shutdown, msecs_to_jiffies(200 )); return 0 ; } static void __exit my_shutdown_exit ( void ) { return ; } module_init( my_shutdown_init ); module_exit( my_shutdown_exit ); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>" ); MODULE_DESCRIPTION("Simple module that trigger a delayed shut down" );
在前面的例子中,使用的API(call_usermodehelper)是Usermode-helper API的一部分,所有函数都定义在kernel/kmod.c内。其用法非常简单。它由内核使用,例如,用于模块的加载/卸载和Cgroup管理。
IO模型
IO操作过程
一个完整的 IO 流程,通常包含以下关键环节:
IO 调用:应用程序通过系统调用接口向内核发起 IO 请求(如读取文件)。
IO 执行:内核收到请求之后,通过驱动程序操作硬件完成具体 IO(如从磁盘读取数据),并将最终结果返回给用户空间。
完整的 IO 过程需要包含以下三个步骤:
用户空间的应用程序向内核发起 IO 调用请求(系统调用)
内核操作系统准备数据,把 IO 设备的数据加载到内核缓冲区
操作系统拷贝数据,把内核缓冲区的数据拷贝到用户进程缓冲区
IO模型的分类
在实际开发中,IO 操作常常成为影响程序性能的关键因素。假设有一个场景:从磁盘读取100MB 数据并处理,读取数据耗时 20 秒,处理数据也需要 20 秒。如果采用最传统的顺序流程——读取完再处理,那么整个流程耗时约 40 秒,效率明显偏低。那么能不能在等待数据的同时对数据进行处理呢?当然可以!这时候就轮到 IO 编程模型来出场了。
在 POSIX / Linux 的定义下:IO模型有阻塞IO,非阻塞IO,信号驱动IO,IO多路复用,异步IO。其中前四个被称之为同步IO
同步和异步的区别在于是否等待IO的执行结果,或者说数据拷贝到用户空间,是谁来完成的 ?。
同步IO: 最终把数据从内核拷贝到用户空间的动作,都是由用户线程在调用 read() 时完成的
异步IO:
用户态发起 IO 请求 → 立刻返回
内核在后台完成 IO
数据已拷贝到用户空间
内核通知进程:IO 完成
同步阻塞IO
进程进行 IO 操作时(如 read 操作),首先会发起一个系统调用,从而转到内核空间进行处理,内核空间的数据没有准备就绪时,进程会被阻塞,不会继续向下执行 ,直到内核空间的数据准备完成后,数据才会从内核空间拷贝到用户空间,最后返回应用进程,由用户空间进行数据的处理。
阻塞 IO 的可以及时地获取结果,并立刻对获取到的结果进行处理,然而在获取结果之前,无法去处理其他任务,需要时刻对结果进行监听。比如C语言的scanf函数 。
同步非阻塞IO
和阻塞 IO 模型不同,非阻塞 IO 进行 IO 操作时,如果内核数据没有准备好,内核会立即向进程返回 err ,不会进行阻塞;如果内核空间数据准备就绪,内核会立即把数据返回给用户空间的进程。
非阻塞 IO 的优点是效率高,同样的时间内可以做别的事。但是缺点也很明显,当需要频繁检查数据就绪状态时可能导致较高的 CPU 占用率。为了解决此问题,非阻塞 IO 通常与 IO多路复用技术结合使用。
IO多路复用
select()、poll()和 epoll()函数是实现 IO 多路复用的机制。
IO 多路复用可以让单个进程监控多个描述符,当发现某个描述符就绪以后,就会通知程序进行相应的读写操作 。
以 select()函数为例,如图所示。使用时需要向 select()传入待监听的文件描述符集合以及超时时间。
当执行 select()时,系统会触发一次系统调用,内核将遍历检查这些描述符是否触发了目标事件 (如可读、可写)。若检测到事件则立即返回,若未检测到事件,进程将进入阻塞状态并休眠 ,直到任一描述符就绪或超时为止 。
当 select()返回后,用户空间需遍历所有描述符,逐一确认具体是哪个触发了事件,从而实现单线程同时管理多个 IO操作的效果。
IO 多路复用的优点是一个进程/线程可以同时监听和处理多路 IO,效率成倍提高。但是 IO多路复用并不是能医治百病的良药,虽然 IO 多路复用可以监听多个 IO,但是实际上对结果的处理也只能依次进行 ,比较适合 IO 密集但是每一路 IO 数据量不多且到达时间分散的场合 (如网络聊天)。
另外 select 监听的描述符有上限(一般描述符最大不超过 1024),而且需要遍历究竟是哪一个 IO 产生了数据 。因此 IO 较多时,效率不高(这个问题被 epoll 解决)。
信号驱动IO
信号驱动 IO 指的是进程会预先告知内核,当某个描述符发生事件时,内核要向该进程发送 SIGIO 信号进行通知 ,进程可以在信号处理函数中对该事件进行处理。
例如在 Linux 系统中,用户按下 ctrl+C 终止运行中的任务时,系统实际上是对该进程发送一个 SIGINT 信号,该信号的默认处理函数就是退出当前程序。
具体到 IO 模型上,进程需要先为 SIGIO 信号注册相应的信号处理函数,并打开对应描述符的信号驱动。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 IO操作函数处理数据。
异步IO
aio_read()函数常常用于异步 IO,当进程使用 aio_read()读取数据时,如果数据尚未准备就绪就立即返回,不会阻塞 。
若数据准备就绪就会把数据从内核空间拷贝到用户空间的缓冲区中,然后执行定义好的回调函数对接收到的数据进行处理 。
但是对于Linux AIO,Linux AIO 只支持 direct I/O 模式的存储文件 (storage file),而且主要用在数据库这一细分领域 ;
而io_uring 支持存储文件和网络文件(network sockets),也支持更多的异步系统调用 (accept/openat/stat/...),而非仅限于 read/write 系统调用。
等待队列实现阻塞IO
在 Linux 驱动程序中,阻塞进程可以使用等待队列来实现 。
步骤一:初始化等待队列头,并将条件置成假(condition=0)。
步骤二:在需要阻塞的地方调用 wait_event(),使进程进入休眠状态
步骤三:当条件满足时,需要解除休眠,先将条件(condition=1),然后调用 wake_up() 函数唤醒等待队列中的休眠进程。
驱动:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 #include <linux/init.h> #include <linux/module.h> #include <linux/cdev.h> #include <linux/slab.h> #include <linux/wait.h> #include <linux/fs.h> #include <linux/string.h> #include <linux/uaccess.h> #include <linux/atomic.h> #define KBUF_CAPACITY 32 struct waitqueue_drv_data { dev_t dev_num; struct cdev cdev ; struct class *class ; struct device *dev ; char kbuf[KBUF_CAPACITY]; bool kbuf_ready; wait_queue_head_t waitque; }; static struct waitqueue_drv_data *drv_dat ;int waitqueue_test_open (struct inode *inode, struct file *file) { file->private_data = drv_dat; pr_info("waitqueue_test_open is called\n" ); return 0 ; } ssize_t waitqueue_test_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { struct waitqueue_drv_data *dat = file->private_data; size_t len; int ret; if (file->f_flags & O_NONBLOCK) { if (!dat->kbuf_ready) return -EAGAIN; } else { ret = wait_event_interruptible(dat->waitque, dat->kbuf_ready == true ); if (ret < 0 ) { pr_err("waitque_test_read is interrupted\n" ); return ret; } } len = min(size, (size_t )strlen (dat->kbuf)); if (copy_to_user(buf, dat->kbuf + *offset, len) != 0 ) return -EFAULT; dat->kbuf_ready = false ; return len; } ssize_t waitqueue_test_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { struct waitqueue_drv_data *dat = file->private_data; size_t len = min(size, (size_t )(KBUF_CAPACITY - 1 )); if (copy_from_user(dat->kbuf, buf, len) != 0 ) return -EFAULT; dat->kbuf[len] = '\0' ; dat->kbuf_ready = true ; wake_up_interruptible(&dat->waitque); return len; } int waitqueue_test_release (struct inode *inode, struct file *file) { pr_info("waitqueue_test_release is called\n" ); return 0 ; } static struct file_operations fops = { .owner = THIS_MODULE, .open = waitqueue_test_open, .read = waitqueue_test_read, .write = waitqueue_test_write, .release = waitqueue_test_release, }; static int __init waitqueue_test_init (void ) { int err; drv_dat = (struct waitqueue_drv_data *)kzalloc(sizeof (struct waitqueue_drv_data), GFP_KERNEL); if (drv_dat == NULL ) goto kzalloc_fail; err = alloc_chrdev_region(&drv_dat->dev_num, 0 , 1 , "waitque_drv_chrdev_region" ); if (err < 0 ) goto alloc_chrdev_region_fail; cdev_init(&drv_dat->cdev, &fops); drv_dat->cdev.owner = THIS_MODULE; err = cdev_add(&drv_dat->cdev, drv_dat->dev_num, 1 ); if (err < 0 ) goto cdev_add_fail; drv_dat->class = class_create(THIS_MODULE, "chrdev" ); if (IS_ERR(drv_dat->class)) { err = PTR_ERR(drv_dat->class); goto class_create_fail; } drv_dat->dev = device_create(drv_dat->class, NULL , drv_dat->dev_num, NULL , "waitqueue_test%d" , 0 ); if (IS_ERR(drv_dat->dev)) { err = PTR_ERR(drv_dat->dev); goto device_create_fail; } init_waitqueue_head(&drv_dat->waitque); return 0 ; device_create_fail: class_destroy(drv_dat->class); class_create_fail: cdev_del(&drv_dat->cdev); cdev_add_fail: unregister_chrdev_region(drv_dat->dev_num, 1 ); alloc_chrdev_region_fail: kfree(drv_dat); kzalloc_fail: return err; } static void __exit waitqueue_test_exit (void ) { device_destroy(drv_dat->class, drv_dat->dev_num); class_destroy(drv_dat->class); cdev_del(&drv_dat->cdev); unregister_chrdev_region(drv_dat->dev_num, 1 ); kfree(drv_dat); } module_init(waitqueue_test_init); module_exit(waitqueue_test_exit); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("even629<asqwgo@163.com>" ); MODULE_DESCRIPTION("waitqueue sample" );
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ cat /dev/waitqueue_test0 & [ 18.186918] waitqueue_test_open is called $ echo "Hello World" > /dev/waitqueue_test0 [ 33.059018] waitqueue_test_open is called [ 33.060401] waitqueue_test_release is called Hello World $ echo "Hello Linux" > /dev/waitqueue_test0 [ 43.618554] waitqueue_test_open is called [ 43.619516] waitqueue_test_release is called Hello Linux
非阻塞式访问
应用程序可以使用如下所示示例代码来实现阻塞访问:
1 2 3 4 5 int fd;int data = 0 ;fd = open("/dev/xxx_dev" , O_RDWR); ret = read(fd, &data, sizeof (data));
可以看出对于设备驱动文件的默认读取方式就是阻塞式的 ,所以之前实验例程测试都是采用阻塞 IO。如果应用程序要采用非阻塞的方式来访问驱动设备文件,可以使用如下所示代码:
1 2 3 4 int fd;int data = 0 ;fd = open("/dev/xxx_dev" , O_RDWR | O_NONBLOCK); ret = read(fd, &data, sizeof (data));
使用 open 函数打开“/dev/xxx_dev”设备文件的时候添加了参数“O_NONBLOCK”,表示以非阻塞方式打开设备,这样从设备中读取数据的时候是非阻塞方式了。
驱动程序
通过file结构体中的f_flags中查询应用程序是否通过O_NONBLOCK方式打开
1 2 3 4 5 6 7 8 static ssize_t cdev_test_read (struct file *file, char __user *buf, size_t size, loff_t *off) { struct device_test *test_dev = (struct device_test *)file->private_data; if (file->f_flags & O_NONBLOCK ){ if (test_dev->flag !=1 ) return -EAGAIN; } ...
IO多路复用的实现
IO 多路复用是一种同步的 IO 模型。IO 多路复用可以实现一个进程监视多个文件描述符 。一旦某个文件描述符准备就绪,就通知应用程序进行相应的读写操作。没有文件描述符就绪时就会阻塞应用程序,从而释放出 CPU 资源。
在应用层 Linux 提供了三种实现 IO 多路复用的模型,分别是 select、poll 和 epoll。
poll和select基本一样,都可以监听多个文件描述符,通过轮询文件描述符来获取已经准备好的文件描述符
epoll是将主动轮询变成了被动通知,当事件发生时,被动地接收通知。
Linux 应用层 poll
项目
内容
作用
监视多个文件描述符的读写事件或异常事件
原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数
- fds:struct pollfd 数组,描述被监视的文件描述符及事件 - nfds:被监视的 fd 数量 - timeout:超时时间(ms) >0:等待指定时间; = 0:立即返回; -1:永远阻塞,直到事件发生
返回值
>0:返回 revents ≠ 0 的 fd 数量 =0:超时 -1:失败
struct pollfd
1 2 3 4 5 struct pollfd { int fd; short events; short revents; };
pollfd的events和revents
事件类型
常值
作为 events 的值
作为 revents 的值
说明
读事件
POLLIN
✔
✔
有普通数据可读
读事件
POLLRDNORM
✔
✔
可读(常规数据)
读事件
POLLRDBAND
✔
✔
可读(带外数据)
读事件
POLLPRI
✔
✔
可读(高优先级数据)
写事件
POLLOUT
✔
✔
可写
写事件
POLLWRNORM
✔
✔
可写(常规数据)
写事件
POLLWRBAND
✔
✔
可写(带外数据)
错误事件
POLLERR
✔
发生错误
错误事件
POLLHUP
✔
发生挂起
错误事件
POLLNVAL
✔
描述符不是打开的文件
驱动层 poll
项目
内容
原型
unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);
作用
告诉内核:当前设备是否可 non-blocking 访问(可读/可写)
参数
- filp:文件结构体指针 - wait:内核传入的 poll_table,用于登记等待队列
返回值
返回状态位掩码(同 POLLIN/POLLOUT 等事件)
如果需要实现被动等待(在感知字符设备时不浪费CPU周期),则必须实现 poll() 函数,每当用户空间程序在与设备关联的文件上执行系统调用 select() 或 poll() 时都会调用 poll() 函数。
这个方法核心的内核函数是poll_wait(),它定义在<linux/poll.h>中,这个头文件应该包含在驱动代码中。
驱动中的实现
poll_wait()
项目
内容
作用
把驱动中的等待队列加入到 poll_table 中,用于 select/poll/epoll
原型
void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);
头文件
#include <linux/poll.h>
参数
- filp:文件 - queue:等待队列头(wait_queue_head_t) - wait:poll_table(来自应用层)
返回值
无
特点
不会阻塞! 只是登记等待队列
poll_wait() 根据注册到 struct poll_table 结构(作为第三个参数传递)中的事件,把与 struct filep结构(作为第一个参数)关联的设备添加到可以唤醒进程的列表(由第二个参数 struct wait_queue_head_t 结构指定,进程在其中处于睡眠状态)中。
用户进程可以运行 poll()、select()或者epoll()系统调用把需要等待的一组文件添加到等待队列上,以了解是否有相关的设备准备就绪。
之后,内核将会调用与每个设备文件相关的驱动程序的poll入口。每个驱动程序的poll方法再调用poll_wait,为需要接收内核通知的进程注册事件,在这些事件发生之前把进程置于睡眠状态,并把驱动程序注册为可以唤醒进程的驱动程序。
步骤
为每个需要实现被动等待的事件类型(读、写、异常)声明等待队列,当无数据可读或设备不可写时,把任务放入该队列:
1 2 static DECLARE_WAIT_QUEUE_HEAD (my_wq) ;static DECLARE_WAIT_QUEUE_HEAD (my_rq) ;
像这样实现poll函数
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <linux/poll.h> static unsigned int eep_poll (struct file *file, poll_table *wait) { unsigned int reval_mask = 0 ; poll_wait(file, &my_wq, wait); poll_wait(file, &my_rq, wait); if (new-data-is-ready) reval_mask |= (POLLIN | POLLRDNORM); if (ready_to_be_written) reval_mask |= (POLLOUT | POLLWRNORM); return reval_mask; }
当有新数据或者是设备可写入时,通知等待队列:
1 2 wake_up_interruptible(&my_rq); wake_up_interruptible(&my_wq);
通知可读事件可以采用以下两种方法:
在驱动程序的write()方法中通知,这意味着写入的数据可以读回;
在IRQ处理程序中通知,这意味着外部设备发送的数据可以读回。
通知可写事件可以采用以下两种方法:
在驱动程序的read()方法中通知,这意味着缓冲区是空的,可以被重新填充;
在IRQ处理程序中通知,这意味着设备已经完成数据发送,准备好再次接收数据。
示例
app read.c
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <poll.h> int main (int argc, char **argv) { pid_t pid; int fd, ret; char prefix[32 ]; int ops_nr = 10 ; pid = fork(); if (pid < 0 ) { perror("[fork error]" ); exit (EXIT_FAILURE); } else if (pid == 0 ) { char buf[32 ]; int i; sprintf (prefix, "[child, pid:%d]:" , getpid()); fd = open("/dev/poll_test0" , O_RDWR); if (fd < 0 ) { fprintf (stderr , "%s Error: %s(errno:%d)\n" , prefix, strerror(errno), errno); exit (EXIT_FAILURE); } for (i = 0 ; i < ops_nr; i++) { sprintf (buf, "%d" , i); ret = write(fd, buf, strlen (buf) + 1 ); if (ret < 0 ) { close(fd); fprintf (stderr , "%s Error: %s(errno:%d)\n" , prefix, strerror(errno), errno); exit (EXIT_FAILURE); } sleep(1 ); } close(fd); } else { char buf[32 ]; struct pollfd poll_fds [1]; sprintf (prefix, "[parent, pid:%d]:" , getpid()); fd = open("/dev/poll_test0" , O_RDWR); if (fd < 0 ) { fprintf (stderr , "%s Error: %s(errno:%d)\n" , prefix, strerror(errno), errno); exit (EXIT_FAILURE); } poll_fds[0 ].fd = fd; poll_fds[0 ].events = POLLIN; for (;;) { ret = poll(poll_fds, sizeof (poll_fds) / sizeof (struct pollfd), 3000 ); if (ret == 0 ) { printf ("timeout\n" ); } else if (ret < 0 ) { close(fd); fprintf (stderr , "%s Error: %s(errno:%d)\n" , prefix, strerror(errno), errno); exit (EXIT_FAILURE); } else { if (poll_fds[0 ].revents & POLLIN) { ret = read(fd, buf, sizeof (buf)); if (ret < 0 ) { close(fd); fprintf (stderr , "%s Error: %s(errno:%d)\n" , prefix, strerror(errno), errno); exit (EXIT_FAILURE); } printf ("%s read: %s, read ret: %d\n" , prefix, buf, ret); ops_nr--; if (ops_nr == 0 ) { break ; } } else { printf ("%s poll_fds[0].revents is %d\n" , prefix, poll_fds[0 ].revents); } } } close(fd); } return 0 ; }
驱动程序
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 #include <linux/module.h> #include <linux/init.h> #include <linux/cdev.h> #include <linux/slab.h> #include <linux/wait.h> #include <linux/sched.h> #include <linux/uaccess.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/poll.h> #include <linux/mutex.h> #define KBUF_SIZE 32 struct poll_test_drv_data { dev_t dev_num; struct cdev cdev ; struct class *class ; struct device *dev ; char kbuf[KBUF_SIZE]; wait_queue_head_t waitque; struct mutex lock ; }; static struct poll_test_drv_data *drv_dat ;int poll_test_open (struct inode *inode, struct file *file) { file->private_data = drv_dat; pr_info("open is called by pid: %d\n" , current->pid); return 0 ; } ssize_t poll_test_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { int ret; size_t len; struct poll_test_drv_data *dat = file->private_data; pr_info("read is called by pid: %d\n" , current->pid); if (file->f_flags & O_NONBLOCK) { mutex_lock(&dat->lock); if (dat->kbuf[0 ] == '\0' ) { mutex_unlock(&dat->lock); return -EAGAIN; } mutex_unlock(&dat->lock); } else { ret = wait_event_interruptible(dat->waitque, ({ int ready; mutex_lock(&dat->lock); ready = (dat->kbuf[0 ] != '\0' ); mutex_unlock(&dat->lock); ready; })); if (ret < 0 ) { pr_info("pid: %d read is interrupted while waiting\n" , current->pid); return ret; } } mutex_lock(&dat->lock); len = min(size, strlen (dat->kbuf) + 1 ); if (copy_to_user(buf, dat->kbuf, len)) { mutex_unlock(&dat->lock); return -EFAULT; } dat->kbuf[0 ] = '\0' ; mutex_unlock(&dat->lock); return len; } ssize_t poll_test_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { size_t len; struct poll_test_drv_data *dat = file->private_data; pr_info("write is called by pid: %d\n" , current->pid); len = min(size, (size_t )(KBUF_SIZE - 1 )); mutex_lock(&dat->lock); if (copy_from_user(dat->kbuf, buf, len)) { mutex_unlock(&dat->lock); return -EFAULT; } dat->kbuf[len] = '\0' ; mutex_unlock(&dat->lock); wake_up_interruptible(&dat->waitque); return len; } __poll_t poll_test_poll (struct file *file, struct poll_table_struct *p) { struct poll_test_drv_data *dat = file->private_data; __poll_t mask = 0 ; pr_info("poll is called by pid: %d\n" , current->pid); poll_wait(file, &dat->waitque, p); mutex_lock(&dat->lock); if (dat->kbuf[0 ] != '\0' ) mask |= POLLIN | POLLRDNORM; mutex_unlock(&dat->lock); return mask; } int poll_test_release (struct inode *inode, struct file *file) { pr_info("release is called by pid: %d\n" , current->pid); return 0 ; } static struct file_operations fops = { .owner = THIS_MODULE, .open = poll_test_open, .read = poll_test_read, .write = poll_test_write, .poll = poll_test_poll, .release = poll_test_release, }; static int __init poll_test_init (void ) { int ret; drv_dat = (struct poll_test_drv_data *)kzalloc(sizeof (struct poll_test_drv_data), GFP_KERNEL); if (drv_dat == NULL ) { ret = -ENOMEM; goto kzalloc_fail; } ret = alloc_chrdev_region(&drv_dat->dev_num, 0 , 1 , "test_chrdev_region" ); if (ret < 0 ) goto alloc_chrdev_region_fail; cdev_init(&drv_dat->cdev, &fops); drv_dat->cdev.owner = THIS_MODULE; ret = cdev_add(&drv_dat->cdev, drv_dat->dev_num, 1 ); if (ret < 0 ) goto cdev_add_fail; drv_dat->class = class_create(THIS_MODULE, "chrdev" ); if (IS_ERR(drv_dat->class)) { ret = PTR_ERR(drv_dat->class); goto class_create_fail; } drv_dat->dev = device_create(drv_dat->class, NULL , drv_dat->dev_num, NULL , "poll_test%d" , 0 ); if (IS_ERR(drv_dat->dev)) { ret = PTR_ERR(drv_dat->dev); goto device_create_fail; } drv_dat->kbuf[0 ] = '\0' ; mutex_init(&drv_dat->lock); init_waitqueue_head(&drv_dat->waitque); return 0 ; device_create_fail: class_destroy(drv_dat->class); class_create_fail: cdev_del(&drv_dat->cdev); cdev_add_fail: unregister_chrdev_region(drv_dat->dev_num, 1 ); alloc_chrdev_region_fail: kfree(drv_dat); kzalloc_fail: return ret; } static void __exit poll_test_exit (void ) { device_destroy(drv_dat->class, drv_dat->dev_num); class_destroy(drv_dat->class); cdev_del(&drv_dat->cdev); unregister_chrdev_region(drv_dat->dev_num, 1 ); kfree(drv_dat); } module_init(poll_test_init); module_exit(poll_test_exit); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("even629<asqwgo@163.com>" ); MODULE_DESCRIPTION("This is a test sample for poll_test" );
测试:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 $ insmod poll_test.ko [ 12.171314] poll_test: loading out-of-tree module taints kernel. $ ./test_poll.o [ 15.908771] open is called by pid: 101 [ 15.908795] open is called by pid: 102 [ 15.909346] write is called by pid: 102 [ 15.909902] poll is called by pid: 101 [ 15.910627] read is called by pid: 101 [parent, pid:101]: read : 0, read ret: 2 [ 15.915991] poll is called by pid: 101 [ 16.914086] write is called by pid: 102 [ 16.917324] poll is called by pid: 101 [ 16.917645] read is called by pid: 101 [parent, pid:101]: read : 1, read ret: 2 [ 16.918526] poll is called by pid: 101 [ 17.917861] write is called by pid: 102 [ 17.919219] poll is called by pid: 101 [ 17.919745] read is called by pid: 101 [parent, pid:101]: read : 2, read ret: 2 [ 17.922355] poll is called by pid: 101 [ 18.920467] write is called by pid: 102 [ 18.921819] poll is called by pid: 101 [ 18.923714] read is called by pid: 101 [parent, pid:101]: read : 3, read ret: 2 [ 18.924807] poll is called by pid: 101 [ 19.923667] write is called by pid: 102 [ 19.925368] poll is called by pid: 101 [ 19.926362] read is called by pid: 101 [parent, pid:101]: read : 4, read ret: 2 [ 19.927909] poll is called by pid: 101 [ 20.926284] write is called by pid: 102 [ 20.927049] poll is called by pid: 101 [ 20.927249] read is called by pid: 101 [parent, pid:101]: read : 5, read ret: 2 [ 20.927689] poll is called by pid: 101 [ 21.927490] write is called by pid: 102 [ 21.928342] poll is called by pid: 101 [ 21.928747] read is called by pid: 101 [parent, pid:101]: read : 6, read ret: 2 [ 21.929097] poll is called by pid: 101 [ 22.929789] write is called by pid: 102 [ 22.931600] poll is called by pid: 101 [ 22.932469] read is called by pid: 101 [parent, pid:101]: read : 7, read ret: 2 [ 22.934791] poll is called by pid: 101 [ 23.932151] write is called by pid: 102 [ 23.933917] poll is called by pid: 101 [ 23.936726] read is called by pid: 101 [parent, pid:101]: read : 8, read ret: 2 [ 23.938596] poll is called by pid: 101 [ 24.933805] write is called by pid: 102 [ 24.935854] poll is called by pid: 101 [ 24.936546] read is called by pid: 101 [parent, pid:101]: read : 9, read ret: 2 [ 24.937991] release is called by pid: 101 [ 25.937143] release is called by pid: 102
信号驱动IO
信号驱动 IO 不需要应用程序查询设备的状态,一旦设备准备就绪,会触发 SIGIO 信号,进而调用注册的处理函数。
如果要实现信号驱动 IO,需要应用程序和驱动程序配合,应用程序使用信号驱动 IO 的步骤有三步:
步骤 1 :注册信号处理函数 应用程序使用 signal 函数来注册 SIGIO 信号的信号处理函数。
步骤 2: 设置能够接收这个信号的进程(fcntl函数)
步骤 3: 开启信号驱动 IO 通常使用 fcntl 函数的 F_SETFL 命令打开 FASYNC 标志。
用户态可以用O_ASYNC也可以用FASYNC
1 2 3 4 5 6 7 8 9 10 #ifdef __USE_MISC # define FAPPEND O_APPEND # define FFSYNC O_FSYNC # define FASYNC O_ASYNC # define FNONBLOCK O_NONBLOCK # define FNDELAY O_NDELAY #endif
驱动实现
当应用程序开启信号驱动 IO 时,会触发驱动中的 fasync 函数。所以首先在 file_operations结构体中实现 fasync 函数,函数原型如下:
1 int (*fasync) (int fd,struct file *filp,int on)
在驱动中的 fasync 函数调用 fasync_helper 函数来操作 fasync_struct 结构体,fasync_helper函数原型如下:
1 int fasync_helper (int fd,struct file *filp,int on,struct fasync_struct **fapp)
其中struct fasync_struct定义如下:
1 2 3 4 5 6 7 8 struct fasync_struct { rwlock_t fa_lock; int magic; int fa_fd; struct fasync_struct *fa_next ; struct file *fa_file ; struct rcu_head fa_rcu ; };
struct fasync_struct 是内核用来管理「哪些进程希望对某个 file 收到 SIGIO / SIGURG 信号」的链表节点。
当设备准备好的时候,驱动程序需要调用 kill_fasync 函数通知应用程序 ,此时应用程序的SIGIO 信号处理函数就会被执行。kill_fasync 负责发送指定的信号,函数原型如下:
1 void kill_fasync (struct fasync_struct **fp,int sig,int band)
函数参数 :
fp : 要操作的 fasync_struct
sig : 发送的信号
band : 可读的时候设置成 POLLIN ,可写的时候设置成 POLLOUT
示例
app
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <limits.h> #include <string.h> #include <signal.h> static int fd;static char buf[32 ];static char prefix[32 ];static volatile sig_atomic_t data_ready = 0 ;void handle_sigio (int sig) { data_ready = 1 ; } int main (int argc, char **argv) { pid_t pid; int ret; pid = fork(); if (pid < 0 ) { perror("[fork error]" ); exit (EXIT_FAILURE); } else if (pid == 0 ) { int i; sprintf (prefix, "[child pid:%d]" , getpid()); fd = open("/dev/signal_io_test0" , O_RDWR); if (fd < 0 ) { perror(prefix); exit (EXIT_FAILURE); } for (i = 0 ; i < INT_MAX; i++) { sprintf (buf, "Hello num %d" , i); ret = write(fd, buf, strlen (buf) + 1 ); if (ret < 0 ) { perror(prefix); close(fd); exit (EXIT_FAILURE); } printf ("%s: write: %s\n" , prefix, buf); sleep(1 ); } close(fd); } else { int flags; sprintf (prefix, "[parent pid:%d]" , getpid()); fd = open("/dev/signal_io_test0" , O_RDWR); if (fd < 0 ) { perror(prefix); exit (EXIT_FAILURE); } sprintf (prefix, "[parent pid:%d]" , getpid()); struct sigaction act ; act.sa_handler = handle_sigio; sigemptyset(&act.sa_mask); act.sa_flags = 0 ; sigaction(SIGIO, &act, NULL ); fcntl(fd, F_SETOWN, getpid()); flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | O_ASYNC); for (;;) { pause(); if (data_ready) { data_ready = 0 ; ret = read(fd, buf, sizeof (buf)); printf ("%s: read: %s\n" , prefix, buf); } } close(fd); } return 0 ; }
驱动程序
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 #include <linux/module.h> #include <linux/init.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/wait.h> #include <linux/mutex.h> #include <linux/slab.h> #include <linux/sched.h> #include <linux/uaccess.h> #include <linux/poll.h> #include <linux/signal.h> #define KBUF_SIZE 64 struct drv_data { dev_t dev_num; struct cdev cdev ; struct class *class ; struct device *dev ; wait_queue_head_t waitq; char kbuf[KBUF_SIZE]; struct mutex lock ; struct fasync_struct *fa ; }; static struct drv_data *drv_dat ;int signal_io_open (struct inode *inode, struct file *file) { file->private_data = drv_dat; pr_info("signal_io_open is called by pid: %d\n" , task_pid_nr(current)); return 0 ; } ssize_t signal_io_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { struct drv_data *dat = file->private_data; size_t len; int ret; if (file->f_flags & O_NONBLOCK) { mutex_lock(&drv_dat->lock); if (dat->kbuf[0 ] == '\0' ) { mutex_unlock(&drv_dat->lock); return -EAGAIN; } mutex_unlock(&drv_dat->lock); } else { ret = wait_event_interruptible(drv_dat->waitq, ({ bool status; mutex_lock(&drv_dat->lock); status = (dat->kbuf[0 ] != '\0' ); mutex_unlock(&drv_dat->lock); status; })); if (ret < 0 ){ pr_info("signal_io_read called by pid: %d is interrupted\n" , task_pid_nr(current)); return ret; } } mutex_lock(&dat->lock); len = min(size, strlen (dat->kbuf) + 1 ); if (copy_to_user(buf, dat->kbuf, len) != 0 ) { mutex_unlock(&dat->lock); return -EFAULT; } dat->kbuf[0 ] = '\0' ; mutex_unlock(&dat->lock); pr_info("signal_io_read is called by pid: %d\n" , task_pid_nr(current)); return len; } ssize_t signal_io_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { struct drv_data *dat = file->private_data; int len = min(size, (size_t )(KBUF_SIZE - 1 )); mutex_lock(&dat->lock); if (copy_from_user(drv_dat->kbuf, buf, len) != 0 ) { mutex_unlock(&dat->lock); return -EFAULT; } dat->kbuf[len] = '\0' ; mutex_unlock(&dat->lock); wake_up_interruptible(&dat->waitq); kill_fasync(&dat->fa, SIGIO, POLLIN); pr_info("signal_io_write is called by pid: %d\n" , task_pid_nr(current)); return len; } __poll_t signal_io_poll (struct file *file, struct poll_table_struct *p) { struct drv_data *dat = file->private_data; __poll_t mask = 0 ; pr_info("signal_io_poll is called by pid: %d\n" , task_pid_nr(current)); poll_wait(file, &dat->waitq, p); mutex_lock(&dat->lock); if (dat->kbuf[0 ] != '\0' ) { mask |= POLLIN | POLLRDNORM; } mutex_unlock(&dat->lock); return mask; } int signal_io_fasync (int fd, struct file *file, int on) { struct drv_data *dat = file->private_data; pr_info("signal_io_fasync is called by pid: %d\n" , task_pid_nr(current)); return fasync_helper(fd, file, on, &dat->fa); } int signal_io_release (struct inode *inode, struct file *file) { pr_info("signal_io_release is called by pid: %d\n" , task_pid_nr(current)); signal_io_fasync(-1 , file, 0 ); return 0 ; } struct file_operations fops = { .owner = THIS_MODULE, .open = signal_io_open, .read = signal_io_read, .write = signal_io_write, .poll = signal_io_poll, .fasync = signal_io_fasync, .release = signal_io_release, }; static int __init signal_io_init (void ) { int ret; drv_dat = (struct drv_data *)kzalloc(sizeof (struct drv_data), GFP_KERNEL); if (drv_dat == NULL ) { ret = -ENOMEM; goto kzalloc_fail; } ret = alloc_chrdev_region(&drv_dat->dev_num, 0 , 1 , "chrdev_test_region" ); if (ret < 0 ) goto alloc_chrdev_region_fail; cdev_init(&drv_dat->cdev, &fops); drv_dat->cdev.owner = THIS_MODULE; ret = cdev_add(&drv_dat->cdev, drv_dat->dev_num, 1 ); if (ret < 0 ) goto cdev_add_fail; drv_dat->class = class_create(THIS_MODULE, "chrdev_test" ); if (IS_ERR(drv_dat->class)) { ret = PTR_ERR(drv_dat->class); goto class_create_fail; } drv_dat->dev = device_create(drv_dat->class, NULL , drv_dat->dev_num, NULL , "signal_io_test%d" , 0 ); if (IS_ERR(drv_dat->dev)) { ret = PTR_ERR(drv_dat->dev); goto device_create_fail; } drv_dat->kbuf[0 ] = '\0' ; init_waitqueue_head(&drv_dat->waitq); mutex_init(&drv_dat->lock); return 0 ; device_create_fail: class_destroy(drv_dat->class); class_create_fail: cdev_del(&drv_dat->cdev); cdev_add_fail: unregister_chrdev_region(drv_dat->dev_num, 1 ); alloc_chrdev_region_fail: kfree(drv_dat); kzalloc_fail: return ret; } static void __exit signal_io_exit (void ) { device_destroy(drv_dat->class, drv_dat->dev_num); class_destroy(drv_dat->class); cdev_del(&drv_dat->cdev); unregister_chrdev_region(drv_dat->dev_num, 1 ); kfree(drv_dat); } module_init(signal_io_init); module_exit(signal_io_exit); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("even629<asqwgo@163.com>" ); MODULE_DESCRIPTION("test sample for signal io" );
测试:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 $ insmod signal_io.ko [ 13.310579] signal_io: loading out-of-tree module taints kernel. $ ./test_signal.o [ 16.052738] signal_io_open is called by pid: 102 [ 16.052761] signal_io_open is called by pid: 101 [ 16.053719] signal_io_write is called by pid: 102 [ 16.053816] signal_io_fasync is called by pid: 101 [child pid:102]: write: Hello num 0 [ 17.062149] signal_io_write is called by pid: 102 [child pid:102]: write: Hello num 1 [ 17.063588] signal_io_read is called by pid: 101 [parent pid:101]: read : Hello num 1 [ 18.066214] signal_io_write is called by pid: 102 [ 18.068176] signal_io_read is called by pid: 101 [child pid:102]: write: Hello num 2 [parent pid:101]: read : Hello num 2 [ 19.072969] signal_io_write is called by pid: 102 [child pid:102]: write: Hello num 3 [ 19.078051] signal_io_read is called by pid: 101 [parent pid:101]: read : Hello num 3 [ 20.078613] signal_io_write is called by pid: 102 [ 20.079554] signal_io_read is called by pid: 101 [child pid:102]: write: Hello num 4 [parent pid:101]: read : Hello num 4 [ 21.081299] signal_io_write is called by pid: 102 [child pid:102]: write: Hello num 5 [ 21.082047] signal_io_read is called by pid: 101 [parent pid:101]: read : Hello num 5 [ 22.082641] signal_io_write is called by pid: 102 [child pid:102]: write: Hello num 6 [ 22.083896] signal_io_read is called by pid: 101 [parent pid:101]: read : Hello num 6 [ 23.085311] signal_io_write is called by pid: 102 [child pid:102]: write: Hello num 7 [ 23.086379] signal_io_read is called by pid: 101 [parent pid:101]: read : Hello num 7 [ 24.088591] signal_io_write is called by pid: 102 [child pid:102]: write: Hello num 8 [ 24.091260] signal_io_read is called by pid: 101 [parent pid:101]: read : Hello num 8 [ 25.094550] signal_io_write is called by pid: 102 [ 25.095438] signal_io_read is called by pid: 101 [child pid:102]: write: Hello num 9 [parent pid:101]: read : Hello num 9 [ 26.098043] signal_io_write is called by pid: 102 [ 26.100002] signal_io_read is called by pid: 101 [child pid:102]: write: Hello num 10 [parent pid:101]: read : Hello num 10 [ 27.103532] signal_io_write is called by pid: 102 [child pid:102]: write: Hello num 11 [ 27.105751] signal_io_read is called by pid: 101 [parent pid:101]: read : Hello num 11 [ 28.112041] signal_io_write is called by pid: 102 [child pid:102]: write: Hello num 12 [ 28.114740] signal_io_read is called by pid: 101 [parent pid:101]: read : Hello num 12 [ 29.118790] signal_io_write is called by pid: 102 [ 29.121123] signal_io_read is called by pid: 101 [child pid:102]: write: Hello num 13 [parent pid:101]: read : Hello num 13 ^C[ 29.356357] signal_io_fasync is called by pid: 101 [ 29.356383] signal_io_release is called by pid: 102 [ 29.356413] signal_io_fasync is called by pid: 102 [ 29.356999] signal_io_release is called by pid: 101 [ 29.357737] signal_io_fasync is called by pid: 101
异步IO
异步IO依赖于应用层glibc中的实现,可以不依赖Linux内核
参考:
对于io_uring 可以参考本站这篇文章
Linux内核打印
dmesg
终端使用 dmseg 命令可以获取内核打印信息,该命令的具体使用方法如下所示:
dmesg 命令
英文全称:display message(显示信息)
作用:kernel 会将打印信息存储在 ring buffer 中。可以利用 dmesg 命令来查看内核打印信息
常用参数:
-C ,–clear 清除内核环形缓冲区
-c ,—-read-clear 读取并清除所有消息
-T ,–显示时间戳
dmesg 命令也可以与 grep 命令组合使用。如查找待用 usb 关键字的打印信息,就可以使用如下命令:
与tail命令结合,查看最后100行
kmsg文件
内核所有的打印信息都会输出到循环缓冲区 ‘log_buf’,为了能够方便的在用户空间读取内核打印信息,Linux 内核驱动将该循环缓冲区映射到了/proc 目录下的文件节点 kmsg。
通过cat 或者其他应用程序读取 Log Buffer 的时候可以不断的等待新的 log,所以访问/proc/kmsg的方式适合长时间的读取 log ,一旦有新的 log 就可以被打印出来。
首先使用以下命令读取 kmsg 文件,在没有新的内核打印信息时会阻塞
调整内核打印等级
内核的日志打印由相应的打印等级来控制,调用 printk 时,内核会将消息日志级别与当前控制台的日志级别进行比较;如果前者比后者更高(值更低),则消息会立即打印到控制台。
可以这样检查日志级别参数:可以通过调整内核打印等级来控制打印日志的输出。使用以下命令查看当前默认打印等级
1 cat /proc/sys/kernel/printk
可以看到内核打印等级由四个数字所决定,“7 4 1 7” 分别对应 console_loglevel、default_message_loglevel、minimum_console_loglevel、default_console_loglevel,具体类型说明如下:
console_loglevel
描述:控制哪些级别的消息可以输出到 终端 (console)。
规则:只有当消息的 log 优先级 高于 console_loglevel 时,才会在终端显示。
举例:“7”表示允许所有级别的消息(0~7)都输出到终端。
default_message_loglevel
描述:printk 函数打印消息时默认的 log 等级(priority)。
举例:“4”表示默认打印的消息级别是 警告(warning) 。
minimum_console_loglevel
描述:console_loglevel 可以设置的最小值。
举例:“1”表示 console_loglevel 最低可以设置为 1,也就是 紧急(emerg) 。
default_console_loglevel
描述:内核启动时 console_loglevel 的缺省值。
举例:“7”表示缺省情况下允许 所有等级 输出到终端。
include/linux/printk.c
1 2 3 4 5 6 7 int console_printk[4 ] = { CONSOLE_LOGLEVEL_DEFAULT, MESSAGE_LOGLEVEL_DEFAULT, CONSOLE_LOGLEVEL_MIN, CONSOLE_LOGLEVEL_DEFAULT, }; EXPORT_SYMBOL_GPL(console_printk);
include/linux/kern_levels.h
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 32 33 34 35 36 37 38 39 #ifndef __KERN_LEVELS_H__ #define __KERN_LEVELS_H__ #define KERN_SOH "\001" #define KERN_SOH_ASCII '\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" #define KERN_WARNING KERN_SOH "4" #define KERN_NOTICE KERN_SOH "5" #define KERN_INFO KERN_SOH "6" #define KERN_DEBUG KERN_SOH "7" #define KERN_DEFAULT "" #define KERN_CONT KERN_SOH "c" #define LOGLEVEL_SCHED -2 #define LOGLEVEL_DEFAULT -1 #define LOGLEVEL_EMERG 0 #define LOGLEVEL_ALERT 1 #define LOGLEVEL_CRIT 2 #define LOGLEVEL_ERR 3 #define LOGLEVEL_WARNING 4 #define LOGLEVEL_NOTICE 5 #define LOGLEVEL_INFO 6 #define LOGLEVEL_DEBUG 7 #endif
数字
等级名称
描述
0
KERN_EMERG
紧急,系统不可用
1
KERN_ALERT
警报,需要立即处理
2
KERN_CRIT
严重错误
3
KERN_ERR
一般错误
4
KERN_WARNING
警告
5
KERN_NOTICE
普通但重要信息
6
KERN_INFO
信息性消息
7
KERN_DEBUG
调试消息
修改内核打印等级:
1 2 3 echo 0 4 1 7 > /proc/sys/kernel/printkecho "7 4 1 7" | sudo tee /proc/sys/kernel/printk
printk 在打印信息前,可以加入相应的打印等级宏定义,具体格式如下所示:
例如:
1 printk(KERN_ERR "This is an error\n" );
如果省略调试级别 printk("This is anerror\n"),则内核将根据CONFIG_DEFAULT_MESSAGE_LOGLEVEL配置选项(这是默认的内核日志级别)向该函数提供一个调试级别。
实际上可以使用以下宏,其名称更有意义,它们是对前面所定义内容的包装 pr_emerg、pr_alert、pr_crit、pr_err、pr_warning、pr_notice、pr_info 和 pr_debug:
1 pr_err("This is the same error\n" );
llseek 定位设备驱动
应用层 lseek()
所有打开的文件都有一个当前文件偏移量(current file offset) ,以下简称为 cfo。cfo 通常是一个非负整数 ,用于表明文件开始处到文件当前位置的字节数。读写操作通常开始于 cfo,并且使 cfo 增大,增量为读写的字节数。文件被打开时,cfo 会被初始化为 0,除非使用了 O_APPEND 。 使用 lseek 函数可以改变文件的 cfo 。
项目
说明
函数定义
off_t lseek(int fd, off_t offset, int whence);
头文件
#include <sys/types.h>#include <unistd.h>
参数 fd
文件描述符
参数 off_t offset
偏移量,单位为字节 ,正负分别表示向前、向后移动
参数 whence
位置基点,可选 SEEK_SET (文件开头)、SEEK_CUR (当前指针位置)、SEEK_END (文件末尾)
功能
移动文件读写指针;获取文件长度;拓展文件空间
返回值
成功返回当前位移(SEEK_CUR) ,失败返回 - 1
例子 :
驱动层 llseek()
1 loff_t (*llseek)(struct file *file, loff_t offset, int whence);
作用 :文件指针偏移操作(类似 lseek 系统调用)。
参数 :
file:文件对象。
offset:相对于文件当前位置的偏移量,定义当前位置将改变多少。
whence:定义从哪里开始查找,可能取值如下:
SEEK_SET:相对于文件开头
SEEK_CUR:相对于当前文件指针
SEEK_END:相对于文件末尾
返回值 :
新的文件指针位置(loff_t)
出错返回负值(如 -EINVAL)
使用步骤
使用switch语句检查每种whence情况,因为其取值有限,所以还要相应调整newpos:
1 2 3 4 5 6 7 8 9 10 11 12 13 switch ( whence ) { case SEEK_SET: newpos = offset; break ; case SEEK_CUR: newpos = file->f_pos + offset; break ; case SEEK_END: newpos = filesize + offset; break ; default : return -EINVAL; }
检查newpos是否有效:
1 2 if ( newpos < 0 ) return -EINVAL;
使用新位置更新f_pos
返回新的文件指针位置
示例
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 #include <linux/init.h> #include <linux/module.h> #include <linux/uaccess.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/slab.h> #include <linux/mutex.h> #define KMEM_SIZE 32 struct test_drv_data { dev_t dev_num; struct cdev cdev ; struct class *class ; struct device *dev ; struct mutex lock ; char kmem[KMEM_SIZE]; }; static struct test_drv_data *drv_dat ;int test_open (struct inode *inode, struct file *file) { file->private_data = drv_dat; return 0 ; } ssize_t test_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { int ret; struct test_drv_data *dat = file->private_data; size_t len; if (*offset >= KMEM_SIZE) return 0 ; len = min(size, (size_t )(KMEM_SIZE - *offset)); ret = mutex_lock_interruptible(&dat->lock); if (ret < 0 ) { pr_info("test_read is interrupted while acquiring the mutex\n" ); return ret; } if (copy_to_user(buf, dat->kmem + *offset, len) != 0 ) { mutex_unlock(&dat->lock); return -EFAULT; } mutex_unlock(&dat->lock); *offset += len; return len; } ssize_t test_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { int ret; struct test_drv_data *dat = file->private_data; size_t len = min(size, (size_t )(KMEM_SIZE - *offset)); if (*offset > KMEM_SIZE) return 0 ; ret = mutex_lock_interruptible(&dat->lock); if (ret < 0 ) { pr_info("test_read is interrupted while acquiring the mutex\n" ); return ret; } if (copy_from_user(dat->kmem + *offset, buf, len) != 0 ) { mutex_unlock(&dat->lock); return -EFAULT; } mutex_unlock(&dat->lock); *offset += len; return len; } loff_t test_llseek (struct file *file, loff_t offset, int whence) { loff_t new_offset; switch (whence) { case SEEK_SET: new_offset = offset; break ; case SEEK_CUR: new_offset = file->f_pos + offset; break ; case SEEK_END: new_offset = KMEM_SIZE + offset; break ; default : return -EINVAL; } if (new_offset < 0 || new_offset > KMEM_SIZE) return -EINVAL; file->f_pos = new_offset; return new_offset; } int test_release (struct inode *inode, struct file *file) { return 0 ; } struct file_operations fops = { .owner = THIS_MODULE, .open = test_open, .read = test_read, .write = test_write, .llseek = test_llseek, .release = test_release, }; static int __init llseek_test_init (void ) { int ret; drv_dat = kzalloc(sizeof (struct test_drv_data), GFP_KERNEL); if (drv_dat == NULL ) { ret = -ENOMEM; goto kzalloc_fail; } ret = alloc_chrdev_region(&drv_dat->dev_num, 0 , 1 , "test_chrdev_region" ); if (ret < 0 ) goto alloc_chrdev_region; cdev_init(&drv_dat->cdev, &fops); drv_dat->cdev.owner = THIS_MODULE; ret = cdev_add(&drv_dat->cdev, drv_dat->dev_num, 1 ); if (ret < 0 ) goto cdev_add_fail; drv_dat->class = class_create(THIS_MODULE, "chrdev" ); if (IS_ERR(drv_dat->class)) { ret = PTR_ERR(drv_dat->class); goto class_create_fail; } drv_dat->dev = device_create(drv_dat->class, NULL , drv_dat->dev_num, NULL , "llseek_test%d" , 0 ); if (IS_ERR(drv_dat->dev)) { ret = PTR_ERR(drv_dat->dev); goto device_create_fail; } mutex_init(&drv_dat->lock); return 0 ; device_create_fail: class_destroy(drv_dat->class); class_create_fail: cdev_del(&drv_dat->cdev); cdev_add_fail: unregister_chrdev_region(drv_dat->dev_num, 1 ); alloc_chrdev_region: kfree(drv_dat); kzalloc_fail: return ret; } static void __exit llseek_test_exit (void ) { device_destroy(drv_dat->class, drv_dat->dev_num); class_destroy(drv_dat->class); cdev_del(&drv_dat->cdev); unregister_chrdev_region(drv_dat->dev_num, 1 ); kfree(drv_dat); } module_init(llseek_test_init); module_exit(llseek_test_exit); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("even629<asqwgo@outlook.com>" ); MODULE_DESCRIPTION("This is a test description for llseek" );
对于固定缓冲区大小的设备,可直接返回内核已经实现好的fixed_size_llseek(file, offset, whence, KMEM_SIZE);
测试用例:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 static void dump_buf(const char *tag, const char *buf, ssize_t len) { printf ("%s (%zd bytes): \"" , tag, len); for (ssize_t i = 0; i < len; i++) { if (buf[i] >= 32 && buf[i] <= 126) putchar(buf[i]); else printf ("\\x%02x" , (unsigned char)buf[i]); } printf ("\"\n" ); } int main(void) { int fd; char buf[BUF_SIZE]; ssize_t ret; off_t off; printf ("open %s\n" , DEV_PATH); fd = open(DEV_PATH, O_RDWR); if (fd < 0) { perror("open" ); return 1; } /* ================= write ================= */ const char *msg = "Hello llseek test!" ; printf ("\n[TEST] write \"%s\"\n" , msg); ret = write(fd, msg, strlen(msg)); if (ret < 0) { perror("write" ); goto out; } printf ("write ret = %zd\n" , ret); /* ================= SEEK_SET ================= */ printf ("\n[TEST] lseek SEEK_SET 0\n" ); off = lseek(fd, 0, SEEK_SET); if (off < 0) { perror("lseek SEEK_SET" ); goto out; } printf ("current offset = %ld\n" , off); memset(buf, 0, sizeof(buf)); ret = read (fd, buf, sizeof(buf)); if (ret < 0) { perror("read" ); goto out; } dump_buf("read" , buf, ret); /* ================= SEEK_CUR ================= */ printf ("\n[TEST] lseek SEEK_CUR -6\n" ); off = lseek(fd, -6, SEEK_CUR); if (off < 0) { perror("lseek SEEK_CUR" ); } else { printf ("current offset = %ld\n" , off); } memset(buf, 0, sizeof(buf)); ret = read (fd, buf, 6); dump_buf("read" , buf, ret); /* ================= SEEK_END ================= */ printf ("\n[TEST] lseek SEEK_END -5\n" ); off = lseek(fd, -5, SEEK_END); if (off < 0) { perror("lseek SEEK_END" ); } else { printf ("current offset = %ld\n" , off); } memset(buf, 0, sizeof(buf)); ret = read (fd, buf, 5); dump_buf("read" , buf, ret); /* ================= EOF ================= */ printf ("\n[TEST] read until EOF\n" ); off = lseek(fd, 0, SEEK_SET); printf ("seek to %ld\n" , off); while (1) { ret = read (fd, buf, 8); if (ret == 0) { printf ("EOF reached\n" ); break ; } if (ret < 0) { perror("read" ); break ; } dump_buf("chunk" , buf, ret); } /* ================= invalid lseek ================= */ printf ("\n[TEST] invalid lseek (beyond end)\n" ); off = lseek(fd, 100, SEEK_SET); if (off < 0) printf ("expected error: %s\n" , strerror(errno)); else printf ("unexpected success, off=%ld\n" , off); out: close(fd); return 0; }
测试:
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 $ ./test_llseek.o open /dev/llseek_test0 [TEST] write "Hello llseek test!" write ret = 18 [TEST] lseek SEEK_SET 0 current offset = 0 read (32 bytes): "Hello llseek test!\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" [TEST] lseek SEEK_CUR -6 current offset = 26 read (6 bytes): "\x00\x00\x00\x00\x00\x00" [TEST] lseek SEEK_END -5 current offset = 27 read (5 bytes): "\x00\x00\x00\x00\x00" [TEST] read until EOF seek to 0 chunk (8 bytes): "Hello ll" chunk (8 bytes): "seek tes" chunk (8 bytes): "t!\x00\x00\x00\x00\x00\x00" chunk (8 bytes): "\x00\x00\x00\x00\x00\x00\x00\x00" EOF reached [TEST] invalid lseek (beyond end) expected error: Invalid argument
ioctl 设备操作
应用层 ioctl()
项目
说明
函数定义
int ioctl(int fd, unsigned long op, … /* arg */ );
头文件
#include <sys/ioctl.h>(可能需要设备相关头文件,如 linux/ioctl.h)
参数 fd
打开的设备文件描述符(如 /dev/...)
参数 op
IO 控制命令(通常通过 _IO, _IOR, _IOW, _IOWR 等宏构建)
参数 arg (可选)
与 request 命令关联的数据,可以为 int*, void*, struct * 等
功能
对设备驱动执行控制命令(非数据读写类),用于配置硬件、获取状态、发送控制指令等
返回值
成功:通常为 0 (也可能返回其他正值,视 request 而定) 失败:返回 -1 ,并设置 errno
上述三个参数中,最重要的是第二个 op参数,为 unsigned int 类型,为了高效的使用 op参数传递更多的控制信息,一个 unsigned int op被拆分为了 4 段,每一段都有各自的意义,unsigned int cmd 位域拆分如下:
1 2 | 31 30 | 29 ... ... ... ... ... . 16 | 15 ... ... 8 | 7 ... ... ... . 0 | | dir | size | type | nr |
op[31:30] dir 数据(args)的传输方向(读写)
op[29:16] size 数据(args)的大小
op[15:8] type 命令的类型,可以理解成命令的密钥 ,一般为 ASCII 码(0-255 的一个字符,有部分字符已经被占用,每个字符的序号段可能部分被占用)
op[7:0] nr 命令的序号 ,是一个 8bits 的数字(序号,0-255 之间)
op 参数由 ioctl 合成宏 定义得到,四个合成宏定义如下所示:
定义一个命令,但是不需要参数:
1 2 #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0 )
定义一个命令,应用程序从驱动程序读参数:
1 2 #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
定义一个命令,应用程序向驱动程序写参数:
1 2 #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
定义一个命令,参数是双向传递的(先写再读):
1 #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
宏定义参数说明如下所示:
type :命令的类型,一般为一个 ASCII 码值,称为魔数,一个驱动程序一般使用一个 type
nr :该命令下序号。一个驱动有多个命令,一般他们的 type,序号不同
size :args 的类型
例如可以使用以下代码定义不需要参数、向驱动程序写参数、向驱动程序读参数三个宏:
1 2 3 #define CMD_TEST0 _IO('L' ,0) #define CMD_TEST1 _IOW('L' ,1,int) #define CMD_TEST2 _IOR('L' ,2,int)
在 Linux 内核的 ioctl 接口中,每个 ioctl 命令只能传递一个用户空间指针(即一个参数)。但这并不意味着只能传一个整数,可以通过 传递一个结构体指针 来实现“多个参数” 。
SIZE = arg 参数的数据结构大小(字节数)它是驱动创建 ioctl 命令码时自动编码进去的。
例:
会自动把:
编码进 op。如果是:
1 _IOW('M' , 2 , struct task_info)
则:
1 sizeof (struct task_info)
用户调用
1 2 struct task_info info ;ioctl(fd, MY_WRITE_TASK, &info);
内核收到后:
1 copy_from_user(&kernel_buffer, (void __user *)arg, sizeof (struct task_info));
内核源码的 Documentation/driver-api/ioctl.rst中会详细说明。
示例eep_ioctl.h::
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 #ifndef PACKT_IOCTL_H #define PACKT_IOCTL_H #define EEP_MAGIC 'E' #define ERASE_SEQ_NO 0x01 #define RENAME_SEQ_NO 0x02 #define ClEAR_BYTE_SEQ_NO 0x03 #define GET_SIZE 0x04 #define MAX_PART_NAME 32 #define EEP_ERASE _IO(EEP_MAGIC, ERASE_SEQ_NO) #define EEP_RENAME_PART _IOW(EEP_MAGIC, RENAME_SEQ_NO, unsigned long) #define EEP_GET_SIZE _IOR(EEP_MAGIC, GET_SIZE,int *) #endif
驱动层 ioctl()
1 2 long unlocked_ioctl (struct file *file, unsigned int cmd, unsigned long arg) ;long compat_ioctl (struct file *file, unsigned int cmd, unsigned long arg) ;
unlocked_ioctl 给“本机位宽”的用户程序用
compat_ioctl 给“32 位用户程序跑在 64 位内核”用
调用未定义的ioctl命令时返回-ENOTTY错误
内核会用 ioctl 分解宏 解析 cmd:
_IOC_DIR(cmd) —— 数据方向
_IOC_TYPE(cmd) —— 设备 magic
_IOC_NR(cmd) —— 命令编号
_IOC_SIZE(cmd) —— 传递数据大小
示例
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 #include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/kdev_t.h> #include <linux/uaccess.h> #include <linux/timer.h> #define TIMER_OPEN _IO('L' ,0) #define TIMER_CLOSE _IO('L' ,1) #define TIMER_SET _IOW('L' ,2,int) struct device_test { dev_t dev_num; int major ; int minor ; struct cdev cdev_test ; struct class *class ; struct device *device ; int counter; }; static struct device_test dev1 ;static void fnction_test (struct timer_list *t) ;DEFINE_TIMER(timer_test,fnction_test); void fnction_test (struct timer_list *t) { printk("this is fnction_test\n" ); mod_timer(&timer_test,jiffies_64 + msecs_to_jiffies(dev1.counter)); } static int cdev_test_open (struct inode *inode, struct file *file) { file->private_data=&dev1; return 0 ; } static int cdev_test_release (struct inode *inode, struct file *file) { file->private_data=&dev1; return 0 ; } static long cdev_test_ioctl (struct file *file, unsigned int cmd, unsigned long arg) { struct device_test *test_dev = (struct device_test *)file->private_data; switch (cmd){ case TIMER_OPEN: add_timer(&timer_test); break ; case TIMER_CLOSE: del_timer(&timer_test); break ; case TIMER_SET: test_dev->counter = arg; timer_test.expires = jiffies_64 + msecs_to_jiffies(test_dev->counter); break ; default : break ; } return 0 ; } struct file_operations cdev_test_fops = { .owner = THIS_MODULE, .open = cdev_test_open, .release = cdev_test_release, .unlocked_ioctl = cdev_test_ioctl, }; static int __init timer_dev_init (void ) { int ret; ret = alloc_chrdev_region(&dev1.dev_num, 0 , 1 , "alloc_name" ); if (ret < 0 ) { goto err_chrdev; } printk("alloc_chrdev_region is ok\n" ); dev1.major = MAJOR(dev1.dev_num); dev1.minor = MINOR(dev1.dev_num); printk("major is %d \r\n" , dev1.major); printk("minor is %d \r\n" , dev1.minor); dev1.cdev_test.owner = THIS_MODULE; cdev_init(&dev1.cdev_test, &cdev_test_fops); ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1 ); if (ret<0 ) { goto err_chr_add; } dev1. class = class_create(THIS_MODULE, "test" ); if (IS_ERR(dev1.class)) { ret=PTR_ERR(dev1.class); goto err_class_create; } dev1.device = device_create(dev1.class, NULL , dev1.dev_num, NULL , "test" ); if (IS_ERR(dev1.device)) { ret=PTR_ERR(dev1.device); goto err_device_create; } return 0 ;err_device_create: class_destroy(dev1.class); err_class_create: cdev_del(&dev1.cdev_test); err_chr_add: unregister_chrdev_region(dev1.dev_num, 1 ); err_chrdev: return ret; } static void __exit timer_dev_exit (void ) { unregister_chrdev_region(dev1.dev_num, 1 ); cdev_del(&dev1.cdev_test); device_destroy(dev1.class, dev1.dev_num); class_destroy(dev1.class); } module_init(timer_dev_init); module_exit(timer_dev_exit); MODULE_LICENSE("GPL v2" ); MODULE_AUTHOR("topeet" );
封装驱动提供的API函数
作为驱动工程师的我们当然可以理解每一行代码所要完成的功能,而一般情况下,应用都是由专业的应用工程师来进行编写的,上述代码编写方式很不利于应用工程师的理解和程序的移植,所以对于应用程序 API 的封装是一件必然的事情。
编译成库文件,每个功能函数一个.c文件 ,每个编译成一个库文件
优化驱动稳定性和效率
检测ioctl命令
ioctl 的 cmd 命令是由合成宏合成得到的,也有相应的分解宏得到各个参数,四个分解宏如下所示:
分解 cmd 命令,得到数据(args)的传输方向:
可以在驱动中通过上述分解宏对传入的 ioctl 命令类型等参数进行判断,从而得到判断传入的参数是否正确,以此优化驱动的稳定性。
检测传递地址是否合理
access_ok()
项目
说明
函数原型
int access_ok(const void __user *addr, unsigned long size);
头文件
#include <linux/uaccess.h>
参数 addr
用户空间的指针变量,指向要检查的内存块起始地址
参数 size
要检查的内存块大小(字节数)
功能
检查指定的用户空间内存块是否可访问(读/写),用于保护内核访问用户空间数据安全
返回值
成功:1 (可访问)失败:0 (不可访问)
例子:
1 2 3 4 len = sizeof (struct args); if (!access_ok(arg,len)){ return -1 ; }
分支预测优化
现在的 CPU 都有 ICache 和流水线机制。即运行当前指令时,ICache 会预读取后面的指令,从而提升效率。但是如果条件分支的结果是跳转到了其他指令,那预取下一条指令就浪费时间了。
likely和unlikely宏
而我们要用到的 likely 和 unlikely 宏 ,会让编译器总是将大概率执行的代码放在靠前的位置,从而提高驱动的效率。
likely 和 unlikely 宏定义在内核源码include/linux/compiler.h文件中,具体定义如下所示:
1 2 #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)
__builtin_expect 的作用是告知编译器预期表达式 exp 等于 c 的可能性更大,编译器可以根据该因素更好的对代码进行优化,所以 likely 与 unlikely 的作用就是表达性 x 为真的可能性更大(likely)和更小(unlikely)。
例子:
1 2 3 if (unlikely(copy_from_user(&test,(int *)arg,sizeof (test)) != 0 )){ printk("copy_from_user error\n" ); }
驱动调试
调试打印
dump_stack()
dump_stack() 的作用是:
打印当前 CPU 的调用堆栈(call trace)到内核日志中。
在内核代码中调用它,相当于执行用户态中的 backtrace() 或者 C 库的 printf("%pS", __builtin_return_address()) 之类的功能。
例子:
1 2 3 4 5 6 7 8 #include <linux/module.h> #include <linux/kernel.h> static int __init helloworld_init (void ) { printk(KERN_EMERG "helloworld_init\r\n" ); dump_stack(); return 0 ; }
WARN_ON(condition)
WARN_ON (condition)函数作用: 在括号中的条件成立时,内核会抛出栈回溯,打印函数的调用关系 。
通常用于内核抛出一个警告,暗示某种不太合理的事情发生了。
WARN_ON 实际上也是调用 dump_stack,只是多了参数 condition 判断条件是否成立 ,例如 WARN_ON (1)则条件判断成功,函数会成功执行。
例子:
1 2 3 4 5 6 7 8 #include <linux/module.h> #include <linux/kernel.h> static int __init helloworld_init (void ) { printk(KERN_EMERG "helloworld_init\r\n" ); WARN_ON(1 ); return 0 ; }
BUG() 和 BUG_ON (condition)
内核中有许多地方调用类似 BUG_ON()的语句,它非常像一个内核运行时的断言,意味着本来不该执行到 BUG_ON()这条语句,一旦 BUG_ON()执行内核就会立刻抛出 oops (只有Linux出现致命错误时会抛出oops),导致栈的回溯和错误信息的打印。
大部分体系结构把 BUG()和 BUG_ON()定义成某种非法操作,这样自然会产生需要的 oops。应用层可以看到产生了段错误。
参数 condition 判断条件是否成立,例如 BUG_ON (1)则条件判断成功,函数会成功执行。
1 2 3 4 5 6 7 8 9 #include <linux/module.h> #include <linux/kernel.h> static int __init helloworld_init (void ) { printk(KERN_EMERG "helloworld_init\r\n" ); BUGON(1 ); return 0 ; }
panic (fmt…)
panic (fmt…)函数: 输出打印会造成系统死机并将函数的调用关系以及寄存器值就都打印出来
1 2 3 4 5 6 7 8 #include <linux/module.h> #include <linux/kernel.h> static int __init helloworld_init (void ) { printk(KERN_EMERG "helloworld_init\r\n" ); panic("!!!!!!!!!!!!!!!!!!!!!!!!!!!!" ); return 0 ; }