IO模型是指计算机在进行输入输出操作时所采用的不同方法和策略。
在计算机系统中,I/O操作是与外部设备之间的数据交换,如磁盘读写、网络传输等。
I/O操作是计算机系统中重要的组成部分,因此IO模型的选择会直接影响系统的性能和吞吐量。
常见的IO模型有以下几种:
1. 阻塞IO模型(Blocking IO Model):
在执行I/O操作时,会一直等待数据传输完成,才返回结果。
优点是简单易用,
缺点是性能低下,因为它会阻塞CPU的执行,导致CPU利用率低。
2. 非阻塞IO模型(Non-Blocking IO Model):
在执行I/O操作时,会立即返回一个结果,无论数据是否已经准备好。
如果数据没有准备好,返回的结果会提示调用者进行其他操作,直到数据准备好。
优点是可以提高系统的并发性,
缺点是需要不断轮询检查数据是否已准备好,导致CPU占用率高。
3. IO多路复用模型(IO Multiplexing Model):
3.1 IO多路复用概述
在执行I/O操作时,通过IO多路复用技术来同时监听多个I/O事件,
这样可以在一个线程内同时处理多个I/O操作。
优点是可以同时处理多个I/O操作,
缺点是在数据准备好之前需要不断轮询,导致CPU占用率高。
3.2 实现IO多路复用的步骤
IO多路复用可以同时监听多个I/O事件,从而可以在一个线程内同时处理多个I/O操作。
其核心实现是基于操作系统提供的select/poll/epoll等系统调用来实现的。
以select为例:
-
创建一个fd_set类型的读集合和写集合,分别用于监听读和写事件。
-
将要监听的文件描述符添加到对应的集合中,通过FD_SET函数来实现。
-
调用select函数等待事件的发生,该函数会阻塞直到事件发生或者超时。
-
当有事件发生时,select函数会修改集合中的文件描述符状态,应用程序可以根据文件描述符的状态来处理事件。
-
重复执行步骤2-4,直到不需要监听文件描述符时,通过FD_CLR函数来从集合中删除对应的文件描述符。
3.3 select()函数
select()是一个系统调用函数,用于在多个文件描述符上等待事件的发生。
它可以实现IO多路复用,允许程序同时监听多个文件描述符,
并且可以在其中任意一个文件描述符有数据到达或者出错时立即得到通知。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数说明:
- nfds:监听的文件描述符数量,即所有文件描述符集合中的最大值加一。
- readfds:读文件描述符集合,包含需要监听读事件的文件描述符。
- writefds:写文件描述符集合,包含需要监听写事件的文件描述符。
- exceptfds:异常文件描述符集合,包含需要监听异常事件的文件描述符。
- timeout:超时时间,表示select()最多阻塞的时间,如果为NULL表示一直阻塞直到有事件发生。
返回值:
准备就绪的文件描述符数量,即有事件发生的文件描述符数量,
如果出错则返回-1。
在使用select()时,需要先将需要监听的文件描述符添加到对应的文件描述符集合中,
然后调用select()函数进行监听。
如果函数返回大于0的值,则需要逐个检查所有文件描述符集合,以确定哪些文件描述符有事件发生。
如果返回值为0,则表示超时,即没有文件描述符有事件发生。
如果返回-1,则表示出错,需要根据错误码进行处理。
🔞注意:select()存在一些限制,
如最大监听的文件描述符数量、每次调用select()时需要重新初始化文件描述符集合等,
使用select的过程中需要遍历整个文件描述符集合,导致性能随着文件描述符的增加而降低。
同时,select函数有最大文件描述符数量的限制,一般为1024或者更少。
另外,与epoll相比,select不支持边缘触发模式(Edge Triggered),只支持水平触发模式(Level Triggered)。
这意味着,当文件描述符的状态变化时,如果不及时处理,会一直触发事件,导致CPU占用率过高。
总的来说,select作为一种IO多路复用机制,在高并发和大规模连接的情况下,其性能和可靠性都无法满足要求。
因此,现在更多的应用epoll等高效的IO多路复用机制来提高系统性能和并发性。
3.4 示例代码
头文件
#ifndef _HEAD_H_
#define _HEAD_H_
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#endif
#include "head.h"
#define SERV_PORT 5005
#define QUIT_STR "quit"
int main()
{
//1.创建流式套接字 监听套接字
int listenFd = socket(AF_INET, SOCK_STREAM, 0);
if(listenFd < 0){
perror("socket error");
exit(-1);
}
printf("socket ok\n");
//设置地址和端口可快速重用
int on = 1;
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
//2.定义结构体 赋值 并绑定
struct sockaddr_in seraddr;
memset(&seraddr, 0, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SERV_PORT);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(listenFd, (struct sockaddr*)&seraddr, sizeof(seraddr));
if(ret < 0){
perror("bind error");
exit(-1);
}
printf("bind ok\n");
//3.设置监听
ret = listen(listenFd, 5);
if(ret < 0){
perror("listen error");
exit(-1);
}
printf("listen ok\n");
//🚲🚲🚲🚲🚲🚲🚲调用salect()实现IO多路复用🚲🚲🚲🚲🚲🚲🚲//
//1️⃣:创建文件描述符集合,将关注的文件描述符加入文件描述符集合
fd_set set1, set2;//set1是处理前,set2是处理后
FD_ZERO(&set1); //清空文件描述符集合
FD_SET(0, &set1); //将标准输入stdin加入到文件描述符集合
FD_SET(listenFd, &set1);
int maxFd = listenFd;
char buf[BUFSIZ];
int connFd;
while(1){
//2️⃣:调用select()监控文件描述符集合,返回有数据发生的文件描述符数量
set1 = set2;
ret = select(maxFd+1, &set1, NULL, NULL, NULL);//ret是有数据发生的文件描述符的数量
if(ret < 0){
perror("select error");
continue;
}
//3️⃣:循环遍历文件描述符集合,处理有数据发生的文件描述符
int i;
for(i=0; i<=maxFd; i++ ){
//跳过没有数据发生文件描述符
if(FD_ISSET(i, &set2) == 0){
continue;
}
if(i == 0){//标准输入上有数据输入
if(fgets(buf, sizeof(buf), stdin) == NULL){
printf("fgets error\n");
continue;
}//输入quit就退出
if(strncmp ( buf, QUIT_STR, strlen(QUIT_STR) ) == 0){
printf("....exiting....exited!\n");
exit(0);
}
}else if(i == listenFd){//有客户端请求连接
connFd = accept(listenFd, NULL, NULL);
if(connFd < 0){
perror("accept error");
continue;
}
//成功连接后,将connFd加入文件描述符集合
FD_SET(connFd, &set1);
//更新集合
if(connFd > maxFd){
maxFd = connFd;
}
}else {//除了以上两种情况,就是处理来自连接套接字的消息
memset(buf, 0, sizeof(buf));
ret = recv(i, buf, sizeof(buf)-1, 0);
if(ret < 0){
perror("recv error");
continue;
}else if(ret == 0){
printf("对端关闭套接字\n");
FD_CLR(i, &set1);
close(i);
}else{
printf("recv:%s", buf);//这里的处理方式就是打印出来
}
}
}
}
close(listenFd);//关闭监听套接字
return 0;
}
这段代码是一个基于select模型的简单服务器程序,主要功能是监听标准输入和网络连接请求,并处理有数据发生的文件描述符。
首先使用select函数监视文件描述符集合set1中的文件描述符,当有文件描述符就绪时,select函数会返回有数据发生的文件描述符的数量。
接着使用FD_ISSET函数遍历set2中的文件描述符,处理有数据发生的文件描述符。
如果文件描述符为标准输入,则使用fgets函数读取标准输入中的数据,
如果数据为退出命令则退出程序。
如果文件描述符为监听套接字,则使用accept函数接收新的连接请求,并将连
接套接字加入文件描述符集合set1中。
如果文件描述符为连接套接字,则使用recv函数接收对端发送的数据,并输出到控制台。
当对端关闭套接字时,需要从set1中清除该套接字,并关闭该套接字。
4. 信号驱动IO模型(Signal-Driven IO Model):
在执行I/O操作时,使用信号通知操作系统数据已经准备好。
当数据准备好时,操作系统会向应用程序发送一个信号,应用程序可以在信号处理函数中处理数据。
优点是可以减少不必要的轮询,
缺点是处理信号的时间可能会影响应用程序的性能。
5. 异步IO模型(Asynchronous IO Model):
在执行I/O操作时,可以通过回调函数的方式来处理数据。
当数据准备好时,操作系统会调用应用程序提供的回调函数来处理数据。
优点是可以提高系统的并发性和性能,
缺点是编程复杂度较高。