文章

epoll

Java NIO selector底层用的操作系统的epoll机制。

  1. 进程阻塞的原理
  2. 进程监听多个socket
    1. select
    2. epoll
    3. select vs. epoll
  3. 总结

进程阻塞的原理

比如进程等待从一个socket读数据,数据没到,进程阻塞。什么是阻塞?

进程(线程)状态

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

Ref:

  • https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.State.html

阻塞就是进程不在可执行队列(runnable)里,而是被扔到了阻塞队列(blocked)里。CPU只给Runnable里的进程分时间片,所以被阻塞的进程不会被CPU执行到,也不会消耗CPU。

这时候,可以认为等待socket的进程被扔到了该socket相关的阻塞队列。

进程什么时候不再阻塞?怎么做到的?

当网线传来数据到网卡的时候:

  1. 网卡给CPU发了个中断,通知CPU从网卡读数据;
  2. CPU此时将数据从网卡copy到socket对应的内存某处(这是哪个socket的数据?数据有port信息,socket也有port信息,CPU会把该port的数据和该socket对应起来)。该socket相当于有数据可读了;
  3. 然后CPU将该socket的等待队列里的进程删掉,放回runnable队列;

现在,进程可执行了。当抢到CPU的时候,进程就能把该socket的数据从内存里读到应用程序内存了,进而执行应用程序逻辑处理这些数据。

Ref:

  • https://zhuanlan.zhihu.com/p/63179839

进程监听多个socket

比如一个服务器进程,可以和多个client建立socket连接,所以服务器需要监听多个socket。

怎么监听这么多socket?

  • BIO:一个线程监听一个socket,死等该socket的数据;
  • NIO:多路复用。当某个socket有数据的时候,由操作系统“通知”线程去读,线程不再阻塞。

这里所谓的“通知”,所谓的不再阻塞,其实就是OS将其中某个(某些)socket的数据从网卡复制到内存后,将这些socket的等待队列里的进程(线程)重新放回runnable队列。

因为linux是c写的,所以os的行为体现在几个c的函数上。

select

1
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval)

Redis - Client & Server中描述了所谓的文件描述符,可知进程内保留有该进程打开的所有文件信息,即文件描述符表。socket也是文件,所以进程读写socket,其实就是把该socket的文件描述信息放在自己的文件描述符表里。

进程调用select函数,传进来三个socket set,代表要对这些socket文件做的行为。然后阻塞。假设进程要做的只有读socket,os从网卡读完数据到内存之后,将进程踢回runnable,select不再阻塞,进程可以继续执行。

然后进程知道已经有socket的数据可读了,哪个可读?不知道,所以要轮询一下:

1
2
3
4
5
6
7
8
9
	select(max+1, &rset, NULL, NULL, NULL);
 
	for(i=0;i<5;i++) {
		if (FD_ISSET(fds[i], &rset)){
			memset(buffer,0,MAXBUF);
			read(fds[i], buffer, MAXBUF);
			puts(buffer);
		}
	}	

缺点:直观地讲,不知道哪个socket的数据准备好了。所以要遍历所有的socket,O(n)操作。如果socket很多,自然就慢些。

还有一个涉及到select实现细节的缺点:每次都要先把进程加入socket对应的等待队列,某socket数据准备好之后再把进程从所有socket等待队列删除,加入runnable队列。所以涉及对进程处理的所有socket的等待队列进行遍历。也因为此,select的实现规定了一个进程最多监听1024个socket。

Ref:

  • https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/

epoll

主要是epoll_create/epoll_ctl/epoll_wait几个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  struct epoll_event events[5];
  int epfd = epoll_create(10);
  ...
  ...
  for (i=0;i<5;i++) 
  {
    static struct epoll_event ev;
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); 
  }
  
  while(1){
  	puts("round again");
  	nfds = epoll_wait(epfd, events, 5, 10000);
	
	for(i=0;i<nfds;i++) {
			memset(buffer,0,MAXBUF);
			read(events[i].data.fd, buffer, MAXBUF);
			puts(buffer);
	}
  }

哪几个socket有数据,os直接return。这样相当于用户可以以O(1)的时间复杂度获取到有数据的socket,快啊!所以每个进程最多监听1024个socket的限制也取消了。

当然,从epoll的实现细节入手的话,还会发现epoll不用来回来去copy进程舰艇的socket们对应的文件描述符列表。

select vs. epoll

Ref:

  • https://www.jianshu.com/p/31cdfd6f5a48

总之,epoll在监听socket数量多、且真正有数据的socket寥寥无几的情况下,很高效。如果监听socket数量很少,select本身就很快了。如果几乎所有的socket都有数据,那select和epoll也没什么区别了。

总结

其实就是c的网络编程、尤其是linux编程的细节……搞Java的可能对这些细节不是很感冒……了解一下就行……

本文由作者按照 CC BY 4.0 进行授权