Linux驱动开发笔记(十一)——阻塞和非阻塞IO
视频:第14.1讲 Linux阻塞和非阻塞IO实验-阻塞与非阻塞简介_哔哩哔哩_bilibili
资料:《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf》五十二章
1、概念
1.1 简介
(直接搬《开发指南》的解释了)
当应用程序对设备驱动进行操作时,如果不能获取到设备资源,阻塞式IO就会将应用程序对应的进程挂起,直到设备资源可以获取为止。阻塞式IO如图52.1.1.1 所示:
应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好时,会向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,往复循环,直到数据读取成功。非阻塞IO如图52.1.2所示:
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */ fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
1.2 阻塞访问——等待队列
1.2.1 等待队列头
若设备资源变为可用,则需要唤醒休眠的进程。唤醒工作一般由中断函数完成。
Linux内核提供等待队列wait queue来实现阻塞进程的唤醒。其结构体定义如下:
//定义在include/linux/wait.h
struct __wait_queue_head {spinlock_t lock;struct list_head task_list;
};
定义好等待队列头,需要对其进行初始化:
void init_waitqueue_head(wait_queue_head_t *q)
也可以使用宏来一次性完成定义、初始化:
#define DECLARE_WAIT_QUEUE_HEAD(name) \wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
1.2.2 等待队列项
等待队列头就是等待队列的头,每个访问设备的进程就是一个队列项。当设备不可用时,就要将这些进程对应的等待队列项添加到等待队列里面。等待队列项结构体定义如下:
//定义在include/linux/wait.h
typedef struct __wait_queue wait_que{unsigned int flags;void *private;wait_queue_func_t func;struct list_head task_list;
};
定义好等待队列等以后需要初始化:
void init_waitqueue_head(wait_queue_head_t *q)
// q就是要初始化的等待队列头
也可以使用宏定义初始化:
#define DECLARE_WAITQUEUE(name, tsk) \wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
1.2.3 添加 / 移除队列项
当设备资源不可访问时,需要将进程对应的等待队列项添加到等待队列头,然后进程才能进入休眠。设备资源可以访问后,再唤醒对应进程并移除对应的等待队列项。
void add_wait_queue(wait_queue_head_t *q, // 等待队列头wait_queue_t *wait) // 要加入的等待队列项
void remove_wait_queue(wait_queue_head_t *q, // 等待队列头wait_queue_t *wait) // 要删除的等待队列项
1.2.4 等待唤醒
当设备资源可以使用时,就要唤醒进入休眠态的进程:
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
// q即是要唤醒的等待队列头
这两个函数会将这个等待队列头中的所有进程都唤醒。
wake_up函数可以唤醒TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程,而wake_up_interruptible函数只能唤醒处于TASK_INTERRUPTIBLE状态的进程。
// 定义在include/linux/sched.h
#define TASK_RUNNING 0 // 运行状态
#define TASK_INTERRUPTIBLE 1 // 可中断的睡眠状态。一旦被唤醒,会切换为TASK_RUNNING
#define TASK_UNINTERRUPTIBLE 2 // 不可中断的睡眠状态
#define __TASK_STOPPED 4 // 停止状态。此时进程不再被调度,收到SIGCONT才会恢复到TASK_RUNNING
#define __TASK_TRACED 8 // 正在被调试器跟踪
1.2.5 等待事件(自动唤醒)
除了手动唤醒外,还可以将等待队列设置为自动唤醒,当满足某个事件以后自动唤醒等待队列中的进程。
wait_event(wq, condition)
// wq : 等待队列头
// condition: 唤醒条件条件
// 当condition为真时唤醒wq的等待队列,否则此函数会将进程设置为TASK_UNINTERRUPTIBLE状态
wait_event_timeout(wq, condition, timeout)
// wq : 等待队列头
// condition: 唤醒条件条件
// timeout : 超时时间(单位jiffies)
// return : 返回0表示超时 且condition为假。
// 返回正数表示condition满足时的剩余时间
wait_event_interruptible(wq, condition)
// 与wait_event类似,但是此函数将进程设置为TASK_INTERRUPTIBLE,可以被信号打断// 比如kill -9 XXX,就是给给进程发送信号9,表示终止进程
// 如果是wait_event_interruptible,就会立刻响应,结束本进程
// 但如果是wait_event,其睡眠不可被中断,不会响应任何信号,这时候用ps查看就会发现无法kill掉
// 只有当进程被唤醒后才会执行kill -9
wait_event_interruptible_timeout(wq, condition, timeout)
// 与wait_event_timeout类似,此函数也将进程设置为TASK_INTERRUPTIBLE,可以被信号打断
1.3 非阻塞访问——轮询
如果应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式——轮询。
poll、epoll和select用于处理轮询。应用程序通过select、epoll或poll函数来查询设备是否可以操作,如果可以操作的话就从设备读取或向设备写入。当应用程序调用select、epoll或poll函数的时候设备驱动程序中的(内核)poll函数就会执行,我们需要在设备驱动程序中编写poll函数。
1.3.1 select
(这里干讲概念还是抽象了,具体用法看1.3.1.5或2.2.4)
int select( int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout)
1.3.1.1 nfds
要检测的文件描述符的范围,为文件最大描述符+1。
1.3.1.2 readfds & writefds & exceptfds
这三个指针指向描述符集合,指明了关心哪些描述符、需要满足哪些条件等等。这三个参数都是fd_set类型的,fd_set类型变量的每一个位都代表了一个文件描述符。
readfds用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取。只要这些集合里面有一个文件可以读取 那么seclect就会返回一个大于0的值 表示文件可以读取。如果没有文件可以读取,那么就会根据timeout参数来判断是否超时。可以将readfs设置为NULL,表示不关心任何文件的读变化。
writefds和readfs 类似,只是writefs用于监视这些文件是否可以进行写操作。
exceptfds用于监视这些文件的异常。
比如要从一个设备文件中读取数据,就可以定义一个fd_set变量,这个变量要传递给参数readfds。定义好一个fd_set变量以后可以使用如下所示几个宏进行操作:
void FD_ZERO(fd_set *set) // 将fd_set变量的所有位都清零
void FD_SET(int fd, fd_set *set) // 将fd_set变量的某个位置1,即向fd_set添加一个文件描述符,fd就是要加入的文件描述符
void FD_CLR(int fd, fd_set *set) // 将fd_set变量的某个位置0,即将一个文件描述符从fd_set中删除,fd就是要删除的文件描述符
int FD_ISSET(int fd, fd_set *set) // 用于测试一个文件是否属于set集合,fd就是要判断的文件描述符。
1.3.1.3 timeout
timeout为超时时间。当我们调用select函数等待某些文件描述符可以设置超时时间,超时时 间使用结构体timeval表示:
// 定义在include/uapi/linux/time.h
struct timeval {__kernel_time_t tv_sec; /* 秒*/__kernel_suseconds_t tv_usec; /* 微秒 */
};
timeout为NULL表示无限等待。
1.3.1.4 return
返回0 :表示超时,且没有任何文件描述符可以进行操作
返回-1:发生错误;
返回其他值:可以进行操作的文件描述符个数
1.3.1.5 示例代码
《开发指南》中给出了示例代码:
// 示例代码52.1.3.1 select函数非阻塞读访问示例 1 void main(void) 2 { 3 int ret, fd; /* 要监视的文件描述符 */ 4 fd_set readfds; /* 读操作文件描述符集 */ 5 struct timeval timeout; /* 超时结构体 */ 6 7 fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */ 8 9 FD_ZERO(&readfds); /* 清除readfds */ 10 FD_SET(fd, &readfds); /* 将fd添加到readfds里面 */ 11 12 /* 构造超时时间 */ 13 timeout.tv_sec = 0; 14 timeout.tv_usec = 500000; /* 500ms */ 15 16 ret = select(fd + 1, &readfds, NULL, NULL, &timeout); 17 switch (ret) { 18 case 0: /* 超时 */ 19 printf("timeout!\r\n"); 20 break; 21 case -1: /* 错误 */ 22 printf("error!\r\n"); 23 break; 24 default: /* 可以读取数据 */ 25 if(FD_ISSET(fd, &readfds)) { /* 判断是否为fd文件描述符 */ 26 /* 使用read函数读取数据 */ 27 } 28 break; 29 } 30 }
1.3.2 poll函数
在单个进程中,select函数能够监视的文件描述符数量有最大的限制,一般为1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用poll函数, poll函数本质上和select没有太大的差别,但是poll函数没有最大文件描述符限制。
int poll(struct pollfd *fds, // 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体pollfd类型nfds_t nfds, // poll函数要监视的文件描述符数量int timeout) // 超时时间,单位为ms// return: // 返回正数:revents域中不为0的pollfd结构体个数,也就是发生事件或错误的文件描述符数量 // 返回0, 超时 // 返回-1,发生错误
其中struct pollfd的定义为:
struct pollfd { int fd; /* 文件描述符 */ short events; /* 请求的事件 */ short revents; /* 返回的事件,不需要我们传入。由Linux内核设置具体的返回事件 */ }; fd是要监视的文件描述符,如果fd无效的话那么events监视事件也就无效,并且revents返回0。events是要监视的事件,可监视的事件类型如下所示:POLLIN 有数据可以读取。 POLLPRI 有紧急的数据需要读取。 POLLOUT 可以写数据。 POLLERR 指定的文件描述符发生错误。 POLLHUP 指定的文件描述符挂起。 POLLNVAL 无效的请求。 POLLRDNORM 等同于POLLIN
《开发指南》中给出的示例代码:
// 示例代码52.1.3.2 poll函数读非阻塞访问示例 1 void main(void) 2 { 3 int ret; 4 int fd; /* 要监视的文件描述符 */ 5 struct pollfd fds; 6 7 fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */ 8 9 /* 构造结构体 */ 10 fds.fd = fd; 11 fds.events = POLLIN; /* 监视数据是否可以读取 */ 12 13 ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时500ms */ 14 if (ret) { /* 数据有效 */ 15 ...... 16 /* 读取数据 */ 17 ...... 18 } else if (ret == 0) { /* 超时 */ 19 ...... 20 } else if (ret < 0) { /* 错误 */ 21 ...... 22 } 23 }
1.3.3 epoll函数(不是重点,视频里也没有)
传统的selcet和poll函数会随着所监听的fd数量的增加而率降低,而且poll函数每次必须遍历所有的描述符来检查就绪的描述符,很浪费时间。为此,epoll用于处理大并发,一般常在网络编程中使用epoll函数。
当涉及的文件描述符fd比少时就适合用selcet和poll,本章就主要使用select和poll。
1.3.3.1 创建epoll句柄
int epoll_create(int size)
// size : 从Linux2.6.8开始此参数已经没有意义了,随便填写一个大于0的值就可以
// return: 返回epoll句柄;若返回-1表示创建失败
1.3.3.2 添加监视
int epoll_ctl(int epfd, // 要操作的epoll句柄(就是epoll_create()返回的东西)int op, // 表示要对epfd(epoll句柄)进行的操作int fd, // 要监视的文件描述符struct epoll_event *event) // 要监视的事件类型// return:0,成功;-1,失败,并且设置errno的值为相应的错误码
其中op参数可以是:
EPOLL_CTL_ADD 向句柄添加文件参数fd表示的描述符
EPOLL_CTL_MOD 修改参数fd的event事件
EPOLL_CTL_DEL 从epfd中删除fd描述符
参数中的epoll_event结构体定义如下:
struct epoll_event { uint32_t events; /* epoll事件 */ epoll_data_t data; /* 用户数据 */
}; 其中events的值可以是:(也可以进行或操作同时监视多个事件)
EPOLLIN 有数据可以读
EPOLLOUT 可以写
EPOLLPRI 有紧急数据需要读
EPOLLERR 指定的文件描述符发生错误
EPOLLHUP 指定的文件描述符挂起
EPOLLET 设置epoll为边沿触发,默认为水平触发
EPOLLONESHOT 一次性的监视。当监视完成以后,若还需要监视某个fd,需要将fd重新添加到epoll里
1.3.3.3 等待
设置好以后应用程序就可以通过epoll_wait函数来等待事件的发生。
int epoll_wait(int epfd, // 要等待的epollstruct epoll_event *events, // 指向epoll_event结构体的数组int maxevents, // events数组大小,必须大于0int timeout // 超时时间(ms)
) // return: 0,超时;-1,错误;其他值,准备就绪的文件描述符数量
1.3.4 Linux驱动下的poll
使用select或poll时,需要在驱动程序file_operations操作集中添加poll函数。当应用程序调用select或poll来对驱动程序进行非阻塞访问的时候,就会执行file_operations中的poll函数。
unsigned int (*poll) (struct file *filp, // 要打开的设备文件(文件描述符fd)struct poll_table_struct *wait) // 由应用程序传递进来。一般将此参数传递给poll_wait函数/* return:向应用程序返回设备或资源状态,可以返回的资源状态如下:POLLIN 有数据可以读取POLLPRI 有紧急的数据需要读取POLLOUT 可以写数据POLLERR 指定的文件描述符发生错误POLLHUP 指定的文件描述符挂起 POLLNVAL 无效的请求POLLRDNORM 等同于POLLIN,普通数据可读*/
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, // 要添加到poll_table中的等待队列头poll_table *p) // poll_table(file_operations中poll函数的wait参数)
示例:
// 这段代码来自drivers/media/pci/ttpci/av7110_ca.c
static const struct file_operations dvb_ca_fops = {………….poll = dvb_ca_poll,…………
};static unsigned int dvb_ca_poll (struct file *file, poll_table *wait){……struct dvb_ringbuffer *rbuf = &av7110->ci_rbuffer;struct dvb_ringbuffer *wbuf = &av7110->ci_wbuffer;……poll_wait(file, &rbuf->queue, wait); // rbuf->queue即是对应的等待队列头poll_wait(file, &wbuf->queue, wait);……
}
2、实验
2.1 阻塞式IO
2.1.1 文件结构
15_BLOCKIO (工作区)
├── .vscode
│ ├── c_cpp_properties.json
│ └── settings.json
├── 15_blockio.code-workspace
├── Makefile
├── blockio.c
└── irqAPP.c
2.1.2 Makefile
CFLAGS_MODULE += -wKERNELDIR := /....../linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek # 内核路径
# KERNELDIR改成自己的 linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路径(这个文件从正点原子“01、例程源码”中直接搜,cp到虚拟机里面)CURRENT_PATH := $(shell pwd) # 当前路径obj-m := blockio.o # 编译文件build: kernel_modules # 编译模块kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
2.1.3 blockio.c
直接在上次中断的驱动代码irq.c基础上改即可。修改了以下部分:
在设备结构体中 增加了等待队列头
在驱动入口函数key_init()中 增加了等待队列头初始化
在读操作函数irq_read()中 增加了等待事件,等待按键有效
在定时器处理函数timer_func增加了唤醒进程
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/atomic.h>
#include <linux/wait.h>
#include <linux/ide.h>#define DEV_CNT 1 /* 设备号数量 */
#define DEV_NAME "irq" /* 设备名 */
#define KEY_NUM 1 /* 按键数量 */
#define KEY0_VALUE 0x01 /* 按键值(假定释放为1,按下为0)*/
#define INVAKEY 0x00 /* 按键无效值 *//* 按键结构体 */
struct irq_keydesc{int gpio; /* io号 */int irqnum; /* 中断号 */unsigned char value; /* 键值 (默认/空闲电平,比如 1) */char name[10]; /* 名字 */irqreturn_t (*handler) (int, void *); /* 中断处理函数*/// struct tasklet_struct tasklet;struct work_struct work;
};/* 中断设备结构体 */
struct irq{dev_t devid; // 设备号int major; // 主设备号int minor; // 次struct cdev cdev; // cdevstruct device *device; // 设备struct class *class; // 类struct device_node *nd; // 节点struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 个按键struct timer_list timer;// 定时器atomic_t releasekey; // 0表示按键释放,1表示按键按下atomic_t keyvalue; // 第八位表示数据是否被读取:每完成一次按键释放,第八位就会置1,用户态调用.read时会将其置0,表示数据被读取// 其余七位表示按键的值wait_queue_head_t r_wait; //读 等待队列头
};
static struct irq key_irq;/* 打开/读操作 */
static int irq_open(struct inode *inode, struct file *filp){filp->private_data = &key_irq;return 0;
}
static ssize_t irq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){int ret = 0;unsigned char keyvalue = 0;unsigned char releasekey = 0;struct irq* dev = filp->private_data;wait_event_interruptible(dev->r_wait, (atomic_read(&dev->keyvalue) & 0x80)); // 等待按键有效keyvalue = (unsigned char)atomic_read(&dev->keyvalue);releasekey = (unsigned char)atomic_read(&dev->releasekey);if(releasekey){ // 如果按键释放,此时可以读取数据if(keyvalue & 0x80){ // 如果最高位为1,表示此时数据还未被读取unsigned char out = keyvalue & 0x7F; // 将最高位置0(最高位为有效位,低7位为实际value)atomic_set(&dev->keyvalue, 0); // 清零,表示数据已被读取atomic_set(&dev->releasekey, 0);if(copy_to_user(buf, &out, sizeof(out))){ // 返回按键值给用户态return -EFAULT;}ret = sizeof(out);} else {goto data_error;}atomic_set(&dev->releasekey, 0); // 释放标志清零return ret;} else {goto data_error;}data_error:return -EINVAL;
}/* 操作集 */
static const struct file_operations key_fops = {.owner = THIS_MODULE,.open = irq_open,.read = irq_read,
};/* 中断处理函数 */
static irqreturn_t key0_handler(int irq, void *dev_id){struct irq *dev = (struct irq*)dev_id;int current_level; // 保存中断时的电平current_level = gpio_get_value(dev->irqkey[0].gpio);atomic_set(&dev->keyvalue, current_level); // 保存基准电平// tasklet_schedule(&dev->irqkey[0].tasklet); // 调度tasklet处理后续逻辑,避免中断耗时schedule_work(&dev->irqkey[0].work);return IRQ_HANDLED;
}/* 定时器处理函数 */
static void timer_func(unsigned long arg){struct irq* dev = (struct irq*)arg;int current_value; // 保存定时器延时后的电平/* 读取当前按键电平 */current_value = gpio_get_value(dev->irqkey[0].gpio);/* 若当前电平与中断发生时读取到的电平一致,则有效 */if (current_value == atomic_read(&dev->keyvalue)) {if (current_value == 1) { // 释放printk("KEY0 release!\r\n");atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效atomic_set(&dev->releasekey, 1); // 表示已释放,此时用户态可读} else { // 按下printk("KEY0 Push!\r\n");atomic_set(&dev->keyvalue, current_value); // 这里0 表示按下// atomic_set(&dev->releasekey, 0); // 表示未释放}}// 唤醒进程if(atomic_read(&dev->releasekey)){wake_up(&dev->r_wait);}
}/* tasklet处理函数 */
static void key_tasklet(unsigned long data){struct irq* dev = (struct irq*)data;printk(" key_tasklet\r\n");dev->timer.data = data;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */}
/* 工作队列处理函数 */
static void key_work(struct work_struct *work_){printk(" key_work\r\n");key_irq.timer.data = (unsigned long)&key_irq;mod_timer(&key_irq.timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */// struct irq* dev = container_of(work_, struct irq, work);
}/* 初始化按键 */
static int keyio_init(struct irq *dev){int ret = 0;int i = 0;/* 1. 按键初始化 */dev->nd = of_find_node_by_path("/key"); // 获取设备节点if(dev->nd == NULL){ret = -EINVAL;goto fail_nd;}for(i=0; i<KEY_NUM; i++){dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 获取设备树中 gpioif (dev->irqkey[i].gpio < 0) {pr_err("get gpio %d failed\n", i);ret = -EINVAL;goto fail_nd;}}for(i=0; i<KEY_NUM; i++){memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));sprintf(dev->irqkey[i].name, "KEY%d", i); // 命名ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申请gpioif (ret) {pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);goto fail_gpio_req;}gpio_direction_input(dev->irqkey[i].gpio); // 设置io方向dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 获取中断号if (dev->irqkey[i].irqnum < 0) {pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);ret = dev->irqkey[i].irqnum;goto fail_gpio_req;}}dev->irqkey[0].handler = key0_handler; // 中断处理函数dev->irqkey[0].value = KEY0_VALUE; // 例如:1 表示 release(高电平),0 表示按下(低电平)/* 2. 中断初始化 */for(i=0; i<KEY_NUM; i++){ret = request_irq(dev->irqkey[i].irqnum,dev->irqkey[i].handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,dev->irqkey[i].name,&key_irq); /* dev_id 传入结构体指针 */if(ret < 0){printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);while (--i >= 0) {free_irq(dev->irqkey[i].irqnum, &key_irq);}goto fail_irq;}// tasklet_init(&dev->irqkey[i].tasklet, key_tasklet, (unsigned long)dev);INIT_WORK(&dev->irqkey[i].work, key_work);}return 0;fail_irq:
fail_gpio_req:for (i = 0; i < KEY_NUM; i++) {if (gpio_is_valid(dev->irqkey[i].gpio))gpio_free(dev->irqkey[i].gpio);}
fail_nd:return ret;
}/* 驱动入口 */
static int __init key__init(void){int ret = 0;/* 1. 注册字符设备驱动 */key_irq.devid = 0;alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);key_irq.major = MAJOR(key_irq.devid);key_irq.minor = MINOR(key_irq.devid);/* 2. 初始化 cdev */key_irq.cdev.owner = THIS_MODULE;cdev_init(&key_irq.cdev, &key_fops);/* 3. 添加 cdev */ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);if (ret) {pr_err("cdev_add failed\n");goto fail_cdev;}/* 4. 创建类 */key_irq.class = class_create(THIS_MODULE, DEV_NAME);if(IS_ERR(key_irq.class)){ret = PTR_ERR(key_irq.class);goto fail_class;}/* 5. 创建设备 */key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);if(IS_ERR(key_irq.device)){ret = PTR_ERR(key_irq.device);goto fail_device;}/* 6. 初始化IO*/ret = keyio_init(&key_irq);if(ret < 0){goto fail_keyinit;}/* 7. 初始化定时器 */init_timer(&key_irq.timer);key_irq.timer.function = timer_func;key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向设备结构体 *//* 8. 初始化原子变量 */atomic_set(&key_irq.keyvalue, INVAKEY);atomic_set(&key_irq.releasekey, 0);/* 9.初始化等待队列头 */init_waitqueue_head(&key_irq.r_wait);return 0;fail_keyinit:device_destroy(key_irq.class, key_irq.devid);
fail_device:class_destroy(key_irq.class);
fail_class:cdev_del(&key_irq.cdev);
fail_cdev:unregister_chrdev_region(key_irq.devid, DEV_CNT);return ret;
}/* 驱动出口 */
static void __exit key_exit(void){int i=0;/* 释放中断 */for(i=0; i<KEY_NUM; i++){free_irq(key_irq.irqkey[i].irqnum, &key_irq);}/* 释放io */for(i=0; i<KEY_NUM; i++){if (gpio_is_valid(key_irq.irqkey[i].gpio))gpio_free(key_irq.irqkey[i].gpio);}/* 删除定时器 */del_timer_sync(&key_irq.timer);/* 注销字符设备驱动 */cdev_del(&key_irq.cdev);unregister_chrdev_region(key_irq.devid, DEV_CNT);device_destroy(key_irq.class, key_irq.devid);class_destroy(key_irq.class);
}module_init(key__init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
2.1.4 irqApp
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/ioctl.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>/* * @description : main主程序 * @param - argc : argv数组元素个数 * @param - argv : 具体参数 * @return : 0 成功; else失败* 调用 ./irqAPP /dev/timer*/ #define LEDOFF 0
#define LEDON 1int main(int argc, char *argv[]){if(argc != 2){ // 判断用法是否错误printf("Error Usage!\r\n");return -1;}char *filename;int fd = 0, ret = 0;unsigned char data;filename = argv[1];fd = open(filename, O_RDWR); // 读写模式打开驱动文件filenameif(fd <0){printf("file %s open failed!\r\n");return -1;}while(1) {ret = read(fd, &data, sizeof(data));if (ret > 0) {printf("key value = %#x\r\n", data);}}close(fd);return 0;
}
2.1.5 测试
先运行上一次中断实验的驱动,看一眼CPU占用,直接被吓昏:
然后再测试这次的实验代码:
# VSCODE终端
make
arm-linux-gnueabihf-gcc irqAPP.c -o irqAPP
sudo cp blockio.ko irqAPP /.../linux/nfs/rootfs/lib/modules/4.1.15/# 串口
cd /lib/modules/4.1.15/
depmod
modprobe blockio.ko
./irqAPP /dev/irq & # 后台启动应用程序
top # 查看CPU占用,此时占用应当很低# 此时按键应当有输出
kill -9 XXX # 关闭后台应用程序
使用阻塞式以后就不会有占用CPU的问题:
2.2 非阻塞式IO
2.2.1 文件结构
与阻塞式完全一致。
16_NOBLOCKIO (工作区)
├── .vscode
│ ├── c_cpp_properties.json
│ └── settings.json
├── 16_noblockio.code-workspace
├── Makefile
├── noblockio.c
└── irqAPP.c
2.2.2 Makefile
与阻塞式完全一致。修改一下obj-m项即可。
2.2.3 noblockio.c
基本和阻塞式一致,只在几个地方有修改。
更新:
修改了read函数
增加了头文件#include <linux/poll.h>
增加了操作集file_operations中的.poll以及对应的irq_poll()函数
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/atomic.h>
#include <linux/wait.h>
#include <linux/ide.h>
#include <linux/poll.h>#define DEV_CNT 1 /* 设备号数量 */
#define DEV_NAME "irq" /* 设备名 */
#define KEY_NUM 1 /* 按键数量 */
#define KEY0_VALUE 0x01 /* 按键值(假定释放为1,按下为0)*/
#define INVAKEY 0x00 /* 按键无效值 *//* 按键结构体 */
struct irq_keydesc{int gpio; /* io号 */int irqnum; /* 中断号 */unsigned char value; /* 键值 (默认/空闲电平,比如 1) */char name[10]; /* 名字 */irqreturn_t (*handler) (int, void *); /* 中断处理函数*/// struct tasklet_struct tasklet;struct work_struct work;
};/* 中断设备结构体 */
struct irq{dev_t devid; // 设备号int major; // 主设备号int minor; // 次struct cdev cdev; // cdevstruct device *device; // 设备struct class *class; // 类struct device_node *nd; // 节点struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 个按键struct timer_list timer;// 定时器atomic_t releasekey; // 0表示按键释放,1表示按键按下atomic_t keyvalue; // 第八位表示数据是否被读取:每完成一次按键释放,第八位就会置1,用户态调用.read时会将其置0,表示数据被读取// 其余七位表示按键的值wait_queue_head_t r_wait; //读 等待队列头
};
static struct irq key_irq;/* 打开/读操作 */
static int irq_open(struct inode *inode, struct file *filp){filp->private_data = &key_irq;return 0;
}
static ssize_t irq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){int ret = 0;unsigned char keyvalue = 0;unsigned char releasekey = 0;struct irq* dev = filp->private_data;if(filp->f_flags & O_NONBLOCK){ // 非阻塞方式if(atomic_read(&dev->releasekey) == 0){ // 无效return -EAGAIN;}}else{ // 阻塞方式wait_event_interruptible(dev->r_wait, (atomic_read(&dev->keyvalue) & 0x80)); // 等待按键有效}keyvalue = (unsigned char)atomic_read(&dev->keyvalue);releasekey = (unsigned char)atomic_read(&dev->releasekey);if(releasekey){ // 按键有效if(keyvalue & 0x80){keyvalue &= ~0x80;ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue));} else {goto data_error;}atomic_set(&dev->releasekey, 0); // 按下标志清零} else {goto data_error;}data_error:ret = -EINVAL;return ret;
}// 应用程序先调用poll,查看是否可读。
// 如果可读,则调用read函数
static unsigned int irq_poll(struct file *filp, struct poll_table * wait){int ret = 0;struct irq *dev = filp->private_data;poll_wait(filp, &dev->r_wait, wait);// 是否可读if(atomic_read(&dev->releasekey)){ // 按键按下,可读ret = POLLIN | POLLRDNORM; // 表示有数据可以读。详见1.3.2部分}return ret;
}/* 操作集 */
static const struct file_operations key_fops = {.owner = THIS_MODULE,.open = irq_open,.read = irq_read,.poll = irq_poll, // 详见1.3.4
};/* 中断处理函数 */
static irqreturn_t key0_handler(int irq, void *dev_id){struct irq *dev = (struct irq*)dev_id;int current_level; // 保存中断时的电平current_level = gpio_get_value(dev->irqkey[0].gpio);atomic_set(&dev->keyvalue, current_level); // 保存基准电平// tasklet_schedule(&dev->irqkey[0].tasklet); // 调度tasklet处理后续逻辑,避免中断耗时schedule_work(&dev->irqkey[0].work);return IRQ_HANDLED;
}/* 定时器处理函数 */
static void timer_func(unsigned long arg){struct irq* dev = (struct irq*)arg;int current_value; // 保存定时器延时后的电平/* 读取当前按键电平 */current_value = gpio_get_value(dev->irqkey[0].gpio);/* 若当前电平与中断发生时读取到的电平一致,则有效 */if (current_value == atomic_read(&dev->keyvalue)) {if (current_value == 1) { // 释放printk("KEY0 release!\r\n");atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效atomic_set(&dev->releasekey, 1); // 表示已释放,此时用户态可读} else { // 按下printk("KEY0 Push!\r\n");atomic_set(&dev->keyvalue, current_value); // 这里0 表示按下// atomic_set(&dev->releasekey, 0); // 表示未释放}}// 唤醒进程if(atomic_read(&dev->releasekey)){wake_up(&dev->r_wait);}
}/* 工作队列处理函数 */
static void key_work(struct work_struct *work_){printk(" key_work\r\n");key_irq.timer.data = (unsigned long)&key_irq;mod_timer(&key_irq.timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */// struct irq* dev = container_of(work_, struct irq, work);
}/* 初始化按键 */
static int keyio_init(struct irq *dev){int ret = 0;int i = 0;/* 1. 按键初始化 */dev->nd = of_find_node_by_path("/key"); // 获取设备节点if(dev->nd == NULL){ret = -EINVAL;goto fail_nd;}for(i=0; i<KEY_NUM; i++){dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 获取设备树中 gpioif (dev->irqkey[i].gpio < 0) {pr_err("get gpio %d failed\n", i);ret = -EINVAL;goto fail_nd;}}for(i=0; i<KEY_NUM; i++){memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));sprintf(dev->irqkey[i].name, "KEY%d", i); // 命名ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申请gpioif (ret) {pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);goto fail_gpio_req;}gpio_direction_input(dev->irqkey[i].gpio); // 设置io方向dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 获取中断号if (dev->irqkey[i].irqnum < 0) {pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);ret = dev->irqkey[i].irqnum;goto fail_gpio_req;}}dev->irqkey[0].handler = key0_handler; // 中断处理函数dev->irqkey[0].value = KEY0_VALUE; // 例如:1 表示 release(高电平),0 表示按下(低电平)/* 2. 中断初始化 */for(i=0; i<KEY_NUM; i++){ret = request_irq(dev->irqkey[i].irqnum,dev->irqkey[i].handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,dev->irqkey[i].name,&key_irq); /* dev_id 传入结构体指针 */if(ret < 0){printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);while (--i >= 0) {free_irq(dev->irqkey[i].irqnum, &key_irq);}goto fail_irq;}INIT_WORK(&dev->irqkey[i].work, key_work);}return 0;fail_irq:
fail_gpio_req:for (i = 0; i < KEY_NUM; i++) {if (gpio_is_valid(dev->irqkey[i].gpio))gpio_free(dev->irqkey[i].gpio);}
fail_nd:return ret;
}/* 驱动入口 */
static int __init key__init(void){int ret = 0;/* 1. 注册字符设备驱动 */key_irq.devid = 0;alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);key_irq.major = MAJOR(key_irq.devid);key_irq.minor = MINOR(key_irq.devid);/* 2. 初始化 cdev */key_irq.cdev.owner = THIS_MODULE;cdev_init(&key_irq.cdev, &key_fops);/* 3. 添加 cdev */ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);if (ret) {pr_err("cdev_add failed\n");goto fail_cdev;}/* 4. 创建类 */key_irq.class = class_create(THIS_MODULE, DEV_NAME);if(IS_ERR(key_irq.class)){ret = PTR_ERR(key_irq.class);goto fail_class;}/* 5. 创建设备 */key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);if(IS_ERR(key_irq.device)){ret = PTR_ERR(key_irq.device);goto fail_device;}/* 6. 初始化IO*/ret = keyio_init(&key_irq);if(ret < 0){goto fail_keyinit;}/* 7. 初始化定时器 */init_timer(&key_irq.timer);key_irq.timer.function = timer_func;key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向设备结构体 *//* 8. 初始化原子变量 */atomic_set(&key_irq.keyvalue, INVAKEY);atomic_set(&key_irq.releasekey, 0);/* 9.初始化等待队列头 */init_waitqueue_head(&key_irq.r_wait);return 0;fail_keyinit:device_destroy(key_irq.class, key_irq.devid);
fail_device:class_destroy(key_irq.class);
fail_class:cdev_del(&key_irq.cdev);
fail_cdev:unregister_chrdev_region(key_irq.devid, DEV_CNT);return ret;
}/* 驱动出口 */
static void __exit key_exit(void){int i=0;/* 释放中断 */for(i=0; i<KEY_NUM; i++){free_irq(key_irq.irqkey[i].irqnum, &key_irq);}/* 释放io */for(i=0; i<KEY_NUM; i++){if (gpio_is_valid(key_irq.irqkey[i].gpio))gpio_free(key_irq.irqkey[i].gpio);}/* 删除定时器 */del_timer_sync(&key_irq.timer);/* 注销字符设备驱动 */cdev_del(&key_irq.cdev);unregister_chrdev_region(key_irq.devid, DEV_CNT);device_destroy(key_irq.class, key_irq.devid);class_destroy(key_irq.class);
}module_init(key__init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
2.2.4 用户应用程序
2.2.4.1 使用select实现非阻塞
代码过程:
- 用户态应用程序调用select。内核会检查文件描述符fd是否就绪(是否有按键事件)
- 如果不就绪,内核将进程加入等待队列r_wait并挂起(TASK_INTERRUPTIBLE状态),CPU会去执行其他任务。
- 当内核事件发生时:(按键中断)
中断处理函数key0_handler调度工作队列schedule_work
工作队列处理函数key_work启动定时器mod_timer
执行定时器处理函数timer_func
内核唤醒用户进程,select返回,用户进程执行read读取数据
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/time.h>
#include<fcntl.h>
#include<sys/select.h>/* * @description : main主程序 * @param - argc : argv数组元素个数 * @param - argv : 具体参数 * @return : 0 成功; else失败* 调用 ./irqAPP /dev/timer*/ #define LEDOFF 0
#define LEDON 1int main(int argc, char *argv[]){if(argc != 2){ // 判断用法是否错误printf("Error Usage!\r\n");return -1;}char *filename;int fd = 0, ret = 0;unsigned char data;fd_set readfds; // 监视指定描述符集的读变化 详见1.3.1.2struct timeval timeout; // 超时时间 详见1.3.1.3filename = argv[1];fd = open(filename, O_RDWR | O_NONBLOCK); // 非阻塞方式打开if(fd <0){printf("file %s open failed!\r\n");return -1;}while(1) {// 构建fds// timeout.tv_sec = 0; // 需要#include <sys/time.h>// timeout.tv_usec = 500000; // 设置超时时间500mstimeout.tv_sec = 1; // 设置超时时间1stimeout.tv_usec = 0; // FD_ZERO(&readfds); // 清零FD_SET(fd, &readfds); // 要监视的文件描述符的集合ret = select(fd+1, &readfds, NULL, NULL, &timeout);//参数为: nfds readfds writefds exceptfds timeoutswitch(ret){ // 返回参数详见1.3.1.4case 0: // 超时printf("select timeout!\r\n");break;case -1:// 错误break;default:// 可以读取数据if(FD_ISSET(fd,&readfds)){ // 如果是fd文件描述符,则调用readret = read(fd, &data, sizeof(data)); // 与阻塞式read部分代码一致if (ret > 0) {printf("key value = %#x\r\n", data);}}break;}}printf("App finished!\r\n");close(fd);return 0;
}
测试
# VSCODE
make
arm-linux-gnueabihf-gcc irqAPP.c -o irqAPP
sudo cp noblockio.ko irqAPP /.../linux/nfs/rootfs/lib/modules/4.1.15/# 串口
rmmod blockio.ko 首先卸载之前的阻塞式代码ps
kill -9 XXX # 关掉之前阻塞式的应用程序/dev/irqAPPmodprobe noblockio.ko
./irqAPP /dev/time
可以看到基本没有CPU占用;如果没有动作,会每秒触发一次timeout;按键能正常触发
2.2.4.2 使用poll函数实现非阻塞
前面提到,select函数能够监视的文件描述符数量有限制。我们还可以使用poll函数。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/time.h>
#include<fcntl.h>
#include<sys/select.h>
#include<poll.h>
#include<signal.h>/* * @description : main主程序 * @param - argc : argv数组元素个数 * @param - argv : 具体参数 * @return : 0 成功; else失败* 调用 ./irqAPP /dev/timer*/ #define LEDOFF 0
#define LEDON 1int main(int argc, char *argv[]){if(argc != 2){ // 判断用法是否错误printf("Error Usage!\r\n");return -1;}char *filename;int fd = 0, ret = 0;unsigned char data;// fd_set readfds;struct pollfd fds;struct timeval timeout; // 超时时间 详见1.3.1.3filename = argv[1];fd = open(filename, O_RDWR | O_NONBLOCK); // 非阻塞方式打开if(fd <0){printf("file %s open failed!\r\n");return -1;}while(1) {fds.fd = fd; // 文件描述符fds.events = POLLIN; // 监视的事件:有数据可以读取时触发ret = poll(&fds, 1, 500); //参数:要监视的文件描述符集合以及要监视的事件,函数要监视的文件描述符数量,500ms超时if(ret==0){ // 超时}else if(ret < 0){ // 错误}else{ // 正常if(fds.revents | POLLIN){ // 可读取ret=read(fd, &data, sizeof(data));if(ret < 0){}else{if(data){printf("key value = %#x\r\n",data);}}}}}printf("App finished!\r\n");close(fd);return 0;
}
测试
驱动程序和select使用的驱动完全一致。直接挂载驱动并执行应用程序:
# VSCODE
make
arm-linux-gnueabihf-gcc irqAPP.c -o irqAPP
sudo cp noblockio.ko irqAPP /.../linux/nfs/rootfs/lib/modules/4.1.15/# 串口
rmmod blockio.ko 首先卸载之前的阻塞式代码ps
kill -9 XXX # 关掉之前阻塞式的应用程序/dev/irqAPPmodprobe noblockio.ko
./irqAPP /dev/time