五种IO模型
阻塞:为了完成功能发起调用,但是如果当前不具备完成条件,则等待
非阻塞:为了完成功能发起调用,但是如果当前不具备完成条件,则报错返回
阻塞与非阻塞区别:不具备完成条件的情况下,发起调用是否会立即返回
同步:为了完成功能发起调用,但是当前不具备完成条件,则等待,直到完成功能.
异步:为了完成功能发起调用,如果当前不具备完成条件,则立即返回.(将完成功能交给操作系统,当操作完成功能则通过一些其他方式,如信号通知告诉我们功能完成)
同步于异步的区别 :要完成某个功能,在条件不具备的情况下,是否会阻塞完成
1.阻塞IO
2.非阻塞IO
非阻塞IO的设置:int fcntl(int fd,int cmd,.../*arg*/)
fd : 文件描述符
cmd:F_GETFL 获取属性状态(通过返回值返回当前属性)
F_SETFL设置属性状态(替换原有属性为当前arg属性)
arg:要设置的属性
O_NONBLOCK 设置描述符为非阻塞
返回值:F_GETFL:当前属性
F_SETFL:成功:0 失败-1
3.信号驱动IO
4.多路转接(多路复用)IO
让别人来替我们监控整个等待过程,看现在有哪一个就绪好了,直接完成相应的操作,让多路转接模型替用户完成监控多个描述符的等待过程,如果哪一个描述符就绪了,则完成监控过程返回并通知我们有那些描述符就绪了。
select多路转接模型
select转接模型执行流程:
select建立监控集合,对描述符监控指定的事件(可写,可读,异常),向这三个集合中添加描述符(集合实际是一个位图,添加描述符就是修改位图相应的比特位,位图大小取决于FD_SETSIZE 1024),对描述符关注什么状态就添加到指定的集合中,将集合中的数据拷贝到内核中进行监控(间隔事件,轮询遍历,判断是否有描述符就绪),
当监控集合中的某个描述符就绪了(缓冲区有一个低水位标记,可读就是当接受缓冲区中的数据大小达到低水位标记大小则可读;可写就是当缓冲区的剩余空间大小大于低水位标记时则可写),并且告诉我们有几个描述符就绪,
如果编译没有描述符就绪,则继续休眠等待或判断是否超时,超时则返回0.如果有描述符 就绪,将监控集合中没有就绪的描述符从集合中移除(因为对集合进行了修改,因此需要每次清空集合,向集合中重新添加描述符).遍历0–nfds的描述符,判断是否在集合中,为了找到具体哪个描述符就绪,然后对就绪的描述符进行相应操作
- 接口:int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *excepthfds,struct timeval *timeout)
nfds:描述符的最大个数
timeout:超时等待事件
NULL:没有描述符就绪则永久阻塞等待
tv_sec 秒
tv_usec 微秒
返回值:<0:出错 ;==0 等待超时 ;>0 就绪的描述符个数
fd_set是描述符集合,最多只能添加1024个描述符,select对描述符进行监控,分了三种监控,分别是可写事件,可读事件,异常事件.如果想要对描述符进行某个事件的监控,则需要将描述符添加到指定集合中,这个集合实际是一个位图,位图大小默认是1024(long int fds_bits[1024/sizeof(long int)]),添加指定描述符到集合中,就是置描述符数据所对应的比特位为1
在tcp服务端程序中,因为我们不知道何时该接受数据,或者何时accept获取新连接,因此在没有数据到来或者没有连接请求到来的时候,调用recv或accept就会阻塞,导致服务端程序不然只能连续处理一个客户端的数据,要不然每个客户端就只能处理一次.
假如我们知道客户端数据或连接请求什么时候来,我们在合适的时候调用recv/accept则不会造成阻塞,并且可以实现谁的数据到来,处理谁的数据,达到高并发服务端程序的编写.
select优缺点:
缺点:
- 能够监控的描述符有最大上限
- 因为select判断集合中描述符就绪后会修改集合内容,因此需要每次重新添加描述符(编程麻烦,效率低)
- 因为select不会告诉我们具体哪个描述符就绪,因此需要用户进行遍历判断(编程麻烦,随着描述符增多,效率降低)
- 因为select每次都需要将集合数据拷贝到内核,并且在内核中是轮询遍历实现监控,因此性能就会随着描述符增多而降低
优点:
1.跨平台
2.超时时间的控制比较精细,可以精细到微秒
poll
相较于select的优点:
1.描述符无上限
2.监控集合只有一个,每个节点可以关注不同的事件,不用针对不同的事件进行多次遍历
缺点:
select的所有缺点
epoll模型
epoll是Linux下实用度最高,性能最高的多路转接模型。
-
epoll_create 创建epoll
-
epoll控制(事件的添加/移除/修改)
epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
epfd:epoll句柄
op:对epoll的操作
EPOLL_CTL_ADD 添加事件
EPOLL_CTL_MOD 修改事件
EPOLL_CTL_DEL 移除事件
fd:要监控的描述符
event:监控描述符对应事件信息
struct epoll_event{
_unit32_t events;
union epoll_data_t data{
void *ptr;
int fd;
}
}
返回值:成功返回0,失败返回-1 -
epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout) 开始监控,返回的时候将就绪的节点事件拷贝一份到数组中给用户使用。
epfd:epoll句柄
events:事件结构体数组(用于就绪事件)
maxevents:数组大小
timeout:epoll阻塞超时时间 -1:永久阻塞
返回值:<0:出错 ==0:超时 >0:就绪事件数量
epoll模型工作流程:
1.每个epoll_create都会创建epollevent结构体,结构体中包含就绪事件双向链表和红黑树事件集合(快速去重,快速避免事件的重复添加,用于在内核中保存事件节点)
2.向事件中添加事件节点
3.epoll采用事件回调机制来实现监控,epoll节点所关注的事件触发后,回调函数中实现将红黑树中对应的节点指针,向rdlist双向链表中添加一份。
4.epoll每隔一段时间,只需要来看一下双向链表是否为空,可快速判断是否有描述符,一旦判断有事件的描述符就绪,则将就绪事件拷贝到用户态供用户操作,即直接向双向链表中的节点中拷贝一份epollevent结构体,意味着用户拿到的节点都是就绪的节点,可以直接处理。
epoll优缺点:
优点:
1.描述符无上限
2.每个事件只需要向内核拷贝一次
3.epoll在内核中使用事件回调将就绪事件添加到双向链表,每隔一会判断双向链表是否为空来判断是否有描述符就绪,性能不会随描述符而降低
4.epoll直接将就绪的事件拷贝给用户,用户直接拿到就绪事件即可操作,不用从所有事件中找就绪事件,相对于select性能也有所提高
缺点:
1.相较于select,无法跨平台
2.阻塞事件不够精细
多路转接的适用场景:应用于有大量连接,但同一时间只有少量连接活跃的场景。因为多路转接是并发处理请求的。
epoll的两种触发方式
边缘触发EPOLLET:每条新数据的到来,将就绪返回一次(并不管缓冲区数据是否大于低水位标记)
水平触发EPOLLLT:只要缓冲数据大于低水平标记,就会触发就绪事件
epoll有两种触发模式可选,默认是水平触发。但是select只有水平触发模式,一旦epoll的触发模式被设置为边缘触发,将要求用户每次都将缓冲区中的数据读完进行处理。(因为不触发就绪,那么socket按照程序逻辑就不会多次处理),若数据过多读不完,数据不全,有可能无法处理,要一次读完的话,多读几次,但是多读几次就可能会阻塞。但是在并发模型中不能存在阻塞情况,因此要将描述符设置为非阻塞状态。
5.异步IO