epoll vs select———— select内核源码赏析

作为老生常谈的epoll和select,每次面试说道网络编程一边会出现在tcp/ip状态图之后,网上的标准答案也很多,但是本文希望能从内核源码来提升一下逼格,同时自己在扒nginx源码,epoll作为其最核心的函数,深入了解也有帮助的。
先来看select源码,网上其实有不少解析了,很多很优秀但是因为是大牛所以不太容易理解,这篇一是自己备忘,二是希望自己能讲清楚点:


一、位置
在fs/select.c里面,其实从函数所在位置就能看出,select和epoll从定义上并不是网络编程(没有在net目录)。
在我的3.13.0内核版本中系统调用是通过类似SYSCALL_DEFINE5(select,...的宏来定义,会扩展成sys_select,原因网上有,这里提出是以后要grep函数定义可以按照这种规则来。


二、预备知识
1.文件描述符fd和文件信息结构体struct file的关系:
刚学linux我记得最深的就是寝室兄弟说的“linux里面任何实体都是文件”,fd能够fork后父子进程共同使用,socket、open、socketpair为什么都能用write/read,select也能容纳各种途径得到的fd...;
http://blog.csdn.net/hunanchenxingyu/article/details/25218351这一篇可以看下,配图出色;
这里留个记号用c++重新封下struct file加深理解,todo。


2.linux进程sleep/wakeup mechanism(睡眠唤醒机制?):
http://blog.chinaunix.net/uid-20543672-id-3267385.html,配图绝妙,但是博主太牛,写得太简略了;我解释下,重点解释右下角的那一块struct poll_wqueues:


a.初始化struct poll_wqueues table,table下pt指向的qproc被指向__pollwait;polling_task赋值为current,也就是当前进程的调度信息,包含pid、表示run或stopped的state、共享内存list、甚至退出时的exit_state;table和inline_entries都是poll_table_entry的载体,后者是栈上分配的,如果不够会用一个内存页插入到table链表中,这种做法在string针对小字符串优化时也见过;
b.对每一个select设置的fd,设fd对应的文件结构体struct file为pfil,调用*(pfil->f_op->poll)(pfil,pt),这里pt就是1中的poll_table结构,具体见2中图;
c.不同的fd类型对应的poll操作是不一样的,以socket函数创建的udp接口为例,poll函数指针实际指向了net/socket.c下的sock_poll,进一步调用了net/ipv4/udp.c下的udp_poll,实际工作在datagram_poll里,unsigned int datagram_poll(struct file *file, struct socket *sock,poll_table *wait)的三个参数file还是pfil,wait还是pt,sock是pfil下的private_data是一个socket结构体,struct file作为一个大杂烩文件的统一结构体,f_op函数指针集合可以用来定义不同的操作,而private_data就是给这些千差万别操作的参数了;


前面如果是废话,下面是重点
d.调用sock_poll_wait(file, sk_sleep(sk), wait),呵呵,但是这样写就跳进黄河也解释不清了,可以翻译成
sock_poll_wait(current->files->fdt[fd],((struct socket *)(current->files->fdt[fd]->private_data)->sk->sk_wq->wait),((poll_wqueues *)(&table))->pt)
源码经常就麻烦在因为开发者太牛设计太先进,导致对一般人指针绕来绕去没有头绪,这样写开了可能清晰点。
注意调用依赖关系,udp的socket文件的“虚函数”poll是net/socket.h下的sock_poll,而socket()函数根据一开始的SOCK_DGRAM(udp)\AF_INET(ipv4)参数又把sock_poll的实际操作指向了net/ipv4/udp.c下的udp_poll,进一步实际调用了数据报核心函数datagram_poll,之后在上面展开的sock_poll_wait内调用了include/linux/poll.h内的poll_wait,poll_wait和sock_poll_wait参数一致。反映了一个分化到集中的过程,socket->ipv4的udp->linux下文件统一的poll处理poll_wait。
poll_wait很简单,把自己的三个参数传给了pt(第三个参数)指向的qproc函数,在这里也就是fs/select.c里的__pollwait static函数,再次展开成了:
(((poll_wqueues *)(&table))->pt->_qproc)(current->files->fdt[fd],((struct socket *)(current->files->fdt[fd]->private_data)->sk->sk_wq->wait),((poll_wqueues *)(&table))->pt)即
__pollwait(current->files->fdt[fd],((struct socket *)(current->files->fdt[fd]->private_data)->sk->sk_wq->wait),((poll_wqueues *)(&table))->pt)。
三个参数,文件描述符对应的文件结构体,文件描述符对应的实体(有博客也叫设备)wait队列,poll表pt。也代表了三方,文件结构体是linux内核;wait队列是具体实体;而poll表中还有一个key字段,包含了用户关心的状态(可读可写错误)代表了用户。
__pollwait代码不多,干了不少事,把http://blog.chinaunix.net/attachment/201207/8/20543672_1341761191EPXB.jpg内的关系都连起来了:
poll_wqueues增加一格entry;
entry->filp指向文件结构体;
entry->wait_address指向udp的wait队列;
entry->key赋值为pt->key,指明用户关心的事件;
entry->wait.func赋值为pollwake;
entry->wait.private指回poll_wqueues;
把entry->wait加入到udp的wait队列;


这样图中各条边算是连接成功了。


e.poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,to, slack),当前进程状态设置为可中断的休眠(中断返回EINTR),接下来调用schedule_hrtimeout_range按照timeout设置与否设置超时之后schedule;


f.等待超时或者等待事件发生被唤醒,或者收到信号。只有了解被唤醒的过程才能真正了解上面结构的意义。发现net/ sk_state_change和sock_def_wakeup有戏,不过这个是tcp的,tcp状态改变会触发sock_def_wakeup函数:
    if (wq_has_sleeper(wq))     //wq就是d中的sk_wq,字面意思,sock waitqueue,当wq->wait链表不为空时返回true,这里是文件的唤醒链表
                wake_up_interruptible_all(&wq->wait);   //展开为__wake_up_sync((&wq->wait), TASK_INTERRUPTIBLE, 1),最终调用__wake_up_common,对每一个链表的__wait_queue结构curr执行curr->func(curr, mode, wake_flags, key),func如前所说是pollwake,mode是TASK_INTERRUPTIBLE
        __pollwake函数:
        struct poll_wqueues *pwq = wait->private;       //如前,private指向poll_wqueues,进程的轮训等待队列
        DECLARE_WAITQUEUE(dummy_wait, pwq->polling_task);       //差不多是wait_queue_t dummy_wait = {0,current,default_wake_function,{NULL,NULL}};
        return default_wake_function(&dummy_wait, mode, sync, key);
        try_to_wake_up函数:
        看不大懂了,猜是根据事件类型看是否把当前挂起的current转到就绪队列。


g.超时、事件触发、收到信号,总之,select要返回了,返回之前调用poll_freewait收尾。
对于poll_wqueues,依此释放栈上和动态分配的table下面的所有entry(一个entry是对应一个关注的文件描述符),对每个entry都从对应的文件等待队列中把本进程的select关注事件删除。




三、流程
预备知识搞定了select流程就比较简单了。
sys_select:如果timeout设置,则加上系统启动至今的时间换算成绝对的时间;
        core_sys_select:按照current的文件表得到最大fd,而不是从用户输入中得到最大fd,根据最大fd看是用栈上的 输入读写错误+输出读写错误fd空间还是重新kmalloc得到。用户空间拷贝到内核空间;       //这一招struct poll_wqueues也用到了
                do_select:尽管有三种事件(可读可写错误),但是只遍历一次所有的fd。对于每一个有事件的fd,都如同预备知识2里面说的————执行fd对应的poll操作,poll主要是找到对应文件的等待链表,之后在本进程的poll等待队列新开一个entry,把文件、文件等待链表、进程poll等待队列联系起来。第一次轮询完如果已经找到了一个fd有事件,那么还需要poll_freewait把刚刚辛辛苦苦建立起来的联系毁掉并返回。
                无论是因为何种原因(超时、信号、被事件唤醒),进程重新被调度后还要再遍历一次fd,也是重新调用fd的poll函数,但这次参数不同不会重新注册事件等,poll只返回当前文件的读写错误情况。最后返回之前需要执行poll_freewait。
        内核空间拷贝到用户空间,如果动态获取内存用于存储fd还需要释放。


四、初步分析
对于一个打开了很多文件的进程,比如一个繁忙的或者连接时间较长的多路复用网络服务器文件描述符最大为10000(假设select上限调整过)。一次select首先得申请6*10000/8=7500B差不多7KB两个内存页的空间,这个倒不多,用户空间到内核空间的拷贝及之后的反向拷贝也有开销。当然,最麻烦的是可能的两次遍历所有文件描述符,在一万次的循环中,每一次都会有好几次函数指针调用;并且针对文件等待链表的修改还需要上锁;一个fd对应一个entry,一个entry在32位机器上是8B,一个内存也1024*4/8能放512个,差不多20页能装满,需要动态请求20次;实际上,因为可以预见的复杂性,每执行一个long的bit数就会调用一次cond_resched试图让很紧急的其他进程先执行。当然,麻烦中的麻烦更是,如果一次遍历下来哪怕只有一个文件描述符发生了事件,那么select就应该返回,这就意味着辛辛苦苦建立起来的结构得释放掉,就是从文件等待链表中删除以及回收动态的内存。
和epoll的性能测试,需要看看已有的实验,todo。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值