文章目录
- 前言
- 阻塞
- 非阻塞
- 异步通知
- 后续
前言
首先来回顾一下“中断”,中断是处理器提供的一种异步机制,我们配置好中断以后就 可以让处理器去处理其他的事情了,当中断发生以后会触发我们事先设置好的中断服务函数, 在中断服务函数中做具体的处理。同样的,Linux 应用程序可以通过阻塞或者非阻塞这两种方式来访问驱动设备,通过阻塞方式访问的话应用程序会处于休眠态,等待驱动设备可以使用,非阻塞方式的话会通过 poll 函数来不断的轮询,查看驱动设备文件是否可以使用。这两种方式都需要应用程序主动的去查询设备的使用情况;“信号”为此应运而生,信号类似于我们硬件上使用的“中断”,只不过信号是软件层次上的。算是在软件层次上对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据了,这就是异步通行,接下来我将逐一分析阻塞,非阻塞和异步通知三种访问模式。
阻塞
原理:应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的 时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给 应用程序。
特点:当设备文件不可操作的时候进程可以进入休眠态,这样可以将 CPU 资源让出来。
使用方法:Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,先创建并初始化一个等待队列------>每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面,添加到等待队列头中以后进程才能进入休眠态------>当设备可以使用的时候就要唤醒进入休眠态的进程,一般在中断函数里面完成唤醒工作。
API函数:API详细介绍肯定没有百度自行搜索详细,这里只列举一些API函数。
- 等待队列头初始化:
void init_waitqueue_head(wait_queue_head_t *q)
- 定义并初始化一个等待队列项:
DECLARE_WAITQUEUE(name, tsk)
- 将队列项添加/移除等待队列头:
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)
- 等待唤醒(手动唤醒):
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
- 等待唤醒(事件唤醒):
非阻塞
原理:应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或 数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。
特点:需要应用程序通过 poll 函数不断的轮询。
使用方法:应用程序通过 select、epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。
API函数:
- select函数:在单个线程中,select 函数能够监视的文件描述符数量有最大的限制,一般为 1024。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
- poll函数:poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制。
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
- epoll函数:epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。
int epoll_create(int size)//size从 Linux2.6.8 开始此参数已经没有意义了,随便填写一个大于 0 的值就可以。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
- 驱动程序 poll 函数:当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序 file_operations 操作集中的 poll 函数就会执行。
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
驱动程序的 poll 函数中调用 poll_wait 函数,poll_wait 函数不会引起阻塞,只是 将应用程序添加到 poll_table 中
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
异步通知
原理:异步通知的核心就是信号,在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所支 持的所有信号:“kill -9 PID”杀死指定进程的方法就是向指定的进程(PID)发送 SIGKILL 这个信号。当按下键盘上的 CTRL+C 组合键以后会向当前正在占用终端的应用程序发 出 SIGINT 信号,SIGINT 信号默认的动作是关闭当前应用程序。
特点:驱动通过主动向应用程序发送信号来报告自己可以访问,而不是通过应用程序来访问自己。
使用方法:如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针,当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变 fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。kill_fasync 函数负责发送指定的信号,应用程序收到信号后就会执行对应的中断回调函数;在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct。
后续
三种访问方式都有对应的例程,可以参考链接:https://github.com/NUAATRY/imx6ull_dev 中的实验12-14。