select,poll,epoll的内部机制调研
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
1 等待队列实现原理
1.1功能介绍
进程有多种状态,当进程做好准备后,它就处于就绪状态(TASK_RUNNING),放入运行队列,等待内核调度器来调度。当然,同一时刻可能有多个进程进入就绪状态,但是却可能只有1个CPU是空闲的,所以最后能不能在CPU上运行,还要取决于优先级等多种因素。当进程进行外部设备的IO等待操作时,由于外部设备的操作速度一般是非常慢的,所以进程会从就绪状态变为等待状态(休眠),进入等待队列,把CPU让给其它进程。直到IO操作完成,内核“唤醒”等待的进程,于是进程再度从等待状态变为就绪状态。
在用户态,进程进行IO操作时,可以有多种处理方式,如阻塞式IO,非阻塞式IO,多路复用(select/poll/epoll),AIO(aio_read/aio_write)等等。这些操作在内核态都要用到等待队列。
1.2 相关的结构体
typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
struct task_struct * task;
wait_queue_func_t func;
struct list_head task_list;
};
这个是等待队列的节点,其中task表示等待队列节点对应的进程。func表示等待队列的回调函数,在进程被唤醒。在很多等待队列里,这个func函数指针默认为空函数。但是,在select/poll/epoll函数中,这个func函数指针不为空,并且扮演着重要的角色。
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
这个是等待队列的头部。其中task_list里有指向下一个节点的指针。为了保证对等待队列的操作是原子的,还需要一个自旋锁lock。
这里需要提一下内核队列中被广泛使用的结构体struct list_head。
struct list_head {
struct list_head *next, *prev;
};
这是一个双向链表。在C++中,可以通过模板来实现一个通用的双向链表list
list_head双向链表进行各种操作。通过这种方式,内核实现了代码的复用,而不用为每个双向链表写一套相同的链表操作的API。理论上来说,这种类型的双向链表可以链接各种类型的结构体,只要这种结构体包含list_head成员。这与C++的模板相比,显得更加灵活。
图1-1 等待队列
1.3 实现原理
可以看到,等待队列的核心是一个list_head组成的双向链表。其中,第一个节点是队列的头,类型为wait_queue_head_t,里面包含了一个list_head类型的成员
task_list。而接下去的每个节点类型为 wait_queue_t,里面也有一个list_head类型的成员task_list,并且有个指针指向等待的进程。通过这种方式,内核组织了一个等待队列。
那么,这个等待队列怎样与一个事件关联呢?在内核中,进程在文件操作等事件上的等待,一定会有一个对应的等待队列的结构体与之对应。例如,等待管道的文件操作(在内核看来,管道也是一种文件)的进程都放在管道对应inode.i_pipe->wait这个等待队列中。这样,如果管道文件操作完成,就可以很方便地通过inode.i_pipe->wait唤醒等待的进程。
在大部分情况下(如系统调用read),当前进程等待IO操作的完成,只要在内核堆栈中分配一个wait_queue_t的结构体,然后初始化,把task指向当前进程的
task_struct,然后调用add_wait_queue()放入等待队列即可。
但是,在select/poll中,由于系统调用要监视多个文件描述符的操作,因此要把当前进程放入多个文件的等待队列,并且要分配多个wait_queue_t结构体。这时候,在堆栈上分配是不合适的。因为内核堆栈很小。所以要通过动态分配的方式来分配wait_queue_t
结构体。
除了在一些结构体里直接定义等待队列的头部,内核的信号量机制也大量使用了等待队列。信号量是为了进行进程同步而引入的。与自旋锁不同的是,当一个进程无法获得信号量时,它会把自己放到这个信号量的等待队列中,转变为等待状态。当其它进程释放信号量
时,会唤醒等待的进程。
2 select的实现原理(内核版本2.6.9)
2.1功能介绍
select系统调用的功能是对多个文件描述符进行监视,当有文件描述符的文件读写
操作完成,发生异常或者超时,该调用会返回这些文件描述符。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
2.2关键的结构体:
这里先介绍一些关键的结构体:
typedef struct {
unsigned long *in, *out, *ex;
unsigned long *res_in, *res_out, *res_ex;
} fd_set_b its;
这个结构体负责保存select在用户态的参数。在select()中,每一个文件描述符用一个位表示,其中1表示这个文件是被监视的。in, out, ex指向的b it数组表示对应的读,写,异常文件的描述符。res_in, res_out, res_ex指向的b it数组表示对应的读,写,异常文件的描述符的检测结果。
struct poll_wqueues {
poll_ta b le pt;
struct poll_ta b le_page * ta b le;
int error;
};
这是最主要的结构体,它保存了select过程中的重要信息。它包括了两个最重要的结构体poll_ta b le和struct poll_ta b le_page。接下去看看这两个结构体。
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct
poll_ta b le_struct *);
struct poll_ta b le_struct {
poll_queue_proc qproc;
} poll_ta b le;
在执行select操作时,会用到回调函数,poll_ta b le就是用来保存回调函数的。这个回调函数非常重要,因为当文件执行poll操作时,一般都会调用这个回调函数。所以,这个回调函数非常重要,通常负责把进程放入等待队列等关键操作。下面可以看到,在select中,这个回调函数是__pollwait(),在epoll中,这个回调函数是 ep_pta b le_queue_proc。struct poll_ta b le_page {
struct poll_ta b le_page * next;
struct poll_ta b le_entry * entry;
struct poll_ta b le_entry entries[0];
};