欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > 【linux】高级IO

【linux】高级IO

2025/11/10 22:13:16 来源:https://blog.csdn.net/2301_79789645/article/details/143880369  浏览:    关键词:【linux】高级IO

1. 五种IO模型

同步IO:

  1. 阻塞式IO (像 read , write ... 都是阻塞IO)
  2. 非阻塞式IO (非阻塞轮询等待)
  3. 信号驱动式IO (信号来了,才知道数据就绪了)
  4. 多路复用/多路转接 (多个读写端共同非阻塞轮询一起等待)

异步IO:

  1. 异步IO:发起IO,但是交给别人完成,完成的时候通知自己读取数据(由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)

注意:

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

2. 高级IO重要概念

(一)同步通信 与 异步通信

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

同步:在发出一个调用时,没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了,即由调用者主动等待这个调用的结果

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

(二)阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态

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

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

3. 非阻塞IO

fcntl 函数

#include <unistd.h>

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

函数参数:

fd : 文件描述符

cmd : 可以设置不同值,有不同作用

cmd 设置:

复制一个现有的描述符(cmd=F_DUPFD)

获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)

获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)

文件状态标记:文件打开时的状态标记,如可读,可写,阻塞等

获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)

获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)

代码(非阻塞IO)

void SetNoBlock(int fd) 
{ int fl = fcntl(fd, F_GETFL); //得到 fd 中原来的文件状态标记if (fl < 0) { perror("fcntl");return; }fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 既保留原来的文件状态标志,也增加设置了非阻塞
}

 注意:

  1. cmd 设置 O_NONBLOCK 代表非阻塞
  2. cmd 设置一个fd成为非阻塞IO时,当 read读取数据为空时,不会阻塞,而是设置 errno 为 EWOULDBLOCK

4. IO多路转接 -- select

(一)初识select

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

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

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

(二)select函数

select 函数

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数:

nfd : 需要监视的最大的文件描述符值+1

readfds , writefds , exceptfds :都是输入输出型参数

timeout : 输入输出型参数,表示等待的最长时间,是一个结构体,如果期间没有接收到有数据就绪,时间一到,函数会自动返回

返回值:返回值 > 0,代表有多少数据就绪 ; 返回值 = 0,在设置的等待最长时间内,没有数据就绪 ; 返回值 = -1,在等待过程中,有 IO出现错误

注意:

  1. readfds , writefds , exceptfds 都是输入输出型参数,实际上传入的类似一张位图,以readfds参数举例,传入的位图代表这张位图,所有设置过的位置(1代表被设置,0代表没被设置),都需要判断是否读就绪(读缓冲区是否有数据),传出的位图代表,只有就绪的位置会被设置,输入输出代表含义不一样如果传入设置成NULL,即代表不关心读缓冲区是否有数据,writefds , exceptfds 的参数设置和前面的类似
  2. timeout 输入等待时间,输出代表剩下的时间,如:设置 5 s ,有一个数据就绪,函数就会返回,等待了 2 s , 输出得到的就是 3 s 。 如果设置成 NULL,即阻塞等待,直到有一个数据就绪

timeval 结构体:

提供了一组操作fd_set的接口, 来比较方便的操作位图:

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位

int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真

void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位

void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

(三)select 使用

代码 (对连接和读做了处理)

#include"SelectServer.h"
int main()
{unique_ptr<select_server> str(new select_server());str->Init();str->start();
}
#include"server.h"
#include <sys/select.h> 
class select_server
{
public:select_server(){for(int i = 0;i < 1024;i++){blank[i] = -1;}}void Init(){_listen.start();}void Handler(fd_set& set){if (FD_ISSET(blank[0], &set)){int lietent_fd = _listen.ListenFd();sockaddr_in x;socklen_t len = sizeof(x);int fd = accept(lietent_fd, (struct sockaddr *)&x, &len);if (fd < 0){cerr << "accept fail" << endl;return;}else{int i = 1;for (; i < 1024; i++){if (blank[i] == -1){cout << "blank[i] = fd" << endl;blank[i] = fd;break;}}if (i == 1024){cout << "over buff" << endl;close(fd);}}}for(int i = 1;i < 1024;i++){if(blank[i] == -1){continue;}if(FD_ISSET(blank[i],&set)){char buff[1024];int n = read(blank[i],buff,sizeof(buff) - 1);if(n > 0){buff[n] = 0;cout << buff << endl;}else if(n == 0){cout << "write close" << endl;close(blank[i]);blank[i] = -1;}else{cerr << " read fail " << endl;}}}}void start(){int max_fd = -1;int lietent_fd = _listen.ListenFd();blank[0] = lietent_fd;fd_set set;while (true){FD_ZERO(&set);for(int i = 0;i < 1024;i++){if(blank[i] != -1){max_fd = max(max_fd,blank[i]);FD_SET(blank[i], &set);}}timeval time({5, 0});int n = select(max_fd + 1, &set, nullptr, nullptr, &time);if (n > 0){cout << " get a link ... " << endl;Handler(set);}else if (n == 0){cout << " wait ... ... " << endl;}else{cerr << "select fail " << endl;}}}~select_server(){}
private:int blank[1024];server _listen;
};
#include<iostream>
#include<sys/types.h> 
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<signal.h>
#include<memory>
using namespace std;
class server
{
public:server(const uint16_t port = 8080,const string& ip = "0.0.0.0"):server_port(port),server_ip(ip){}void start(){//构建套接字server_fd = socket(AF_INET,SOCK_STREAM,0);if(server_fd < 0){cout << "server scoket fail" << endl;exit(1);}else{cout << "socket success" << endl;}//绑定端口号sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());socklen_t len = sizeof(server);int tmp = bind(server_fd,(struct sockaddr*)&server,len);if(tmp < 0){cout << "server bind fail" << endl;exit(2);}//开始监听int n  = listen(server_fd,10);if(n < 0){cout << "server listen fail" << endl;exit(3);}}void run(){signal(SIGCHLD,SIG_IGN);while(true){// 等待客户端sockaddr_in client;socklen_t len = sizeof(client);int cilent_id = accept(server_fd, (struct sockaddr *)&client, &len);if(cilent_id < 0){cout << "server : accept fail" << endl;}cout << "get a new link ...." << endl;int id = fork();if (id == 0){close(server_fd);while (true){// 读取数据char buff[1024];int n = read(cilent_id, buff, sizeof(buff));if (n < 0){cout << "server read fail" << endl;break;}buff[n] = 0;cout << buff << endl;// 写入数据string s = "server say : ";s += buff;write(cilent_id, s.c_str(), s.size());}exit(0);}close(cilent_id);}}int ListenFd(){return server_fd;}
private:int server_fd;uint16_t server_port;string server_ip;
};

(四)select 缺点

  1. 等待的fd是有上限的(最多只能等待1024个)
  2. 输入输出型参数较多,数据拷贝频繁
  3. 输入输出型参数较多,每次都要对关心的数据进行事件重置(重新设置关心的读数据,写数据,或者出错数据)
  4. 用户层使用第三方数组管理用户fd,需要遍历很多次;内核检测fd是否就绪,也需要遍历很多次

5. IO多路转接 -- poll

(一)poll函数

poll 函数

#include <poll.h>

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

参数:

fds:一个结构体地址,存放fd信息,关心数据信息,是否关心数据就绪信息

nfds:struct pollfd 结构体个数

timeout : 等待时间 (设置成 0 ,是非阻塞等待;设置成 - 1,是阻塞等待)

返回值:

返回值小于0, 表示出错; 返回值等于0, 表示poll函数等待超时; 返回值大于0, 表示poll由于文件描述符就绪而返回

注意:

timeout 参数代表的数据,单位是毫秒

// pollfd结构

struct pollfd {

int fd; /* file descriptor */

short events; /* requested events */

short revents; /* returned events */

};

struct pollfd

{

int fd; //fd信息

short events; /* requested events */

short revents; /* returned events */

};

注意:

这些取值实际上是宏

(二)poll 使用

代码 (对连接和读做了处理)

#include"PollServer.h"
int main()
{unique_ptr<poll_server> ptr(new poll_server());ptr->Init();ptr->start();return 0;
}
#include"server.h"
#include"poll.h"
class poll_server
{
public:poll_server(){for(int i = 0;i < 1024;i++){polls[i].fd = -1;}}void Init(){sv.Init();}void Handler(){for(int i = 0;i < 1024;i++){int fd = polls[i].fd;if(fd == -1){continue;}else if(fd == sv.ListenFd()){if(polls[i].revents & POLL_IN){int client_fd = sv.Accept();for(int j = 1;j < 1024;j++){if(polls[j].fd == -1){polls[j].events = POLL_IN;;polls[j].fd = client_fd;break;}}}}else{if(polls[i].revents & POLL_IN){//读数据char buff[1024];int n = read(fd,buff,sizeof(buff) - 1);if(n > 0){buff[n] = 0;cout << buff << endl;}else if(n == 0){close(polls[i].fd);polls[i].fd = -1;cout << "client read close" << endl;}else{close(polls[i].fd);polls[i].fd = -1;cout << "read fail" << endl;}}}}}void start(){int fd = sv.ListenFd();polls[0].fd = fd;polls[0].events = POLL_IN;while (true){int n = poll(polls, 1024, 3000);if (n > 0){Handler();}else if (n == 0){cout << "wait ..." << endl;}else{cerr << "poll fail" << endl;}}}~poll_server(){}private:server sv;pollfd polls[1024];
};
#include<iostream>
#include<sys/types.h> 
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<signal.h>
#include<memory>
using namespace std;
class server
{
public:server(const uint16_t port = 8080,const string& ip = "0.0.0.0"):server_port(port),server_ip(ip){}void Init(){//构建套接字server_fd = socket(AF_INET,SOCK_STREAM,0);if(server_fd < 0){cout << "server scoket fail" << endl;exit(1);}else{cout << "socket success" << endl;}//绑定端口号sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());socklen_t len = sizeof(server);int tmp = bind(server_fd,(struct sockaddr*)&server,len);if(tmp < 0){cout << "server bind fail" << endl;exit(2);}//开始监听int n  = listen(server_fd,10);if(n < 0){cout << "server listen fail" << endl;exit(3);}}int Accept(){sockaddr_in client;socklen_t len = sizeof(client);int cilent_id = accept(server_fd, (struct sockaddr *)&client, &len);if (cilent_id < 0){cout << "server : accept fail" << endl;}else{cout << "get a new link ...." << endl;}return cilent_id;}void run(){signal(SIGCHLD,SIG_IGN);while(true){int cilent_id = Accept();// 等待客户端int id = fork();if (id == 0){close(server_fd);while (true){// 读取数据char buff[1024];int n = read(cilent_id, buff, sizeof(buff));if (n < 0){cout << "server read fail" << endl;break;}buff[n] = 0;cout << buff << endl;// 写入数据string s = "server say : ";s += buff;write(cilent_id, s.c_str(), s.size());}exit(0);}close(cilent_id);}}int ListenFd(){return server_fd;}
private:int server_fd;uint16_t server_port;string server_ip;
};

 (三)poll的缺点

poll中监听的文件描述符数目增多时,和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符. 因此随着监视的描述符数量的增长, 其效率也会线性下降

6. IO多路转接 -- epoll

(一)epoll初识

是为处理大批量等待fd而作了改进的poll

优化了 select 和 poll 的缺点

(二)epoll的相关系统调用

epoll_create 函数

作用

创建一个epoll的模型

原型

int epoll_create(int size);

参数:

size : 填写的值大于0即可

注意:

用完之后, 必须调用close()关闭

epoll_ctl 函数

作用

epoll的事件注册函数,增减关心的数据

原型

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数:

epfd : epoll_create()的返回值(epoll的句柄).

op :表示动作, 用三个宏来表示

fd : 需要监听的fd

event : 告诉内核需要监听什么事

第二个参数的取值:

EPOLL_CTL_ADD :注册新的fd到epfd中

EPOLL_CTL_MOD :修改已经注册的fd的监听事件

EPOLL_CTL_DEL :从epfd中删除一个fd

struct epoll_event结构如图:

events可以是以下几个宏的集合:

EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭)

EPOLLOUT : 表示对应的文件描述符可以写

EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来)

EPOLLERR : 表示对应的文件描述符发生错误

EPOLLHUP : 表示对应的文件描述符被挂断

EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的

EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要

再次把这个socket加入到EPOLL队列里

epoll_wait 函数

作用

收集在epoll监控的事件中已经发送的事件

原型

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

参数:

epfd :epoll_create() 的返回值

events : 是分配好的epoll_event结构体数组

maxevents : 告诉内核这个events有多大

timeout :超时时间 (毫秒,0会立即返回,-1是永久阻塞)

返回值:

函数调用成功,返回对应IO上已准备好的文件描述符数目;返回0表示已超时;返回小于0表示函数失败

注意:

epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).

(三)epoll 工作原理

实际上,创建epoll,等于创建一个文件,文件里面储存了上述信息

(四)epoll 使用

代码

#include"EpollServer.h"
int main()
{unique_ptr<epoll_server> ptr(new epoll_server());ptr->Init();ptr->start();return 0;
}
#include"server.h"
#include <sys/epoll.h>
class epoll_server
{
public:epoll_server(){for(int i = 0;i < 1024;i++){events[i].data.fd = -1;}}void Init(){sv.Init();epoll_fd = epoll_create(2);if( epoll_fd < 0){cerr << "epoll_create fail" << endl;exit(1);}cout << "epoll_fd :" << epoll_fd << endl;}void EpollCtl(int op,int fd,epoll_event* event){if(op == EPOLL_CTL_DEL){//删除最后一个传参是 nullptr,为了更方便显示细节,就不合并了int n = epoll_ctl(epoll_fd,op,fd,nullptr);if(n < 0){cerr << "epoll_ctl delete fail" << endl;}}else{int n = epoll_ctl(epoll_fd,op,fd,event);if(n < 0){cerr << "epoll_ctl add fail" << endl;}}}void Handler(int n){for (int i = 0; i < n; i++){int fd = events[i].data.fd;cout << "fd: "<< fd << endl;if (fd == sv.ListenFd() && events[i].events & EPOLLIN){int client_fd = sv.Accept();epoll_event event;event.events = EPOLLIN;event.data.fd = client_fd;cout << "client_fd:" << client_fd << endl;EpollCtl(EPOLL_CTL_ADD,client_fd, &event);}else{if(events[i].events & EPOLLIN){//写端char buff[1024];int n = read(fd,buff,sizeof(buff) - 1);if(n > 0){buff[n] = 0;cout << buff << endl;}else if(n == 0){EpollCtl(EPOLL_CTL_DEL,fd, nullptr);close(fd);cerr << "write close" << endl;}else{EpollCtl(EPOLL_CTL_DEL,fd, nullptr);close(fd);cerr << "read fail" << endl;}}}}}void start(){int fd = sv.ListenFd();epoll_event event;event.events = EPOLLIN;event.data.fd = fd;EpollCtl(EPOLL_CTL_ADD,fd,&event);while (true){int n = epoll_wait(epoll_fd, events, 1024, 3000);if (n > 0){Handler(n);}else if (n == 0){cout << "wait ..." << endl;}else{cerr << "epoll_wait fail" << endl;}}}~epoll_server(){}
private:int epoll_fd;epoll_event events[1024];server sv;
};
#include<iostream>
#include<sys/types.h> 
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<signal.h>
#include<memory>
using namespace std;
class server
{
public:server(const uint16_t port = 8080,const string& ip = "0.0.0.0"):server_port(port),server_ip(ip){}void Init(){//构建套接字server_fd = socket(AF_INET,SOCK_STREAM,0);if(server_fd < 0){cout << "server scoket fail" << endl;exit(1);}else{cout << "socket success" << endl;}//绑定端口号sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());socklen_t len = sizeof(server);int tmp = bind(server_fd,(struct sockaddr*)&server,len);if(tmp < 0){cout << "server bind fail" << endl;exit(2);}//开始监听int n  = listen(server_fd,10);if(n < 0){cout << "server listen fail" << endl;exit(3);}}int Accept(){sockaddr_in client;socklen_t len = sizeof(client);int cilent_id = accept(server_fd, (struct sockaddr *)&client, &len);if (cilent_id < 0){cout << "server : accept fail" << endl;}else{cout << "get a new link ...." << endl;}return cilent_id;}void run(){signal(SIGCHLD,SIG_IGN);while(true){int cilent_id = Accept();// 等待客户端int id = fork();if (id == 0){close(server_fd);while (true){// 读取数据char buff[1024];int n = read(cilent_id, buff, sizeof(buff));if (n < 0){cout << "server read fail" << endl;break;}buff[n] = 0;cout << buff << endl;// 写入数据string s = "server say : ";s += buff;write(cilent_id, s.c_str(), s.size());}exit(0);}close(cilent_id);}}int ListenFd(){return server_fd;}
private:int server_fd;uint16_t server_port;string server_ip;
};

(五)epoll的优点(和 select 的缺点对应)

  1. 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  2. 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  3. 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,
  4. epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
  5. 没有数量限制: 文件描述符数目无上限

7. 水平触发 和 边缘触发

(一)水平触发Level Triggered 工作模式

epoll默认状态下就是LT工作模式

当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分,当发生上面两者情况,下一次调用 epoll_wait 时,事件依然处于就绪中,值得数据全部被处理完,epoll_wait 才不会提醒

如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait

举例:

向一个接收缓冲区内发送数据,如果上层没有读,或者没读完,检测 epoll_wait 的时候仍然能检测到,直到这个接收缓冲区内的数据全部读完,就不会接收到提醒

(二)边缘触发Edge Triggered工作模式

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.

当数据发生变化时(即从无到有,从有到多),检测该数据是否会就绪只会提醒一次,如果数据仍然没有读完,那么 epoll_wait 不再提醒,除非下次数据发生变化

举例:

向一个接收缓冲区内发送数据,第一次检测 epoll_wait 是能检测成功的,如果不读,或者只读一部分,后面再次检测 epoll_wait 是收不到提示的,除非再向这个接收缓冲区内发送数据,则下一次 epoll_wait 是能检测成功的

(三)对比LT和ET

LT是 epoll 的默认行为

ET看上去效率更高,因为使用 ET 能够减少 epoll 触发的次数. 但是代价是:为了防止数据不完整,必须一次响应就绪,就立刻把所有的数据都处理完

LT 也能提高效率:接收到一次提醒后,也把所有数据都进行处理

要做到上述,就必须使用处理数据使用非阻塞等待,即对 fd 使用 fcntl 函数,设置成O_NONBLOCK

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词