云计算百科
云计算领域专业知识百科平台

[Linux复习 -- 五种IO模型] —— 阻塞IO、非阻塞IO、信号驱动IO IO多路转接、异步IO,包括相关接口的使用、select 、poll、epoll 以及通过这些接口封装服务器

1 五种IO模型

        阻塞IO:在内核数据准备好前,系统调用会一直等待。所有的套接字默认都是阻塞方式

        非阻塞IO:如果系统未将数据准备好,系统调用仍会直接返回,并且返回EWOULDBLOCK错误码。

        一般来讲对于一个可能产生数据的文件描述符我们希望在他有数据时就将数据读上来,但是非阻塞IO直接就返回了并不能达到这个效果,如果要通过非阻塞IO来达到这个目的,就需要不断的尝试读写文件描述符,这个改成称为轮询,但是轮询是对CPU的一种极大的浪费,一般只在特殊情况下使用。

        信号驱动IO:内核将数据准备好时,使用SIGIO信号通知应用程序进行IO操作

        IO多路转接:从流程图上看和阻塞IO类似,但是IO多路转接的核心在于能够同时等待多个文件描述符的就绪状态。

        异步IO:由内核在数据拷贝完成时,通知应用程序(信号驱动是告诉应用程序何时可以开始拷贝数据)

        小结:任何IO过程中,都包含两个步骤,第一个是等待,第二个是拷贝,而且在实际的应用场景中,等待消耗的时间远远高于拷贝的时间,让IO更高效,最核心的方法就是让等待的时间尽量的少

2 高级IO的重要概念

2.1 同步通信  vs  异步通信

        同步和异步关注的是消息通信机制

        所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果;

        异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

2.2 阻塞和非阻塞

        阻塞和非阻塞关注的是程序在等待调用结果返回时的状态。

        阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.         非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

3 详解IO模式相关的接口

3.1 非阻塞IO

        fcntl这个函数可以设置文件描述符的属性

        我们仅通过第三种功能就可以将一个文件描述符设置为非阻塞

        先使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图),也就是代码中的fl

        然后使用F_SETFL将文件描述符设置回去的同时,加上一个O_NONBLOCK参数

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
void SetNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

        下面我们通过轮询的方式来读取标准输入

        实现的思路就是将标准输入设置为非阻塞,然后不断的从标准输入读取数据

        

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
void SetNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
SetNoBlock(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;
}

        运行结果如图

3.2 I/O多路转接之select

认识select

        系统提供的select函数是用来实现多路复用输入/输出模型

        select系统调用是用来让我们的程序监视多个文件描述符的状态变化的

        程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态变化

select函数原型以及相关参数的含义

select使用示例:检测标准输入输出

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/select.h>

int main()
{
fd_set fds;
FD_ZERO(&fds);
FD_SET(0,&fds);
for(;;)
{
int n = select(1,&fds,NULL,NULL,NULL);
if(n<0)
{
perror("select");
continue;
}
if(FD_ISSET(0,&fds))
{
char buffer[1024] = {0};
read(0,buffer,sizeof(buffer));
printf("input : %s",buffer);
}
else{
printf("error ! invail fd\\n");
continue;
}
FD_ZERO(&fds);
FD_SET(0,&fds);
}
return 0;
}

        这个代码的其实就是通过检测标准输入是否就绪来完成读取,通过这个代码就能很明显的看到select的缺点,fds在每次select前要重新设置并从用户区拷贝到内核态,这相当的不方便。

通过select实现TCP服务器

        我们定义一个类,在这个类中维护一个数组用来存放需要关心的文件描述符,再在这个类中维护一个listenfd用来监听客户的连接。在我的代码中相关的网络套接字编程的接口是封装过的,在文末会给,还有日志文件文末也会给。

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/select.h>
#include "Socket.hpp"

#define BUFFERMAX 1024

const int defaultprot = 8888;
const int defaultfd = 1024;
const int defaultnot = -1;

class Select_Server
{
public:
Select_Server()
:listenfd_(new Sock)
{
for (int i = 0; i < defaultfd; i++)
rfds[i] = defaultnot;
}
~Select_Server() {}

public:
void Init()
{
listenfd_->Socket();
listenfd_->Bind(defaultprot);
listenfd_->Listen();
}
void Start()
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(listenfd_->Fd(), &readfds);
rfds[0] = listenfd_->Fd();
for (;;)
{
int maxfd = 0;
FD_ZERO(&readfds);
for (int i = 0; i < defaultfd; i++)
{
if (rfds[i] != defaultnot)
{
FD_SET(rfds[i], &readfds); // 添加要关心事件到readfds里
if (rfds[i] > maxfd)
maxfd = rfds[i];
}
}
//这里是select的一个弊端,每一吃等待事件前都要重新添加待关心事件

//通过select等待事件就绪
int n = select(maxfd + 1, &readfds, nullptr, nullptr, nullptr);
if (n == 0)
{
std::cout << "timeout " << std::endl;
}
else if (n == -1)
{
std::cout << "select error" << std::endl;
exit(1);
}
else
{
// 意味着有事件就绪
if (FD_ISSET(listenfd_->Fd(), &readfds)) // 确认是不是listenfd
{
// 说明有链接到来
std::string clientip;
uint16_t clientport;
int sock = listenfd_->Accept(&clientip, &clientport);
// 此时不能从sock中读数据,理解请求连接并不意味着发数据
int i = 0;
for (i = 0; i < defaultfd; i++)
{
// int pos = 0;
// for (; pos < defaultfd; pos++)
// {
// if (rfds[pos] != defaultnot)
// continue;
// if (rfds[pos] == defaultnot)
// break;
// }
// if (pos != defaultfd)
// {
// rfds[pos] = sock;
// std::cout<<sock<<"Added successfully";
// }
// else
// std::cout << "The connection is full" << std::endl;
if(rfds[i] == defaultnot) break;
}

if(i == defaultfd)
{
std::cout << "The connection is full" << std::endl;
}
else
{
rfds[i] = sock;
std::cout<<sock<<"Added successfully";
}
}
else
{
// 说明读事件就绪
for (int i = 1; i < defaultfd; i++)
{
if (rfds[i] != defaultnot && FD_ISSET(rfds[i], &readfds))
{
char buffer[BUFFERMAX];
memset(buffer, 0, sizeof(buffer));
// 确定要读的事件
int m = read(rfds[i], buffer, sizeof(buffer));
if (m == 0)
{
std::cout << "Clear Connection" << std::endl;
rfds[i] = defaultnot;
}
else if (m == -1)
rfds[i] = defaultnot;
else
{
std::cout << "rfd["<<i<<"]"<< "buffer:" << buffer << std::endl;
}
}
}
}
}
}
}

private:
Sock *listenfd_;
int rfds[defaultfd];
};

       

#include"Socket.hpp"
#include"Log.hpp"
#include"Select_Server.hpp"
#include<memory>

int main()
{
std::unique_ptr<Select_Server> select(new Select_Server);
select->Init();
select->Start();
return 0;
}

        运行效果如图,可以完成对多个用户的读写。

        第一个用户

        第二个用户

        第三个用户

        第四个用户

        服务端的响应

        这个代码仅仅完成了读写并没有加其他的功能,你们可以通过定制协议来限定数据格式来完成其他的功能,比如设计一个select网络版本计算机,或者一个select网络版本字典。

3.3 I/O多路转接之poll(了解即可)

        说白了其实poll就是针对select的缺点设计的,你select需要重复设计待关心事件,我就poll就通过一个结构体来完成事件存储,监听的事件,返回事件集合,这样就达到了优化的目的

 poll的优点:

        pollfd结构包含了要监视的event和发生的event,不再使用select"参数-值"传递的方式,接口使用比select方便

        poll没有最大数量限制,但是数量过大性能会下降 

poll的缺点:

        和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符

        每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中

        同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效 率也会线性下降

通过poll实现一个TCP服务器

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/poll.h>
#include <vector>

#include "Socket.hpp"

#define BUFFERMAX 1024

const int defaultprot = 8888;
const int defaultfd = 64;
const int defaultnot = -1;
const int non_event = 0;

class Poll_Server
{
public:
Poll_Server()
: listenfd_(new Sock)
{
for (int i = 0; i < defaultfd; i++)
{
read_fds[i].fd = defaultnot;
read_fds[i].events = non_event;
read_fds[i].revents = non_event;
}
}
~Poll_Server() {}

public:
void Init()
{
listenfd_->Socket();
listenfd_->Bind(defaultprot);
listenfd_->Listen();
}
void Accpet_()
{
uint16_t Client_Port = 0;
std::string Client_ip;
int newsock = listenfd_->Accept(&Client_ip, &Client_Port);
int i = 1;
for (; i < defaultfd; i++)
{
if (read_fds[i].fd != defaultnot)
continue;
else break;
}
read_fds[i].fd = newsock;
read_fds[i].events = POLLIN;
read_fds[i].revents = non_event;
Print_Fd();
}
void Read_(struct pollfd sock)
{
char buffer[BUFFERMAX];
memset(buffer, 0, sizeof(buffer));
int n = read(sock.fd, buffer, sizeof(buffer));
if (n < 0)
{
std::cout << "read error" << std::endl;
close(sock.fd);
sock.fd = defaultnot;
//close(sock.fd);
//sock.events = non_event;
}
else if (n == 0)
{
std::cout << "timeout" << std::endl;
close(sock.fd);
sock.fd = defaultnot;

//sock.events = non_event;
}
else
{
std::string buf = buffer;
std::cout << "Client #" << buf << std::endl;
}
}
void Start()
{
// fd_set readfds;
// FD_ZERO(&readfds);
// FD_SET(listenfd_->Fd(), &readfds);
read_fds[0].fd = listenfd_->Fd();
read_fds[0].events = POLLIN; //设置0号文件描述符的事件为可读
read_fds[0].revents = non_event;
int timeout = 3000;
for (;;) //1.服务器主循环
{
// int maxfd = 0;
// FD_ZERO(&readfds);
// for (int i = 0; i < defaultfd; i++)
// {
// if (rfds[i] != defaultnot)
// {
// FD_SET(rfds[i], &readfds); // 添加要关心事件到readfds里
// if (rfds[i] > maxfd)
// maxfd = rfds[i];
// }
// }
// 通过select等待事件就绪

int n = poll(read_fds, defaultfd, timeout); //poll等待事件
std::cout<<n<<std::endl;
if (n == 0)
{
// std::cout << "timeout " << std::endl;
std::cout << "poll timeout" << std::endl;
}
else if (n == -1)
{
std::cout << "poll error" << std::endl;
exit(1);
}
else
{
// 意味着有事件就绪
for (int i = 0; i < defaultfd; i++)
{
int fd = read_fds[i].fd;
if (fd == defaultnot)
continue; //跳出本次循环以找到就绪的事件
else
{
if (read_fds[i].revents & POLLIN)
{
if (fd == listenfd_->Fd())
{
Accpet_();
}
else
{
Read_(read_fds[i]);
}
}
//Print_Fd();
}
}
}
}
}
void Print_Fd()
{
for(int i = 0;i<defaultfd;i++)
if(read_fds[i].fd != defaultnot) std::cout<<read_fds[i].fd<<" ";
}
private:
Sock *listenfd_;
// int rfds[defaultfd];
struct pollfd read_fds[defaultfd];
};

#include<iostream>
#include"Poll_Server.hpp"
//#include"bit_poll.hpp"

int main()
{
Poll_Server* poll_(new Poll_Server);
poll_->Init();
poll_->Start();
return 0;
}

3.4 I/O多路转接之epoll

epoll初识        

        按照man手册的说法:epoll是为了处理大批量句柄而做了改进的poll

epoll相关系统调用

                            

赞(0)
未经允许不得转载:网硕互联帮助中心 » [Linux复习 -- 五种IO模型] —— 阻塞IO、非阻塞IO、信号驱动IO IO多路转接、异步IO,包括相关接口的使用、select 、poll、epoll 以及通过这些接口封装服务器
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!