以下是阿鲤对五种常用IO的总结,希望可以帮助到大家。
一:同步异步&阻塞非阻塞
二:五种IO模型
三:fcntl函数
四:select,poll,epoll
一:同步异步&阻塞非阻塞a:
1:同步和异步:
同步:在发出一个调用,自没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到一个返回值;也就是得到了结果
异步:和同步相反,这个调用会直接返回,调用者不会立即得到结果,当有结果时,被调用者会通过状态,通知或回调函数通知调用者。
对于同步和异步的理解是同步是自己来做这件事;而异步是交给别人做这件事,做好之后通知你。
2:阻塞和非阻塞:
阻塞:是指调用结果返回之前,当前线程会被挂起,只有得到结果之后才会返回。
非阻塞:不能立即得到结果之前,该调用不会阻塞当前线程。
注:大家不要把同步异步和阻塞非阻塞搞混,这是两个概念。
二:五种IO模型
1:阻塞IO:
再内核将数据准备好之前,系统调用会一直等待;(所有的套接字默认都是阻塞方式)
2:非阻塞IO:
如果内核未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。(非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程成为轮回。对cpu来说是很大的浪费,往往只在特定的场景使用)
3:信号驱动IO:
如果将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
4:IO多路转接:
相较于信号驱动IO,多路转接IO能够同时等待多个文件描述符的就绪状态;对描述符进行监控,避免了因为描述符没有就绪而导致的阻塞。下面我们会介绍select,poll,epoll模型。
5:异步IO:
由内核在数据拷贝完成时,通知应用程序。
三:fcntl函数
fcntl是计算机中的一种函数,通过fcntl可以改变已打开的文件性质。fcntl针对描述符提供控制。参数fd是被参数cmd操作的描述符。针对cmd的值,fcntl能够接受第三个参数int arg。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
对于传入不同cmd和不同的参数,fcntl也有不同的功能:复制一个现有描述符 (cmd=F_DUPFD);获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD);获得/设置文件状态标记(md=F_GETFL或F_SETFL);获得/设置异步IO状态标记(cmd=F_GETOWN或cmd=F_SETTOWN);获得/设置异步IO所有权;获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW))
eg:
基于fcntl函数实现一个set_no_block函数。
void set_no_block(int fd) {
int fl = fcntl(fd, F_GETFL);//获取文件属性
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);//在原文件属性的基础上加上非阻塞
}
int main() {
set_no_block(0);
while (1) {
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0) {
perror("read");
sleep(1);
continue;
}
printf("input:%s\n", buf);
}
return 0;
}
结果:证明为非阻塞
四:select,poll,epoll
1:select:
操作流程
1:定义某个事件的描述符集合,初始化清空集合
2:添加关心的描述符事件(读,写,异常)
3:发起监控调用(将集合拷贝到内核中进行轮询遍历监控)
4:监控调用返回(储错,就绪,超时),并移除未就绪的描述符,因此每次监控都需要重新添加集合
5:程序员判断哪个描述符依旧在集合中,就确定这个描述符就绪了。
接口:
#include<sys/select.h>
void FD_ZERO(fd_set *set);//初始化清空集合
void FD_SET(int fd, fd_set *set);//将fd描述符添加到set集合中
int select(int nfds, fd_set *readfds, fd_set *writerfds, fd_set *exceptfds, struct timeval *timeout);//发起监控调用
nfds:是监控的最大文件描述符+1(减少遍历次数,提高效率);
readfds:writerfds,exceptfds:分别是可读描述符,可写描述符,异常文件描述符的集合;其内部实现为一个位图,添加描述符只需要将相关的比特位置为1即可。因此select所能监控的大小取决于宏_FD_SETSIZE的大小(默认1024byte)。
timeout:为结构timeval,用来设置select()的等待时间。通过这个事件这是阻塞或非阻塞,和遍历事件。设置null为阻塞
返回值:>0:就绪描述符个数,=0:无就绪;<0:超时返回
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
int FD_ISSET(int fd, fd_set *set); // 判断fd是否在集合中
void FD_CLR(int fd, fd_set *set); // 从set集合中删除fd
优缺点分析:
缺点:1:select对描述符进行监控有最大数量上限,上限取决于宏:_FD_SETSIZE(默认大小1024)
2:在内核中轮询便利监控,性能会因为描述符的大小而下降
3:只能返回就绪的集合,需要轮询遍历判断才能得知那个描述符就绪
4:每次监控都需要重新添加描述符,且需要重新将集合重新拷贝到内核
优点:1:遵循posix标准,跨平台移植性好
2:poll
操作流程:
1:定义监控的描述符事件结构体数组,将需要监控的描述符以及事件标识信息,添加到数组的各个节点中
2:发起调用,开始监控;将描述符事件结构体数组拷贝到内核中进行轮询便利,若有就绪/等待超时则调用返回,并且对每个描述符对应的事件结构体中,标识当前就绪的事件
3:进程轮询遍历数组,判断数组中的每个节点中是否有就绪事件,决定是否就绪以及如何对描述符操作
接口:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:事件结构体数组,填充要监控的描述符信息以及事件信息
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
nfds:数组中有校节点个数(防止无效遍历)
time_out:监控超时等待时间
返回值:>0:就绪描述符个数;=0:等待超时;<0:监控出错
优缺点分析:
优点:1:使用事件结构体监控,简化了select中三种事件集合的操作流程
2:监控的描述符数量没有最大限制
3:不需要每次重新定义事件节点
缺点:1:跨平台移植性差
2:每次监控需要向内核中拷贝监控数据
3:在内核中监控依然采用轮询遍历,性能会随着描述符的增多而下降
3:epoll
操作流程:
1:内核中创建epoll句柄epollevent结构体(红黑树+双向链表)
2:对内核中的epollevent结构添加/删除/修改所监控的描述符监控信息
3:发起调用开始监控,在内核中采用异步阻塞实现监控,等待超时/就绪,返回给用户就绪描述符的事件结构体信息
4:进程直接对就绪的事件结构体中的描述符成员进行操作即可
接口:
#include <sys/epoll.h>
int epoll_create(int size);//创建epoll句柄
//size:在linux2.6.2之后被忽略,动态增长,只要大于0就行
//返回值:epoll的操作句柄
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epfd:epoll_create创建的操作句柄
//op:针对fd描述符的监控信息要进行的操作 EPOLL_CTL_ADD:添加
EPOLL_CTL_MOD:修改
EPOLL_CTL_DEL:删除
//fd:要监控操作的描述符
//event:fd描述符对应的事件结构信息
//struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
// typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//epfd:epoll操作句柄
//events:struct epoll_event结构体数组首地址,用于接收就绪描述符对应的事件结构体信息
//maxevents:本次监控想要获取的就绪事件的最大数量,不大与evs数组的节点个数,防止访问越界
//timeout:超时等待时间,ms
//返回值:返回值:>0:就绪描述符个数;
=0:等待超时;
<0:监控出错
epoll的监控原理:
监控由系统完成,用户添加监控的描述符以及对应事件的结构体会被添加到eventpoll结构体中的红黑树中,一旦发起调用开始监控,则操作系统为每个描述符的事件做了一个回调函数,功能是当描述符就绪了关心事件,则将描述符对应的事件结构体添加到双向链表中;进程自身只是每隔一段时间,判断双向链表是否为null,决定是否就绪。
epoll对描述符就绪事件的触发原理:
水平触发:EPOLLT 默认 ,select和poll只支持水平触发
可读事件:只要接收缓冲区中数据大小大与低水位标记(默认为1),则会触发可读事件就绪
可写事件:只要发送缓冲区中剩余空间大小大与低水位标记,则会触发可写事件就绪
边缘触发:EPOLLET只有epoll支持
可读事件:只有新数据到来的时候才会触发可读事件就绪(不管上次的数据有没有读完,缓冲区有没有遗留数据)
可写事件:只要发生缓冲区中剩余空间大小大与低水位标记,则会触发可写事件就绪
两种触发方式比较:边缘出发主要是为了避免水平触发导致程序不断的对误操作事件进行大量的遍历判断,但是边缘触发要求我们在一次触发中完成所有操作
优缺点分析:
优点:1:监控的描述符无上限
2:监控采用异步阻塞操作,性能不会随着描述符的增多而下降
3:直接给进程提供就绪的事件以及描述符进行操作,不需要进程进行空遍历操作
4:描述符的事件信息,只需要向内核拷贝一次
5:给进程返回的就绪事件信息,通过内存映射完成,节省了数据拷贝的过程
缺点:1:无法跨平台
eg:
/****************************************************
* 封装一个epoll服务器,只考虑可读事件就绪
* belongal
****************************************************/
#pragma once
#include<vector>
#include<functional>
#include<sys/epoll.h>
#include<stdio.h>
#include"tcp_socket.hpp"
typedef void(*Handler) (const std::string&, std::string&);
class Epoll{
public:
Epoll(){
m_epoll_fd = epoll_create(10);
}
~Epoll(){
close(m_epoll_fd);
}
//往epoll中添加描述符
bool Add(const TcpSocket &sock)const {
int fd = sock.GetFd();
printf("[EPOLL ADD] fd = %d\n", fd);
epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN;//可读
int ret = epoll_ctl(m_epoll_fd, EPOLL_CTL_ADD, fd, &ev);
if(ret < 0) {
perror("epoll_ctl ADD");
return false;
}
return true;
}
//删除epoll中的描述符
bool Del(const TcpSocket &sock)const {
int fd = sock.GetFd();
printf("[EPOLL DEL] fd = %d\n", fd);
int ret = epoll_ctl(m_epoll_fd, EPOLL_CTL_DEL, fd, NULL);
if(ret < 0){
perror("epoll_ctl DEL");
return false;
}
return false;
}
//获取准备好的描述符
bool Wait(std::vector<TcpSocket> &output)const {
output.clear();
epoll_event events[1000];
int nfds = epoll_wait(m_epoll_fd, events, sizeof(events)/sizeof(events[0]), -1);
if(nfds < 0){
perror("epoll_wait");
return false;
}
for(int i = 0; i < nfds; ++i) {
TcpSocket sock(events[i].data.fd);
output.push_back(sock);
}
return true;
}
private:
int m_epoll_fd;
};
class TcpEpollServer {
public:
TcpEpollServer(const std::string ip, uint16_t port):
m_ip(ip),
m_port(port){}
bool start(Handler handler){
//创建套接字
TcpSocket listen_sock;
CHECK_RET(listen_sock.Socket());
//绑定地址信息
CHECK_RET(listen_sock.Bind(m_ip, m_port));
//监听
CHECK_RET(listen_sock.Listen());
//创建epoll对象,并把listen_sock添加进去
Epoll epoll;
epoll.Add(listen_sock);
//对事件进行循环判断
while(true){
std::vector<TcpSocket> output;
if(!epoll.Wait(output)){
continue;
}
//根据文件描述符的种类决定如何处理
for(size_t i = 0; i < output.size(); ++i){
if(output[i].GetFd() == listen_sock.GetFd()){
TcpSocket new_sock;
CHECK_RET(listen_sock.Accept(new_sock));
epoll.Add(new_sock);
}else{
std::string req, resp;
bool ret = output[i].Recv(req);
if(!ret){
epoll.Del(output[i]);
output[i].Close();
continue;
}
handler(req, resp);
output[i].Send(resp);
}
}
}
return true;
}
private:
std::string m_ip;
uint16_t m_port;
};