等待队列

先补充个基础知识――等待队列

认识

定义

wait_queue_head_t wait_queue;

初始化

init_waitqueue_head(&wait_queue);

等待

wait_event(queue, condition)   等待某个条件而进入睡眠

wait_event_interruptible(queue, condition)  等待某个条件而进入睡眠并允许信号中断睡眠

wait_event_timeout(queue, condition,timeout) 等待某个条件而进入睡眠 最多等待timeout时间

wait_event_interruptible_timeout(queue, condition,timeout)

唤醒

void wake_up(wait_queue_head_t *queue);   唤醒阻塞在该等待队列上的进程

void wake_up_interruptible(wait_queue_head_t *queue);

使用

假设你的设备驱动程序在中断中接收数据,为用户空间提供读取的操作。

你可以这样处理:

1、为简单说明,不考虑同步。

read()

{

            If(len > 0)

                    Read...

                    Return len;

    }else {

                Return 0;

    }

}

Irq_handler()

{

       Recv...

Add Len

    }

这是一种非阻塞的实现

2、

Read()

{

          If(wait_event_interruptible(wait_queue, len > 0)) {

              Return error;

}

Read...

Return len;

}

Irq_handler()

{

        recv

        Add len

        wake_up_interruptible(&wait_queue);

}

利用等待队列实现的阻塞方式,无数据会把自己放到等待队列中进入睡眠,当数据到来发生中断时,在中断中唤醒睡眠中等待队列上的进程进行处理。当然阻塞其实是和睡眠无关的,这里你无数据可以忙等,但睡眠是更优雅的方式。

进一步分析

wait_event

跟进wait_event(queue, condition)会发现他定义了一个wait_queue_t __wait {.private = current, .func = autoremove_wake_function, },然后将__wait放到了等待队列queue中,即放到了queue的task_list链表中。

接下来设置当前进程的状态为TASK_UNINTERRUPTIBLE,并调用schedule(),调度并切换到一个新的进程开始运行。

设置为TASK_UNINTERRUPTIBLE的进程,不会再被系统调度执行,会一直死在这里。到此,该进程让出了CPU不再执行,可以认为他进入了睡眠。

wake_up

跟进wake_up(queue),他其实遍历queue的task_list链表,对每个结点(wait_queue_t类型),调用其func函数。

而此时queue里面应该放着wait_event时放入的__wait,于是wake_up调用了__wait->func函数,__wait->func即autoremove_wake_function函数。

跟进autoremove_wake_function,发现函数里面调用了try_to_wake_up,其参数就是__wait中赋予的current值,这样就实现了在其他进程或中断中,唤醒之前睡眠的进程。

try_to_wake_up中的处理比较复杂,不再继续跟了,我们可以确定try_to_wake_up将之前睡眠的进程状态设为TASK_RUNING,这样之前的进程就可以继续被调度执行了,即被唤醒了。

执行完try_to_wake_up后,将__wait从queue中删除,wake_up的工作就完成了。

再次回到wait_event

之前我们知道,进程在调用schedule后就睡了,然后被其他进程或者中断wake_up唤醒了,那么进程唤醒后应该继续在schedule后继续执行。

继续跟进,schedule返回后,会首先判断条件condition是否成立,如果不成立,再次定义__wait,然后添加到等待队列,schedule睡眠。如果成立,那么wait_event执行完成,进程等待的条件满足,可以继续处理了。

wait_event_timeout

wait_event_timeout与wait_event的不同是wait_event调用的是schedule,而wait_event_timeout调用的是schedule_timeout。

schedule_timeout里面又调用了schedule,但在调用之前,他定义了一个定时器,定时器在指定的timeout超时时,调用wake_up_process,进而调用try_to_wake_up唤醒进程。也就是说wait_event_timetou除了依赖于其他进程或中断唤醒自己,本身还有个定时器可以唤醒自己。

select

我们知道select同时可以监视多个描述符,只要任一个有事件,就可以直接返回处理。如果都没有事件则select睡眠等待,并且任一一个描述符有事件就可以唤醒select。其实现是基于等待队列的。原理简单的讲就是每个描述符都对应一个等待队列,每个描述符对应的驱动都提供一个poll方法。Select调用描述符的poll方法,检查是否有事件,当没有事件时,定义一个wait_queut_t的对象,放到描述符的等待队列中。当select检查到没有事件进入睡眠后,任一个描述符有事件,执行唤醒等待队列的操作就可以唤醒select。

Select的系统调用sys_select,在fs/select.c中(linux 2.6.27内核),其调用路径为sys_select -> core_sys_select -> do_select。接下来我们看下slect系统调用的具体实现,代码比较多,只捡重点的部分看,其他细节有时间再研究。

用户空间在使用select时,会定义fd_set类型的变量,对应于不同的事件有readset、writeset、exceptset,其实他们都是unsigned long类型的数组,数组中的每一位标识一个fd,我们常用的FD_SET(fd, set),是将set中的数组的第fd位设为1。我们关心fd的那几个事件,就将相应的set的第fd位置一,传给内核,通知内核帮我监视,有情况告诉我。通过看内核对fd_set的定义,可以看出fd_set是一个1024位的数组,也就是最多支持1024个fd,如果需要支持更多的fd,需要修改代码重新编译内核了。

内核空间中,core_sys_select函数首先定义了一个long类型的数组,如果fd个数多,数组不够,他会调用kmalloc,动态申请一个数组。数组的使用分为六块,如下图所示,每块其实都是一个小的fd_set,只是fd_set是固定长度(1024位,注意是位不是字节)的数组,但这里每块的长度是和真实的fd的个数有关的。

接下来core_sys_select调用get_fd_set将用户空间传递的readset、writeset、exceptset拷贝到in、out、ex中,然后调用do_select,将这个大数组传给他。do_select通过in、out、ex里面的位标识,得到要监视哪些fd,监视哪些事件(read、write、except),将监视的结果记录到res_in、res_out、res_ex中。返回到core_sys_select,程序调用set_fd_set将res_in、res_out、res_ex中的结果,拷贝到用户空间。select系统调用返回,就获得事件处理了。

上一步提到了do_select,我们进一步研究研究他。

首先设置当前进程状态 set_current_state(TASK_INTERRUPTIBLE);(这块我还不是很了解,内核没有抢占吗,如果设置状态后,切换出去了,岂不永远都切不回来了,一是此时还没添加唤醒的处理,不会有其他进程唤醒他,二是CPU不会调度TASK_INTERRUPTIBLE状态的进程执行。那么这里是没有内核抢占还是设置了TASK_INTERRUPTILBE的进程不会没抢占?)

然后循环扫描的in、out、ex中的信息(哪些fd关心read事件、哪些fd关心write事件、哪些fd关心except事件),调用具体的fd的驱动相关的poll函数获取fd的事件的状态,根据返回的状态,将结果设置到res_in、res_out、res_ex。其实很简单,如果in中的第n位为一,标识fd=n的描述符关心read事件,在调用fd=n对应的驱动的poll之后,如果有read事件,则将res_in中的n位置一。

(cond_resched这个函数是做什么的?)

在处理完一轮后(处理完了in、out、ex中的请求),如果fd请求的事件发生了,则返回,如果都没有发生则调用schedule_timeout,进入睡眠,等待事件到来时被唤醒。

好,我们看看,do_select是怎样在有事件时被唤醒的。在这之前,我们先想想如果我们自己来做,如何利用等待队列实现。大体思路,我们应该定义一个等待队列wait_queue_head_t queue,select在没有事件时,定义一个wait_queue_t的对象wait放到queue中,然后调度schedule进入睡眠。在驱动中,当事件到来时,遍历等待在queue的wait并唤醒。其实内核实现就是这个思路,支持阻塞IO的驱动实现中,通常会定义三个等待队列,对应于read、write、except,select调用到poll中时,如果没有事件,会定义一个wait_queue_t的wait放到等待队列中,当驱动检查到事件发生时,会唤醒睡在等待队列上的进程。

接下来看看select在睡之前做了哪些准备工作,怎样将wait加入到等待队列中的。

先了解一下do_select中使用的一个数据结构

struct poll_wqueues {

  poll_table pt;

  struct poll_table_page *table;

  struct task_struct *polling_task;

  int triggered;

  int error;

  int inline_index;

  struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];

}

do_select声明了这样类型的一个对象table,然后初始化其成员polling_task = current, pt->qproc = __pollwait。

接下来在调用各fd对应的驱动的poll时,将table.pt(poll_table类型)作为参数传入。

我们知道各个驱动模块实现的各自的poll函数中,如果自己没有read、write、except事件,会调用poll_wait函数,参数wait_address是驱动中声明的等待队列,p是调用poll时传入的table.pt。以下是poll_wait的实现:

static inline void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)

{

  p->qproc(file, wait_address, p);

}

可以看到poll_wait中调用的p->qproc就是之前初始化poll_wqueue时,指定的__pollwait函数。

static void __pollwait(struct file *filp,wait_queue_head_t *wait_address, poll_table *p);

struct poll_table_entry {

  struct file *filp;

  unsigned long key;

  wait_queue_t wait;

  wait_queue_head_t *wait_address;

}

__pollwait中首先获取一个poll_table_entry类型的变量entry,获取其实是在poll_wqueue的inline_entries中拿的。然后初始化entry,entry->file = file;entry->key = p->key;entry->wait.func = pollwake,最后将entry->wait添加到等待队列wait_address中。

所有的准备工作做好了,如果没有事件产生,do_select调度schedule进入了睡眠。

唤醒一般在中断或者软中断中处理的,一般在检查到事件到来时,驱动中会调用wake_up函数,参数为驱动中定义的等待队列。

追踪wake_up函数,最终调用了__wake_up_common,在这个函数中,遍历wait_queue_head_t中的结点,每个结点是wait_queue_t类型,调用每个结点的func指针指向的函数。前面我们知道func指针指向了pollwake,pollwake最终通过调用try_wake_up唤醒了进程。

pollwake->__pollwake->default_wake_function->try_to_wake_up

wait_queue_t中记录了要被唤醒的进程的task_struct结构,因此通过以上系列调用,最终实现了睡眠进程的唤醒。

POLL

poll与select的流程基本一致,其调用路径为sys_poll->do_sys_poll->do_poll->do_pollfd

do_sys_poll将用户空间的pollfd拷贝到内核空间,初始化poll_wqueues table对象,其使用与select相同。调用do_poll,取得需监视的fd的状态,然后将状态拷贝到用户空间,返回。

do_poll与do_select类似,查询事件,没事件睡眠。只是do_poll中使用pollfd,do_select使用long类型中的每一位记录状态。

do_pollfd实现对poll的调用,然后将状态记录到pollfd中。

我们看看select与poll的不同

select使用fd_set记录要检查的描述符,该结构本身是1024位,也就限制了最多只能检测1024个描述符。

poll使用pollfd结构的数组,检测多少个描述符,就传递多大的数组就可以了。

struct pollfd {

    int fd;

    short events;

    short revents;

};

select使用的fd_set记录输入输出,每次返回后,返回的结果就把系统调用时传入的信息给覆盖掉了,因此每次调用select都需要给fd_set赋值。

poll使用pollfd结构,events记录要检测的事件,revents记录结果,pollfd初始化一次就可以了,以后每次poll调用不需要重新初始化pollfd。

不知不觉写这么多了,epoll的探究再开一片吧。

由于也是边查资料边看代码边整理,是一个学习的过程,思路有点跳跃不连贯,欢迎拍砖,接下来我会再次整理,屡屡思路。

I/O多路复用 SELECT POLL -- 内核实现的更多相关文章

  1. Linux 网络编程的5种IO模型:多路复用(select/poll/epoll)

    Linux 网络编程的5种IO模型:多路复用(select/poll/epoll) 背景 我们在上一讲 Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO中,对于其中的 阻塞/非阻塞IO 进行了 ...

  2. 【操作系统】I/O多路复用 select poll epoll

    @ 目录 I/O模式 I/O多路复用 select poll epoll 事件触发模式 I/O模式 阻塞I/O 非阻塞I/O I/O多路复用 信号驱动I/O 异步I/O I/O多路复用 I/O 多路复 ...

  3. Linux 多路复用 select / poll

    多路复用都是在阻塞模式下有效! linux中的系统调用函数默认都是阻塞模式,例如应用层读不到驱动层的数据时,就会阻塞等待,直到有数据可读为止. 问题:在一个进程中,同时打开了两个或者两个以上的文件,读 ...

  4. I/O多路复用 select poll epoll

    I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作. select select最早于1983年出现在4.2BSD中,它通 ...

  5. IO多路复用select/poll/epoll详解以及在Python中的应用

    IO multiplexing(IO多路复用) IO多路复用,有些地方称之为event driven IO(事件驱动IO). 它的好处在于单个进程可以处理多个网络IO请求.select/epoll这两 ...

  6. I/O多路复用select/poll/epoll

    前言 早期操作系统通常将进程中可创建的线程数限制在一个较低的阈值,大约几百个.因此, 操作系统会提供一些高效的方法来实现多路IO,例如Unix的select和poll.现代操作系统中,线程数已经得到了 ...

  7. 转一贴,今天实在写累了,也看累了--【Python异步非阻塞IO多路复用Select/Poll/Epoll使用】

    下面这篇,原理理解了, 再结合 这一周来的心得体会,整个框架就差不多了... http://www.haiyun.me/archives/1056.html 有许多封装好的异步非阻塞IO多路复用框架, ...

  8. Python异步非阻塞IO多路复用Select/Poll/Epoll使用,线程,进程,协程

    1.使用select模拟socketserver伪并发处理客户端请求,代码如下: import socket import select sk = socket.socket() sk.bind((' ...

  9. 最快理解 - IO多路复用:select / poll / epoll 的区别.

    目录 第一个解决方案(多线程) 第二个解决方案(select) 第三个解决方案(poll) 最终解决方案(epoll) 客栈遇到的问题 从开始学习编程后,我就想开一个 Hello World 餐厅,由 ...

随机推荐

  1. 对hashmap与hashcode()、equals()的理解

    1.equals方法没被重写的时候   比较的只是对象的地址  重写之后 比较的才是对象里的内容 2.重写equals的时候 务必需要重写hashcode 不然在用到容器的时候 会出现问题 因为容器会 ...

  2. 转: 带你玩转Visual Studio——带你理解多字节编码与Unicode码

    上一篇文章带你玩转Visual Studio——带你跳出坑爹的Runtime Library坑帮我们理解了Windows中的各种类型C/C++运行时库及它的来龙去脉,这是C++开发中特别容易误入歧途的 ...

  3. jpg转png

    对于jpg图片来说,有损压缩因子设置为0.5 可以大大减少图片的体积,而对图片的质量几乎没有太大影响: 下面是测试图片结果:     // UIImage *image_jpg = [UIImage ...

  4. PHP---------PHP函数里面的static静态变量

    工作一年了,一年里很少用到static这个关键词,不管是类里面还是方法里面基本都没怎么用过.平时看到类里面有这个都没什么好奇的,今天在函数里面看到了这个,就去百度了一下. <?phpfuncti ...

  5. Tomcat JSP提交参数中文乱码问题解决

    参考: http://blog.csdn.net/error_case/article/details/8250209 中文乱码是个老生常谈的问题,一般情况下,只要保证页面,web服务器,数据库的编码 ...

  6. jquery之empty()与remove()区别

    要用到移除指定元素的时候,发现empty()与remove([expr])都可以用来实现.可仔细观察效果的话就可以发现.empty()是只移除了 指定元素中的所有子节点,拿$("p" ...

  7. 33、mybatis(二)

    第十六章回顾SQL99中的连接查询 1)内连接 2)外连接 3)自连接 第十七章回顾hibernate多表开发 1)一对一 2)一对多 3)多对多 第十八章 mybatis一对一映射[学生与身份证] ...

  8. mina IoBuffer 常用方法

    Limit(int) 如果position>limit, position = limit,如果mark>limit, 重置mark Mark() 取当前的position的快照标记mar ...

  9. 谈谈对AOP的理解

    Aspect Oriented Programming  面向切面编程.解耦是程序员编码开发过程中一直追求的.AOP也是为了解耦所诞生. 具体思想是:定义一个切面,在切面的纵向定义处理方法,处理完成之 ...

  10. LeetCode----66. Plus One(Java)

    package plusOne66; /* Given a non-negative number represented as an array of digits, plus one to the ...