平平酱紫
阻塞
和非阻塞
IO 是 Linux 驱动开发里面很常见的两种设备访问模式,在编写驱动的时候=要考虑到阻塞和非阻塞。
简介
阻塞和非阻塞
谁阻塞呢?答案是 IO (Input/Output)。
这是用户空间的应用程序与内核空间的驱动程序下的设备之间信息交流的通道。
一般编写驱动代码都是使用阻塞方式,这样进入休眠态以后可以节省CPU资源。
阻塞 IO
- 应用程序向设备请求数据却发现设备不让用。
- 程序(进程)耐心地等待直到设备可用(休眠状态)。
- 应用程序终于获得了数据。
示例代码:
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
非阻塞 IO
- 应用程序向设备请求数据发现设备不让用。
- 收到了返回地错误码。
- 非常不耐心地多次请求数据。
- 直到获得了数据才罢休/直接就放弃了。
示例代码:
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
O_NONBLOCK 表示非阻塞方式打开
阻塞访问—等待队列
等待队列由两部分组成:等待队列头
和等待队列项
。
主要用于常用地阻塞访问
。
头和项
- 等待队列头
位于队列的头部,文件 include/linux/wait.h 中结构体wait_queue_head_t 表示:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
定义+初始化
方法一:
wait_queue_head_t q
void init_waitqueue_head(wait_queue_head_t *q)
参数 q 就是要初始化的等待队列头。
方法二:
DECLARE_WAIT_QUEUE_HEAD(name)
- 等待队列项
一个队列由一个个队列项
组成,我们要做的就是将某些需要地队列项放到队列头。结构体 wait_queue_t 表示。
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
定义+初始化
DECLARE_WAITQUEUE(name, tsk)
name 就是等待队列项的名字,
tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current
在 Linux 内 核 中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。
将队列项添加/移除等待队列头
设备不可访问时—等待队列项添加到等待队列头中—进程休眠;
设备可访问时—将等待队列项从等待队列头中移除。
- 添加头
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
q: 等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
返回值:无。
- 移除头
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
q: 要删除的等待队列项所处的等待队列头。
wait:要删除的等待队列项。
返回值:无。
唤醒进程
设备可访问时—唤醒休眠态的进程。
- 手动唤醒函数
唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程
void wake_up(wait_queue_head_t *q)
或
只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程
void wake_up_interruptible(wait_queue_head_t *q)
- 触发某事件自动唤醒
相关API函数
非阻塞访问—轮询
应用程序查询设备通过select、 epoll 或 poll 函数
。
主要用于非阻塞方式
。
- select 函数
用于监视设备驱动文件的文件描述符,最大1024个。
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout)
nfds: 其中的最大文件描述符加 1,感觉文件描述符本质就是数字。
readfds 用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取
writefs 用于监视这些文件是否可以进行写操作
exceptfds 用于监视这些文件的异常
timeout 超时时间
返回值:
0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作;
-1,发生错误;
其他值,可以进行操作的文件描述符个数。
一些说明:
fd_set:文件描述符集
fd:文件描述符
void FD_ZERO(fd_set *set) //将 fd_set 变量的所有位都清零
void FD_SET(int fd, fd_set *set)//将 fd_set 变量的某个位置 1,也就是向 fd_set 添加一个文件描述符
void FD_CLR(int fd, fd_set *set)//将 fd_set变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除
int FD_ISSET(int fd, fd_set *set)//测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符
结构体struct timeval
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
};
select 函数读部分代码示例
void main(void)
{
......
fd_set readfds; /* 读操作文件描述符集 */
fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
FD_ZERO(&readfds); /* 清除 readfds */
FD_SET(fd, &readfds); /* 将 fd 添加到 readfds 里面 */
......
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
switch(ret)
{
case 0: /* 超时 */
...
break;
case -1: /* 错误 */
...
break;
default:
if(FD_ISSET(fd, &readfds)) /* 判断是否为 fd 文件描述符 */
{
/* 使用 read 函数读取数据 */
}
break;
}
}
- poll 函数
poll 函数本质上和 select 没有太大的差别, select 有最大描述符限制,poll 就是没有最大文件描述符限制。
int poll(struct pollfd *fds,
nfds_t nfds,
int timeout)
fds: 要监视的文件描述符集合以及要监视的事件,为一个数组
nfds: poll 函数要监视的文件描述符数量。
timeout: 超时时间,单位为 ms。
返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述符数量;
0,超时;
-1,发生错误,并且设置 errno 为错误类型。
一些说明
pollfd 结构体
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};
fd 是要监视的文件描述符,如果 fd 无效的话那么 events 监视事件也就无效,并且 revents返回 0。
events 是要监视的事件
POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN
revents 是返回参数,也就是返回的事件
== poll 函数读非阻塞访问部分代码示例==
void main(void)
{
...
struct pollfd fds;
fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
/* 构造结构体 */
fds.fd = fd;
fds.events = POLLIN; /* 监视数据是否可以读取 */
ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
switch(ret)
{
...
}
}
- epoll 函数
fd 数量大时,selcet 和 poll 函数都会效率低。而epoll 就是为处理大并发而准备的,常常应用于网络编程。
接下来学学怎么用。
1.epoll_create 函数创建一个 epoll 句柄
int epoll_create(int size)
size: 从 Linux2.6.8 开始此参数已经没有意义了,随便填写一个大于 0 的值就可以。
返回值: epoll 句柄,如果为-1 的话表示创建失败。
2.epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事
件
int epoll_ctl(int epfd,
int op,
int fd,
struct epoll_event *event)
epfd: 要操作的 epoll 句柄
op: 表示要对 epfd(epoll 句柄)进行的操作
EPOLL_CTL_ADD 向 epfd 添加文件参数 fd 表示的描述符。
EPOLL_CTL_MOD 修改参数 fd 的 event 事件。
EPOLL_CTL_DEL 从 epfd 中删除 fd 描述符。
fd:要监视的文件描述符
event: 要监视的事件类型
返回值: 0,成功; -1,失败,并且设置 errno 的值为相应的错误码。
其中
struct epoll_event {
uint32_t events; /* epoll 事件 */
epoll_data_t data; /* 用户数据 */
};
events 成员变量表示要监视的事件
EPOLLIN 有数据可以读取。
EPOLLOUT 可以写数据。
EPOLLPRI 有紧急的数据需要读取。
EPOLLERR 指定的文件描述符发生错误。
EPOLLHUP 指定的文件描述符挂起。
EPOLLET 设置 epoll 为边沿触发,默认触发模式为水平触发。
EPOLLONESHOT 一次性的监视,当监视完成以后还需要再次监视某个 fd,那么就需要将fd 重新添加到 epoll 里面。
可以进行“或”操作,也就是说可以设置监视多个事件。
3.epoll_wait 函数来等待事件的发生
int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout)
epfd: 要等待的 epoll。
events: 指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events,调用者可以根据 events 判断发生了哪些事件。
maxevents: events 数组大小,必须大于 0。
timeout: 超时时间,单位为 ms。
返回值: 0,超时; -1,错误;其他值,准备就绪的文件描述符数量。
Linux 驱动下的 poll 操作函数
前边儿写的都是应用程序中的poll函数,接下来了解驱动程序中的poll函数。
poll 函数原型
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
filp: 要打开的设备文件(文件描述符)。
wait: 结构体 poll_table_struct 类型指针, 由应用程序传递进来的。一般将此参数传递给poll_wait 函数。
返回值:向应用程序返回设备或者资源状态
poll 函数中调用 poll_wait 函数,且不会引起阻塞,只是将应用程序添加到 poll_table队列
中。
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
wait_address 是要添加到 poll_table 中的等待队列头,
p 就是 poll_table,就是file_operations 中 poll 函数的 wait 参数。
阻塞 IO 实验
之前的中断实验使用top 命令
查看应用程序的 CPU 使用率。
哇!这么高,这是因为 while 循环中通过 read 函数一直读取按键值,软件一直运行而非休眠。
接下来就是用阻塞的方法将应用程序休眠,降低CPU的使用率。
驱动程序部分代码
...
/* 中断 IO 描述结构体 */
struct irq_keydesc {
...
};
/* imx6uirq 设备结构体 */
struct imx6uirq_dev{
...
wait_queue_head_t r_wait; /* 读等待队列头 */
};
struct imx6uirq_dev imx6uirq; /* irq 设备 */
/*中断服务函数 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
...
}
/*定时器服务函数 */
void timer_function(unsigned long arg)
{
...
/* 唤醒进程 */
if(atomic_read(&dev->releasekey)) { /* 完成一次按键过程 */
/* wake_up(&dev->r_wait); */
wake_up_interruptible(&dev->r_wait);
}
}
/*按键 IO 初始化 */
static int keyio_init(void)
{
...
/* 初始化等待队列头 */
init_waitqueue_head(&imx6uirq.r_wait);
...
}
/*打开设备 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
...
}
/*从设备读取数据 */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
...
DECLARE_WAITQUEUE(wait, current); /* 定义一个等待队列 */
if(atomic_read(&dev->releasekey) == 0) { /* 没有按键按下 */
add_wait_queue(&dev->r_wait, &wait); /* 添加到等待队列头 */
__set_current_state(TASK_INTERRUPTIBLE);/* 设置任务状态 */
schedule(); /* 进行一次任务切换 进入休眠态*/
********************休眠点(唤醒后从这里运行)************************
if(signal_pending(current)) { /* 判断是否为信号引起的唤醒 */
ret = -ERESTARTSYS;
goto wait_error;
}
__set_current_state(TASK_RUNNING); /*设置为运行状态 */
remove_wait_queue(&dev->r_wait, &wait); /*将等待队列移除 */
}
...
wait_error:
set_current_state(TASK_RUNNING); /* 设置任务为运行态 */
remove_wait_queue(&dev->r_wait, &wait); /* 将等待队列移除 */
return ret;
}
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
.owner = THIS_MODULE,
.open = imx6uirq_open,
.read = imx6uirq_read,
};
/*驱动入口函数 */
static int __init imx6uirq_init(void)
{
...
}
/*驱动出口函数 */
static void __exit imx6uirq_exit(void)
{
...
}
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
注意点
schedule 函数
进行一次任务切换(切换其他进程),当前进程就会进入到休眠态。如果有按键按下,那么进入休眠态的进程就会唤醒,然后接着从休眠点开始运行。- 将任务或者进程加入到等待队列头,
- 在合适的点唤醒等待队列,一般都是中断处理函数里面。
应用程序
同中断实验APP一样。
调试
depmod //第一次加载驱动的时候需要运行此命令
modprobe blockio.ko //加载驱动
./blockioApp /dev/blockio & //后台模式运行
top //查看CPU占用率极小
ps //查看进程PID
kill -9 149 //杀死blockioApp
非阻塞 IO 实验
驱动程序部分代码
...
/* 中断 IO 描述结构体 */
struct irq_keydesc {
...
};
/* imx6uirq 设备结构体 */
struct imx6uirq_dev{
...
wait_queue_head_t r_wait; /* 读等待队列头 */
};
struct imx6uirq_dev imx6uirq; /* irq 设备 */
/*中断服务函数 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
...
}
/*定时器服务函数 */
void timer_function(unsigned long arg)
{
...
/* 唤醒进程 */
if(atomic_read(&dev->releasekey)) { /* 完成一次按键过程 */
/* wake_up(&dev->r_wait); */
wake_up_interruptible(&dev->r_wait);
}
}
/*按键 IO 初始化 */
static int keyio_init(void)
{
...
/* 初始化等待队列头 */
init_waitqueue_head(&imx6uirq.r_wait);
...
}
/*打开设备 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
...
}
/*从设备读取数据 */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
...
if (filp->f_flags & O_NONBLOCK) { /* 非阻塞访问 */
if(atomic_read(&dev->releasekey) == 0) /* 没有按键按下,返回-EAGAIN */
return -EAGAIN;
} else { /* 阻塞访问 */
/* 加入等待队列,等待被唤醒,也就是有按键按下 */
ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));
if (ret) {
goto wait_error;
}
}
...
wait_error:
return ret;
}
/*poll函数,用于处理非阻塞访问 */
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
poll_wait(filp, &dev->r_wait, wait); /* 将等待队列头添加到poll_table中 */
if(atomic_read(&dev->releasekey)) { /* 按键按下 */
mask = POLLIN | POLLRDNORM; /* 返回PLLIN */
}
return mask;
}
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
.owner = THIS_MODULE,
.open = imx6uirq_open,
.read = imx6uirq_read,
.poll = imx6uirq_poll,
};
/*驱动入口函数 */
static int __init imx6uirq_init(void)
{
...
}
/*驱动出口函数 */
static void __exit imx6uirq_exit(void)
{
...
}
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
注意
当应用程序调用 select 或者 poll 函数的时候 imx6uirq_poll 函数就会执行。
感觉非阻塞访问还是有阻塞访问的味道。
应用程序代码
int main(int argc, char *argv[])
{
int fd;
int ret = 0;
char *filename;
struct pollfd fds;
fd_set readfds;
struct timeval timeout;
unsigned char data;
if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞访问 */
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}
//poll函数
#if 0
/* 构造结构体 */
fds.fd = fd;
fds.events = POLLIN;
while (1) {
ret = poll(&fds, 1, 500);
if (ret) { /* 数据有效 */
ret = read(fd, &data, sizeof(data));
if(ret < 0) {
/* 读取错误 */
} else {
if(data)
printf("key value = %d \r\n", data);
}
} else if (ret == 0) { /* 超时 */
/* 用户自定义超时处理 */
} else if (ret < 0) { /* 错误 */
/* 用户自定义错误处理 */
}
}
#endif
//select函数
while (1) {
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
/* 构造超时时间 */
timeout.tv_sec = 0;
timeout.tv_usec = 500000; /* 500ms */
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
switch (ret) {
case 0: /* 超时 */
/* 用户自定义超时处理 */
break;
case -1: /* 错误 */
/* 用户自定义错误处理 */
break;
default: /* 可以读取数据 */
if(FD_ISSET(fd, &readfds)) {
ret = read(fd, &data, sizeof(data));
if (ret < 0) {
/* 读取错误 */
} else {
if (data)
printf("key value=%d\r\n", data);
}
}
break;
}
}
close(fd);
return ret;
}
测试
同上