轮询、select、 epoll

时间:2021-07-26 06:54:31

  网卡设备对应一个中断号, 当网卡收到网络端的消息的时候会向CPU发起中断请求, 然后CPU处理该请求. 通过驱动程序 进而操作系统得到通知,

系统然后通知epoll, epoll通知用户代码. 

一。 阻塞 VS 非阻塞

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

  我们需要从流中读数据,但是流中还没有数据,典型的例子为,客户端要从socket读如数据,但是服务器还没有把数据传回来,这时候该怎么办?

  • 阻塞:  比如你在等快递,但是你不知道快递什么时候过来,而且接下来的事要等快递来了才能做,那你只能一直等到快递来。
  • 非阻塞轮询。如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他电话:你到了没?

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

二。了解阻塞是如何进行的

当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。

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

  1.假设一开始内核缓冲区是空的,R作为读出方,被阻塞着。然后首先W往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个

事件告诉R该醒来了,这个事件姑且称之为“缓冲区非空”。

  2.但是“缓冲区非空”事件通知R后,R却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,W写入的数据会滞留在内核缓冲区中,

如果内核也缓冲区满了,R仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程W,你该等等(阻塞)了,我们把这个事件

定义为“缓冲区满”。

  3.假设后来R终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉W,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,

我们把这个事件叫做“缓冲区非满”

  4.也许事件Y1已经通知了W,但是W也没有数据写入了,而R继续读出数据,知道内核缓冲区空了。这个时候内核就告诉R,你需要阻塞了!,我们

把这个时间定为“缓冲区空”。

  这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四个术语都是我生造的,仅为解释

其原理而造)。这四个I/O事件是进行阻塞同步的根本。

三。轮询

阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,

1.多进程(fork)

2.多线程(pthread_create)

3.非阻塞忙轮询、无差别轮询、

while true {
  for i in stream[]: {
    if i has data
      read until unavailable
  }
}
我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪

费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(select以及epoll)处理。

四。 select 

  为了避免CPU空转,可以引进了一个代理。这个代理可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事

件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:

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

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

复杂度降低到了O(k),k为产生I/O事件的流的个数。一个epoll模式的代码大概的样子是:

epollfd = epoll_create()
while true {
  active_stream[] = epoll_wait(epollfd)
  for i in active_stream[] {
    read or write till unavailable
  }
}

六。参考

1.文章出处:http://www.zhihu.com/question/20122137

2.epoll API

int epoll_create(int size)

该函数生成一个epoll专用的文件描述符

参数:

  size就是你在这个epollfd上能关注的最大socketfd数

返回文件描述符

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件

参数:

  epfd:由 epoll_create 生成的epoll专用的文件描述符

  op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 、EPOLL_CTL_MOD 、EPOLL_CTL_DEL

  fd:关联的文件描述符

  event:指向epoll_event的指针

调用成功返回0,不成功返回-1

int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)

该函数用于轮询I/O事件的发生

参数:

  epfd: 由epoll_create 生成的epoll专用的文件描述符

  epoll_event:用于回传代处理事件的数组

  maxevents:每次能处理的事件数

  timeout:等待I/O事件发生的超时值;-1相当于阻塞,0相当于非阻塞

返回发生事件数。