五种IO模型的介绍(内附epoll服务器的简单实现)

以下是阿鲤对五种常用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_GETFLF_SETFL);获得/设置异步IO状态标记(cmd=F_GETOWN或cmd=F_SETTOWN);获得/设置异步IO所有权;获得/设置记录锁(cmd=F_GETLK,F_SETLKF_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:监控出错

events:
 
EPOLLIN:表示对应的文件描述符可以读(包括对端socket正常关闭)
EPOLLOUT:表示对应的文件的描述符可写  
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有外带数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应文件描述符被挂断
EPOLLET:将EPOLL设置为边缘触发
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监控这个socket的话需要再次把这个socket加入到EPOLL队列里   
 

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;
};

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux IO 模型是指 Linux 操作系统中的 IO 处理机制。它的目的是解决多个程序同时使用 IO 设备时的资源竞争问题,以及提供一种高效的 IO 处理方式。 Linux IO 模型主要分为三种:阻塞 IO、非阻塞 IOIO 多路复用。 阻塞 IO 指的是当程序进行 IO 操作时,会被挂起直到 IO 操作完成,这种方式简单易用,但是对于高并发环境不太适用。 非阻塞 IO 指的是程序进行 IO 操作时,如果无法立即完成,会立即返回一个错误码,程序可以通过循环不断地进行 IO 操作来实现轮询的效果。非阻塞 IO 可以提高程序的响应速度,但是会增加程序的复杂度。 IO 多路复用指的是程序可以同时监听多个 IO 设备,一旦有 IO 事件发生,就会立即执行相应的操作。IO 多路复用可以提高程序的效率,但是需要程序员手动编写代码来实现。 Linux IO 模型还有其他的实现方式,比如信号驱动 IO 和异步 IO 等。但是这些方式的使用比较复杂,一般不常用。 ### 回答2: Linux中的IO模型是指操作系统在处理输入输出的过程中所遵循的一种方式。它主要包括阻塞IO、非阻塞IO、多路复用IO和异步IO四种模型。 阻塞IO是最简单IO模型,当一个IO操作发生时,应用程序会被阻塞,直到IO操作完成才能继续执行。这种模型的特点是简单直接,但是当有多个IO操作时会造成线程的阻塞,影响系统的性能。 非阻塞IO是在阻塞IO的基础上发展而来的,应用程序在发起一个IO操作后可以继续执行其他任务,不必等待IO操作的完成。但是需要通过轮询来不断地检查IO操作是否完成,效率相对较低。 多路复用IO使用select、poll、epoll等系统调用来监听多个IO事件,当某个IO事件就绪时,应用程序才会进行读写操作,避免了前两种模型的效率问题。多路复用IO模型适用于连接数较多时的场景,如服务器的网络通信。 异步IO是最高效的IO模型,应用程序发起一个IO操作后,立即可以执行其他任务,不需要等待IO操作的完成。当IO操作完成后,操作系统会通知应用程序进行后续处理。异步IO模型常用于高吞吐量、低延迟的应用,如高性能服务器和数据库等。 总之,Linux IO模型提供了多种不同的方式来处理输入输出,每种模型都有其适用的场景和特点。选择合适的IO模型可以提高系统的性能和效率。 ### 回答3: Linux IO模型是指操作系统中用于处理输入输出操作的一种方法或机制。在Linux中,常见的IO模型有阻塞IO、非阻塞IOIO多路复用和异步IO。 阻塞IO是最基本的IO模型,当应用程序发起一个IO请求时,它将一直阻塞等待直到IO操作完成,期间无法做其他任务。虽然简单易用,但是对资源的利用不高。 非阻塞IO在发起一个IO请求后,不会阻塞等待IO操作完成,而是立即返回并继续做其他任务。应用程序需要不断地轮询IO操作状态,直到操作完成。由于需要不断轮询,对CPU的占用较高,但可以提高资源的利用率。 IO多路复用是通过一个线程同时监听多个IO事件,从而实现并发处理多个IO操作。在IO多路复用模型中,应用程序不需要进行轮询,而是通过调用select、poll或epoll等系统调用监听多个文件描述符的IO事件。这样可以在单个线程中处理多个IO操作,提高并发性能。 异步IO模型在发起一个IO请求后,应用程序不需要等待IO操作完成,而是继续做其他任务。当IO操作完成后,操作系统会通知应用程序。异步IO模型需要操作系统的支持,效率较高,但实现较为复杂。 通过选择合适的IO模型,可以根据不同的应用场景来提高IO操作的效率和性能。例如,对于需要同时处理大量连接的服务器应用,IO多路复用是一种常见的选择;而对于需要处理大量IO操作的高性能服务器,则可以考虑使用异步IO模型

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值