关于epoll:Epoll-源码剖析-视频笔记

epoll_createwq: 期待过程队列的队列头 (task_struct),通常只有一个 rdllist: readylist 1 1 rbn: 指向红黑树quqList : poll wait listrdllink: 筹备好的列表epoll_event: epoll_ctl加进来的构造体ffd: epoll 监听文件 111 111 1 1 11 阻塞版本 11 1 2

May 25, 2021 · 1 min · jiezi

关于epoll:epoll

epoll、poll、select的区别?目前的罕用的IO复用模型有三种:select,poll,epoll。================================== (1)select==>工夫复杂度O(n) 它仅仅晓得了,有I/O事件产生了,却并不知道是哪那几个流(可能有一个,多个,甚至全副),咱们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具备O(n)的无差别轮询复杂度,同时解决的流越多,无差别轮询工夫就越长。 (2)poll==>工夫复杂度O(n) poll实质上和select没有区别,它将用户传入的数组拷贝到内核空间,而后查问每个fd对应的设施状态, 然而它没有最大连接数的限度,起因是它是基于链表来存储的. (3)epoll==>工夫复杂度O(1) epoll能够了解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流产生了怎么的I/O事件告诉咱们。所以咱们说epoll实际上是事件驱动(每个事件关联上fd)的,此时咱们对这些流的操作都是有意义的。(复杂度升高到了O(1)) select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,能够监督多个描述符,一旦某个描述符就绪(个别是读就绪或者写就绪),可能告诉程序进行相应的读写操作。但select,poll,epoll实质上都是同步I/O,因为他们都须要在读写事件就绪后本人负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本人负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。   epoll跟select都能提供多路I/O复用的解决方案。在当初的Linux内核里有都可能反对,其中epoll是Linux所特有,而select则应该是POSIX所规定,个别操作系统均有实现 参考https://www.cnblogs.com/fnlin...

January 11, 2021 · 1 min · jiezi

关于epoll:epoll的具体实现与epoll线程安全互斥锁自旋锁CAS原子操作

https://www.bilibili.com/vide...

December 2, 2020 · 1 min · jiezi

关于epoll:浅析IO模型selectpollepoll

I/O流概念(1)c++中将数据的输入输出称之为流(stream),在c++中,流被定义为类,成为流类(stream class),其定义的对象为流对象。 (2)文件,套接字(socket),管道(pipe)等可能进行I/O操作的对象,能够被看做为流 工作机制(1)大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,读取数据时,都会将数据先拷贝到操作系统内核的缓冲区中,而后将操作系统内核缓冲区的数据拷贝到应用程序的地址空间,写的过程则相同。 (2)缓存I/O应用操作系统内核缓冲区,在肯定水平上拆散了应用程序空间和理论的物理设施,通过将数据写入缓冲区后,再一次性解决,缩小了读盘的次数,从而进步了性能 I/O模型同步与异步:关注的是音讯通信机制同步(synchronous):调用者会始终“期待”被调用者返回音讯,能力继续执行,在此期间,调用者不能做其它事 异步(asynchronous):被调用者通过状态、告诉或回调机制被动告诉调用者被调用者的运行状态,在此期间,调用者能够边“期待”,边做其它事 阻塞和非阻塞:关注调用者的状态阻塞(blocking):调用者始终“期待”所处的状态 非阻塞(blocking):调用者可能边“期待”,边做其它事的状态 同步I/O(1)阻塞式I/O:程序收回I/O申请,如果内核缓冲区为空,此时进行读操作,那么该程序就会阻塞 (2)非阻塞式I/O:程序收回I/O申请,如果内核缓冲区为空,此时进行读操作,此时就会立即返回一个谬误 (3)I/O复用 a. 这是一种机制,程序注册一组文件描述符给操作系统,监督多个描述符,一旦某个描述符就绪(个别是读就绪或者写就绪),可能告诉程序进行相应的读写操作。示意“我要监督这些fd是否有I/O事件产生,有了就通知程序处理”。 b. 当多个I/O流共用一个期待机制时,该模型会阻塞过程,然而过程时阻塞在这种机制的零碎调用上,不是阻塞在真正的I/O操作上 c. I/O多路复用须要和非阻塞I/O一起应用,非阻塞I/O和I/O多路复用式绝对独立的。非阻塞I/O仅仅指流对象立即返回,不会被阻塞;而I/O多路复用只是操作系统提供的一种便当的告诉机制。 (4)信号驱动式I/O a. 用户过程能够通过零碎调用注册一个信号处理程序,而后主程序能够持续向下执行,当有I/O操作准备就绪时,由内核告诉触发一个SIGIO信号处理程序执行,而后将用户过程所须要的数据从内核空间拷贝到用户空间 b. 此模型的劣势在于期待数据报达到期间过程不被阻塞。用户主程序能够继续执行,只有期待来自信号处理函数的告诉。 异步I/Oa. 程序过程向内核发送I/O调用后,不必期待内核响应,能够持续承受其余申请,内核调用的I/O如果不能立刻返回,内核会持续解决其余事物,直到I/O实现后将后果告诉给内核 b. 信号驱动式IO是由内核告诉咱们何时启动一个IO操作,而异步IO是由内核告诉咱们IO操作何时实现。 I/O复用模型selectselect的大抵工作流程: (1)采纳数组组织文件描述符 (2)通过遍历数组的形式,监督文件描述符的状态(可读,可写,异样) (3)如果没有可读/可写的文件描述符,过程会阻塞期待一段事件,超时就返回 (4)当有一个可读/可写的文件描述符存在时,过程会从阻塞状态醒来 (5)进行无差别轮询,找出可能操作的I/O流,若解决后,会移除对应的文件描述符 select的毛病: (1)每次调用select,都须要把文件描述符汇合从用户空间贝到内核空间,这个开销在I/O流很多时会很大 (2)同时每次调用select都须要在内核遍历传递进来的所文件描述符数组,这个开销在I/O流很多时也很大 (3)select反对的文件描述符数量太小了,默认是1024 poll(1)采纳链表组织文件描述符 (2)原理和select统一 (3)只是解决了反对的文件描述符受限的毛病 (4)select和poll都是程度触发:找到可操作的I/O流并告诉过程,但过程本次没有解决,文件描述符没有被移除,下次轮询时依旧会告诉 epoll工作原理: (1)红黑树和就绪链表,红黑树用于治理所有的文件描述符,就绪链表用于保留有事件产生的文件描述符。 (2)接管到I/O申请,会在红黑树查找是否存在,不存在就增加到红黑树中,存在则将对应的文件描述符放入就绪链表中 (3)如果就绪链表为空,过程则阻塞否则遍历就绪链表,并告诉利用过程解决文件描述符对应的I/O 工作模式: (1)LT模式(程度触发):检测到可解决的文件描述符时,告诉应用程序,应用程序能够不立刻解决该事件。后续会再次告诉 (2)ET模式(边缘触发):检测到可解决的文件描述符时,告诉应用程序,应用程序必须立刻解决该事件。如果本次不解决,则后续不再告诉 参考资料IO五种模型和select与epoll工作原理(引入nginx) - osc_1ont5xz2的个人空间 - OSCHINA - 中文开源技术交换社区 IO模型:同步、异步、阻塞、非阻塞 | 神奕的博客 (songlee24.github.io) (3) io复用与epoll模型详解_集体文章 - SegmentFault 思否 (3) Linux IO模式及 select、poll、epoll详解_人云思云 - SegmentFault 思否 ...

November 3, 2020 · 1 min · jiezi

用-iouring-替代-epoll-实现高速-polling

前面的文章说到 io_uring 是 Linux 中最新的原生异步 I/O 实现,实际上 io_uring 也支持 polling,是良好的 epoll 替代品。 API使用 io_uring 来 poll 一个 fd 很简单。首先初始化 io_uring 对象(io_uring_queue_init),拿到 sqe(io_uring_get_sqe)是所有 io_uring 操作都必要的,前文已经介绍这里不做过多说明。拿到 sqe 之后,使用 io_uring_prep_poll_add 初始化 sqe 指针。 static inline void io_uring_prep_poll_add(struct io_uring_sqe *sqe, int fd, short poll_mask);第一个参数就是前面获得的 sqe 指针;第二个参数是你要 poll 的文件描述符;第三个是标志位,这里 io_uring 没有引入新的标志(宏),而是沿用了 poll(2) 定义的标志,如 POLLIN、POLLOUT 等。 如其他 I/O 请求一样,每个 sqe 都可以设置一个用户自己的值在里面,使用 io_uring_sqe_set_data 可以看到一次只能添加一个 poll 请求。如果有多个 fd,那么重复调用 io_uring_get_sqe 获取多个 sqe 指针分别 io_uring_prep_poll_add 即可。io_uring_get_sqe 不是系统调用不会进入内核,io_uring_prep_poll_add 则是简单的结构体参数赋值,所以没有速度问题。 ...

June 2, 2019 · 1 min · jiezi

并发异步同步阻塞非阻塞模型

网络从网卡到线程过程 1.一个以太网接口接收发送到它的单播地址和以太网广播地址的帧。当一个完整的帧可用时,接口就产生一个硬中断,并且内核调用接口层函数leintr2.leintr检测硬件,并且如果有一个帧到达,就调用leread把这个帧从接口转移到一个mbuf(各层之间传输数据都用这个)链中,构造单独的地址信息etherheaher。etherinput检查结构etherheaher来判断接收到的数据的类型,根据以太网类型字段来跳转。对于一个IP分组,schednetisr调度一个IP软件中断,并选择IP输入队列,ipintrq。对于一个ARP分组,调度ARP软件中断,并选择arpintrq。并将接收到的分组加入到队列中等待处理。3.当收到的数据报的协议字段指明这是一个TCP报文段时,ipintrq(通过协议转换表中的prinput函数)会调用tcpinp t进行处理.从mbuf中取ip,tcp首部,寻找pcb,发送给插口层3.1个关于pcb每个线程的task_struct都有打开文件描述符,如果是sock类型还会关联到全局inpcb中.listen某个fd时(new socket()=>bind(listen的端口)=>lisnten())在pcb中该fd是listen状态3.2in_pcblookup搜索它的整个Internet PCB表,找到一个匹配。完全匹配获得最高优先级,包含通配的最小通配数量的优先级高。所以当同一个端口已经建立连接后有了外部地址和端口,再有数据会选择该插口。比如当140.252.1.11:1500来的数据直接就匹配到第三个的插口,其他地址的发送到第一个插口。4.插口层4.1 listen 如果监听插口收到了报文段listen状态下只接收SYN,即使携带了数据也等建立连接后才发送,创建新的so,在收到三次握手的最后一个报文段后,调用soisconnected唤醒插口层accept 4.2 插口层acceptwhile (so->so_qlen == 0 && so->so_error == 0) {tsleep((caddr_t)&so->so_timeo, PSOCK | PCATCH,netcon, 0))} 当so_qlen不是0,调用falloc(p, &fp, &tmpfd)创建新的插口fd,从插口队列中复制,返回,此时该fd也在线程的打开文件中了。调用soqremque将插口从接收队列中删除。4.3 插口层read,send 从缓冲区复制mbuf。略5.线程调用内核的accept,从调用开始会sleep直到此时可以返回fd。【后文若用了epoll在lsfd有事件时通知线程再调用accept会节省调用到sleep时间。】 epoll原理 poll/select/epoll的实现都是基于文件提供的poll方法(f_op->poll),该方法利用poll_table提供的_qproc方法向文件内部事件掩码_key对应的的一个或多个等待队列(wait_queue_head_t)上添加包含唤醒函数(wait_queue_t.func)的节点(wait_queue_t),并检查文件当前就绪的状态返回给poll的调用者(依赖于文件的实现)。当文件的状态发生改变时(例如网络数据包到达),文件就会遍历事件对应的等待队列并调用回调函数(wait_queue_t.func)唤醒等待线程。数据结构:epoll_crete 创建event_poll,实际上创建了一个socketfd,称epfd。epoll_ctl 将回调为ep_poll_callback的节点 加入到epitem对应fd的等待队列中(即sk_sleep的wait_queue_head_t),关联到event_poll的红黑树等结构体中epoll_wait 将回调为try_to_wake_up的节点 加入到epfd的等待队列中你。 当发生事件,socket调用ep_poll_callback 会调用try_to_wake_up 进而唤醒wait的线程,向用户赋值rdlist数据,用户线程继续执行 (以scoket为例,当socket数据ready,终端会调用相应的接口函数比如rawv6_rcv_skb,此函数会调用sock_def_readable然后,通过sk_has_sleeper判断sk_sleep上是否有等待的进程,如果有那么通过wake_up_interruptible_sync_poll函数调用ep_poll_callback。从wait队列中调出epitem,检查状态epitem的event.events,若是感兴趣的事情发生,加入到rdllist或者ovflist中,调用try_to_wake_up。)数据拷贝: 1.拷贝新添加的events YSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,struct epoll_event __user *, event) copy_from_user(&epds, event, sizeof(struct epoll_event))) 2.传给用户的就绪的event 在ep_send_events_proc时__put_user(revents, &uevent->events。__put_user(epi->event.data, &uevent->data)与select区别 重复读入参数,全量的扫描文件描述符;调用开始,将进程加入到每个文件描述符的等待队列,在调用结束后又把进程从等待队列中删除;(每次发生事件fd位图结构改变,重新清除再select)select最多支持1024个文件描述符。 epoll 注册事件只需要一次拷贝(增量拷贝,依靠回调),另外返回就绪fd,不需要遍历所有的。运行模型是否立即返回。 阻塞:空cpu,IO阻塞线程非阻塞是否由本线程执行 同步IO异步1.所有请求单进程/线程2.一个请求一个线程/进程accept后一个连接全部过程在一个进程/线程 ,结束后结束线程/进程,每次有新连接创建一个新的进程/线程去处理请求 一个连接一个进程:父进程fork子进程 =》fork代价大 百级别prefork 多个进程会accept同一个lsfd,linux2.6已经支持就觉多进程同时accept一个时的惊群现象。一个连接一个线程 万级别限制,线程相互影响不稳定prethread 多线程共享数据,可以直接accept后分配给线程,也可以多个线程共同accept(accept实现了线程安全?).3.一个进程/线程处理多个请求线程池/进程池+非阻塞+IO多路复用 (非阻塞+IO多路复用 少了哪个这种模型都没有意义)reactor 监听所有类型事件,区分accept和业务处理 ...

May 11, 2019 · 1 min · jiezi

epoll 是如何工作的

本文包含以下内容:epoll是如何工作的本文不包含以下内容:epoll 的用法epoll 的缺陷我实在非常喜欢像epoll这样使用方便、原理不深却有大用处的东西,即使它可能已经比较老了select 和 poll 的缺点epoll 对于动辄需要处理上万连接的网络服务应用的意义可以说是革命性的。对于普通的本地应用,select 和 poll可能就很好用了,但对于像C10K这类高并发的网络场景,select 和 poll就捉襟见肘了。看看他们的APIint select(int nfds, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval timeout); int poll(struct pollfd fds, nfds_t nfds, int timeout);它们有一个共同点,用户需要将监控的文件描述符集合打包当做参数传入,每次调用时,这个集合都会从用户空间拷贝到内核空间,这么做的原因是内核对这个集合是无记忆的。对于绝大部分应用,这是一种十足的浪费,因为应用需要监控的描述符在大部分时间内基本都是不变的,也许会有变化,但都不大.epoll 对此的改进epoll对此的改进也正是它的实现方式,它需要完成以下两件事描述符添加—内核可以记下用户关心哪些文件的哪些事件.事件发生—内核可以记下哪些文件的哪些事件真正发生了,当用户前来获取时,能把结果提供给用户.描述符添加既然要有记忆,那么理所当然的内核需要需要一个数据结构来记, 这个数据结构简单点就像下面这个图中的epoll_instance, 它有一个链表头,链表上的元素epoll_item就是用户添加上去的, 每一项都记录了描述符fd和感兴趣的事件组合event事件发生事件有多种类型, 其中POLLIN表示的可读事件是用户使用的最多的。比如:当一个TCP的socket收到报文,它会变得可读;当一个pipe受到对端发送的数据,它会变得可读;当一个timerfd对应的定时器超时,它会变得可读;那么现在需要将这些可读事件和前面的epoll_instance关联起来。linux中,每一个文件描述符在内核都有一个struct file结构对应, 这个struct file有一个private_data指针,根据文件的实际类型,它们指向不同的数据结构。那么我能想到的最方便的做法就是epoll_item中增加一个指向struct file的指针,在struct file中增加一个指回epoll item的指针。为了能记录有事件发生的文件,我们还需要在epoll_instance中增加一个就绪链表readylist,在private_data指针指向的各种数据结构中增加一个指针回指到 struct file,在epoll item中增加一个挂接点字段,当一个文件可读时,就把它对应的epoll item挂接到epoll_instance在这之后,用户通过系统调用下来读取readylist就可以知道哪些文件就绪了。好了,以上纯属我个人一拍脑袋想到的epoll大概的工作方式,其中一定包含不少缺陷。不过真实的epoll的实现思想上与上面也差不多,下面来说一下创建 epoll 实例如同上面的epoll_instance,内核需要一个数据结构保存记录用户的注册项,这个结构在内核中就是struct eventpoll, 当用户使用epoll_create(2)或者epoll_create1(2)时,内核fs/eventpoll.c实际就会创建一个这样的结构./ * Create the internal data structure (“struct eventpoll”). /error = ep_alloc(&ep);这个结构中比较重要的部分就是几个链表了,不过实例刚创建时它们都是空的,后续可以看到它们的作用epoll_create()最终会向用户返回一个文件描述符,用来方便用户之后操作该 epoll实例,所以在创建epoll实例之后,内核就会分配一个文件描述符fd和对应的struct file结构/ Creates all the items needed to setup an eventpoll file. That is, a file structure and a free file descriptor./fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));最后就是把它们和刚才的epoll实例 关联起来,然后向用户返回fdep->file = file;fd_install(fd, file);return fd;完成后,epoll实例 就成这样了。向 epoll 实例添加一个文件描述符用户可以通过 epoll_ctl(2)向 epoll实例 添加要监控的描述符和感兴趣的事件。如同前面的epoll item,内核实际创建的是一个叫struct epitem的结构作为注册表项。如下图所示为了在描述符很多时的也能有较高的搜索效率, epoll实例 以红黑树的形式来组织每个struct epitem (取代上面例子中链表)。struct epitem结构中ffd是用来记录关联文件的字段, 同时它也作为该表项添加到红黑树上的Key;rdllink的作用是当fd对应的文件准备好(关心的事件发生)时,内核会将它作为挂载点挂接到epoll实例中ep->rdllist链表上fllink的作用是作为挂载点挂接到fd对应的文件的file->f_tfile_llink链表上,一般这个链表最多只有一个元素,除非发生了dup。pwqlist是一个链表头,用来连接 poll wait queue。虽然它是链表,但其实链表上最多只会再挂接一个元素。创建struct epitem的代码在fs/evnetpoll.c的ep_insert()中if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL))) return -ENOMEM;之后会进行各个字段初始化/ Item initialization follow here … /INIT_LIST_HEAD(&epi->rdllink);INIT_LIST_HEAD(&epi->fllink);INIT_LIST_HEAD(&epi->pwqlist);epi->ep = ep;ep_set_ffd(&epi->ffd, tfile, fd);epi->event = event;epi->nwait = 0;epi->next = EP_UNACTIVE_PTR;然后是设置局部变量epqstruct ep_pqueue epq;epq.epi = epi;init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);epq的数据结构是struct ep_pqueue,它是poll table的一层包装(加了一个struct epitem 的指针)struct ep_pqueue{ poll_table pt; struct epitem epi;}poll table包含一个函数和一个事件掩码typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct );typedef struct poll_table_struct { poll_queue_proc _qproc; unsigned long _key; // store the interested event masks}poll_table;这个poll table用在哪里呢 ? 答案是,用在了struct file_operations的poll操作 (这和本文开始说的selectpoll不是一个东西)struct file_operations { // code omitted… unsigned int (poll)(struct file, struct poll_table_struct); // code omitted…}不同的文件有不同poll实现方式, 但一般它们的实现方式差不多是下面这种形式static unsigned int XXXX_poll(struct file *file, poll_table *wait){ 私有数据 = file->private_data; unsigned int events = 0; poll_wait(file, &私有数据->wqh, wait); if (文件可读了) events |= POLLIN; return events;} 它们主要实现两个功能将XXX放到文件私有数据的等待队列上 (一般file->private_data中都有一个等待队列头wait_queue_head_t wqh), 至于XXX是啥,各种类型文件实现各异,取决于poll_table参数查询是否真的有事件了,若有则返回.有兴趣的读者可以 timerfd_poll() 或者 pipe_poll() 它们的实现poll_wait的实现很简单, 就是调用poll_table中设置的函数, 将文件私有的等待队列当作了参数.static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table p){ if (p && p->_qproc && wait_address) p->_qproc(filp, wait_address, p);} 回到 ep_insert()所以这里设置的poll_table就是ep_ptable_queue_proc().然后revents = ep_item_poll(epi, &epq.pt); 看其实现可以看到,其实就是主动去调用文件的poll函数. 这里以TCP socket文件为例好了(毕竟网络应用是最广泛的)/ * ep_item_poll -> sock_poll -> tcp_poll */unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait) { sock_poll_wait(file, sk_sleep(sk), wait); // will call poll_wait() // code omitted…}可以看到,最终还是调用到了poll_wait(),所以注册的ep_ptable_queue_proc()会执行 struct epitem *epi = ep_item_from_epqueue(pt); struct eppoll_entry *pwq; pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL)这里面, 又分配了一个struct eppoll_entry结构. 其实它和struct epitem 结构是一一对应的.随后就是一些初始化 init_waitqueue_func_entry(&pwq->wait, ep_poll_callback); // set func:ep_poll_callback pwq->whead = whead; pwq->base = epi; add_wait_queue(whead, &pwq->wait) list_add_tail(&pwq->llink, &epi->pwqlist); epi->nwait++; 这其中比较重要的是设置pwd->wait.func = ep_poll_callback。现在, struct epitem 和struct eppoll_entry的关系就像下面这样文件可读之后对于TCP socket, 当收到对端报文后,最初设置的sk->sk_data_ready函数将被调用void sock_init_data(struct socket *sock, struct sock *sk){ // code omitted… sk->sk_data_ready = sock_def_readable; // code omitted…}经过层层调用,最终会调用到 __wake_up_common 这里面会遍历挂在socket.wq上的等待队列上的函数static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key){ wait_queue_t *curr, *next; list_for_each_entry_safe(curr, next, &q->task_list, task_list) { unsigned flags = curr->flags; if (curr->func(curr, mode, wake_flags, key) && (flags & WQ_FLAG_EXCLUSIVE) && !–nr_exclusive) break; }} 于是, 顺着图中的这条红色轨迹, 就会调用到我们设置的ep_poll_callback,那么接下来就是要让epoll实例能够知有文件已经可读了先从入参中取出当前表项epi和ep struct epitem *epi = ep_item_from_wait(wait); struct eventpoll ep = epi->ep; 再把epi挂到ep的就绪队列if (!ep_is_linked(&epi->rdllink)) { list_add_tail(&epi->rdllink, &ep->rdllist) } 接着唤醒阻塞在(如果有)该epoll实例的用户.waitqueue_active(&ep->wq) 用户获取事件谁有可能阻塞在epoll实例的等待队列上呢? 当然就是使用epoll_wait来从epoll实例获取发生了感兴趣事件的的描述符的用户.epoll_wait会调用到ep_poll()函数.if (!ep_events_available(ep)) { / * We don’t have any available event to return to the caller. * We need to sleep here, and we will be wake up by * ep_poll_callback() when events will become available. */ init_waitqueue_entry(&wait, current); __add_wait_queue_exclusive(&ep->wq, &wait); 如果没有事件,我们就将自己挂在epoll实例的等待队列上然后睡去…..如果有事件,那么我们就要将事件返回给用户ep_send_events(ep, events, maxevents) 参考资料the-implementation-of-epoll ...

March 15, 2019 · 3 min · jiezi

深入剖析epoll

select的不足在IO多路复用中select(poll)有诸多限制,很多人会说select的缺点是在Linux内核中,select所用到的FD_SET是有限的,(内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数)。但是更重要的缺点却触及不到:select 采用的是轮询机制,select被调用时fd_set会被遍历,导致其时间复杂度为O(n)。内核/用户空间拷贝问题:当有事件发生,且select轮询完后,fd_set会从内核态拷贝到用户态当并发上来后,轮询的低效率和频繁的内核态用户态切换会导致select的性能急剧下降:epoll的内部调用流程(0) 红黑树是在mmap出的内存上的,减少了用户空间和内核空间的拷贝(1) epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,知道rdlist不空时进程才被唤醒。(2) 文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。(3) ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。(4) ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。(5) ep_send_events函数,它扫描txlist中的每个epitem,调用其关联fd对用的poll方法。之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。(6) 如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的,则将其重新加入回rdlist。否则(ET模式)不在加入rdlist。ET模式与LT模式的不同ET和LT模式下的epitem都可以通过插入红黑树时的回调(ep_poll_callback)方式加入rdlist从而唤醒epoll_wait,但LT模式下的epitem还可以通过txlist(ep_send_events)重新加入rdlist唤醒epoll_wait。所以ET模式下,fd就绪,只会被通知一次,而LT模式下只要满足相应读写条件就返回就绪(通过txlist加入rdlist)。2.1 直接回调插入:fd状态改变才会触发。对于读取操作:(1) 当buffer由不可读状态变为可读的时候,即由空变为不空的时候。(2) 当有新数据到达时,即buffer中的待读内容变多的时候。对于写操作:(1) 当buffer由不可写变为可写的时候,即由满状态变为不满状态的时候。(2) 当有旧数据被发送走时,即buffer中待写的内容变少得时候。2.2 txlist(ep_send_events):fd的events中有相应的事件(位置1)即会触发。对于读操作:(1) buffer中有数据可读的时候,即buffer不空的时候fd的events的可读为就置1。对于写操作:(1) buffer中有空间可写的时候,即buffer不满的时候fd的events的可写位就置1。3 总结与select相比,epoll的回调机制使得资源能够直接使用在有活动的事件上,而不用线性轮询所有的事件。同时 epoll通过内核与用户空间mmap同一块内存,减少了用户空间和内核空间的数据交换,解决了select的重要痛点。另一方面,LT是epoll的默认操作模式,当epoll_wait函数检测到有事件发生并将通知应用程序,而应用程序不一定必须立即进行处理,这样epoll_wait函数再次检测到此事件的时候还会通知应用程序,直到事件被处理。而ET模式,只要epoll_wait函数检测到事件发生,通知应用程序立即进行处理,后续的epoll_wait函数将不再检测此事件。因此ET模式在很大程度上降低了同一个事件被epoll触发的次数,因此效率比LT模式高。

March 10, 2019 · 1 min · jiezi

PHP socket初探 --- 一些零碎细节的拾漏补缺

原文:https://t.ti-node.com/thread/…前面可以说是弄了一系列的php socket和多进程的一大坨内容,知识浅显、代码粗暴、风格简陋,总的说来,还是差了一些细节。今天,就一些漏掉的细节补充一下。一些有志青年可能最近手刃了Workerman源码,对于里面那一大坨stream_select()、stream_socket_server()表示疑惑,这个玩意和socket_create、socket_set_nonblock()有啥区别?其实,php官方手册里也提到过一嘴,socket系函数就是基于BSD Socket那一套玩意搞的,几乎就是将那些东西简单包装了一下直接抄过来用的,抄到甚至连名字都和C语言操控socket的函数一模一样,所以说socket系函数是一种比较低级(Low-Level,这里的低级是指软件工程中分层中层次的高低)socket操控方式,可以最大程度给你操作socket的自由以及细腻度。在php中,socket系本身是作为php扩展而体现的,这个你可以通过php -m来查看有没有socket,这件事情意味着有些php环境可能没有安装这个扩展,这个时候你就无法使用socket系的函数了。但stream则不同了,这货是内建于php中的,除了能处理socket网络IO外,还能操控普通文件的打开写入读取等,stream系将这些输入输出统一抽象成了流,通过流来对待一切。有人可能会问二者性能上差距,但是本人没有测试过,这个我就不敢轻易妄言了,但是从正常逻辑上推演的话,应该不会有什么太大差距之类的。一定要分清楚监听socket和连接socket,我们服务器监听的是监听socket,然后accept一个客户端连接后的叫做连接socket。关于“异步非阻塞”,这五个字到底体现在哪儿了。swoole我就不说了,我源码也才阅读了一小部分,我就说Workerman吧,它在github上称:“Workerman is an asynchronous event driven PHP framework with high performance for easily building fast, scalable network applications.”,看到其中有asynchronous(异步)的字样,打我脸的是我并没有看到有non-block(非阻塞)的字样,不过无妨,脸什么的不重要,重要的是我文章里那一坨又一坨的代码里哪里体现了非阻塞、哪里体现了异步。来吧,看代码吧。看代码前,你要理解异步和非阻塞的区别是什么,因为这二者在表现结果上看起来是有点儿相似的,如果你没搞明白,那么一定要通过这个来理解一下《PHP socket初探 — 关于IO的一些枯燥理论》。<?php// 创建一个监听socket,这个一个阻塞IO的socket$listen = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );socket_bind( $listen, ‘0.0.0.0’, 9999 );socket_listen( $listen );while( true ){ // socket_accept也是阻塞的,虽然有while,但是由于accpet是阻塞的,所以这段代码不会进入无限死循环中 $connect = socket_accept( $listen ); if( $connect ){ echo “有新的客户端”.PHP_EOL; } else { echo “客户端连接失败”.PHP_EOL; }}将上面代码保存了运行一下,然后用telnet可以连接上去。但是,这段代码中有两处是阻塞的,最主要就是监听socket是阻塞的。那么,非阻塞的监听socket会是什么感受?<?php// 创建一个监听socket,将其设置为非阻塞$listen = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );socket_bind( $listen, ‘0.0.0.0’, 9999 );socket_listen( $listen );// ⚠️⚠️⚠️⚠️⚠️⚠️ 这里设置非阻塞!socket_set_nonblock( $listen );while( true ){ $connect = socket_accept( $listen ); if( $connect ){ echo “有新的客户端”.PHP_EOL; } else { echo “客户端连接失败”.PHP_EOL; }}将代码保存了运行一下,告诉我:来来来,分析一波儿,为啥会出现这种现象。因为监听socket被设置成了非阻塞,我们知道非阻塞就是程序立马返回,然后再过段时间回来询问,用例子就是“等馒头过程中,看下微博,抬头问馒头好了吗?然后看下微信,抬头问馒头好了吗?然后看下v2ex,抬头问馒头好了吗?。。。 。。。”,这样你是不是就能理解了?因为并没有客户端连接进来,所以每当询问一次socket_accept后得到的反馈都是“没有连接”,所以就直接走到“客户端连接失败”的分支中去了,而且是不断的不停的。这个时候,你用htop或者top命令查看服务器CPU,不出意外应该是100%,这是非阻塞的极大缺点。紧接着是异步呢?异步体现在哪儿了?我们说异步,是你去阿梅那里买馒头,阿梅告诉你说“馒头还没好,你去干别的吧,好了我打电话通知你”,然后你就专心去打游戏去了,直到电话响了你去拿馒头。Workerman的异步更多是体现在对一个完整请求的处理流上,而不是正儿八经的异步的定义概念,如果你没听明白,那也可能正常,慢慢理解。最后,我补充一句:epoll是同步的,而不是异步。 ...

November 21, 2018 · 1 min · jiezi