0、等待队列
在 Linux内核 中等待队列有很多用途,可用于中断处理、进程同步及定时。等待队列实现了在事件上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件为真时,由内核唤醒它们。
等待队列由循环链表实现,由等待队列头(wait_queue_head_t)和等待队列项(wait_queue)组成,其元素(等待队列项)包含指向进程描述符的指针。(linux/include/wait.h)。
等待队列头结构体的定义:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
1.通过DECLARE_WAIT_QUEUE_HEAD宏定义一个wait_queue_head等待队列头,这是静态定义的方法。该宏会定义一个 wait_queue_head_t,并且初始化结构中的锁以及等待队列。
2.Linux中等待队列的实现思想如下图所示,当一个任务需要在某个wait_queue_head_t上睡眠时,将自己的进程控制块PCB信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head_t上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。
![](https://i-blog.csdnimg.cn/blog_migrate/011324adcdab83dc3e3f857a13e0eddc.jpeg)
3.等待队列中存放的是在执行设备操作时不能获得资源而挂起的进程:
定义等待对列:
struct __wait_queue{
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void * private; //通常指向当前任务控制块PCB
wait_queue_func_t func; //唤醒阻塞任务的函数 ,决定了唤醒的方式
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
-----------------------------------------------
1、poll 内核实现实现分析(通过等待队列实现)
1.主要数据结构:
(1) struct poll_table_entry {
struct file* filp;
wait_queue_t wait;
wait_queue_head_t* wait_address;
};
(2) struct poll_table_page {
struct poll_table_page* next;
struct poll_table_entry* entry;
struct poll_table_entry entries[0];
};
---------
(3)处理函数指针,通常指向 __pollwait 或 null:
typedef void (*poll_queue_proc)(struct file *,
wait_queue_head_t *,
struct poll_table_struct *);
封装在结构体 poll_table 中:
typedef struct poll_table_struct {
poll_queue_proc qproc;
} poll_table;
(4)poll 封装的等待队列:
struct poll_wqueues {
poll_table pt; //__pollwait 或 null
struct poll_table_page * table;
int error;
};
---------
(5)存放用户空间的信息:
struct poll_list {
struct poll_list *next;//按内存页连接,因为kmalloc 数据限制
int len; //用户空间传入fd的数量
struct pollfd entries[0];//存放用户空间的数据
};
struct pollfd {
int fd;short events;
short revents;
};
2.poll实现[fs/select.c -->sys_poll]
2.1asmlinkage long sys_poll (struct pollfd __user * ufds, unsigned int nfds, long timeout) {
struct poll_wqueues table;
struct poll_list *head; /* 用户空间数据的链表头 */
struct poll_list *walk; /* poll 链表最后一个有效项 */
……
poll_initwait(&table); /* 初始化poll等待队列,注册回调函数pt */
……
while(i!=0) {
struct poll_list *pp;
/* 分配内存页,存放来自用户空间的信息,通常是4k */
pp = kmalloc(sizeof(struct poll_list)+ sizeof(struct pollfd) *
(i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i),
GFP_KERNEL));
if (head == NULL)
head = pp;
else
walk->next = pp;
walk = pp;
if (copy_from_user(pp->entries,ufds + nfds-i,
sizeof(struct pollfd)*pp->len)) {
err = -EFAULT;
goto out_fds;
}
i -= pp->len;
}//end while
fdcount = do_poll(nfds, head, &table, timeout);
}
2.2 sys_poll 函数说明:
0)拷贝用户空间信息:循环的作用是把用户态的 struct pollfd 拷进 entries 里,通常用户程序的 poll 调用就监控几个fd,所以 head 链表通常也就只需要一个节点,即操作系统的一页。但是,当用户传入的 fd 很多时,由于 poll 系统调用每次都要把所有 struct pollfd 拷进内核,故参数传递和页分配此时就成了poll系统调用的性能瓶颈。
1)poll_initwait 初始化变量 table,注意该 table 在整个执行 poll 的过程中是很关键的变量:
&(pwq->pt)->qproc = __pollwait; /* 主要功能:设置回调函数 */
……
}
》驱动中自定义 xxx_poll 函数中的调用函数:
void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);/* __pollwait 不仅是 poll 系统调用需要,select 系统调用也一样是用这个__pollwait,是操作系统的异步操作的“御用”回调函数。epoll 没有用这个,另外新增了一个回调函数,以达到其高效运转的目的 */
3)do_poll 函数说明:
static int do_poll(unsigned int nfds,
struct poll_list *list,
struct poll_wqueues *wait,
long timeout)
{
int count = 0;
poll_table* pt = &wait->pt;/* 回调函数 */
for (;;) {/* 阻塞于此 */
struct poll_list *walk;
set_current_state(TASK_INTERRUPTIBLE);
walk = list;
while (walk != NULL) {
/* 针对每个传进来的fd,调用各自对应的 xxx_poll 函数 */
do_pollfd( walk->len, walk->entries, &pt, &count);
walk = walk->next;
}
if (count || !timeout || signal_pending(current))
break;
timeout = schedule_timeout(timeout); /* 挂起 current */
}//end while
__set_current_state( TASK_RUNNING );
return count;
}
NOTE:注意 set_current_state(TASK_INTERRUPTIBLE)和 signal_pending,保障了当用户程序在调用 poll 后挂起时,发信号可以让程序迅速退出poll调用,而通常的系统调用是不会被信号打断的。纵览do_poll函数,主要是在循环内等待,直到count大于0才跳出循环,而count主要是靠do_pollfd函数处理。注意while循环,当用户传入的fd很多时(比如1000个),对 do_pollfd 就会调用很多次,poll效率瓶颈的另一原因就在这里。
4)do_pollfd() 针对每个传进来的fd,调用它们各自对应的 xxx_poll函数,如下:
[fs/select.c-->sys_poll()-->do_poll()-->do_pollfd()]
static void do_pollfd(unsigned int num, struct pollfd * fdpage, poll_table ** pwait, int *count){
……
file = fget_light(fd, &fput_needed);
if (file->f_op && file->f_op->poll) {
if (pwait)
pwait->key = pollfd->events |POLLERR | POLLHUP;
mask = file->f_op->poll(file, pwait);
}
return mask;
}
NOTE:如果 fd 对应的是某个socket,do_pollfd调用的就是网络设备驱动实现的poll;如果fd对应的是某个ext3文件系统上的一个打开文件,那do_pollfd调用的就是ext3文件系统驱动实现的poll,即file->f_op->poll是设备驱动程序实现的。设备驱动程序的标准实现是:调用poll_wait,即以设备自己的等待队列为参数(通常设备都有自己的等待队列,避免不支持异步操作的设备)调用 struct poll_table的回调函数。
5)例:socket tcp时的实例:[net/ipv4/tcp.c-->tcp_poll]
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait){
poll_wait(file, sk->sk_sleep, wait);
}
Note:tcp_poll的核心实现就是poll_wait,而poll_wait就是调用 struct poll_table 对应的回调函数,那 poll 系统调用对应的回调函数就是__poll_wait,所以几乎可以把tcp_poll理解为一个语句:
__pollwait (file, sk->sk_sleep, wait);
由此也可以看出,每个socket自己都带有一个等待队列(head) sk_sleep,所以上面所说的“设备的等待队列”,其实不止一个。
6) __poll_wait的实现:[fs/select.c-->__poll_wait()]
void __pollwait (struct file *filp, wait_queue_head_t *wait_address, poll_table *_p){
……
}
Note:__pollwait 的作用:一次设备poll调用只创建一个 poll_table_entry,并通过struct poll_table_entry 的 wait 成员,把current 挂在了设备的等待队列上,此处的等待队列是 wait_address,对应 tcp_poll 里的 sk->sk_sleep。
7)poll系统调用的原理:
1.先注册回调函数 __pollwait,再初始化 table 变量(类型为struct poll_wqueues) ;
2.拷贝用户传入的struct pollfd(其实主要是fd)(瓶颈1) ;
3.do_poll轮流调用所有fd对应的 poll(__poll_wait把current挂到各个fd对应的设备等待队列上)(瓶颈2);
4.在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上的进程,即current被唤醒.
8)select/poll 缺点:
1.每次调用时要重复地从用户态读入参数;
2.每次调用时要重复地扫描文件描述符;
3.每次在调用开始时,要把当前进程放入各个文件描述符的等待队列, 在调用结束后,又把进程从各个等待队列中删除。