linux epoll浅析

首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。

    不管是文件,还是套接字,还是管道,我们都可以把他们看作流。

    之后我们来讨论I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。现在假定一个情形,我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读如数据,但是服务器还没有把数据传回来),这时候该怎么办?

阻塞:阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。

非阻塞忙轮询:接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:“你到了没?”

    很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。

    大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。

    为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为了减少频繁I/O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。

假设有一个管道,进程A为管道的写入方,B为管道的读出方。

假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。

    但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。

假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”

    也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。

这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。

    然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。

    于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论):

?


1

2

3

4

5

6

while true {

    for i in stream[]; {

        if i has data

            read until unavailable

    }

}

    我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。

    为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:

?


1

2

3

4

5

6

7

while true {

    select(streams[])

    for i in streams[] {

        if i has data

            read until unavailable

    }

}

    于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。

    但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,没一次无差别轮询时间就越长。再次

说了这么多,终于能好好解释epoll了

    epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

    在讨论epoll的实现细节之前,先把epoll的相关操作列出:

epoll_create 创建一个epoll对象,一般epollfd = epoll_create()

epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件

比如

epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入

epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注册缓冲区非满事件,即流可以被写入

epoll_wait(epollfd,...)等待直到注册的事件发生

(注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。

一个epoll模式的代码大概的样子是:

?


1

2

3

4

5

6

while true {

    active_stream[] = epoll_wait(epollfd)

    for i in active_stream[] {

        read or write till

    }

}

    限于篇幅,我只说这么多,以揭示原理性的东西,至于epoll的使用细节,请参考man和google,实现细节,请参阅linux kernel source。

时间: 2024-09-17 22:48:26

linux epoll浅析的相关文章

linux进程调度浅析

操作系统要实现多进程,进程调度必不可少. 有人说,进程调度是操作系统中最为重要的一个部分.我觉得这种说法说得太绝对了一点,就像很多人动辄就说"某某函数比某某函数效率高XX倍"一样,脱离了实际环境,这些结论是比较片面的. 而进程调度究竟有多重要呢? 首先,我们需要明确一点:进程调度是对TASK_RUNNING状态的进程进行调度(参见<linux进程状态浅析>).如果进程不可执行(正在睡眠或其他),那么它跟进程调度没多大关系. 所以,如果你的系统负载非常低,盼星星盼月亮才出现一

linux pi_futex浅析

Priority Inheritance,优先级继承,是解决优先级反转的一种办法. 一个经典的例子:A/B/C三个实时进程,优先级A>B>C.C持有a锁,而A等待a锁被挂起.原本C释放a锁之后,A进程就可以继续执行的,但是偏偏有个比C优先级高的B进程存在,导致C得不到运行,也就没法释放a锁,从而导致A进程一直挂起.从整体上看,进程B虽然比A优先级低,但它却成功的抢占掉了A.这就是所谓的优先级反转. 一种解决办法是优先级继承,C在持有a锁期间临时继承等待者A的优先级,那么B进程就无法从中捣乱了.

linux进程状态浅析

众所周知,现在的分时操作系统能够在一个CPU上运行多个程序,让这些程序表面上看起来是在同时运行的.linux就是这样的一个操作系统. 在linux系统中,每个被运行的程序实例对应一个或多个进程.linux内核需要对这些进程进行管理,以使它们在系统中"同时"运行.linux内核对进程的这种管理分两个方面:进程状态管理,和进程调度.本文主要介绍进程状态管理,进程调度见<linux进程调度浅析>. 进程状态 在linux下,通过ps命令我们能够查看到系统中存在的进程,以及它们的状

linux网桥浅析

什么是桥接? 简单来说,桥接就是把一台机器上的若干个网络接口"连接"起来.其结果是,其中一个网口收到的报文会被复制给其他网口并发送出去.以使得网口之间的报文能够互相转发. 交换机就是这样一个设备,它有若干个网口,并且这些网口是桥接起来的.于是,与交换机相连的若干主机就能够通过交换机的报文转发而互相通信. 如下图:主机A发送的报文被送到交换机S1的eth0口,由于eth0与eth1.eth2桥接在一起,故而报文被复制到eth1和eth2,并且发送出去,然后被主机B和交换机S2接收到.而S

Linux Epoll介绍和程序实例

1. Epoll 是何方神圣?Epoll 可是当前在 Linux 下开发大规模并发网络程序的热门人选, Epoll 在 Linux2.6 内核中正式引入,和 select 相似,其实都 I/O 多路复用技术而已 ,并没有什么神秘的. 其实在 Linux 下设计并发网络程序,向来不缺少方法,比如典型的 Apache 模型( Process Per Connection ,简称 PPC ), TPC ( Thread Per Connection )模型,以及 select 模型和 poll 模型,

linux kswapd浅析

kswapd是linux中用于页面回收的内核线程. 页面回收,并不是回收得越多越好,而是力求达到一种balanced.因为页面回收总是以cache丢弃.内存swap.等为代价的,对系统性能会有一定程度的影响.而balanced,就是既要保证性能,又要应付好新来的页面分配请求. 在讨论kswapd如何工作之前,我们先得搞清楚balanced是如何定义的. 物理内存在kernel中主要有这么几个层次的划分:全体内存.一个NUMA节点的内存.一个NUMA节点中的一个zone的内存(参见<linux内核

linux futex浅析

Futex,Fast Userspace muTEXes,作为linux下的一种快速同步(互斥)机制,已经存在了很长一段时间了(since linux 2.5.7).它有什么优势?又提供了怎样一些功能,本文就简单探讨一下. futex诞生之前 在futex诞生之前,linux下的同步机制可以归为两类:用户态的同步机制 和 内核同步机制. 用户态的同步机制基本上就是利用原子指令实现的spinlock.最简单的实现就是使用一个整型数,0表示未上锁,1表示已上锁.trylock操作就利用原子指令尝试将

Server Develop (六) Linux epoll总结

epoll是Kernel 2.6后新加入的事件机制,在高并发条件下,远优于select.epoll最大的好处在于它不会随着监听fd数目的增长而降低效率.因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多.并且,在linux/posix_types.h头文件有这样的声明:  #define __FD_SETSIZE 1024 //select最多同时监听1024个fd 当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本. 所以在Nginx中采

C++ linux epoll并发服务器模型初探

socket通讯流程图 最简单的可以通讯的C++服务器端代码: #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #define SERV_PORT 8000 int main(void) { int lfd, cfd;