因为多线程存在上下文切换的,开销很大,所以采用单线程的方案。
可以利用DMA(Data Memory Access)来防止数据丢失。
select、poll、epoll 都是 同步多路IO复用
select 网络模型
一个网络连接对应一个文件描述符。

伪代码如下
while(1){
for(Fdsx in (FdsA--FdsC)){
if(Fdsx 有数据){
读取Fdsx;
处理;
}
}
}
"===" 上面的代码主要是创建文件描述符fds 文件描述符fds其实代表的是一个树,
sockfd=socket(AF_INT,SOCK_STREAM,0);
memset(&addr,0,sizeof(addr));
addr.sin_family=AF_INT;
addr.sin_port=htons(2000);
addr.sin_addr.s_addr=INADDR_ANY;
bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
listen(sockfd,3);
for(int i=0;i<3;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){
FD_ZERO(&rset);
for(int i=0;i<3;i++){
FD_SET(fds[i],&rset);
}
puts("round again");
//后面三个NULL
select(max+1,&rset,NULL,NULL,NULL);
for(int i=0;i<3;i++){
if(FD_ISSET(fds[i],&rset)){
memset(buffer,0,MAXBUF);
read(fds[i],buffer,MAXBUF);
puts(buffer);
}
}
}
select(max+1,&rset,NULL,NULL,NULL); 中max为最大的文件描述符+1,第二个参数&rset为读文件描述符集合,剩下三个参数分别为 写文件描述符集合、异常信息、超时时间。我们最关心的其实是&rest 因为我们是读取网络数据。
select API 接口为:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
&rset 是一个 bitmap 用来表示哪一个文件描述符被启用了或者说被监听了。假设我们有3个请求,3个请求对应的文件描述符为:1,5,9,那bitmap为010001000100000...为"1"就代表第几个文件描述符被占用。
bitmap在select中默认大小是1024位的,也就是说它有1024个“0”或者“1”这样的坑位。

注意这里的FD置位中FD指的其实是rset中对应的那一位,而不是真正的fds中的元素,如果有数据就将FD对应的位置置位为1,并需要把&rset从“用户态”拷贝到“内核态”
select 是阻塞的,只有等select返回了,下面的循环才会执行,执行
FD_ISSET(fds[i],&rset)
FD_ISSET(int fd,fd_set *fdset);用于测试指定的文件描述符是否在该集合中。
然后执行read(fds[i],buffer,MAXBUF)将数据读出来
select 有几个缺点:
1、fd_set(监听的端口个数)的bitmap默认是1024的,虽然我们可以调整它的大小,但是是有上限的,32位机默认是1024个,64位机默认是2048。
2、FDSET 不可重用,每次while 循环 FD_ZERO(&rset)都要重置。
3、从内核态到用户态的切换是有一个开销,无论多少个文件修改,1个文件修改,3个文件修改 都要切换拷贝。
4、不能知道是哪一个文件被修改,select 无法通知是哪一个或者哪几个文件描述符被修改,需要整个重新遍历一遍,这个复杂度是O(n)的。

select有这么多缺点也不需要太纠结,因为这是198几年的API,现在已经被淘汰了。
poll
poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。
#include <poll.h>
int poll(struct pollfd fd[], nfds_t nfds, int timeout);
poll使用方法如下:
//poll 结构体
struct pollfd{
int fd; //文件描述符
short events;//请求的事件
short revents; //返回的事件 默认是0
}
===============================
for(int i=0;i<3;i++){
memset(&client,0,sizeof(client));
addrlen=sizeof(client);
pollfds[i].fd=accept(sockfd,(struct sockaddr*)&client,&addrlen);
pollfds[i].events=POLLIN; //读的话是 POLLIN 写是POLLOUT
}
sleep(1)
==================================
while(1){
puts("round again");
poll(pollfds,3,50000);
for(int i=0;i<3;i++){
if(pollfds[i].revents&POLLIN){
pollfds[i].revents=0; //置位恢复之后,重置为0
memset(buffer,0,MAXBUF);
read(pollfds[i].fd,buffer,MAXBUF);
puts(buffer);
}
}
}
poll 和 select 一样 都需要从“内核态”拷贝到“用户态”,只不过它用的是pollfd,
poll函数的事件标志符值
常量 说明
POLLIN 普通或优先级带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读
POLLOUT 普通数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件
1、有数据会将 pollfd.revents置位位1。
2、poll返回,返回之后 还是会做一个轮询判断
poll 解决了select 的1、2两个缺点,3、4实现原理一样
1、它没有文件限制
2、pollfd 每次重置以后可以继续重复使用。
3、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低;
4、用户空间和内核空间的复制非常消耗资源;
epoll epoll可以理解为event pol
epoll使用方法如下:
struct epoll_event events[3];
int epfd=epoll_create(10);// 这个10 可以任意赋值
...
...
for(int i=0;i<3;i++){
static struct epoll_event ev;
memset(&client,0,sizeof(client));
addrlen=sizeof(client);
ev.events=EPOLLIN; //EPOLLIN 为读 EPOLLOUT为写
epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
}
====================上面为准备阶段=============================
while(1){
puts("round again");
nfds=epoll_wait(epfd,events,3,10000);
for(i=0;i<nfds;i++){
memset(buffer,0,MAXBUF);
read(events[i].data.fd,buffer,MAXBUF);
puts(buffer);
}
}

创建了3个 fd-events ,通过 epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);函数 绑定

利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

这里说的是水平触发,如果设置为边缘触发,稍微有些不同
epoll的置位于select 和poll 有所不同,epoll的置位是“重排”
比如刚开始时 第1、3、4个FD有数据

会将有数据的FD重排 放到前面:

while(1){
puts("round again");
nfds=epoll_wait(epfd,events,3,10000);
for(i=0;i<nfds;i++){
memset(buffer,0,MAXBUF);
read(events[i].data.fd,buffer,MAXBUF);
puts(buffer);
}
}
如上面代码所示 ,epoll_wait会返回有数据的nfds, 如果有2个文件有数据,则nfds=2, 那么循环遍历 i=0 到 2(nfds=2)即可,由于重排过,所以只需要遍历前2个元素就可以了。
有效遍历,解决了select 没有指定是哪一个文件变更的问题,所以复杂度为O(1)
epoll的优点:
1、没有最大并发连接限制,能打开的FD的上限远远大于1024(1G的内存上能监听10万个端口)
2、效率提升,不是轮询方式,不会随着FD数目的增加效率下降,只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管“活跃”的连接,而跟连接总数无关。因此在实际的网络环境中,Epoll的效率会远远高于select和poll。
3、内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递,即epoll使用mmap实现不用从内核态到用户态的拷贝,也就是零拷贝
在linux环境下,大名鼎鼎的redis、nginx、java NIO 都是用的epoll
三个问题:
1、SSD为什么比机械硬盘快
2、哪些DB对SSD做了优化
3、优化的原理是什么
参考地址:
https://www.bilibili.com/video/av68126222
https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/#.XYD0TygzaUl
https://notes.shichao.io/unp/ch6/
https://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/
网友评论