Linux---五种IO模型

概念说明

  • 用户空间与内核空间
  • 同步与异步
  • 进程的阻塞
  • 阻塞与非阻塞
  • 缓存IO

1. 用户空间与内核空间

对于32位操作系统而言,它的寻址空间(虚拟地址空间)为4G,操作系统的核心是内核, 他独立于其他的应用程序, 既可以访内存空间,也可以访问外部的硬盘, 为了保证内核的安全, 使用户进程不能直接访问内核对于Linux操作系统而言, 将寻址空间的高1G个字节置为内核空间, 将低3G字节的空间置为用户空间, 供用户进程使用.

2.进程的阻塞

正常运行的进程, 由于等待某些事情未发生, 如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等, 则进程会使自己进入一种阻塞的状态来等待事件的完成 (自由运行中的程序才可能有阻塞状态, 并且进入阻塞状态时, 是不会占用CPU资源的)

3.缓存IO

缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

4. 阻塞与非阻塞 : 发起一个调用是否能立即返回

  • 阻塞:为了完成功能发起一个调用, 当前不具备完成条件, 则调用一直等待
  • 非阻塞: 为了完成功能发起一个调用, 当前不具备完成条件, 则调用直接报错返回

5. 同步与异步 : 功能是否由自身完成

  • 同步: 为了完成功能发起一个调用, 当前不具备完成条件, 则调用一直等待,直到功能完成后返回
  • 异步: 为了完成功能发起一个调用, 但是功能的具体完成不有自身完成
  • 同步与异步的优缺点分析
    同步流程控制更加简单, 对资源的利用率不足(CPU的利用率)
    异步对资源的利用更加充分,但是流程控制更加复杂一些, 同一时间的组员占的更多

五种IO模型

网络IO的本质在于对socket的读取, socket被Linux操作系统抽象为流, 而IO就是对流的操作. 在Linux的缓存IO中, 操作系统会将IO的数据先拷贝到操作系统内部的缓冲区中, 在有缓冲区拷贝的应用程序的地址空间 ; 以一次read操作为例, 当发生一次read操作的时候,会经历两个步骤, 第一步: 等待数据准备, 第二步: 将数据从缓冲区拷贝到地址空间中.

接下来, 有一个生动的例子来说明,五种IO模型, 例子: 周末我和女友去逛街,中午饿了,我们准备去吃饭。周末人多,吃饭需要排队,我和女友有以下几种方案

同步阻塞IO

1.场景描述

女朋友和我点完餐之后,由于不知道什么时候餐会做好,所以就在这里等着, 不能去逛街, 等饭做好之后,我们吃完着在去逛街这就是典型的阻塞

2.网络模型

在这个IO模型中, 用户空间的应用程序会执行一个系统调用,这会导致应用程序什么也不干,直到数据准备好,并且从缓冲区总拷贝到应用程序之中, 才回去对数据进行处理.
因此应用程序在调用recv()或者recvfrom()接口时,发生在内核中的过程大致如下图
在这里插入图片描述

同步非阻塞IO

1.场景描述

女朋友和我点完餐之后, 由于饭还没有及时做出来,女朋友又心急着去逛街, 所以我们就先去逛街, 逛上一会,回来问一下饭做好了没有, 逛上一会,回来问一下饭做好了没有, 来来回回反复的询问。

2.网络模型

在这个IO模型中,当调用的IO操作不能完成时, 会进行一个报错返回.当进程返回之后,可以去干一点其他的事情, 之后,在发起一次IO调用, 若数据还没有准备好,再次报错返回. 如此反复进行,直到有一次IO调用时, 数据已将准备就绪, 则拷贝数据, 之后进程处理数据
在这里插入图片描述
在这里插入图片描述

信号驱动IO

1.场景描述

点完餐之后,我们先去逛街, 当餐厅将饭做好之后,打电话通知我们, 饭已经做好了, 此时,我和女朋友返回餐厅吃饭

2.网络模型

添加一个信号处理函数, 之后,进程不进入阻塞,可以执行其他的事情, 当数据准备好之后, 进程会收到一个SIGIO信号,接受到这个信号之后,在信号处理方式中调用IO对数据进行处理.
在这里插入图片描述

异步IO

1.场景描述

女友不想逛街,又餐厅太吵了,回家好好休息一下。于是我们叫外卖,打个电话点餐,然后我和女友可以在家好好休息一下,饭好了送货员送到家里来。这就是典型的异步,只需要打个电话说一下,然后可以做自己的事情,饭好了就送来了

2.网络模型

用户进程先定义一个IO信号处理, 之后,发起IO调用, 操作系统进行条件等待, 并且进行数据拷贝,之后通过信号通知进程,让进程考处理数据

多路转接IO

对大量的描述符进行事件监控(可读/可写/异常),能够让用户直接对事件就绪的描述符进行操作. 在网络通信中, 如果仅仅对描述符进行操作,则流程在一个执行流中就不会阻塞, 可以实现在一个执行流中进行多个描述符并发操作

select模型

就绪事件的判断 :
可读事件 : 接受缓冲区中的数据大小大于低水位标记(默认是一个字节)
可写事件 : 发送缓冲区中的空闲空间的大小大于低水位标记(默认一个字节)

fd_set结构
fd_set实际上是一个整数数组, 更严格的说他是一个 “位图” , 他的最大位为1024, 每一个位对应一个描述符, (意思是位图中的第0位,就对应可第0个文件描述符—标准输入), 所以 , select所能监控的描述符最大为1024 ; 当我们要监控某一个描述符的时候, 我们就把fd_set中对应的位置置为1

实现流程

  1. 用户定义事件集合
  2. 将集合拷贝到内核进行监控( 对集合中的所有描述符进行遍历判断, 判断事件是否就绪)
  3. 若某个描述符就绪了用户关心的事件, 就将所有集合中没有就绪的描述符移除, 返回给用户就绪的描述符集合
  4. 用户拿到就绪描述符集合后,通过遍历判断哪些描述符还在就绪的集合中,进而取到就绪的描述符进行操作

接口

 int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
//nfds:      集合中最大的描述符+1
//readfds:   可读事件集合
//writefds:  可写事件集合
//exceptfds: 异常事件集合
//timeout 阻塞的时间  NULL表示永远阻塞直到有描述符阻塞
//返回值:>0 表示的就绪的描述符个数
	  //=0  等待超时(没有描述符就绪)
	  //<0   监控出错 
       void FD_CLR(int fd, fd_set *set);//从set集合中移除指定的描述符fd
       int  FD_ISSET(int fd, fd_set *set);//判断指定的描述符fd是否在集合set中
       void FD_SET(int fd, fd_set *set);//将指定的fd描述符添加到set集合中
       void FD_ZERO(fd_set *set);//清空set集合内容

//返回值:小于0时, select监控出错
	   //等待0时, 等待超时
	   //大于0时返回就绪的文件描述符个数

实现select类用来监控TCP服务端
在前面我们实现TCP服务端的时候, 由于用于监听的套接字与用于通信的套接字是不同的socket, 因此,在代码运行的时候,服务端程序就会阻塞到accpet或者recv接口上, 前面我们的解决方法是通过利用创建线程或者进程的方法,让多个执行流分别执行不同的操作,来完成TCP服务端的程序.
现在我们通过封装select类,让select来监控不同的套接字.

select类的封装需要三个接口

  1. 向事件集合中添加套接字
  2. 对添加的集合进行监控,返回一个集合,这个集合中包含的是所有已将就绪的描述符
  3. 删除集合中的套接字
class Select{
	public:
		Select(){
			FD_ZERO(&_rfds)
		}
		~Select(){
		}
	public:
		bool Add(TcpSocket &sock){
			int fd=sock.Getfd();
			FD_SET(fd,&_rfds);
			//此处需要对_maxfd进行更改
			_maxfd=_maxfd>fd?_maxfd:fd;
			return true;
		}
		bool CLR(TcpSocket &sock){
			int fd=sock.Getfd();
			FD_CLR(fd,&_rfds);
			for(int i=_maxfd;i>=0;i--){\
				if(FD_ISSET(fd,&_rfds)){
					_maxfd=i;
					return true
				}
			}
			_maxfd=-1;
			return true;
		}
		bool Wait(vector<TcpSocket> &list,int sec=3){
			struct timeval tv;
			tv.tv_sec=sec;
			tv.tv_usec=0;
			int count=select(_maxfd+1,&_rfds,NULL,NULL,&tv);
			if(count<0){
				coout<<"select error"<<endl;
				return false;
			}else if(count==0){
				cout<<"wait timeout"<<endl;
				return false;
			}
			for(int i=0;i<=_maxfd;i++){
				if(FD_ISSET(i,&_rfds)){
					TcpSocket sock;
					sock.Setfd(i);
					list.push_back(sock);
				}
			}
		return true;			
}
	private:
		fd_set _rfds;//描述符集合
		int _maxfd;//集合中的最大描述符
};

实现Select监控的TCP服务端层序

int main(int argc,char* argv){
	if(argc!=3){
		cout<<"./tcp_select ip port"<<endl;
	}
	string ip=argv[1];
	uint16_t port=atoi(argv[2]);
	TcpSocket lis_sock;
	CHECK_RET(lis_sock.Sokcet());
	CHECK_RET(lis_sock.Bind(ip,port));
	CHECK_RET(lis_sock.Listen());
	Select s;
	s.Add(lis_sock);
	while(1){
		vector<TcpSocket> list;
		if(s.Wait(lise)==false){
			continue;
		}
		for(auto newsock:list){
			if(newsock.Getfd()=lis_sock.Getfd()){
				//当前就绪的套接字为监听套接字
				TcpSocket cli_sock;
				bool ret=lis_sock(cli_sock);
				if(ret==false){
					continue;
				}else{
					//当前套接字为通讯的套接字
					string buf;
					bool ret=newsock.Recv(buf);
					if(ret==false){
						s.CLR(newsock);
						newsock.Close();
						continue;
					}
					cout<<"cli say:"<<buf<<endl;
					buf.clear();
					cin>>buf;
					ret=newsock.Send(buf);
					if(ret=false){
						s.CLR(newsock);
						newsock.Close();
						continue;
					}
				}
			}
		}
	lis_sock.Close();
	return 0;
}

select的优缺点分析
缺点:

  1. select所能监控的描述符是有上限控制的, 它由一个宏控制着FD_SETDIZE=1024 最大为1024
  2. 每一次监控都需要重新将监控集合拷贝到内核中
  3. 态内核中监控是,是通过轮询遍历监控的, 这种监控会随着集合中的描述符的增多而使性能下降
  4. 返回的是就绪的一个集合, 还需要用户进行遍历判断,才能对描述符进行操作(无法随就绪的描述符进行操作)
  5. 因为每次都要清空未就绪的描述符,所以每次监控的时候都需要重新将所有描述符添加进集合中

优点 :

  1. select遵循posix标准可以跨平台使用

poll模型

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

 struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */POLLIN   POLLOUT
               short revents;    /* returned events */
           };


在select模型中, 我们要对多种时间进行监控的时候, 就需要多种事件集合分别对每种事件进行监控, 但是在poll模型中, 我们定义了一个pollfd结构体, 这个结构体中包含了三个成员

  1. fd—文件描述符
  2. events —对当前描述符我们用户所关心的事件
    其中 : POLLIN–可读事件 | POLLOUT–只写事件
  3. revents : 这次监控返回之后这个描述符就绪的事件
    这个成员中只会存放这个描述符中已经就绪的事件, 同时意为着有可能这个成员中没有东西, 也就是这个描述符没有事件就绪

接口中的第二个参数是用来确定范围, 因为我们的事件结构是一个数组, 而当我们监控少量描述符的时候, 我们可以通过这个参数来限制遍历范围的大小, 避免过多的资源损耗

实现流程
poll采用事件结构的方式对描述符进行监控, 当在使用的时候, 用户可以将对该描述符关心的时间添加到事件结构的events中, 当有描述符就绪了用户所关心的事件时, 就将该事假添加到对应的revents中
之后进行遍历查看每一个结点中的revents, 如果revents只不过有就绪的事件, 就开始操作

poll的优缺点分析
缺点:

  1. poll是Linux下独有的, 无法跨越平台
  2. 依然需要将监控的描述符事件数组拷贝到内核中
  3. 在内核中同样也是轮询遍历: 性能随着描述符的增多而下降
  4. 只是将就绪的时间放在了revents中,需要用户遍历判断

优点:

  1. 没有描述符数量的上限
  2. 采用事件结构数组进行事件监控, 简化了select三种事件集合的操作流程
  3. 不需要每次重新想集合中添加数组

epoll

epoll在监控描述符时,首相通过epoll_create接口创建出一个eventpoll结构, 然后将用户所关心的事件,创建出一个epoll_event事件结构,通过epoll_ctl接口将该描述符的事件结构添加到eventpoll结构中的红黑树中; 当有用户关心的事件的描述符就绪时, 操作系统就将这个描述符所对应的epoll_event结构添加到eventpoll结构中的双向链表中, 最后, 用户程序每隔一段时间查看该双向链表, 如果双向链表不为空, 则就将链表中就绪的时事件结构节点添加到通过epoll_wait接口传入的类型为epoll_event的数组中, 用户就可以直接操作该数组对就绪的描述符进行操作

//在内核中创建eventpoll结构, 返回一个文件描述符,结构中主要包含两个信息:(双向链表, 红黑树)
int epoll_create(int size);

//向内核中的eventpoll结构体中添加事件结构(主要添加到红黑树中)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epfd: eventpoll的操作句柄
//op : EPOLL_CTL_ADD (添加)/ EPOLL_CTL_MOD(修改) / EPOLL_CTL_DEL(移除)
	//描述了向内核中添加事件的方式
//fd : 用户关心的文件描述符
//event : 对于fd描述符所要监控的事件

//开始监控
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//epfd : eventpoll结构 	
//events 用于接受已经就绪的事件结构的数组
// maxevents : 数组的最大节点数
//timeout : 超时事件 (以毫秒为单位)
//返回值 : 小于0时出错, 等于0时超时, 大于0时为就绪的时间个数

epoll_ctl中的event参数信息

 typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

struct epoll_event {
	  //对用户多关心的事件
      uint32_t     events;      /* Epoll events */
      //通过data数据操作fd
      epoll_data_t data;        /* User data variable */
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值