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);
#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) { 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
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_event和wait_event_interruptible两个版本。不带_interruptible的版本不能被信号打断——也就是说用户按Ctrl+C也没用,进程会一直睡在那里。除非你非常确定需要这个行为,否则一律用_interruptible版本,这样用户至少能用Ctrl+C把卡住的程序杀掉。唤醒端也有对应的wake_up和wake_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) { 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;
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);
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; 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");
|
驱动写好了,用户态怎么配合呢?三步走:
- 注册SIGIO信号的处理函数(
signal(SIGIO, handler)) - 告诉内核”这个fd的信号发给我这个进程”(
fcntl(fd, F_SETOWN, getpid())) - 开启异步通知(
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;
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);
fd = open("/dev/fasync_dev", O_RDWR); if (fd < 0) { perror("open"); return 1; }
fcntl(fd, F_SETOWN, getpid());
int flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | FASYNC);
printf("等待 SIGIO 信号... (PID: %d)\n", getpid());
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回调。这个回调做两件事:
- 调用
poll_wait把当前进程挂到你的等待队列上(这样数据来了你叫醒就行) - 返回一个掩码,告诉内核当前设备是什么状态(可读?可写?出错了?)
常用的返回掩码就是这几个:
| 掩码 | 啥意思 |
|---|
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; }
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(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); 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回调只是返回个掩码。
另外提一嘴,在真实的项目中,epoll比poll好用得多。poll每次都传整个fd数组,epoll只需要epoll_ctl注册一次,之后epoll_wait只返回就绪的fd。但这不是我们驱动的活——驱动只管把.poll回调写好,上层用poll还是epoll都是自动工作的。
四种模型该选哪个?一张表说清楚
| 模型 | 怎么实现的 | 用户态会卡住吗 | CPU开销 | 什么时候用 |
|---|
| 阻塞IO | 等待队列 | 会 | 低 | 简单的读写、数据流——最常用 |
| 非阻塞IO | O_NONBLOCK | 不会 | 高(纯轮询的话) | 需要快速响应但不能用多路复用的场景 |
| 信号驱动IO | fasync+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)
void timer_setup(struct timer_list *timer, void (*callback)(struct timer_list *), unsigned int flags);
int mod_timer(struct timer_list *timer, unsigned long expires);
int del_timer(struct timer_list *timer);
int del_timer_sync(struct timer_list *timer);
msecs_to_jiffies(ms); usecs_to_jiffies(us);
|
直接来一个完整的例子——用定时器让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"
#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;
static void blink_timer_callback(struct timer_list *t) { 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; else val = 0x00080000;
writel(val, dev->gpio_base + GPIO_SWPORT_DR);
printk(KERN_INFO "[TimerLED]: LED %s\n", dev->led_state ? "亮" : "灭");
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; }
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);
timer_setup(&my_dev.blink_timer, blink_timer_callback, 0); my_dev.led_state = 0; my_dev.period_ms = 500;
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) { 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
| sudo echo "500" > /dev/timer_led
sudo echo "200" > /dev/timer_led
sudo echo "0" > /dev/timer_led
|
搞定时器有两个容易踩的坑:
第一个坑:del_timer和del_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(s64 secs, unsigned long nsecs);
|
clock_id的选择:
| clock_id | 说明 |
|---|
CLOCK_MONOTONIC | 单调递增时钟,不受系统时间调整影响,绝大多数场景用这个 |
CLOCK_REALTIME | 系统实时时钟,受NTP和用户调时间影响——可能往回跳,慎用 |
hrtimer的回调函数原型跟普通定时器不一样:
1 2 3
|
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(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;
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); 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
| sudo echo "100000" > /dev/hrtimer_dev
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
| #ifndef _IOCTL_CMD_H #define _IOCTL_CMD_H
#include <linux/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;
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: 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: 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: 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, .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; }
struct device_info info; if (ioctl(fd, IOCTL_GET_INFO, &info) == 0) printf("设备名: %s, 版本: %d\n", info.name, info.version);
int baud = 115200; if (ioctl(fd, IOCTL_SET_BAUD, &baud) == 0) printf("波特率已设为: %d\n", baud);
int status; if (ioctl(fd, IOCTL_GET_STATUS, &status) == 0) printf("状态: %d (1=已配置)\n", status);
if (ioctl(fd, IOCTL_RESET_DEVICE) == 0) printf("设备已复位\n");
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)在启动时已经帮你初始好了。驱动开发只需要:
- 知道自己GPIO对应的IRQ号
- 调用
request_irq()注册中断处理函数 - 处理好上半部和下半部
那怎么知道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);
|
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);
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
|
参数一个个解释:
| 参数 | 说明 |
|---|
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);
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);
bool schedule_work(struct work_struct *work);
bool cancel_work_sync(struct work_struct *work);
void (*work_func_t)(struct work_struct *work);
|
完整例程: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"
#define GPIO2_BASE 0xFEC30000 #define GPIO_SWPORT_DR 0x0004 #define GPIO_SWPORT_DDR 0x000C #define GPIO_INT_STATUS 0x0040 #define GPIO_INT_EN 0x0030
#define DEBOUNCE_MS 20 #define KEY_BUF_SIZE 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;
struct tasklet_struct key_tasklet;
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; }
static void key_tasklet_handler(unsigned long data) { struct key_dev *dev = (struct key_dev *)data; unsigned int val;
udelay(DEBOUNCE_MS * 1000);
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"); } }
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"); }
static irqreturn_t key_irq_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);
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; }
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);
ret = alloc_chrdev_region(&my_dev.dev_number, 0, 1, DEV_NAME); if (ret < 0) goto fail0;
my_dev.gpio_base = ioremap(GPIO2_BASE, 0x1000); if (!my_dev.gpio_base) { ret = -ENOMEM; goto fail1; }
writel(0x00080000, my_dev.gpio_base + GPIO_SWPORT_DDR);
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);
tasklet_init(&my_dev.key_tasklet, key_tasklet_handler, (unsigned long)&my_dev); INIT_WORK(&my_dev.key_work, key_work_handler);
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);
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; }
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;
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) {
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
sudo ./key_test
sudo rmmod key_irq
|
注意看上面dmesg里面的时间戳——中断触发(上半部)和按键确认(tasklet消抖)之间差了大约20ms,这就是udelay(DEBOUNCE_MS * 1000)正在干活。
中断编程的避坑指南(血泪换来的经验)
中断这部分搞不好分分钟内核panic,下面是我以前踩过的坑,你务必记住:
1、中断上下文绝对不能睡。 这是第一铁律。上半部和tasklet里所有可能睡眠的函数都不能用——msleep、mutex_lock、kmalloc(GFP_KERNEL)(用GFP_ATOMIC代替)、copy_to_user/copy_from_user、down/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
| 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; }
static irqreturn_t key_thread_handler(int irq, void *dev_id) { struct key_dev *dev = (struct key_dev *)dev_id; unsigned int val;
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.h、include/linux/timer.h、include/linux/hrtimer.h 源码注释- 寄存器资料:https://gitee.com/flying-cat/rk3588-TRM-and-Datasheet