IO模块(同步异步,多路复用)
一、IO分类:
- 阻塞和非阻塞IO
在发起读取文件时,应用层会调用系统内核IO接口。
阻塞型IO,系统调用之后,应用层则被挂起,直到等到系统内核从磁盘读取完数据并返回给应用层之后,应用层才继续其他操作;
非阻塞型IO,系统调用之后,系统内核会立即返回,应用层并不会被挂起,它可以做其他任意想做的操作。
- 同步IO/异步IO
同步和异步的区别在于,系统读取完数据之后如何返回给应用层。
对于同步型IO,应用层需要不断向系统内核询问,如果数据没有读取完毕,应用层根据阻塞和非阻塞类型,要么挂起要么去做其他任意的事;如果读取完毕,当应用层再次询问时,系统内核则将数据返回给应用层,应用层获得数据。(系统内核被动返回读取信息)
对于异步型IO,应用层不需要向系统内核进行询问,在系统内核读取完成之后,会主动通知应用层数据读取完毕,应用层即可接收系统内核返回的数据。(系统内核主动返回读取信息)
二、Reactor模式和Proactor模式
Reactor模式(基于同步IO)
- 线程等待读取socket数据,将socketfd添加到事件分派器中,如添加到epoll
- 事件分派器阻塞等待socketfd可读事件发生
- 若数据到达,socketfd变成可读状态,事件分派器通知线程处理
- 线程阻塞完成socket读数据
Proactor模式(基于异步IO)
- 线程等待读取socket数据,将存储事件的缓冲区和读事件请求交给事件分派器;
- 事件分派器等待socket数据到达;
- 若数据到达,事件分派器不通知线程读取,二是直接完成数据的读取;
- 通知线程数据读取完成,并已经存入提供的缓冲区中。
区别:Reactor是数据可读的时候通知线程,Proactor是数据读取完成之后通知线程
三、三种IO复用类型
- Select系统调用
#include<sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* execptfds,struct timeval* timeout);
#nfds表示监听的文件描述符总数;
#readfds,writefds,execptfds分别表示对应的fd_set类型的集合
可以如下定义:fd_set readfds,writefds,execptfds
#timeout表示select函数的超时时间
Struct timeval
{
Long tv_sec;
Long tv_usec;
}
如果timeval成员变量均为0,则select立即返回;如果timeout设置为NULL,则select将一直阻塞,直到某个文件描述符就绪。
# FD_ZERO(fd_set *fdset);清楚fdset的所有位,如FD_ZERO(&readfds);
#FD_SET(int fd,fd_set* fdset);设置fdset的位,也就是将某个文件描述符加入到fdset中,如FD_SET(0,&readfds),将标准输入加入到fdset中
#int FD_ISSET(int fd,fd_set * fdset);测试fdset的某个位是否被设置,也就是测试文件描述符中的fd是否有事件发生
#select成功时返回就绪(可读,可写和异常事件)文件
- Poll系统调用
#include<poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);
#fds是pollfd类型的数组
Struct pollfd
{
int fd;//文件描述符
int events;//注册该文件描述符要监听的事件
short revents;//由内核修改,判断该文件描述符实际发生的事件
}
#nfds表示文件描述符数组的大小
#timeout指定poll的超时值;当timeout位-1时,poll调用将永远阻塞;当timeout为0时,poll调用将立即返回。
例:
pollfd fds[2];//pollfd的数组
fds[0].fd=0;//注册标注输入
fds[0].events=POLLIN;//监听输入事件
fds[0].revents=0;//初始化
fds[1].fd=sockfd;//注册网络socket文件描述符
fds[1].events=POLLIN | POLLRFHUP;//监听
fds[1].revents=0;
int ret=poll(fds,2,-1);
#poll调用成功时返回就绪(可读,可写和异常事件)文件
- Epoll系统调用
#include<sys/epoll.h>
int epoll_create(int size);
#epoll把文件描述符放在内核的一个时间表中
#改函数注册内核事件表需要创建多大size
#返回值作为内核事件表的索引
#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout)
#在一段时间内timeout等待一组文件描述符上的事件
#epfd表示上面注册的内核事件表的索引
#events用于存储内核事件表中就绪的事件,将内核事件表中就绪的事件复制到events中,events只用来输出epoll_wait检测到的就绪事件
#maxevents指定epoll_wait最大监听多少个事件
#timeout
#返回值表示就绪的文件描述符的个数,且就绪的文件描述符就存储于events中,直接遍历events[0,ret-1]
# struct epoll_event
{
_uint32_t events;
//表示注册的文件描述符索要监听的事件,与pollfd结构体中events类似(E)
epoll_data_t data;
}
#typedef union epoll_data//联合体
{
void *ptr;
int fd;//最常用fd,指向目标文件描述符
unint32_t u32;
uint64 u64;
}
#include<sys/epoll.h>
int epoll_ctl(int epfd,int op,struct epoll_event *event);//操作内核事件表
- op选项:EPOLL_CTL_ADD,往事件表中注册fd事件
EPOLL_CTL_MOD,修改fd上的注册事件
EPOLL_CTL_DEL,删除fd上注册的事件
event表示对内核事件表进行操作的具体情况
#向内核事件表中注册事件
void addfd(int epollfd,int fd)//epollfd表示内核事件表索引fd表示事件描述符
{
epoll_event event;
event.events=EPOLLIN;
event.data.fd=fd;//将文件描述符添加到内核事件表中
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
}
例:
epoll_event events[MAX_EVENT_NUMBER];
int epollfd=epoll_create(5);//注册大小为5的内核事件表
assert(epollfd!=-1);
addfd(epollfd,int fd);//将文件描述符fd添加到内核事件表
while(1)
{
int number=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1)
for(int i=0;i<number;i++)
{
//遍历监听到的事件
int sockfd=events[i].data.fd;
if(sockfd==listenfd)
//对于socket,如果是监听端口有事件到来,则创建新的连接
{
struct sockaddr_in client_address;
socklen_t client_addrlength=sizeof(client_address);
int connfd=accept(listenfd,(struct sockaddr*) &client_address,&client_addrlength) ;//从监听端口中取出事件
addfd(epollfd,connfd);//将事件注册到内核事件表中
}
else if(events[i].events & EPOLLIN)//表示客户端有数据发送到服务端
{
//接收客户端的数据
}
}
}