IO模型

回顾上一期,我们学会了如何用寄存器控制GPIO,并且真正掌握了字符设备的开发。这一期我们来聊聊IO模型、内核定时器、IOCTL命令以及最核心的中断机制。

先来搞清楚一个问题:什么叫IO模型?说白了就是file_operations里面read和write的执行策略。Linux内核给我们提供了四种经典的玩法:

  • 阻塞IO(Blocking IO)
  • 非阻塞IO(Non-blocking IO)
  • 信号驱动IO(Signal-driven IO)
  • IO多路复用(IO Multiplexing)

光看名字可能有点抽象,我用小明买菜的例子解释一下你就明白了:

阻塞模式:小明去买菜,商店没菜,小明就在门口站着等,啥也不干,等到菜来了再走。

非阻塞模式:小明去买菜,商店没菜,小明扭头就走,回家干别的事,过一会儿再去看看,如此反复。

信号驱动:小明去买菜,商店没菜,小明跟老板说”菜到了给我打电话”,然后回家该干嘛干嘛,电话一响就知道菜来了。

IO复用:小明要买一堆东西,跑了好几家店,每家都留了电话,哪家有货小明就去哪家买,一次性解决所有需求。

是不是一下子就理解了?好,接下来我们挨个看看这四种模式在内核里怎么实现。

阻塞IO

阻塞IO是最常见的一种。用户态调用read(),如果设备里还没数据,内核就让这个进程睡过去(状态变成TASK_INTERRUPTIBLE——可中断睡眠),等数据来了再把它叫醒,数据原路返回给用户态。

实现阻塞IO的关键就是等待队列(wait queue)。这玩意儿说白了就是一个链表,上面挂着所有在等数据的进程,数据就绪了驱动就去这个链表上把人叫醒。API都在<linux/wait.h>里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义加初始化一条龙
#define DECLARE_WAIT_QUEUE_HEAD(name)

// 如果你的等待队列头嵌在结构体里,用这个动态初始化
void init_waitqueue_head(wait_queue_head_t *q);

// 让进程睡觉,睡到 condition 为真才醒。_interruptible 的意思是这个睡眠可以被信号(比如 Ctrl+C)打断
#define wait_event_interruptible(wq_head, condition)

// 把队列上所有睡着的进程叫醒
#define wake_up_interruptible(x)

// 只叫醒一个(如果你确定只有一个进程在等)
#define wake_up_interruptible_sync(x)

注意一个细节:上面这些宏里面用的是wq_head(等待队列),不是你自己的队列。你只需要在设备结构体里塞一个wait_queue_head_t,然后init_waitqueue_head一把梭就行了。

原理讲完了,直接上代码。这个驱动很简单:用一个have_data标记有没有数据可读,read里没数据就睡,write写入后设标记然后叫醒:

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>

#define DEV_NAME "block_io"
#define BUF_SIZE 64

struct block_io_dev {
dev_t dev_number;
struct cdev chr_dev;
struct class *cls;
struct device *dev;
char kbuf[BUF_SIZE];
int have_data; // 有这个标志才能读
wait_queue_head_t read_wq; // 没数据就睡在这上面
};

static struct block_io_dev my_dev;

static int block_open(struct inode *inode, struct file *filp)
{
filp->private_data = &my_dev;
printk(KERN_INFO "[BlockIO]: Device opened\n");
return 0;
}

static ssize_t block_read(struct file *filp, char __user *buf,
size_t count, loff_t *off)
{
struct block_io_dev *dev = filp->private_data;
int ret;

printk(KERN_INFO "[BlockIO]: read() 被调用,等数据中...\n");

// 没数据就睡,有人叫醒了再检查一下条件(经典的"条件等待")
ret = wait_event_interruptible(dev->read_wq, dev->have_data != 0);
if (ret) {
// 不是被数据叫醒的,是被信号(比如用户按了Ctrl+C)打断的
printk(KERN_INFO "[BlockIO]: read() 被信号打断了\n");
return -ERESTARTSYS;
}

if (count > BUF_SIZE)
count = BUF_SIZE;

if (copy_to_user(buf, dev->kbuf, count)) {
printk(KERN_ERR "[BlockIO]: copy_to_user 失败\n");
return -EFAULT;
}

// 数据读走了,标志清掉
dev->have_data = 0;
printk(KERN_INFO "[BlockIO]: read() 完成,返回 %zu 字节\n", count);
return count;
}

static ssize_t block_write(struct file *filp, const char __user *buf,
size_t count, loff_t *off)
{
struct block_io_dev *dev = filp->private_data;

if (count > BUF_SIZE)
count = BUF_SIZE;

if (copy_from_user(dev->kbuf, buf, count)) {
printk(KERN_ERR "[BlockIO]: copy_from_user 失败\n");
return -EFAULT;
}

// 数据来了!设标志,叫醒等着的进程
dev->have_data = 1;
wake_up_interruptible(&dev->read_wq);

printk(KERN_INFO "[BlockIO]: write() 完成,%zu 字节\n", count);
return count;
}

static int block_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "[BlockIO]: 设备关了\n");
return 0;
}

static struct file_operations block_fops = {
.owner = THIS_MODULE,
.open = block_open,
.read = block_read,
.write = block_write,
.release = block_release,
};

static int __init block_init(void)
{
int ret;
ret = alloc_chrdev_region(&my_dev.dev_number, 0, 1, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "[BlockIO]: 申请设备号失败\n");
return ret;
}

// 等待队列一定要在用之前初始化!!
init_waitqueue_head(&my_dev.read_wq);
my_dev.have_data = 0;

cdev_init(&my_dev.chr_dev, &block_fops);
my_dev.chr_dev.owner = THIS_MODULE;
ret = cdev_add(&my_dev.chr_dev, my_dev.dev_number, 1);
if (ret < 0) {
unregister_chrdev_region(my_dev.dev_number, 1);
return ret;
}

my_dev.cls = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(my_dev.cls)) {
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
return PTR_ERR(my_dev.cls);
}
my_dev.dev = device_create(my_dev.cls, NULL, my_dev.dev_number,
NULL, DEV_NAME);

printk(KERN_INFO "[BlockIO]: 模块加载完成\n");
return 0;
}

static void __exit block_exit(void)
{
device_destroy(my_dev.cls, my_dev.dev_number);
class_destroy(my_dev.cls);
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
printk(KERN_INFO "[BlockIO]: 模块卸载\n");
}

module_init(block_init);
module_exit(block_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");

编译好ko文件放到板子上,我们来实际玩一下。开两个终端:

终端一执行 cat /dev/block_io——这时候cat就卡住了(内核里进程正在等待队列上睡觉呢)。

终端二执行 echo "hello rk3588" > /dev/block_io——终端一立马就输出”hello rk3588”,然后cat退出。

1
2
3
4
5
6
# 终端一(你会发现它卡住了,啥也不输出)
topeet@topeet:~$ sudo cat /dev/block_io
hello rk3588 # 终端二 echo 完,这里立刻就出来了

# 终端二
topeet@topeet:~$ sudo echo "hello rk3588" > /dev/block_io

用dmesg看看内核里发生了什么:

1
2
3
4
5
6
[ 1024.123456] [BlockIO]: 模块加载完成
[ 1025.111111] [BlockIO]: Device opened
[ 1025.111222] [BlockIO]: read() 被调用,等数据中...
[ 1030.222333] [BlockIO]: write() 完成,15 字节
[ 1030.222444] [BlockIO]: read() 完成,返回 15 字节
[ 1030.222555] [BlockIO]: 设备关了

整个链路清楚了吧?read被阻塞→write触发唤醒→read拿到数据返回。这就是最基础的阻塞IO模型。

另外需要注意一个小坑:等待队列有wait_eventwait_event_interruptible两个版本。不带_interruptible的版本不能被信号打断——也就是说用户按Ctrl+C也没用,进程会一直睡在那里。除非你非常确定需要这个行为,否则一律用_interruptible版本,这样用户至少能用Ctrl+C把卡住的程序杀掉。唤醒端也有对应的wake_upwake_up_interruptible,必须配对使用——_interruptible睡的必须用_interruptible来唤醒。

非阻塞IO

阻塞IO的问题一目了然:数据没来进程就得等,对于有些场景这不行。比如你写一个GUI程序,你不可能让界面线程在等数据的时候整个窗口都卡死吧?这时候就要用非阻塞IO了。

非阻塞IO的实现其实非常简单——打开设备的时候加一个O_NONBLOCK标志,驱动里检查这个标志位,如果数据还没好就直接返回-EAGAIN,告诉用户态”现在没数据,你先忙别的,等会儿再来试试”。

检查非阻塞标志非常简单,就看filp->f_flags里有没有O_NONBLOCK

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
static ssize_t block_read(struct file *filp, char __user *buf,
size_t count, loff_t *off)
{
struct block_io_dev *dev = filp->private_data;
int ret;

// 如果是非阻塞模式,没数据就直接走人
if (filp->f_flags & O_NONBLOCK) {
if (!dev->have_data)
return -EAGAIN; // 告诉用户:现在没货,等会儿再来
} else {
// 阻塞模式,走等待队列老路子
ret = wait_event_interruptible(dev->read_wq,
dev->have_data != 0);
if (ret)
return -ERESTARTSYS;
}

if (count > BUF_SIZE)
count = BUF_SIZE;

if (copy_to_user(buf, dev->kbuf, count))
return -EFAULT;

dev->have_data = 0;
return count;
}

就改了一个if判断,阻塞和非阻塞就都支持了。然后用户态这样用:

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main(void)
{
// 关键:O_NONBLOCK
int fd = open("/dev/block_io", O_RDWR | O_NONBLOCK);
if (fd < 0) {
perror("open");
return 1;
}

char buf[64];
ssize_t ret = read(fd, buf, sizeof(buf));
if (ret < 0) {
if (errno == EAGAIN) {
printf("现在没数据,等会儿再来吧\n");
} else {
perror("read");
}
} else {
printf("读到了: %s\n", buf);
}

close(fd);
return 0;
}

编译运行一下,如果还没人往设备里写数据,你就会看到”现在没数据,等会儿再来吧”。

有人可能会问:非阻塞模式是不是就意味着要在用户态写一个while循环不停地read?没错,早期确实这样干,但这样太费CPU了。更好的做法是把轮询交给内核——也就是我们接下来要讲的poll多路复用。不过非阻塞模式本身依然很重要,因为它可以和select/poll/epoll配合使用,你想嘛,如果poll告诉你”这个fd可读了”,结果你read的时候设备正好又没数据了(极少发生但确实可能),阻塞read还是会卡住。所以最佳实践是:open的时候加上O_NONBLOCK,然后用poll/epoll等待,收到通知后再read

信号驱动IO(fasync)

信号驱动IO是一种异步通知机制,理解起来就是那个”菜到了打电话”的比喻。用户态进程先跟内核登记一下”数据来了请给我发SIGIO信号”,然后就可以安心干别的事。当数据就绪时内核就发SIGIO信号过来,进程在信号处理函数里读数据就行了——全程不用轮询也不阻塞。

这个机制用到了file_operations里两个东西:.fasync回调 + kill_fasync发信号。内核提供了fasync_helper这个辅助函数帮我们管理异步通知队列,我们只需要在.fasync回调里调它,在write里调kill_fasync发SIGIO就行了:

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/wait.h>

#define DEV_NAME "fasync_dev"
#define BUF_SIZE 64

struct fasync_dev {
dev_t dev_number;
struct cdev chr_dev;
struct class *cls;
struct device *dev;
char kbuf[BUF_SIZE];
int have_data;
wait_queue_head_t read_wq;
struct fasync_struct *fasync_queue; // 这个队列记录着所有等信号的进程
};

static struct fasync_dev my_dev;

// 就这一个回调,fasync_helper 帮你搞定了注册和注销
static int fasync_fasync(int fd, struct file *filp, int mode)
{
struct fasync_dev *dev = filp->private_data;
return fasync_helper(fd, filp, mode, &dev->fasync_queue);
}

static ssize_t fasync_read(struct file *filp, char __user *buf,
size_t count, loff_t *off)
{
struct fasync_dev *dev = filp->private_data;
int ret;

if (filp->f_flags & O_NONBLOCK) {
if (!dev->have_data)
return -EAGAIN;
} else {
ret = wait_event_interruptible(dev->read_wq,
dev->have_data != 0);
if (ret)
return -ERESTARTSYS;
}

if (count > BUF_SIZE)
count = BUF_SIZE;

if (copy_to_user(buf, dev->kbuf, count))
return -EFAULT;

dev->have_data = 0;
return count;
}

static ssize_t fasync_write(struct file *filp, const char __user *buf,
size_t count, loff_t *off)
{
struct fasync_dev *dev = filp->private_data;

if (count > BUF_SIZE)
count = BUF_SIZE;

if (copy_from_user(dev->kbuf, buf, count))
return -EFAULT;

dev->have_data = 1;
wake_up_interruptible(&dev->read_wq);

// 关键!向所有登记过的进程发 SIGIO 信号
// POLL_IN 表示"可读",还有 POLL_OUT(可写)、POLL_ERR(出错了)
kill_fasync(&dev->fasync_queue, SIGIO, POLL_IN);

printk(KERN_INFO "[Fasync]: 已向用户进程发送 SIGIO\n");
return count;
}

static int fasync_open(struct inode *inode, struct file *filp)
{
filp->private_data = &my_dev;
return 0;
}

static int fasync_release(struct inode *inode, struct file *filp)
{
struct fasync_dev *dev = filp->private_data;
// release的时候记得把自己从异步队列里摘出去
fasync_fasync(-1, filp, 0);
printk(KERN_INFO "[Fasync]: 设备关闭\n");
return 0;
}

static struct file_operations fasync_fops = {
.owner = THIS_MODULE,
.open = fasync_open,
.read = fasync_read,
.write = fasync_write,
.fasync = fasync_fasync, // 别忘了注册这个
.release = fasync_release,
};

static int __init fasync_init(void)
{
int ret;
ret = alloc_chrdev_region(&my_dev.dev_number, 0, 1, DEV_NAME);
if (ret < 0) return ret;

init_waitqueue_head(&my_dev.read_wq);
my_dev.have_data = 0;
my_dev.fasync_queue = NULL; // 一开始没人登记

cdev_init(&my_dev.chr_dev, &fasync_fops);
my_dev.chr_dev.owner = THIS_MODULE;
ret = cdev_add(&my_dev.chr_dev, my_dev.dev_number, 1);
if (ret < 0) {
unregister_chrdev_region(my_dev.dev_number, 1);
return ret;
}

my_dev.cls = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(my_dev.cls)) {
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
return PTR_ERR(my_dev.cls);
}
my_dev.dev = device_create(my_dev.cls, NULL, my_dev.dev_number,
NULL, DEV_NAME);

printk(KERN_INFO "[Fasync]: 模块加载完成\n");
return 0;
}

static void __exit fasync_exit(void)
{
// 卸载前通知所有人:设备没了,别等了
kill_fasync(&my_dev.fasync_queue, SIGIO, POLL_HUP);
device_destroy(my_dev.cls, my_dev.dev_number);
class_destroy(my_dev.cls);
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
printk(KERN_INFO "[Fasync]: 模块卸载\n");
}

module_init(fasync_init);
module_exit(fasync_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");

驱动写好了,用户态怎么配合呢?三步走:

  1. 注册SIGIO信号的处理函数(signal(SIGIO, handler)
  2. 告诉内核”这个fd的信号发给我这个进程”(fcntl(fd, F_SETOWN, getpid())
  3. 开启异步通知(fcntl(fd, F_SETFL, flags | FASYNC)

完整用户态代码:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

int fd;

// 信号来了就read
void sigio_handler(int signo)
{
char buf[64] = {0};
ssize_t ret = read(fd, buf, sizeof(buf));
if (ret > 0)
printf("[SIGIO] 收到数据: %s\n", buf);
else
printf("[SIGIO] read出错: ret=%zd\n", ret);
}

int main(void)
{
signal(SIGIO, sigio_handler); // 第1步:注册处理函数

fd = open("/dev/fasync_dev", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}

fcntl(fd, F_SETOWN, getpid()); // 第2步:告诉内核谁收信号

int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC); // 第3步:开启异步通知

printf("等待 SIGIO 信号... (PID: %d)\n", getpid());

// 主循环该干嘛干嘛,数据来了SIGIO自然会触发
while (1) {
printf("主循环在忙别的事...\n");
sleep(3);
}

close(fd);
return 0;
}

在板子上测试的时候,先启动用户态程序,然后在另一个终端往/dev/fasync_dev写数据,用户进程会立刻收到SIGIO信号并打印数据。你可以在主循环里加一些真实的业务逻辑(比如更新UI、处理传感器数据等),完全不会被IO阻塞。

有几个小细节需要注意:

第一,kill_fasync的第三个参数用来告诉用户态”发生了什么事件”。POLL_IN表示可读,POLL_OUT表示可写,POLL_ERR表示出错了。模块卸载时传POLL_HUP表示”设备挂了”。

第二,fasync_helper里面用的fd是用户态的文件描述符,mode是操作类型——用户态调用fcntl(fd, F_SETFL, flags | FASYNC)的时候内核会调用你的.fasync回调,mode非0表示注册,mode为0(用户close文件时)表示注销。

IO多路复用(poll)

前面说了非阻塞IO需要用户态轮询,轮询的效率太低了——每次都要切到内核态,大部分时候都是白跑一趟。于是内核提供了一种更好的方案:用户态通过select()poll()或者epoll()一次性把多个fd丢给内核,内核帮你蹲着,哪个fd就绪了就通知你。

驱动层要实现的就是file_operations里的.poll回调。这个回调做两件事:

  1. 调用poll_wait把当前进程挂到你的等待队列上(这样数据来了你叫醒就行)
  2. 返回一个掩码,告诉内核当前设备是什么状态(可读?可写?出错了?)

常用的返回掩码就是这几个:

掩码啥意思
POLLIN / POLLRDNORM有数据可读
POLLOUT / POLLWRNORM可以往里写
POLLERR设备出错了
POLLHUP设备挂了(连接断开)

完整驱动代码,基于前面的阻塞IO版本,加个poll回调就行:

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/poll.h>

#define DEV_NAME "poll_dev"
#define BUF_SIZE 64

struct poll_dev {
dev_t dev_number;
struct cdev chr_dev;
struct class *cls;
struct device *dev;
char kbuf[BUF_SIZE];
int have_data;
wait_queue_head_t read_wq;
};

static struct poll_dev my_dev;

static int poll_open(struct inode *inode, struct file *filp)
{
filp->private_data = &my_dev;
return 0;
}

static ssize_t poll_read(struct file *filp, char __user *buf,
size_t count, loff_t *off)
{
struct poll_dev *dev = filp->private_data;
int ret;

if (filp->f_flags & O_NONBLOCK) {
if (!dev->have_data)
return -EAGAIN;
} else {
ret = wait_event_interruptible(dev->read_wq,
dev->have_data != 0);
if (ret)
return -ERESTARTSYS;
}

if (count > BUF_SIZE)
count = BUF_SIZE;

if (copy_to_user(buf, dev->kbuf, count))
return -EFAULT;

dev->have_data = 0;
return count;
}

static ssize_t poll_write(struct file *filp, const char __user *buf,
size_t count, loff_t *off)
{
struct poll_dev *dev = filp->private_data;

if (count > BUF_SIZE)
count = BUF_SIZE;

if (copy_from_user(dev->kbuf, buf, count))
return -EFAULT;

dev->have_data = 1;
wake_up_interruptible(&dev->read_wq);
return count;
}

/*
* poll 回调:核心就两行代码
* 1. poll_wait —— 把进程挂到等待队列
* 2. return mask —— 告诉内核设备现在啥状态
*/
static __poll_t poll_poll(struct file *filp,
struct poll_table_struct *wait)
{
struct poll_dev *dev = filp->private_data;
__poll_t mask = 0;

// poll_wait 不会阻塞!它只是在 poll_table 里记录一下
// "如果这个等待队列被叫醒了,记得叫醒我"
poll_wait(filp, &dev->read_wq, wait);

if (dev->have_data)
mask |= (POLLIN | POLLRDNORM); // 有数据,可读

mask |= (POLLOUT | POLLWRNORM); // 本设备总是可写

return mask;
}

static int poll_release(struct inode *inode, struct file *filp)
{
return 0;
}

static struct file_operations poll_fops = {
.owner = THIS_MODULE,
.open = poll_open,
.read = poll_read,
.write = poll_write,
.poll = poll_poll,
.release = poll_release,
};

static int __init poll_init(void)
{
int ret;
ret = alloc_chrdev_region(&my_dev.dev_number, 0, 1, DEV_NAME);
if (ret < 0) return ret;

init_waitqueue_head(&my_dev.read_wq);
my_dev.have_data = 0;

cdev_init(&my_dev.chr_dev, &poll_fops);
my_dev.chr_dev.owner = THIS_MODULE;
ret = cdev_add(&my_dev.chr_dev, my_dev.dev_number, 1);
if (ret < 0) {
unregister_chrdev_region(my_dev.dev_number, 1);
return ret;
}

my_dev.cls = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(my_dev.cls)) {
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
return PTR_ERR(my_dev.cls);
}
my_dev.dev = device_create(my_dev.cls, NULL, my_dev.dev_number,
NULL, DEV_NAME);

printk(KERN_INFO "[Poll]: 模块加载完成\n");
return 0;
}

static void __exit poll_exit(void)
{
device_destroy(my_dev.cls, my_dev.dev_number);
class_destroy(my_dev.cls);
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
printk(KERN_INFO "[Poll]: 模块卸载\n");
}

module_init(poll_init);
module_exit(poll_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");

用户态测试就用标准的poll()系统调用:

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <string.h>
#include <errno.h>

int main(void)
{
int fd = open("/dev/poll_dev", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}

struct pollfd fds[1];
fds[0].fd = fd;
fds[0].events = POLLIN; // 我对"可读"感兴趣

printf("poll 等待中... (超时时间: 5000ms)\n");

int ret = poll(fds, 1, 5000); // 等5秒
if (ret < 0) {
perror("poll");
} else if (ret == 0) {
printf("超时了,5秒内没人写入\n");
} else {
if (fds[0].revents & POLLIN) {
char buf[64] = {0};
read(fd, buf, sizeof(buf));
printf("收到数据: %s\n", buf);
}
}

close(fd);
return 0;
}

这里有一个很容易搞错的地方:poll_wait并不会阻塞进程。很多人第一眼看到poll_wait以为是它让进程睡着的,其实不是。poll_wait只是把当前进程跟你提供的等待队列关联起来——“如果将来这个队列被wake_up了,记得也把对应的poll进程叫醒”。真正让进程睡觉的是内核的poll调度框架,你的.poll回调只是返回个掩码。

另外提一嘴,在真实的项目中,epollpoll好用得多。poll每次都传整个fd数组,epoll只需要epoll_ctl注册一次,之后epoll_wait只返回就绪的fd。但这不是我们驱动的活——驱动只管把.poll回调写好,上层用poll还是epoll都是自动工作的。

四种模型该选哪个?一张表说清楚

模型怎么实现的用户态会卡住吗CPU开销什么时候用
阻塞IO等待队列简单的读写、数据流——最常用
非阻塞IOO_NONBLOCK不会高(纯轮询的话)需要快速响应但不能用多路复用的场景
信号驱动IOfasync+SIGIO不会单个设备的异步通知,但写起来有点绕
IO多路复用poll/epoll可配同时监听多个设备——高性能服务器的标配

个人经验:实际项目里,阻塞IO+epoll的组合用得最多。阻塞IO写起来最省心,epoll负责在多个fd之间调度,各司其职。


内核定时器

驱动开发经常需要在未来某个时间点干一件事,比如按键消抖(等20ms再确认是不是真的按下了)、LED周期性闪烁、传感器定时采样等等。Linux内核提供了两套定时器:标准内核定时器(timer_list,基于jiffies)和高精度定时器(hrtimer,微秒/纳秒级)。

标准内核定时器(timer_list)

这套定时器基于内核的时钟节拍——jiffies。每当时钟中断触发,jiffies就加一。Hz的值决定了jiffies的粒度,在RK3588上一般是250或300(对应4ms或~3.3ms),也就是说标准定时器的最高精度大概在毫秒级别。

核心API都在<linux/timer.h>里,很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 一条龙定义+初始化
#define DEFINE_TIMER(_name, _function)

// 动态初始化,替代了老掉牙的 setup_timer
void timer_setup(struct timer_list *timer,
void (*callback)(struct timer_list *),
unsigned int flags);

// 启动/修改定时器,在 expires 个 jiffies 后触发
int mod_timer(struct timer_list *timer, unsigned long expires);

// 删除定时器(如果定时器还没触发的话)
int del_timer(struct timer_list *timer);

// 删除定时器并等待正在执行的handler执行完(多核安全)
int del_timer_sync(struct timer_list *timer);

// jiffies 转换
msecs_to_jiffies(ms); // 毫秒 → jiffies
usecs_to_jiffies(us); // 微秒 → jiffies

直接来一个完整的例子——用定时器让GPIO2_C3周期性翻转,实现一个LED闪烁驱动:

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/io.h>

#define DEV_NAME "timer_led"

// 还是老朋友 GPIO2_C3
#define GPIO2_BASE 0xFEC30000
#define GPIO_SWPORT_DR 0x0004
#define GPIO_SWPORT_DDR 0x000C

struct timer_led_dev {
dev_t dev_number;
struct cdev chr_dev;
struct class *cls;
struct device *device;
void __iomem *gpio_base;
struct timer_list blink_timer;
int led_state;
int period_ms;
};

static struct timer_led_dev my_dev;

// 定时器回调——每次触发就翻转LED,然后重新设置定时器
static void blink_timer_callback(struct timer_list *t)
{
// container_of 从 timer_list 反推回设备结构体,标准套路
struct timer_led_dev *dev = container_of(t, struct timer_led_dev,
blink_timer);
unsigned int val;

dev->led_state = !dev->led_state;

if (dev->led_state)
val = 0x00080008; // 高电平,LED亮
else
val = 0x00080000; // 低电平,LED灭

writel(val, dev->gpio_base + GPIO_SWPORT_DR);

printk(KERN_INFO "[TimerLED]: LED %s\n",
dev->led_state ? "亮" : "灭");

// 重新设置定时器,形成周期——jiffies + 周期(ms→jiffies)
mod_timer(&dev->blink_timer,
jiffies + msecs_to_jiffies(dev->period_ms));
}

static int timer_open(struct inode *inode, struct file *filp)
{
filp->private_data = &my_dev;
return 0;
}

// 用户往设备写一个数字(毫秒),就按这个周期闪烁;写0停止
static ssize_t timer_write(struct file *filp, const char __user *buf,
size_t count, loff_t *off)
{
struct timer_led_dev *dev = filp->private_data;
char kbuf[16] = {0};
int period;

if (count > sizeof(kbuf) - 1)
count = sizeof(kbuf) - 1;

if (copy_from_user(kbuf, buf, count))
return -EFAULT;

if (sscanf(kbuf, "%d", &period) != 1)
return -EINVAL;

if (period > 0) {
dev->period_ms = period;
mod_timer(&dev->blink_timer,
jiffies + msecs_to_jiffies(dev->period_ms));
printk(KERN_INFO "[TimerLED]: 定时器启动,周期=%d ms\n", period);
} else {
del_timer(&dev->blink_timer);
printk(KERN_INFO "[TimerLED]: 定时器停止\n");
}

return count;
}

static int timer_release(struct inode *inode, struct file *filp)
{
return 0;
}

static struct file_operations timer_fops = {
.owner = THIS_MODULE,
.open = timer_open,
.write = timer_write,
.release = timer_release,
};

static int __init timer_init(void)
{
int ret;

ret = alloc_chrdev_region(&my_dev.dev_number, 0, 1, DEV_NAME);
if (ret < 0) return ret;

my_dev.gpio_base = ioremap(GPIO2_BASE, 0x1000);
if (!my_dev.gpio_base) {
unregister_chrdev_region(my_dev.dev_number, 1);
return -ENOMEM;
}

// 先设成输出
writel(0x00080008, my_dev.gpio_base + GPIO_SWPORT_DDR);

// 初始化定时器,第三个参数flags一般写0
timer_setup(&my_dev.blink_timer, blink_timer_callback, 0);
my_dev.led_state = 0;
my_dev.period_ms = 500; // 默认0.5秒

cdev_init(&my_dev.chr_dev, &timer_fops);
my_dev.chr_dev.owner = THIS_MODULE;
ret = cdev_add(&my_dev.chr_dev, my_dev.dev_number, 1);
if (ret < 0) {
iounmap(my_dev.gpio_base);
unregister_chrdev_region(my_dev.dev_number, 1);
return ret;
}

my_dev.cls = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(my_dev.cls)) {
cdev_del(&my_dev.chr_dev);
iounmap(my_dev.gpio_base);
unregister_chrdev_region(my_dev.dev_number, 1);
return PTR_ERR(my_dev.cls);
}
my_dev.device = device_create(my_dev.cls, NULL, my_dev.dev_number,
NULL, DEV_NAME);

printk(KERN_INFO "[TimerLED]: 模块加载完成\n");
return 0;
}

static void __exit timer_exit(void)
{
// 一定先删定时器!否则handler可能访问已经释放的资源
del_timer_sync(&my_dev.blink_timer);
device_destroy(my_dev.cls, my_dev.dev_number);
class_destroy(my_dev.cls);
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
iounmap(my_dev.gpio_base);
printk(KERN_INFO "[TimerLED]: 模块卸载\n");
}

module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");

在板子上测试:

1
2
3
4
5
6
# 500ms 周期闪烁
sudo echo "500" > /dev/timer_led
# 200ms 快速闪
sudo echo "200" > /dev/timer_led
# 停掉
sudo echo "0" > /dev/timer_led

搞定时器有两个容易踩的坑:

第一个坑del_timerdel_timer_sync的区别。del_timer只是把定时器从队列里摘掉,但如果定时器正在另一个CPU上执行回调,它不等。del_timer_sync会等回调执行完毕再返回。在模块卸载的时候,你的设备结构体马上就要被释放了,如果回调还在跑就会访问已释放的内存——内核直接panic。所以卸载的时候请务必要用del_timer_sync

第二个坑:回调函数跑在中断上下文里(软中断上下文,具体是TIMER_SOFTIRQ),所以不能在回调里睡觉——不能用msleep、不能调copy_to_user、不能拿mutex。如果你需要在定时器回调里干一些可能阻塞的事,就得像后面中断章节讲的那样,把工作丢给workqueue去做。

高精度定时器(hrtimer)

标准定时器的精度被jiffies限制在毫秒级别。如果你的场景需要微秒甚至纳秒级的精度——比如音频采集、高精度PWM、高速ADC读数——那就要请出hrtimer了。

hrtimer基于ktime(内核高精度时间抽象),不依赖jiffies,直接读硬件定时器,精度取决于你的硬件。RK3588的ARM Cortex-A76/A55核上有Generic Timer(ARM Arch Timer),精度可以到几十纳秒。

API都在<linux/hrtimer.h>里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化
void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
enum hrtimer_mode mode);

// 启动
int hrtimer_start(struct hrtimer *timer, ktime_t tim,
const enum hrtimer_mode mode);

// 取消
int hrtimer_cancel(struct hrtimer *timer);

// 把到期时间往前推一个间隔(周期定时器用这个不会有累积误差)
u64 hrtimer_forward_now(struct hrtimer *timer, ktime_t interval);

// 纳秒转 ktime_t:ktime_set(秒, 纳秒)
ktime_t ktime_set(s64 secs, unsigned long nsecs);

clock_id的选择:

clock_id说明
CLOCK_MONOTONIC单调递增时钟,不受系统时间调整影响,绝大多数场景用这个
CLOCK_REALTIME系统实时时钟,受NTP和用户调时间影响——可能往回跳,慎用

hrtimer的回调函数原型跟普通定时器不一样:

1
2
3
// 返回 HRTIMER_NORESTART 表示"就触发一次"
// 返回 HRTIMER_RESTART 表示"自动重新排队"
static enum hrtimer_restart callback(struct hrtimer *timer);

完整例子——用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
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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#define DEV_NAME "hrtimer_dev"

struct hrtimer_dev {
dev_t dev_number;
struct cdev chr_dev;
struct class *cls;
struct device *device;
struct hrtimer hr_timer;
ktime_t interval;
int counter;
};

static struct hrtimer_dev my_dev;

static enum hrtimer_restart hrtimer_callback(struct hrtimer *timer)
{
struct hrtimer_dev *dev = container_of(timer, struct hrtimer_dev,
hr_timer);
dev->counter++;
printk(KERN_INFO "[HRTimer]: 第 %d 次触发 (间隔 %lld ns)\n",
dev->counter, ktime_to_ns(dev->interval));

// hrtimer_forward_now 不会累积误差,比 "now + interval" 靠谱
hrtimer_forward_now(timer, dev->interval);
return HRTIMER_RESTART; // 继续排队
}

static int hrtimer_open(struct inode *inode, struct file *filp)
{
filp->private_data = &my_dev;
return 0;
}

static ssize_t hrtimer_write(struct file *filp, const char __user *buf,
size_t count, loff_t *off)
{
struct hrtimer_dev *dev = filp->private_data;
char kbuf[16] = {0};
unsigned long interval_ns;

if (count > sizeof(kbuf) - 1)
count = sizeof(kbuf) - 1;

if (copy_from_user(kbuf, buf, count))
return -EFAULT;

if (kstrtoul(kbuf, 10, &interval_ns) != 0)
return -EINVAL;

if (interval_ns > 0) {
dev->interval = ktime_set(0, interval_ns);
dev->counter = 0;
hrtimer_start(&dev->hr_timer, dev->interval, HRTIMER_MODE_REL);
printk(KERN_INFO "[HRTimer]: 启动,间隔=%lu ns\n", interval_ns);
} else {
hrtimer_cancel(&dev->hr_timer);
printk(KERN_INFO "[HRTimer]: 停止,总计触发 %d 次\n",
dev->counter);
}

return count;
}

static int hrtimer_release(struct inode *inode, struct file *filp)
{
return 0;
}

static struct file_operations hrtimer_fops = {
.owner = THIS_MODULE,
.open = hrtimer_open,
.write = hrtimer_write,
.release = hrtimer_release,
};

static int __init hrtimer_init_module(void)
{
int ret;
ret = alloc_chrdev_region(&my_dev.dev_number, 0, 1, DEV_NAME);
if (ret < 0) return ret;

// CLOCK_MONOTONIC——不受系统时间调整影响,定时最稳
// HRTIMER_MODE_REL——相对时间模式,"从现在开始算"
hrtimer_init(&my_dev.hr_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_dev.hr_timer.function = hrtimer_callback;
my_dev.counter = 0;

cdev_init(&my_dev.chr_dev, &hrtimer_fops);
my_dev.chr_dev.owner = THIS_MODULE;
ret = cdev_add(&my_dev.chr_dev, my_dev.dev_number, 1);
if (ret < 0) {
unregister_chrdev_region(my_dev.dev_number, 1);
return ret;
}

my_dev.cls = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(my_dev.cls)) {
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
return PTR_ERR(my_dev.cls);
}
my_dev.device = device_create(my_dev.cls, NULL, my_dev.dev_number,
NULL, DEV_NAME);

printk(KERN_INFO "[HRTimer]: 模块加载完成\n");
return 0;
}

static void __exit hrtimer_exit(void)
{
hrtimer_cancel(&my_dev.hr_timer); // 比 del_timer_sync 还安全
device_destroy(my_dev.cls, my_dev.dev_number);
class_destroy(my_dev.cls);
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
printk(KERN_INFO "[HRTimer]: 模块卸载\n");
}

module_init(hrtimer_init_module);
module_exit(hrtimer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");

测试:

1
2
3
4
5
6
# 每100微秒(100000 ns)触发一次
sudo echo "100000" > /dev/hrtimer_dev
# 每1毫秒触发一次
sudo echo "1000000" > /dev/hrtimer_dev
# 停掉
sudo echo "0" > /dev/hrtimer_dev

两种定时器怎么选?

  • timer_list:精度要求不高的通用场景(毫秒级就够用),API简单,开销小。LED闪烁、按键消抖、看门狗这些都够用了。
  • hrtimer:需要微秒甚至纳秒精度的时候用,但回调也跑在中断上下文(不能睡),且比timer_list重。频繁触发时CPU开销明显更大。

IOCTL

前面我们一直在用read/write跟设备打交道,这俩朴实无华,就是纯数据传输。但有时候我们需要跟设备说”把你的波特率设成115200”或者”把模式切换到低功耗”——这些属于控制操作,不是数据流的读写。ioctl就是干这个的。

ioctl命令怎么定义的?

ioctl命令是个32位的整数,里面的bit位被分成了四个字段,我们来看看:

位段干啥的
[31:30]数据传输方向(读/写/啥也不传)
[29:16]数据大小(用了sizeof()帮你算好的)
[15:8]魔数(type)——一个ASCII字符,标识”这是哪个设备的命令”
[7:0]命令序号(nr),0~255,同一个设备的命令按顺序编号

生成命令用这四个宏:

1
2
3
4
_IO(type, nr)             // 不传数据,单纯发个命令
_IOR(type, nr, datatype) // 从设备"读"数据(内核→用户空间)
_IOW(type, nr, datatype) // 往设备"写"数据(用户空间→内核)
_IOWR(type, nr, datatype) // 双向传输(既有读又有写)

datatype就是你传的数据类型,宏会自动用sizeof(datatype)算出size字段,你不需要手动去算。

实际项目里,最好把命令定义放在一个共享的头文件里,内核和用户态都include它,避免命令码对不上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ioctl_cmd.h —— 内核和用户态共用
#ifndef _IOCTL_CMD_H
#define _IOCTL_CMD_H

#include <linux/ioctl.h> // 内核里用这个
// 用户态用 #include <sys/ioctl.h>

#define DEVICE_MAGIC 'k' // 选个没人用的字符当魔数,别跟别的驱动撞车

#define IOCTL_GET_STATUS _IOR(DEVICE_MAGIC, 0, int)
#define IOCTL_SET_BAUD _IOW(DEVICE_MAGIC, 1, int)
#define IOCTL_RESET_DEVICE _IO(DEVICE_MAGIC, 2)
#define IOCTL_GET_INFO _IOR(DEVICE_MAGIC, 3, struct device_info)

#define IOCTL_MAX_NR 3

// 可选:定义一个结构体用于传输复杂数据
struct device_info {
char name[32];
int version;
int status;
};

#endif

unlocked_ioctl的实现

file_operations里ioctl相关的回调有两个:

1
2
long (*unlocked_ioctl) (struct file *, unsigned int cmd, unsigned long arg);
long (*compat_ioctl) (struct file *, unsigned int cmd, unsigned long arg);

unlocked_ioctl是主要的,适用于原生位宽(arm64就是64位)。compat_ioctl是兼容32位用户态程序在64位内核上运行的情况——32位和64位的结构体对齐不一样,arg需要转换。如果你不确定有没有32位用户态,可以把compat_ioctl也指到同一个函数上。

ioctl回调里拿到的是cmd(命令码)和arg(用户态传过来的参数)。arg有两种用法:

  • 如果arg本身就是个值(int、bool这种小类型),直接强转:int val = (int)arg;
  • 如果arg是个指向用户空间内存的指针(结构体、字符串、数组),那就要用put_user/get_user(简单类型)或者copy_from_user/copy_to_user(复杂类型)来搬运数据。

我们来写一个完整的ioctl驱动,覆盖这几种情况:

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/ioctl.h>
#include <linux/slab.h>

#define DEV_NAME "ioctl_dev"

// ====== 命令定义(正式项目放到共享头文件里) ======
#define DEVICE_MAGIC 'k'
#define IOCTL_GET_STATUS _IOR(DEVICE_MAGIC, 0, int)
#define IOCTL_SET_BAUD _IOW(DEVICE_MAGIC, 1, int)
#define IOCTL_RESET_DEVICE _IO(DEVICE_MAGIC, 2)
#define IOCTL_GET_INFO _IOR(DEVICE_MAGIC, 3, struct device_info)
#define IOCTL_MAX_NR 3

struct device_info {
char name[32];
int version;
int status;
};

struct ioctl_dev {
dev_t dev_number;
struct cdev chr_dev;
struct class *cls;
struct device *device;
int status;
int baud_rate;
struct device_info info;
};

static struct ioctl_dev my_dev;

static int ioctl_open(struct inode *inode, struct file *filp)
{
filp->private_data = &my_dev;
printk(KERN_INFO "[Ioctl]: 设备打开\n");
return 0;
}

static long ioctl_unlocked_ioctl(struct file *filp,
unsigned int cmd,
unsigned long arg)
{
struct ioctl_dev *dev = filp->private_data;
int ret = 0;
int val;

/*
* 先做合法性校验。魔数不匹配或者命令号超出范围,
* 说明用户态可能拿错了设备,直接返回 -ENOTTY。
* _IOC_TYPE 和 _IOC_NR 是内核提供的解码宏。
*/
if (_IOC_TYPE(cmd) != DEVICE_MAGIC) {
printk(KERN_ERR "[Ioctl]: 魔数不对!\n");
return -ENOTTY;
}
if (_IOC_NR(cmd) > IOCTL_MAX_NR) {
printk(KERN_ERR "[Ioctl]: 命令号超出范围\n");
return -ENOTTY;
}

switch (cmd) {

case IOCTL_GET_STATUS:
// put_user 适合传单个 int——比 copy_to_user 轻量
ret = put_user(dev->status, (int __user *)arg);
if (ret) return -EFAULT;
printk(KERN_INFO "[Ioctl]: GET_STATUS → %d\n", dev->status);
break;

case IOCTL_SET_BAUD:
// get_user 读用户态传进来的 int
ret = get_user(val, (int __user *)arg);
if (ret) return -EFAULT;
dev->baud_rate = val;
dev->status = 1; // 标记:已配置
printk(KERN_INFO "[Ioctl]: SET_BAUD → %d\n", val);
break;

case IOCTL_RESET_DEVICE:
// 纯命令,没有数据传输
dev->status = 0;
dev->baud_rate = 9600;
printk(KERN_INFO "[Ioctl]: RESET——设备已复位\n");
break;

case IOCTL_GET_INFO:
// 传结构体——用 copy_to_user,稳
if (copy_to_user((struct device_info __user *)arg,
&dev->info, sizeof(dev->info)))
return -EFAULT;
printk(KERN_INFO "[Ioctl]: GET_INFO → %s v%d\n",
dev->info.name, dev->info.version);
break;

default:
return -ENOTTY;
}

return ret;
}

static int ioctl_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "[Ioctl]: 设备关闭\n");
return 0;
}

static struct file_operations ioctl_fops = {
.owner = THIS_MODULE,
.open = ioctl_open,
.unlocked_ioctl = ioctl_unlocked_ioctl,
.compat_ioctl = ioctl_unlocked_ioctl, // 直接用同一个,兼容32位
.release = ioctl_release,
};

static int __init ioctl_init(void)
{
int ret;

ret = alloc_chrdev_region(&my_dev.dev_number, 0, 1, DEV_NAME);
if (ret < 0) return ret;

// 初始化设备信息
my_dev.status = 0;
my_dev.baud_rate = 9600;
snprintf(my_dev.info.name, sizeof(my_dev.info.name),
"RK3588-IOCTL-DEV");
my_dev.info.version = 1;
my_dev.info.status = 0;

cdev_init(&my_dev.chr_dev, &ioctl_fops);
my_dev.chr_dev.owner = THIS_MODULE;
ret = cdev_add(&my_dev.chr_dev, my_dev.dev_number, 1);
if (ret < 0) {
unregister_chrdev_region(my_dev.dev_number, 1);
return ret;
}

my_dev.cls = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(my_dev.cls)) {
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
return PTR_ERR(my_dev.cls);
}
my_dev.device = device_create(my_dev.cls, NULL, my_dev.dev_number,
NULL, DEV_NAME);

printk(KERN_INFO "[Ioctl]: 模块加载完成\n");
return 0;
}

static void __exit ioctl_exit(void)
{
device_destroy(my_dev.cls, my_dev.dev_number);
class_destroy(my_dev.cls);
cdev_del(&my_dev.chr_dev);
unregister_chrdev_region(my_dev.dev_number, 1);
printk(KERN_INFO "[Ioctl]: 模块卸载\n");
}

module_init(ioctl_init);
module_exit(ioctl_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");

用户态测试:

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>

// 命令定义——和内核里一模一样
#define DEVICE_MAGIC 'k'
#define IOCTL_GET_STATUS _IOR(DEVICE_MAGIC, 0, int)
#define IOCTL_SET_BAUD _IOW(DEVICE_MAGIC, 1, int)
#define IOCTL_RESET_DEVICE _IO(DEVICE_MAGIC, 2)
#define IOCTL_GET_INFO _IOR(DEVICE_MAGIC, 3, struct device_info)

struct device_info {
char name[32];
int version;
int status;
};

int main(void)
{
int fd = open("/dev/ioctl_dev", O_RDWR);
if (fd < 0) { perror("open"); return 1; }

// 1. 查信息
struct device_info info;
if (ioctl(fd, IOCTL_GET_INFO, &info) == 0)
printf("设备名: %s, 版本: %d\n", info.name, info.version);

// 2. 设波特率
int baud = 115200;
if (ioctl(fd, IOCTL_SET_BAUD, &baud) == 0)
printf("波特率已设为: %d\n", baud);

// 3. 查状态
int status;
if (ioctl(fd, IOCTL_GET_STATUS, &status) == 0)
printf("状态: %d (1=已配置)\n", status);

// 4. 复位
if (ioctl(fd, IOCTL_RESET_DEVICE) == 0)
printf("设备已复位\n");

// 5. 再次查状态
if (ioctl(fd, IOCTL_GET_STATUS, &status) == 0)
printf("复位后状态: %d\n", status);

close(fd);
return 0;
}

在板子上跑一下:

1
2
3
4
5
6
topeet@topeet:~$ sudo ./ioctl_test
设备名: RK3588-IOCTL-DEV, 版本: 1
波特率已设为: 115200
状态: 1 (=已配置)
设备已复位
复位后状态: 0

ioctl的避坑经验

写ioctl有几个地方容易被人忽视:

1、魔数冲突。Linux内核里的魔数是全局的——所有驱动共享同一个命名空间。如果你用了一个已经被别的驱动占用的魔数,命令码就会撞车,user space调ioctl的时候可能调到错误的驱动去。官方文档Documentation/ioctl/ioctl-number.txt列出了已经被占用的魔数,选魔数之前去翻一下避免撞车。一般个人项目用小写字母风险不大,但商业驱动最好去登记一个。

2、命令号不要重复。同一个魔数下每个命令号必须唯一。如果你有两个命令共用同一个nr,内核没法区分。

3、put_user和get_user只能传1、2、4、8字节的简单类型。int、long、指针这些没问题,但结构体不行——结构体用copy_from_user/copy_to_user

4、ioctl的arg可能是值也可能是指针,取决于你用哪个宏生成的命令。_IO不传数据,arg可以忽略。_IOR/_IOW/_IOWR的arg是指向用户空间的指针,必须用put_user/get_user或者copy_*_user来访问,绝对不能直接解引用——因为它是用户空间的地址,内核直接*arg会触发Oops。

5、compat_ioctl在arm64上其实不太容易碰到,因为现在大部分arm64 Linux发行版跑的都是64位程序。但如果你用chroot跑32位的Debian或者用Android(Android有很多32位组件),那就必须处理compat_ioctl了。


中断

中断是内核驱动开发的重中之重。没有中断的时候,CPU要不停问外设”数据到了吗?按键按了吗?”,这叫轮询,费电又费算力。有了中断,外设有事件的时候自己通知CPU,CPU该睡觉睡觉,该干活干活——这就是异步事件驱动的核心思想。

中断是怎么走的?

来一张图,理解一下ARM(包括RK3588)上中断的整个流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
硬件事件发生(按键按下、数据到达……)


GIC 中断控制器
(仲裁谁优先、屏蔽不需要的、发给对应CPU


CPU收到IRQ异常


保存现场(寄存器全压栈)


跳到中断向量表 → GIC驱动分发到对应handler


执行中断处理(上半部 → 下半部)


恢复现场 → 回到被打断的代码继续跑

几个重要概念先搞清楚:

概念大白话解释
IRQ号系统给每个中断源分配的唯一编号。RK3588上GPIO中断的IRQ号是GIC驱动动态分的,你在/proc/interrupts里看到的就是它
GIC中断控制器,所有外设中断都要经过它。RK3588用的是GIC-600,ARM的标准配置
上半部在关中断的环境里跑,越快越好!一般就应答一下中断+读寄存器+调度下半部
下半部延后执行那些不紧急的活。有三种玩法:softirq、tasklet、workqueue

RK3588的GIC中断控制器

RK3588用ARM的GIC-600(Generic Interrupt Controller v3/v4),这玩意儿是ARM Cortex-A系列的标配。GIC-600的具体能力:

  • 最多支持960个SPI(Shared Peripheral Interrupt,共享外设中断)。通俗地说就是外设和GPIO用的中断线
  • 16个PPI(Private Peripheral Interrupt),每个CPU核私有——比如CPU的本地定时器中断
  • 16个SGI(Software Generated Interrupt),软件生成中断——一个CPU可以给另一个CPU发中断
  • 支持MSI(Message Signaled Interrupts),PCIe设备爱用
  • 支持LPI(Locality-specific Peripheral Interrupt),跟ITS配合用的,普通驱动开发不用管

对于RK3588,GIC的寄存器分布在:

1
2
3
GICD  (Distributor,分发器):    0xFE600000
GICR (Redistributor,再分发器):每个CPU有自己的一份
GICC (CPU Interface): 通过系统寄存器访问,不走MMIO

实际开发驱动的时候,你不需要手动碰GIC寄存器。内核的GIC驱动(drivers/irqchip/irq-gic-v3.c)在启动时已经帮你初始好了。驱动开发只需要:

  1. 知道自己GPIO对应的IRQ号
  2. 调用request_irq()注册中断处理函数
  3. 处理好上半部和下半部

那怎么知道GPIO的IRQ号呢?两种方式:

  • gpio_to_irq(gpio):传全局GPIO编号,返回Linux分配的虚拟IRQ号。这个方法最方便,我们在例程里就用它
  • 从设备树里拿:如果用的是platform_driver框架,platform_get_irq(pdev, 0)直接从设备树解析好的资源里取

对于我们要用的GPIO2_C3(全局编号83):

1
2
int irq = gpio_to_irq(83);   // 拿到的就是 Linux 虚拟IRQ号
// 在RK3588上,这个值大概在408~440之间,取决于内核版本和设备树

request_irq——中断申请的入口

核心API很简单,就两个,定义在<linux/interrupt.h>里:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 申请中断
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);

// 释放中断
void free_irq(unsigned int irq, void *dev);

// handler的原型
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
// 返回值:IRQ_HANDLED(我处理了)或 IRQ_NONE(不是我的中断,共享中断线时用到)

参数一个个解释:

参数说明
irq要申请的中断号(就是gpio_to_irq返回的那个)
handler中断处理函数,跑在中断上下文,绝对不能睡觉!
flags一堆IRQF_*标志位或在一起,控制中断的触发方式和行为
name给中断起个名字,会显示在/proc/interrupts里,方便调试
dev传给handler的私有数据,通常是你的设备结构体指针。如果是共享中断,这个不能为NULL

常用的flags:

标志含义
IRQF_TRIGGER_RISING上升沿触发(电平从低变高)
IRQF_TRIGGER_FALLING下降沿触发(电平从高变低)——按键按下的典型选择
IRQF_TRIGGER_HIGH高电平触发(慎用!容易中断风暴
IRQF_TRIGGER_LOW低电平触发(同样慎用!
IRQF_SHARED允许多个设备共享这根中断线,dev参数必须唯一且非空
IRQF_ONESHOT在handler执行期间保持中断屏蔽,handler跑完才重新打开——适合threaded interrupt

强烈建议用边沿触发(RISING/FALLING),别用电平触发。为什么?电平触发意味着只要电平条件还在,中断就会不停地来。比如按键按下后电平持续为低,你的handler会在里面反复触发——这就是”中断风暴”,一个中断能把一个CPU核打满。

上半部和下半部——为什么要拆?

中断处理的黄金法则是:handler要越快越好。因为handler执行的时候,CPU上至少同优先级及以下的中断都被屏蔽了。如果你在里面搞一个msleep(100)——不好意思,整个系统的中断响应都会延迟100ms。更可怕的是,中断上下文根本就不允许你调用msleep——你这么干内核直接Oops给你看。

所以内核把中断处理切成了两块:

  • 上半部(Top Half)request_irq注册的handler,跑在硬中断上下文不能睡、不能用mutex、不能调copy_to_user。就干三件事:应答中断、读关键数据、调度下半部。
  • 下半部(Bottom Half):延后执行的非紧急工作,根据实现机制不同可以睡也可以不睡。

下半部的三种实现:

机制执行上下文能睡吗并发用在哪
softirq中断上下文不能同类可并发(需要额外锁)网络收发包、块设备——不归我们普通驱动管
tasklet中断上下文不能同一个tasklet绝不在多核同时跑(省锁)大多数驱动的下半部——按键消抖、DMA完成通知……
workqueue进程上下文能!看队列类型需要睡的操作——文件读写、网络发数据、长延时

普通驱动开发,99%的情况选tasklet或者workqueue。需要睡就用workqueue,不需要睡就用tasklet(更轻量)。

先看tasklet怎么用

tasklet基于softirq实现,但是同一个tasklet不会在多个CPU上并行——内核帮你串行化了,你不用加锁。API就这几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 静态定义
DECLARE_TASKLET(name, func, data);

// 动态初始化
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);

// 调度执行(一般在上半部handler里调)
void tasklet_schedule(struct tasklet_struct *t);

// 杀掉(等正在跑的完成了再销毁)
void tasklet_kill(struct tasklet_struct *t);

注意:tasklet_schedule只是把tasklet标记为”待执行”,并不立刻跑。它会在当前中断处理全部完成后,由内核在软中断阶段统一调度。

再看workqueue怎么用

workqueue把工作丢给一个内核线程(叫events/x),所以跑在进程上下文——你可以随便睡、随便拿mutex。API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 静态定义
DECLARE_WORK(name, func);

// 动态初始化
INIT_WORK(work, func);

// 提交到默认队列(system_wq),最常用
bool schedule_work(struct work_struct *work);

// 取消还没跑的work
bool cancel_work_sync(struct work_struct *work);

// work函数原型
void (*work_func_t)(struct work_struct *work);
// 从work指针用container_of反推设备结构体,跟timer_list一模一样

完整例程:GPIO按键中断驱动(带tasklet消抖 + workqueue日志)

好了,理论讲得够多了,来一个能真跑起来的完整驱动。我们用GPIO2_C3当按键输入——硬件上按键一端接GPIO2_C3,另一端接GND,按下为低电平,松开后内部上拉把它拉回高电平。我们抓下降沿中断来判断按键按下。

这个驱动集成了前面讲的所有概念:

  • 中断注册(request_irq
  • 上半部handler(应答中断 + 调度tasklet)
  • tasklet下半部(消抖 + 二级确认 + 事件入环形缓冲 + 叫醒等待队列)
  • workqueue(异步日志)
  • 阻塞IO(read等按键事件)
  • poll支持(用户态可以用select/poll/epoll)

先说一下环形缓冲区。为什么不用简单的全局变量?因为按键可能在很短的时间内被按多次——你如果只有一个char last_key,在用户还没来得及read的时候新的按键事件就会把旧的覆盖掉。环形缓冲解决了这个问题,它可以缓存多个事件。

另外需要解释自旋锁。环形缓冲区被中断handler(通过tasklet)和用户态read同时访问,这是一个典型的”生产者-消费者”并发场景。我们需要spin_lock_irqsave来保护缓冲区的头尾指针,防止读到一半被中断打断导致数据损坏。

好,上代码:

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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/io.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/jiffies.h>
#include <linux/slab.h>

#define DEV_NAME "key_irq"

/*
* GPIO2_C3 的信息总结:
* GPIO控制器基地址:0xFEC30000
* Bank: GPIO2, 组: C, 位: 3
* 全局编号:2×32 + 2×8 + 3 = 83
* 用 gpio_to_irq(83) 拿到 Linux 虚拟 IRQ 号
*/
#define GPIO2_BASE 0xFEC30000
#define GPIO_SWPORT_DR 0x0004
#define GPIO_SWPORT_DDR 0x000C
#define GPIO_INT_STATUS 0x0040 // 中断状态寄存器
#define GPIO_INT_EN 0x0030 // 中断使能寄存器

// 按键消抖——机械按键的抖动一般在5~30ms,取20ms比较稳
#define DEBOUNCE_MS 20
#define KEY_BUF_SIZE 16 // 环形缓冲区最多缓存16个按键事件

struct key_dev {
dev_t dev_number;
struct cdev chr_dev;
struct class *cls;
struct device *device;
void __iomem *gpio_base;
int irq;

// 环形缓冲区
char key_buf[KEY_BUF_SIZE];
int buf_head;
int buf_tail;
int buf_count;
spinlock_t buf_lock; // 保护环形缓冲区的锁

// 读等待队列
wait_queue_head_t read_wq;

// tasklet —— 做消抖
struct tasklet_struct key_tasklet;

// workqueue —— 做异步日志
struct work_struct key_work;
};

static struct key_dev my_dev;

// ============== 环形缓冲区操作 ==============

// 往环形缓冲里塞一个事件
static int buf_put(struct key_dev *dev, char val)
{
unsigned long flags;
spin_lock_irqsave(&dev->buf_lock, flags);

if (dev->buf_count >= KEY_BUF_SIZE) {
// 满了就把最旧的丢掉——总比丢最新的好
dev->buf_head = (dev->buf_head + 1) % KEY_BUF_SIZE;
dev->buf_count--;
}

dev->key_buf[dev->buf_tail] = val;
dev->buf_tail = (dev->buf_tail + 1) % KEY_BUF_SIZE;
dev->buf_count++;

spin_unlock_irqrestore(&dev->buf_lock, flags);
return 0;
}

// 从环形缓冲里取一个事件
static int buf_get(struct key_dev *dev, char *val)
{
unsigned long flags;
spin_lock_irqsave(&dev->buf_lock, flags);

if (dev->buf_count == 0) {
spin_unlock_irqrestore(&dev->buf_lock, flags);
return -1;
}

*val = dev->key_buf[dev->buf_head];
dev->buf_head = (dev->buf_head + 1) % KEY_BUF_SIZE;
dev->buf_count--;

spin_unlock_irqrestore(&dev->buf_lock, flags);
return 0;
}

// ============== 下半部:tasklet(消抖 + 确认按键) ==============

/*
* tasklet 跑在软中断上下文,还是不能睡。
* 消抖的原理很简单:等20ms再读一次电平,
* 如果还是低说明是真的按下了(毛刺已经过了),
* 如果变高了说明刚才那个是抖动,忽略。
*/
static void key_tasklet_handler(unsigned long data)
{
struct key_dev *dev = (struct key_dev *)data;
unsigned int val;

// udelay 是忙等——在中断上下文不能用 msleep
udelay(DEBOUNCE_MS * 1000);

// 再读一次 GPIO 的输入电平
// GPIO_EXT_PORT(0x50)可以直接读引脚状态,不管方向寄存器设的是啥
val = readl(dev->gpio_base + 0x50);

if (!(val & (1 << 3))) {
// 20ms后还是低——真按键,不是抖动
printk(KERN_INFO "[KeyIRQ]: 按键确认按下 (已消抖)\n");

buf_put(dev, 'K'); // 'K' = 按键事件

// 叫醒在read上睡觉的用户进程
wake_up_interruptible(&dev->read_wq);

// 丢给workqueue做异步日志(不需要实时完成的工作)
schedule_work(&dev->key_work);
} else {
printk(KERN_INFO "[KeyIRQ]: 抖动,已忽略\n");
}
}

// ============== 下半部:workqueue(异步日志) ==============

/*
* 跑在进程上下文,想睡就睡。
* 实际项目中这里可以写文件、发网络包、更新sysfs节点等。
*/
static void key_work_handler(struct work_struct *work)
{
struct key_dev *dev = container_of(work, struct key_dev, key_work);

// 模拟做一些耗时操作
printk(KERN_INFO "[KeyIRQ]: [Workqueue] 按键事件已记录到日志\n");
}

// ============== 上半部:中断handler ==============

/*
* 这就是中断的上半部。要求:快!快!快!
* 1. 清GPIO中断状态(RK3588写1到中断状态寄存器对应位即可清除)
* 2. 调度tasklet去慢慢处理剩下的活
*/
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
struct key_dev *dev = (struct key_dev *)dev_id;
unsigned int status;

// 读中断状态,然后写回去清掉(RK3588是写1清中断)
status = readl(dev->gpio_base + GPIO_INT_STATUS);
writel(status, dev->gpio_base + GPIO_INT_STATUS);

printk(KERN_INFO "[KeyIRQ]: 中断触发!IRQ=%d (上半部)\n", irq);

// 甩给tasklet去做消抖,上半部这就结束了
tasklet_schedule(&dev->key_tasklet);

return IRQ_HANDLED;
}

// ============== 文件操作 ==============

static int key_open(struct inode *inode, struct file *filp)
{
filp->private_data = &my_dev;
printk(KERN_INFO "[KeyIRQ]: 设备打开\n");
return 0;
}

// 读就是取环形缓冲里的按键事件,没事件就睡
static ssize_t key_read(struct file *filp, char __user *buf,
size_t count, loff_t *off)
{
struct key_dev *dev = filp->private_data;
char event_val;
int ret;

if (filp->f_flags & O_NONBLOCK) {
if (dev->buf_count == 0)
return -EAGAIN;
} else {
ret = wait_event_interruptible(dev->read_wq,
dev->buf_count > 0);
if (ret)
return -ERESTARTSYS;
}

if (buf_get(dev, &event_val) < 0)
return -EAGAIN;

if (copy_to_user(buf, &event_val, 1))
return -EFAULT;

return 1;
}

// poll——让用户态能用select/epoll等按键
static __poll_t key_poll(struct file *filp, struct poll_table_struct *wait)
{
struct key_dev *dev = filp->private_data;
__poll_t mask = 0;

poll_wait(filp, &dev->read_wq, wait);

if (dev->buf_count > 0)
mask |= (POLLIN | POLLRDNORM);

return mask;
}

static int key_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "[KeyIRQ]: 设备关闭\n");
return 0;
}

static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.poll = key_poll,
.release = key_release,
};

// ============== 模块初始化 / 退出 ==============

static int __init key_init(void)
{
int ret;
int gpio_num = 83;

printk(KERN_INFO "[KeyIRQ]: 初始化 GPIO%d 按键中断驱动\n", gpio_num);

// 1. 分配设备号
ret = alloc_chrdev_region(&my_dev.dev_number, 0, 1, DEV_NAME);
if (ret < 0) goto fail0;

// 2. 映射GPIO寄存器
my_dev.gpio_base = ioremap(GPIO2_BASE, 0x1000);
if (!my_dev.gpio_base) {
ret = -ENOMEM;
goto fail1;
}

// 3. 设置GPIO2_C3为输入
// 低16位C3对应的bit写0 = 输入;高16位对应bit写1 = 写使能
writel(0x00080000, my_dev.gpio_base + GPIO_SWPORT_DDR);

// 4. 初始化环形缓冲 & 锁 & 等待队列
my_dev.buf_head = 0;
my_dev.buf_tail = 0;
my_dev.buf_count = 0;
spin_lock_init(&my_dev.buf_lock);
init_waitqueue_head(&my_dev.read_wq);

// 5. 初始化下半部
tasklet_init(&my_dev.key_tasklet, key_tasklet_handler,
(unsigned long)&my_dev);
INIT_WORK(&my_dev.key_work, key_work_handler);

// 6. 获取GPIO对应的中断号
my_dev.irq = gpio_to_irq(gpio_num);
if (my_dev.irq < 0) {
printk(KERN_ERR "[KeyIRQ]: gpio_to_irq 失败!\n");
ret = my_dev.irq;
goto fail2;
}
printk(KERN_INFO "[KeyIRQ]: GPIO%d → IRQ %d\n", gpio_num, my_dev.irq);

/*
* 7. 申请中断
* IRQF_TRIGGER_FALLING:下降沿触发——按键按下时电平从高变低
* 最后一个参数是传给handler的dev_id,共享中断时必须唯一且非空
*/
ret = request_irq(my_dev.irq, key_irq_handler,
IRQF_TRIGGER_FALLING,
"rk3588_key", &my_dev);
if (ret) {
printk(KERN_ERR "[KeyIRQ]: request_irq 失败!ret=%d\n", ret);
goto fail2;
}

// 8. 注册字符设备
cdev_init(&my_dev.chr_dev, &key_fops);
my_dev.chr_dev.owner = THIS_MODULE;
ret = cdev_add(&my_dev.chr_dev, my_dev.dev_number, 1);
if (ret < 0) goto fail3;

// 9. 创建设备节点
my_dev.cls = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(my_dev.cls)) {
ret = PTR_ERR(my_dev.cls);
goto fail4;
}
my_dev.device = device_create(my_dev.cls, NULL, my_dev.dev_number,
NULL, DEV_NAME);
if (IS_ERR(my_dev.device)) {
ret = PTR_ERR(my_dev.device);
goto fail5;
}

printk(KERN_INFO "[KeyIRQ]: 驱动加载成功!IRQ=%d, 设备=/dev/%s\n",
my_dev.irq, DEV_NAME);
return 0;

// 按照"后申请的先释放"一路回滚
fail5:
class_destroy(my_dev.cls);
fail4:
cdev_del(&my_dev.chr_dev);
fail3:
free_irq(my_dev.irq, &my_dev);
fail2:
iounmap(my_dev.gpio_base);
fail1:
unregister_chrdev_region(my_dev.dev_number, 1);
fail0:
return ret;
}

static void __exit key_exit(void)
{
/*
* 释放资源的顺序非常重要!!
* 1. 先关中断——确保不会再有新的handler被调用
* 2. 再杀tasklet和workqueue——等正在执行的完成
* 3. 最后才释放内存映射和设备号
*/
free_irq(my_dev.irq, &my_dev);
tasklet_kill(&my_dev.key_tasklet);
cancel_work_sync(&my_dev.key_work);

device_destroy(my_dev.cls, my_dev.dev_number);
class_destroy(my_dev.cls);
cdev_del(&my_dev.chr_dev);
iounmap(my_dev.gpio_base);
unregister_chrdev_region(my_dev.dev_number, 1);

printk(KERN_INFO "[KeyIRQ]: 驱动卸载完成\n");
}

module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");
MODULE_DESCRIPTION("RK3588 GPIO Key Interrupt Driver");

用户态测试程序

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

int running = 1;

void sigint_handler(int sig) { running = 0; }

int main(void)
{
int fd = open("/dev/key_irq", O_RDWR);
if (fd < 0) {
perror("打开 /dev/key_irq 失败");
return 1;
}

signal(SIGINT, sigint_handler);

printf("===== RK3588 按键中断监视器 =====\n");
printf("按一下 GPIO2_C3 上的按键试试\n");
printf("Ctrl+C 退出\n\n");

struct pollfd fds[1];
fds[0].fd = fd;
fds[0].events = POLLIN;

int count = 0;
while (running) {
int ret = poll(fds, 1, 1000);
if (ret < 0) {
if (errno == EINTR) break;
perror("poll");
break;
}

if (ret == 0) continue; // 超时,继续等

if (fds[0].revents & POLLIN) {
char event;
ssize_t n = read(fd, &event, 1);
if (n > 0) {
count++;
printf(">>> 第 %d 次按键!事件=%c\n", count, event);
}
}
}

printf("\n共检测到 %d 次按键。再见!\n", count);
close(fd);
return 0;
}

在RK3588上跑一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 加载驱动
sudo insmod key_irq.ko

# 查看中断是否注册成功
cat /proc/interrupts | grep rk3588
# 应该看到类似:
# 412: 0 0 0 0 0 0 0 0 GICv3 83 Edge rk3588_key

# 运行测试程序
sudo ./key_test

# 按按键,观察输出:
# >>> 第 1 次按键!事件=K
# >>> 第 2 次按键!事件=K

# 同时看dmesg:
# [ 123.456] [KeyIRQ]: 中断触发!IRQ=412 (上半部)
# [ 123.476] [KeyIRQ]: 按键确认按下 (已消抖)
# [ 123.478] [KeyIRQ]: [Workqueue] 按键事件已记录到日志

# 卸载
sudo rmmod key_irq

注意看上面dmesg里面的时间戳——中断触发(上半部)和按键确认(tasklet消抖)之间差了大约20ms,这就是udelay(DEBOUNCE_MS * 1000)正在干活。

中断编程的避坑指南(血泪换来的经验)

中断这部分搞不好分分钟内核panic,下面是我以前踩过的坑,你务必记住:

1、中断上下文绝对不能睡。 这是第一铁律。上半部和tasklet里所有可能睡眠的函数都不能用——msleepmutex_lockkmalloc(GFP_KERNEL)(用GFP_ATOMIC代替)、copy_to_user/copy_from_userdown/up信号量等等。如果你不确定一个函数能不能在中端上下文调用,就去查它的文档或者看源码里有没有might_sleep()检查——有的话就一定不能。必须用这些的话放workqueue里去。

2、中断处理要快。 一个IRQ handler执行超过1微秒就应该考虑把活推给下半部了。超过10微秒基本上是设计问题。

3、free_irq必须在iounmap之前。 因为中断handler可能正在访问GPIO寄存器。如果你先unmap了寄存器而handler还没跑完,handler就会访问已释放的内存——内核直接panic。资源释放顺序一定是:先关中断→等下半部完成→释放内存映射→释放设备号。

4、共享中断的dev_id不能为NULL。 内核用dev_id来区分共享同一根中断线的不同设备。如果是共享中断(IRQF_SHARED),dev_id必须是唯一的非空值。另外handler返回IRQ_NONE表示”不是我的中断”时也要检查——当共享中断线上所有handler都返回IRQ_NONE时内核会报”nobody cared”。

5、GPIO中断一定要消抖。 机械开关在通断瞬间的接触不是干净的,会产生几十微秒到几十毫秒的抖动。不消抖的话按一次按键可能触发几十次中断。软件消抖的最低成本做法就是在上半部里udelay然后二次确认电平——简单粗暴但有效。如果要在硬件层面消抖,可以并一个0.1µF电容到地。

6、tasklet不能长时间运行。 虽然tasklet比上半部宽松,但它毕竟跑在软中断上下文,会推迟其他软中断和进程调度。如果你在tasklet里做了超过几毫秒的工作,就该考虑换workqueue了。

7、注意中断重入。 如果你的中断handler里调了可能触发新中断的函数,或者你的下半部把自己重新调度了(虽然tasklet不会自触发),一定要设计好状态机防止无限循环。

8、用spin_lock_irqsave而不是spin_lock来保护中断和进程共享的数据。 spin_lock不会关中断——如果在进程上下文中拿着锁然后中断来了,中断handler试图拿同一把锁就会死锁。spin_lock_irqsave会在拿锁的同时关掉本地CPU的中断,这样就不会有中断来抢。我们环形缓冲的代码用的就是它。

9、调试中断优先用/proc/interrupts 这个文件告诉你每个中断被触发了多少次、在哪个CPU上触发的、是谁注册的。如果发现某个中断计数疯涨而handler没反应,多半是中断风暴——先检查是不是用了电平触发。

补充:线程化中断(request_threaded_irq)

最后再介绍一个更现代的玩法——线程化中断(threaded interrupt)。它的思路是:不用手动写tasklet/workqueue,内核直接帮你把handler拆成上下半部:

1
2
3
4
5
6
int request_threaded_irq(unsigned int irq,
irq_handler_t handler, // 上半部(硬中断上下文)
irq_handler_t thread_fn, // 下半部(内核线程,可以睡!)
unsigned long flags,
const char *name,
void *dev);
  • handler:跟普通的上半部一样,跑在硬中断上下文,越快越好。如果想让它返回IRQ_WAKE_THREAD来唤醒下半部线程
  • thread_fn:跑在一个专门的内核线程里(你会看到irq/xxx-xxxx这样的线程),可以睡觉、可以调mutex、可以copy_to_user——爽歪歪
  • 如果handler是NULL,内核会自动帮你放一个默认的上半部(只应答中断然后唤醒thread_fn)

这个机制的最大好处是:你不需要自己管理下半部的调度细节,内核帮你搞定一切。而且它天然支持IRQF_ONESHOT——在thread_fn跑完之前中断保持屏蔽,避免了电平触发的中断风暴。

用线程化中断改写我们的按键驱动(只改中断相关的部分):

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
// 上半部:只清中断,返回 IRQ_WAKE_THREAD 让内核唤醒下半部线程
static irqreturn_t key_hard_handler(int irq, void *dev_id)
{
struct key_dev *dev = (struct key_dev *)dev_id;
unsigned int status;

status = readl(dev->gpio_base + GPIO_INT_STATUS);
writel(status, dev->gpio_base + GPIO_INT_STATUS);

printk(KERN_INFO "[KeyIRQ]: 硬中断 (IRQ=%d)\n", irq);

return IRQ_WAKE_THREAD; // 告诉内核:去跑下半部线程
}

// 下半部:跑在线程里,可以睡觉!消抖直接 msleep
static irqreturn_t key_thread_handler(int irq, void *dev_id)
{
struct key_dev *dev = (struct key_dev *)dev_id;
unsigned int val;

// 线程里可以睡!直接用 msleep,不需要 udelay
msleep(DEBOUNCE_MS);

val = readl(dev->gpio_base + 0x50);
if (!(val & (1 << 3))) {
printk(KERN_INFO "[KeyIRQ]: 按键确认按下 (消抖)\n");
buf_put(dev, 'K');
wake_up_interruptible(&dev->read_wq);
schedule_work(&dev->key_work);
} else {
printk(KERN_INFO "[KeyIRQ]: 抖动,忽略\n");
}

return IRQ_HANDLED;
}

// 注册的时候这样:
ret = request_threaded_irq(my_dev.irq,
key_hard_handler, // 上半部
key_thread_handler, // 下半部线程
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"rk3588_key", &my_dev);

对比一下两种方式:

对比项tasklet + workqueue 手动档request_threaded_irq 自动档
代码量多(要手动初始化+调度)少(内核搞定)
下半部要么软中断(不能睡)要么workqueue(能睡但要手动管理)统一的内核线程,能睡
灵活性高——可以精确控制调度时机和顺序较低——走内核的标准流程
调试要自己加log追踪tasklet有没有调度ps里能看到irq/xxx线程

新手推荐用线程化中断,写起来省心不容易出错。老手在需要极致性能的场景下(比如频繁触发的高速数据采集)用tasklet手动档。


总结

这期博客信息量大,咱们来捋一下学了什么:

  • 四种IO模型:阻塞IO是基础中的基础(等待队列一把梭),非阻塞IO给上层更多控制权(O_NONBLOCK),信号驱动让内核主动通知用户态(fasync + SIGIO),IO多路复用是高性能服务器的基石(poll,用户态再包一层epoll就是完全体)。
  • 两种定时器:标准timer_list(毫秒级,够用)和高精度hrtimer(微秒/纳秒,但代码重一些)。选哪个看精度需求,大部分场景timer_list足矣。
  • IOCTL命令控制:从命令码的32位结构到unlocked_ioctl的实现,再到魔数冲突的避坑——设备控制的总开关。
  • 中断——重头戏:从GIC-600控制器到request_irq的每一个参数,到上半部/下半部的拆分哲学,到tasklet和workqueue的实操差别,再到线程化中断这个现代化的简化方案。最后那个GPIO按键驱动的完整例子,把中断注册、消抖、环形缓冲、等待队列唤醒、poll回调、错误回滚处理全部串了起来。

这些东西是Linux内核驱动开发的根基。你后面学I2C、SPI、PCIe、USB,无非就是在这套框架上换了个外设——本质还是中断+字符设备+IO模型。把基础打牢,后面就都是套路了。

这期的Makefile跟之前一样,不多重复了,记得加-std=gnu11

1
2
3
4
5
6
7
8
9
10
11
12
obj-m := key_irq.o

ARCH := arm64
CROSS_COMPILE := /你的路径/aarch64-none-linux-gnu-
ccflags-y := -std=gnu11
KERNEL_DIR := /你的内核源码路径

all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

如果觉得博客有用,欢迎分享给你的朋友们。

参考:

  • Rockchip RK3588 TRM(技术参考手册)Part 1 & Part 2
  • ARM GIC-600 Technical Reference Manual
  • Linux Kernel Documentation: Documentation/driver-api/Documentation/core-api/
  • include/linux/interrupt.hinclude/linux/timer.hinclude/linux/hrtimer.h 源码注释
  • 寄存器资料:https://gitee.com/flying-cat/rk3588-TRM-and-Datasheet