多路复用器
概述
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
select
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
...
DESCRIPTION
select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O operation (e.g., input possible). A file descriptor is considered ready
if it is possible to perform a corresponding I/O operation (e.g., read(2) without blocking, or a sufficiently small write(2)).
select() can monitor only file descriptors numbers that are less than FD_SETSIZE; poll(2) does not have this limitation. See BUGS.
select函数监视的文件描述分3类,分别是writefds、readfds和execptfds。调用后函数会阻塞,直到有描述符就绪(有数据 可读 可写 异常或者超时(timeout指定等待时间))函数返回;当select函数返回后,在通过遍历fdset,来找到就绪的描述符。
优点:select目前几乎在所有的平台上支持,
缺点:单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024;可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
...
DESCRIPTION
poll() performs a similar task to select(2): it waits for one of a set of file descriptors to become ready to perform I/O.
The set of file descriptors to be monitored is specified in the fds argument, which is an array of structures of the following form:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
The caller should specify the number of items in the fds array in nfds.
不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
select与poll的弊端:
- 每次都需要重复传递fd
- 每次都要重新遍历全量fd
综上引出epoll;内核开辟空间保留fd
epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
解决select和poll的两个问题:
- 在内核开辟空间,使每次系统调用的时候,不传递全量的fds
- 通过epoll_ctl,让内核在cpu工作时,把IO状态变化的fd放入到ready区,用户程序一旦调用epoll_wait,则直接获取到那些fd。最优的时间复杂度变为O(1)
//创建一个epoll的句柄,size用来控制内核这个监听的数目一共有多大
int epoll_create(int size);
/*
函数是对指定描述符fd执行op操作。
epfd:是epoll_create()的返回值。
op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
fd:是需要监听的fd(文件描述符)
epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待epfd上的io事件,最多返回maxevents个事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
伪代码及日志片段
#伪代码
socket()=3 #返回socket文件描述符3
bind(3, 9090) #把socket和9090端口绑定起来
listen(3) #监听FD 3
epoll_create() = 7 #通过epoll创建内核空间的fd存储空间:假设为7
epoll_ctl(7, ADD, 3, accept); #往空间7里添加对socket3,关注事件为accept。即客户端一旦连接到3这个socket,这个fd就被选中
#用户程序调用内核(APP调用),获取哪些IO状态发生了变化。本方法是阻塞方法,但是可以传参代表超时时间,如阻塞500毫秒,超时返回-1
epoll_wait();
#日志片段
socket(PF_INTE,SOCK_STREAM,IPPROTO_IP) = 4
fcntl(4,F_SETFL,O_RDWR|O_NONBLOCK) = 0
bind(4,{sa_family=AF_INET,sin_port=htons(9090)})
listen(4,50)
...
epoll_create(256) = 7
epoll_ctl(7,EPOLL_CTL_ADD,4,
epoll_wait(7
...
#有连接时 在添加一个文件描述符 8
accept(4,{sa_family=AF_INET,sin_port=htons(53687)}) = 8
epoll_ctl(7,EPOLL_CTL_ADD,8,
#这时 7的空间里 有4和8文件描述符
epoll_wait(7
总结
无论BIO、NIO、多路复用,在linux系统下的网络通信都离不开: socket、bind、listen这三个系统调用。
佐证
java代码示例
public class SocketMultiplexingThread1 {
private ServerSocketChannel server = null;
private Selector selector = null;
public void initServer(){
try{
//实例server 约等于listen状态的fd3
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(9090));
//得到的这个selector是jvm抽象的多路复用器 具体是select、poll还是epoll由不同的系统决定
//open等价于底层epoll_create,在内核开辟了空间,存放所有的文件描述符fd
//假设这里epoll_create 返回的是fd7,fd7代表的就是内核里面存放fd的空间,他自己也是一个fd,即fd7
selector = Selector.open();
//对select、poll来说,JVM开辟了空间,fd传进去
//对epoll,epoll_ctl(fd7, EPOLL_CTL_ADD, fd3, EPOLLIN) EPOLLIN既可能是客户端连接,也可能是数据到达
server.register(selector, SelectionKey.OP_ACCEPT);
}catch (Exception e){
e.printStackTrace();
}
}
public void start(){
initServer();
try{
while (true){
Set<SelectionKey> keys = selector.keys();
/**
* 调用多路复用器
* selector.select(500)
* 对select、poll来说,== 内核的系统调用select(fd3)、poll(fd3)
* 对epoll来说,==内核的系统调用epoll_wait(500)
*/
while (selector.select(500)>0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()){
}else if(key.isReadable()){
}
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
strace追踪… 未完待续…