欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux阻塞与非阻塞IO

程序员文章站 2022-04-18 10:12:54
...

Linux阻塞与非阻塞IO

当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式IO就会将应用程序对应的线程挂起,直到设备资源可以才做为止。对于非阻塞IO,应用程序对应的线程不会挂起,它要么一直轮询等待, 直到设备资源可以使用,要么就直接放弃。

阻塞IO示意图

Linux阻塞与非阻塞IO

非阻塞IO示意图

Linux阻塞与非阻塞IO

等待队列

阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来,那么当设备文件可以操作的时候要怎么才能唤醒线程呢?
Linux内核提供了等待队列机制。

Linux内核的等待队列是以双循环链表为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。它有两种数据结构:等待队列头(wait_queue_head_t)和等待队列项(wait_queue_t)。
Linux阻塞与非阻塞IO

等待队列头

等待队列头使用结构体wait_queue_head_t 表示, wait_queue_head_t 结构体定义在文件include/linux/wait.h 中,结构体内容如下:

struct __wait_queue_head {
      spinlock_t lock;                    /* 保护等待队列的原子锁
                                        (自旋锁),在对task_list与操作的过程中,使用该锁实现对等待队列的互斥访问*/
      struct list_head task_list;          /* 等待队列,双向循环链表,存放等待的进程 */
};


等待队列项

等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体wait_queue_t表示等待队列项,结构体内容如下:

/*__wait_queue,该结构是对一个等待任务的抽象。每个等待任务都会抽象成一个wait_queue,
并且挂载到wait_queue_head上。该结构定义如下:*/

struct __wait_queue {
 unsigned int flags;
#define WQ_FLAG_EXCLUSIVE   0x01
 void *private;                       /* 通常指向当前任务控制块 */
 wait_queue_func_t func;             
 struct list_head task_list;              /* 挂入wait_queue_head的挂载点 */
};
typedef struct __wait_queue wait_queue_t;
/* 任务唤醒操作方法,该方法在内核中提供,通常为auto remove_wake_function */
等待队列相关操作
  1. 定义并初始化等待队列头
wait_queue_head_t my_queue;  
init_waitqueue_head(&my_queue);  //会将自旋锁初始化为未锁,等待队列初始化为空的双向循环链表。
//宏名用于定义并初始化,相当于"快捷方式"
DECLARE_WAIT_QUEUE_HEAD (my_queue);  
  1. 定义一个等待队列项
DECLARE_WAITQUEUE(name, tsk);

name 就是等待队列项的名字, tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current,在Linux内核中current相当于一个全局变量。表示当前进程。因此宏DECLARE_WAITQUEUE就是给当前正在运行的进程创建并初始化了一个等待队列项。

  1. 从等待队列头中添加或删除等待队列项
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. 等待唤醒

当设备可以使用的时候就要唤醒进入休眠状态的进程。

void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)

这两个函数的区别在于:wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程

5.等待事件

处于休眠中的进程,除了主动唤醒,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程。

wait_event(queue,condition);//等待以queue为等待队列头等待队列被唤醒,condition必须满足,否则阻塞  
wait_event_interruptible(queue,condition);//可被信号打断  
wait_event_timeout(queue,condition,timeout);//阻塞等待的超时时间,时间到了,不论condition是否满足,都要返回  
wait_event_interruptible_timeout(queue,condition,timeout) 

非阻塞的处理方式——轮询

poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。

应用程序下的轮询函数
select函数
int select(int nfds,
           fd_set *readfds,
           fd_set *writefds,
           fd_set *exceptfds,
           struct timeval *timeout)

readfds、writefds、exiceptfds这三个指针指向文件描述符的集合,都是fd_set类型。
fd_set的每一个位都代表一个文件描述符。readfds用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么seclect就会返回一个大于 0 的值表示文件可以读取。

fd_set的操作宏

void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)

timeout表示轮询时间片,多长时间轮询一次

struct timeval结构体

struct timeval {
                long tv_sec; /* 秒 */
                long tv_usec; /* 微妙 */
                }
poll函数

select函数能够监视的文件描述符有最大数量限制,一般为1024,并且监控的文件描述符越多效率越低。这时可以使用poll函数

int poll(struct pollfd *fds,
        nfds_t nfds,    /*文件描述符的个数*/
        int timeout)    /*超时时间单位ms*/
        
        
struct pollfd
{
    int fd;  //文件描述符
    short events;  //注册的事件,要监视的事件
    short revents  //实际发生的事件,由内核填充
}

events代表要监视的事件,可以有以下几种,用宏定义表示

宏名 说明
POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN
epoll函数

select函数和poll函数都会随着要监视的fd数量的增加而效率降低,而且poll函数需要遍历所有的文件描述符检测就绪的描述符。epoll函数就为并发而生的

Linux 驱动下的 poll 操作函数

当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations 操作集中的 poll 函数就会执行。所以驱动程序的编写者需要提供驱对应的 poll 函数

unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)

这个struct poll_table_struct 结构体是用户空间传来的一般是给 poll_wait的参数

返回值 描述
POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN,普通数据可读

在驱动程序的 poll函数中调用poll_wait函数,poll_wait函数不会引起阻塞,只是将应用程序添加到 poll_table中

void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
相关标签: 设备驱动