文章目录
IO多路复用
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
bitmap
bitmap使用bit来存一些不同的数字,如存1~9中的1,3,5,6,则bitmap只需要10位置,对应1,3,5,6位为1,即[1,0,1,0,1,1,0,0,0],这样可以节省空间。
select
文件描述符
在Linux系统中,所有进程都是文件,用文件描述符的形式表示。一个文件描述符可以对应一个连接。文件描述符的范围在32位系统中,最大值为1024个,而在64位系统中,最大值为2048个。可以调用下述命令查看
cat /proc/sys/fs/file-max
select指令
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
- 参数
nfds:文件描述符的最大值
readfds:监视的文件描述符集(bitmap),以查看是否有数据可读取
writefds:监视的文件描述符集(bitmap),以查看写操作是否将完成而不会阻塞。
exceptfds:监视的文件描述符集(bitmap),以查看是否发生异常
timeout:超时时间 - 参数可以为NULL,在这种情况下,select()不会监视
- 对select()的调用将一直阻塞,直到给定的文件描述符准备好执行I / O为止,或者直到经过可选的指定超时为止
- 成功返回后,将修改对应的变化的集(置位)
如果返回值为-1,表明发生了错误
如果返回值为0,表明超时了
如果返回值为正数,表明有n个fd准备就绪了
`
实现IO多路复用
for (i=0;i<5;i++)
{
memset(&client, 0, sizeof (client));
addrlen = sizeof(client);
//获得文件描述符的编号
fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);
//获得最大文件描述符的编号
if(fds[i] > max)
max = fds[i];
}
while(1){
//rest置0
FD_ZERO(&rset);
for (i = 0; i< 5; i++ ) {
//根据文件描述符对rset置为
FD_SET(fds[i],&rset);
}
// 内核会将有数据的rset置为
select(max+1, &rset, NULL, NULL, NULL);
for(i=0;i<5;i++) {
//找到就绪(被置位)的文件描述符,进行IO操作
if (FD_ISSET(fds[i], &rset)){
memset(buffer,0,MAXBUF);
read(fds[i], buffer, MAXBUF);
puts(buffer);
}
}
}
return 0;
分析:
- 判断IO是否有数据需要切换到内核态,因此每次select()需要对rset进行复制(如果不复制则用户态每次读取rset都要切换到用户态,更加频繁),开销很大。
- 每次循环都要对FSET置位,使得FSET不可重用。
- 每次需要O(n)的复杂度去检查rset中fds数组中哪一个对应的rset被置位了。
- 单个进程可监控的fd数量有限制。
poll
poll指令
struct pollfd {
int fd; //文件描述符
short events; //事件比如:读;写;读写
short revents;
};```
```c
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
- 参数
fds:文件描述符数组
nfds:文件描述符的数量
timeout:超时时间 - 在poll函数中对就绪的文件描述符对应结构体的revent进行置位而不是event
实现IO多路复用
for (i=0;i<5;i++)
{
memset(&client, 0, sizeof (client));
addrlen = sizeof(client);
//获取文件描述符
pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
//文件描述符(连接)的事件为读
pollfds[i].events = POLLIN;
}
sleep(1);
while(1){
puts("round again");
poll(pollfds, 5, 50000);
for(i=0;i<5;i++) {
if (pollfds[i].revents & POLLIN){
//置位,可复用
pollfds[i].revents = 0;
memset(buffer,0,MAXBUF);
read(pollfds[i].fd, buffer, MAXBUF);
puts(buffer);
}
}
}
分析:
- fds数组没有大小限制(和select的bitmap相比)。
- 置位操作只对revents,因此每次循环只需要恢复revents即可,可以复用。
- 依然无法解决select的用户态到内核态复制fds数组,以及O(n)的复杂度检查每一个文件描述符。
epoll
是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll接口
Epoll 系统调用在内核中创建和管理上下文,将任务分为3个步骤:
- 使用epoll_create在内核中创建上下文
- 使用epoll_ctl向/从上下文中添加和删除文件描述符
- 使用epoll_wait等待上下文中的事件
epoll_create
int epoll_create(int size);
- 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。epoll_create会在内核建立一颗专门用来存放fd结点的红黑树,后续如果有新增的fd结点,都会注册到这个epoll红黑树上(提高了描述符集合注册和删除操作的效率)。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数
epfd:epoll_create()的返回值
op:操作,用三个宏表示:1)注册新的fd到epfd中;2)修改已经注册的fd的监听事件;3)从epfd中删除一个fd;
fd:需要监听的文件描述符
event:需要监听什么事件,用一个结构体表示。(和poll比没有revent) - select和poll会一次性将监听的所有fd都复制到内核中,而epoll不一样,当需要添加一个新的fd时,会调用epoll_ctr,给这个fd注册一个回调函数,然后将该fd结点注册到内核中的红黑树中。当该fd对应的设备活跃时,会调用该fd上的回调函数,将该结点存放在一个就绪链表中。
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- 参数:
epfd:epoll_create()的返回值
event:需要监听什么事件,用一个结构体表示。(和poll比没有revent)
maxevents:告之内核这个(所有event的个数)events的大小,不能大于创建epoll_create()时的size。
timeout:超时时间 - 返回结果是需要处理事件的个数,0表示超时。
- epoll_wait的做法也很简单,其实直接就是从就绪链表中取结点,这也解决了轮询的问题,时间复杂度变成O(1)。
实现IO多路复用
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;
// 将ev添加到epfd中,注册回调事件
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);
}
}
工作模式(水平触发和边缘触发)
LT模式(默认):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
参考
select、poll、epoll之间的区别(搜狗面试)
浅谈select,poll和epoll的区别
【并发】IO多路复用select/poll/epoll介绍
https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/#.XYD0TygzaUl