第六课 阻塞与非阻塞I/O

阻塞和非阻塞I/O是用户空间对设备访问的两种方式,驱动程序应支持这两种方式。

阻塞操作指在执行设备操作时,若不能获得资源,则挂起该进程直至资源可获得后,再唤醒该进程去操作资源。

非阻塞操作指在执行设备操作时,若不能获得资源,则该进程要么放弃进行设备操作,要么不停查询直至资源可获得,该进程不会被挂起。

驱动程序需要完成的工作:

  1. 当用户程序以阻塞方式进行read()write()等系统调用时,若资源不可获得,驱动程序应在设备驱动的xxx_read()xxx_write()等操作中将进程阻塞,直到资源可以获取时再唤醒该进程。
  2. 当用户程序以非阻塞方式进行read()write()等系统调用时,若资源不可获得,驱动程序的xxx_read()xxx_write()等操作应立即返回-EAGAIN

等待队列

在Linux驱动程序中,可使用等待队列实现阻塞进程的唤醒。

等待队列以队列为基础数据结构,与进程调度机制紧密结合,可用来同步对系统资源的访问,如信号量就是依赖等待队列实现的。

等待队列的相关操作:

/* 定义等待队列头部 */
wait_queue_head_t my_queue;
/* 初始化等待队列头部 */
init_waitqueue_head(&my_queue);
/* 定义并初始化名为name的等待队列头部 */
DECLARE_WAIT_QUEUE_HEAD(name);

/* 定义并初始化名为name的等待队列元素 */
DECLARE_WAITQUEUE(name, tsk);/* tsk为当前任务的指针,如current */
/* 添加或移除等待队列元素 */
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);/* 添加等待队列元素wait至等待队列头部q所指向的双向链表中 */
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);/* 从等待队列头部q所指向的双向链表中移除等待队列元素wait */

/* 等待事件 */
/* 等待以queue作为等待队列头部的队列被唤醒,queue为等待队列头部,condition条件为true时该函数会直接返回 */
/* 应与wake_up函数成对使用 */
wait_event(queue, condition); /* 无法被信号打断 */
wait_event_timeout(queue, condition, timeout); /* 允许超时返回,超时时间以jiffy为单位 */
/* 应与wake_up_interruptible函数成对使用 */
wait_event_interruptible(queue, condition); /* 可被信号打断 */
wait_event_interruptible_timeout(queue, condition, timeout);

/* 唤醒队列 */
/* 可唤醒以queue作为等待队列头部的队列中所有的进程 */
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

/* 在等待队列上睡眠 */
/* 将当前进程的状态设置为TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,再将该元素挂到等待队列头部q指向的双向链表,然后将当前进程调度出去,直至被信号或wake_up函数唤醒 */
sleep_on(wait_queue_head_t *q);
/* 将当前进程的状态设置为TASK_INTERRUPTIBLE,并定义一个等待队列元素,再将该元素挂到等待队列头部q指向的双向链表,然后将当前进程调度出去,直至被wake_up_interruptible函数唤醒 */
interruptible_sleep_on(wait_queue_head_t *q);

等待队列头部wait_queue_head_t、等待队列元素wait_queue_t和任务task_struct之间的关系如下:
在这里插入图片描述
宏定义wait_event(queue, condition)展开如下:
prepare_to_wait函数将等待队列元素__wait加入到等待队列头部wq指向的双向链表中,并将当前任务current状态置为TASK_UNINTERRUPTIBLE。当条件condition仍不满足时就将当前任务调度出去。

#define wait_event(wq, condition) 					\
do {									\
	if (condition)	 						\
		break;							\
    do {									\
        wait_queue_t __wait = {						\
            .private	= current,				\
            .func		= autoremove_wake_function,				\
            .task_list	= {&__wait.task_list, &__wait.task_list},	\
        }
                                        \
        for (;;) {							\
            prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);	\
            if (condition)						\
                break;						\
            schedule();						\
        }								\
        finish_wait(&wq, &__wait);					\
    } while (0);
} while (0);

由此可以看出,等待队列最基本的用法是:

  1. 定义等待队列头部wait_queue_head_t
  2. 定义等待队列元素wait_queue_t,并将当前任务挂到该元素上,设置该元素的回调函数func
  3. 将等待队列元素添加到等待队列头部,可使用add_wait_queue函数
  4. 设置当前任务的状态set_current_state(state)
  5. 调用schedule()函数调度其他任务执行
  6. 等待被唤醒
  7. 将等待队列元素从等待队列头部指向的双向链表中移除
  8. 设置当前任务状态为TASK_RUNNING

轮询操作

使用非阻塞I/O的应用程序通常会使用select()poll()系统调用查询是否可对设备进行无阻塞的访问。这两个系统调用最终会使设备驱动中的poll()函数被执行。

select()

该系统调用是应用程序中使用最广泛的,原型为:

int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//numfds:需要检查的号码最高的fd加1
//readfds:被监视的读文件描述符
//writefds:被监视的写文件描述符
//exceptfds:被监视的异常处理文件描述符
//timeout:超时时间
struct timeval{
    int tv_sec;/* 秒 */
    int tv_usec;/* 毫秒 */
};

任何一个文件满足要求时,select()直接返回。否则调用select()的进程阻塞且睡眠。

select()底层调用驱动的poll接口,阻塞的进程会被挂到驱动的等待队列上。

timeout参数是一个指向struct timeval类型的指针,可以使select()在等待timeout时间后若仍然没有文件描述符准备好就超时返回。

对文件描述符集合的各种操作:

FD_ZERO(fd_set *set);/* 清除一个文件描述符集合 */
FD_SET(int fd, fd_set *set);/* 将一个文件描述符加到集合中 */
FD_CLR(int fd, fd_set *set);/* 将一个文件描述符从集合中删除 */
FD_ISSET(int fd, fd_set *set);/* 判断文件描述符是否被置位 */

poll()

该函数功能和实现原理与select()相似。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

select()poll()适用于文件数量较少的场景。当多路复用的文件数量庞大、I/O流量频繁的时候,不适合使用select()poll(),它们的效率会下降。此时应该使用epoll(),它不会随着文件数量增加而降低效率。

epoll()

epoll()相关的用户空间编程接口:

/*
创建一个epoll句柄,size用于告诉内核需要监听多少个fd。
创建好epoll句柄后,句柄本身也会占用一个fd,因此需要使用close关闭该fd。
*/
int epoll_create(int size);

/*
告诉内核要监听什么类型的事件。
第1个参数是epoll句柄,即epoll_create函数的返回值。
第2个参数表示动作:
    EPOLL_CTL_ADD:注册新的fd到epfd中
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件
    EPOLL_CTL_DEL:从epfd中删除一个fd
第3个参数是需要监听的fd。
第4个参数是告诉内核需要监听的事件类型。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event{
    __uint32_t events;/* Epoll events */
    epoll_data_t data;/* User data variable */
};

/*
等待事件的产生。
events参数是输出参数,用来从内核得到事件的集合。
maxevents告诉内核本次最多收多少个事件,该值不能大于epoll_create时的size。
timeout是超时时间,单位是毫秒,0表示立即返回,-1表示永久等待。
函数返回值表示需要处理的事件数目,返回0表示超时。
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

事件类型events可以是以下几个宏的或:

  • EPOLLIN:表示对应的文件描述符可以读
  • EPOLLOUT:表示对应的文件描述符可以写
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(socket带外数据到来)
  • EPOLLERR:表示对应的文件描述符发生错误
  • EPOLLHUP:表示对应的文件描述符被挂断
  • EPOLLET:并将epoll设为边缘触发模式(ET),该模式属于高速工作方式,当fd从未就绪变为就绪时,内核通过epoll告诉用户,然后它会假设用户知道fd已经就绪,并且不会再为那个fd发送更多的就绪通知;默认模式是水平触发模式(LT),该模式下内核告诉用户一个fd是否就绪,如果用户不进行任何操作,该事件也不会丢失。
  • EPOLLONESHOT:一次性监听,即监听完这次事件之后,若还需要继续监听这个fd,需要再次把这个fd加入到epoll队列里。

设备驱动中的轮询编程

设备驱动中poll函数原型:

/*
第1个参数为file结构体指针
第2个参数为轮询表指针
该函数需要完成两项工作:
    1、对可能引起设备文件状态变化的等待队列调用poll_wait函数,将对应的等待队列头部添加到poll_table中
    2、返回表示是否能对设备进行无阻塞读、写访问的掩码
*/
unsigned int (*poll)(struct file *filp, struct poll_table *wait);

用于向poll_table注册等待队列的关键poll_wait函数原型如下:

void poll_wait(struct file *filp, wait_queue_head_t *queue, struct poll_table *wait);

该函数不会引起阻塞,只是把当前进程添加到wait参数指定的等待列表,实际作用是让唤醒参数queue对应的等待队列可以唤醒因select()而睡眠的进程。

驱动程序poll函数应该返回设备资源的可获取状态,即:

  • POLLIN:设备可无阻塞地读
  • POLLOUT:设备可无阻塞地写
  • POLLPRI
  • POLLERR
  • POLLNVAL

在设备驱动中,阻塞I/O一般基于等待队列或者基于等待队列的其他内核API来实现。等待队列可用于同步驱动中事件发生的先后顺序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,它可以在服务器端运行 JavaScript 代码。Node.js 的高效性和阻塞 I/O 机制使其成为一个很好的爬虫系统开发工具。 以下是基于 Node.js 的爬虫系统设计与应用的步骤: 1. 确定需求:确定需要爬取的数据类型和来源网站。 2. 分析网站结构:分析目标网站的页面结构,确定需要爬取的内容位置和标签。 3. 编写爬虫代码:使用 Node.js 编写爬虫代码,利用 HTTP 请求模块获取网站数据,并使用 DOM 解析模块解析数据。 4. 存储数据:将爬取到的数据存储到数据库或文件中,以备后续使用。 5. 处理异常:对于无法获取的网站或异常数据进行处理,避免程序崩溃。 6. 自动化部署:将爬虫系统部署到服务器上,并设置定时任务自动运行。 Node.js 的优势在于它的异步编程模型和事件驱动机制,这使得它可以高效地处理大量并发请求。同时,Node.js 的生态系统常丰富,有大量的第三方模块可供使用,如请求模块 request、DOM 解析模块 cheerio 等。 在应用方面,Node.js 的爬虫系统可以应用于各种数据采集场景,如新闻、商品、论坛等,同时也可以用于数据分析和挖掘。例如,可以使用 Node.js 爬虫系统获取某一领域的论文信息,并进行关键词提取和分类分析,以帮助研究人员更好地掌握该领域的发展动态。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值