linux中poll系统调用实现了对文件描述符的轮询,由于poll的实现问题,每当一个或者多个文件描述符上有事件发生的时候,poll的核心并没有什么好的办法可以知道到底是哪些文件描述符上发生了事件,于是不得不采用遍历所有的fd_set中的文件描述符的办法,但是这种方式很低效,如果有很多的描述符但是只有最后一个上发生了事件,那么将会消耗很多的时间,于是出现了epoll,epoll本质就是应用唤醒回调函数,只将被唤醒的wait队列元素加入到一个表中,然后只需要遍历该表的元素就可以了,如果还是上面的情况,那么只有一个wait元素被加入到表中,只要在这个表中的元素上poll一下就完成了。
事情到此就结束了吗?没有!现有的epoll已经定位到了发生事件的具体的文件描述符,但是一个描述符上可以监控很多的事件,如果监控的是read,然而write事件到来的时候也会将该描述符唤醒,那么按照epoll的设计思想,是否可以定位到事件呢?是的,可以,这就是keyed-epoll补丁的思想,但是在引入这个补丁之前linux的方式就是一步一步来,先在传统的poll机制上实现keyed扩展,这样就做到了影响最小化,keyed就可以从一个扩展抽象成了一个机制,它就不再和poll机制绑定,其实它也没有必要和poll机制绑定,于是单纯的keyed补丁就被提出来了,它就是实现了另外一套唤醒函数,这个系列函数的参数中添加了一个key参数,也就是加了一层判断,只有当key值符合一定条件时才会进行真正的唤醒,具体怎么判断key的逻辑,就由用户来制定。千言万语敌不过几行代码:
-static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
+static int __pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
struct poll_wqueues *pwq = wait->private;
DECLARE_WAITQUEUE(dummy_wait, pwq->polling_task);
@@ -194,6 +194,16 @@ static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
return default_wake_function(&dummy_wait, mode, sync, key);
}
+static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
+{
+ struct poll_table_entry *entry;
+
+ entry = container_of(wait, struct poll_table_entry, wait);
+ if (key && !((unsigned long)key & entry->key)) //如果事件与该entry无关,就不再执行唤醒操作
+ return 0;
+ return __pollwake(wait, mode, sync, key);
+}
+
/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
@@ -205,6 +215,7 @@ static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
get_file(filp);
entry->filp = filp;
entry->wait_address = wait_address;
+ entry->key = p->key;
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
add_wait_queue(wait_address, &entry->wait);
@@ -418,8 +429,16 @@ int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
- if (f_op && f_op->poll)
+ if (f_op && f_op->poll) {
+ if (wait) { //在进行调用vfs的poll之前,先将需要监控的事件加入到key,在唤醒的时候要作为参考
+ wait->key = POLLEX_SET;
+ if (in & bit)
+ wait->key |= POLLIN_SET;
+ if (out & bit)
+ wait->key |= POLLOUT_SET;
+ }
mask = (*f_op->poll)(file, retval ? NULL : wait);
+ }
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
这个补丁正如作者所说,节省了不少开销,避免了不少唤醒操作,我个人认为,它的意义要比epoll还要大。如果一个进程监控fd1的read事件,另一个进程监控fd1的write事件,那么这两个进程都要加入到该fd1的vfs底层的的wait队列中,一旦fd1上发生事件,则这两个进程都要被唤醒而不管是什么事件。打上这个补丁之后,如果是read事件,那么就只用唤醒监控read事件的那个进程就可以了,节省了一半的唤醒动作,唤醒操作是一件很耗时的操作,因为涉及到抢占和切换,特别是毫无意义的唤醒更是要避免的,没有事情无故唤醒别人是一件很不好的事,这个补丁就是细化了事件检测机制。如果将这个机制加入到epoll中,那更是如虎添翼,不但精确到了文件描述符,更是精确到了文件描述符的事件,在文件描述符之下再检测一个事件,这里该补丁和epoll的区别在于epoll节省了轮询遍历的开销但是避免不了唤醒,而keyed机制节省不了轮询但是可以最小化唤醒操作。传统的poll一旦被唤醒之后必须遍历所有poll列表的文件描述符从而确定哪一个上有事件发生,而epoll不用;传统的poll在底层,只要有事件发生就会唤醒其睡眠队列的所有进程而不管进程是否关心该事件,而keyed机制可以避免这种鲁莽。两个机制在诸多文件描述符和诸多事件中定位到了一个文件描述符的一个事件,可谓妙。
最后看一个linux中的层次问题,总有人说linux没有实现内核级别的线程而只有进程,可是clone中克隆的是什么?是task_struct,task_struct是什么?是进程吗?不是,是线程吗?不是,那么它是什么?它是进程和线程的超集,它比进程和线程的层次要高,包含进程和线程而不能说它是进程或者线程,因此不要按照windows中线程的意义来理解linux的实现,linux的架构其实很松散,打破了传统操作系统理论对操作系统的规定。linux中统一的进程线程实现方式确实很好,很灵活,做到了正交化,意义就是进程和线程不再具有从属关系,而是没有关系,linux内核中没有定义进程和线程,只有task_struct,如果一个task_struct独享资源,那么它就是进程,如果很多task_struct共享资源,那么每个task_struct就是一个线程然后它们组成一个进程,到底是什么由是否共享资源这个第三方的开关而不是它们本身来定义,这样很不错,将内涵和外延分离开来。linux的方式可以实现很多的进程/线程的实现方式,这种低耦合高内聚的正交化机制是很强大的。内涵没有意义,就是一个task_struct,加上一个有意义的“资源使用方式”这个第三方的策略就构成了外延--进程和线程