I/O多路转接之select和非阻塞IO
Posted 昨天;明天。今天。
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了I/O多路转接之select和非阻塞IO相关的知识,希望对你有一定的参考价值。
文章目录
1. 非阻塞IO
1-1 fcntl
-
一个文件描述符, 默认都是阻塞IO.
-
函数原型如下:
#include <unistd.h> #include <fcntl.h> fcntl - manipulate file descriptor
-
传入的cmd的值不同, 后面追加的参数也不相同(可变参数列表)
fcntl函数有5种功能:
参数 | 功能 |
---|---|
cmd=F_DUPFD | 复制一个现有的描述符 |
cmd=F_GETFD或F_SETFD | 获得/设置文件描述符标记 |
cmd=F_GETFL或F_SETFL | 获得/设置文件状态标记 |
cmd=F_GETOWN或F_SETOWN | 获得/设置异步I/O所有权 |
cmd=F_GETLK,F_SETLK或F_SETLKW | 获得/设置记录锁 |
- 我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞。
1-2 实现函数SetNoBlock
- 代码链接
轮询方式读取标准输入
void SetNoBlock(int fd)
int f1 = fcntl(fd, F_GETFL); // 获取状态标记位 --- 位图
if(f1<0)
perror("fcntl");
return;
fcntl(fd, F_SETFL, f1 | O_NONBLOCK); // 设置状态标记位
- 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
- 使用F_SETFL将文件描述符设置回去, 设置回去的同时, 加上一个O_NONBLOCK参数。
2. I/O多路转接之select
1-1 初识select
系统提供select函数来实现多路复用输入/输出模型
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
1-2 select函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数解释:
- 参数nfds是需要监视的最大的文件描述符值+1;
- rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合、可写文件描述符的集合、异常文件描述符的集合;
- 这些参数都是输入输出型参数,也就是说;
- 例如:readfds 读集合我们设置成功了,知道有一个或多个文件就绪它就返回;不一定我们设置的所有文件全部就绪完成,所有我们需要不断的重新设置。
- 参数timeout为结构timeval,用来设置select()的等待时间.
参数timeout取值:
- NULL:则表示select()没有timeout, select将一直被阻塞,直到某个文件描述符上发生了事件;
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
- 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
关于fd_set结构
- 其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符。
提供了一组操作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的全部位 |
关于timeval结构
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0
函数返回值:
- 执行成功则返回文件描述词状态已改变的个数;
- 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;
- 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds, writefds, exceptfds和timeout的值变成不可预测。
错误值可能为:
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数n 为负值。
- ENOMEM 核心内存不足
常见的程序片段如下:
fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset))……
1-3 理解select执行过程
- 理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节, fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set); 后set变0001,0000(第5位置为1)
(3)若再加入fd= 2, fd=1,则set变为0001,0011
(4)执行 select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为 0000,0011。
注意:没有事件发生的fd=5被清空。
1-4 socket就绪条件
读就绪
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件
描述符, 并且返回值大于0; - socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
写就绪 - socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大 于 等 于 低 水 位 标 记 SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
- socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE
信号; - socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误;
1-5 select的特点
- 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)= 128,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是128*8=1024。
- 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
- 一是用于再select 返回后, array作为源数据和fd_set进行FD_ISSET判断。
- 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
1-6 select缺点
- 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小
1-7 代码链接
我这份代码只写了读端;数组我用vector实现; 数组poll的使用数组
https://gitee.com/ding-xushengyun/linux__cpp/tree/master/23NO3_27/1_selectServer
Linux 高级IO
文章目录
五种IO模型
- 阻塞IO
在内核将数据准备好之前, 系统调用会一直等待。 所有的套接字, 默认都是阻塞方式。
阻塞IO是最常见的IO模型
- 非阻塞IO
如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用。
- 信号驱动IO
内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。
- IO多路转接
虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
- 异步IO
由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
小结: 任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少。
高级IO重要概念
什么叫做高效的IO?
IO=等待+数据拷贝,而高效的IO就是在整个周期内,等的比重特别少,一直在做拷贝。提高IO效率就是减少IO过程等待的比重。
同步通信 vs 异步通信
同步和异步关注的是消息通信机制
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果。
- 异步则是相反, 调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后, 被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相干的概念
- 进程/线程同步也是进程/线程之间直接的制约关系。
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候。
同学们以后在看到 “同步” 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步与互斥的同步。
阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
- 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
其他高级IO
非阻塞IO,纪录锁,系统V流机制, I/O多路转接(也叫I/O多路复用) ,readv和writev函数以及存储映射IO(mmap),这些统称为高级IO。
非阻塞IO
fcntl
一个文件描述符, 默认都是阻塞IO
函数原型:
int fcntl(int fd,int cmd,.../* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同
fcntl函数有5种功能
- 复制一个现有的描述符(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)
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞
实现函数SetNoBlock
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞
void SetNonBlock(int fd)
int f1=fcntl(fd,F_GETFL);
if(f1<0)
std::cerr<<"获取文件标记位失败..."<<std::endl;
return;
fcntl(fd,F_SETFL,f1|O_NONBLOCK);
- 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)
- 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数
轮询方式读取标准输入
实现代码
#include<iostream>
#include<string>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
void SetNonBlock(int fd)
int f1=fcntl(fd,F_GETFL);
if(f1<0)
std::cerr<<"获取文件标记位失败..."<<std::endl;
return;
fcntl(fd,F_SETFL,f1|O_NONBLOCK);
int main()
char buffer[1024];
SetNonBlock(0);
while(true)
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0)
buffer[s]=0;
std::cout<<"buffer: "<<buffer<<std::endl;
else
if(errno==EAGAIN || errno == EWOULDBLOCK)
sleep(2);
std::cout << "当前没有出错,仅仅底层数据没有就绪罢了..." << std::endl;
continue;
if(errno == EINTR)
std::cout << "读取被信号中断" << std::endl;
continue;
std::cout<<"read error"<<std::endl;
break;
return 0;
运行结果
I/O多路转接之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);
参数解释
- 参数nfds是需要监视的最大的文件描述符值+1
- rdset 对应于需要检测的可读文件描述符的集合
- wrset 对应于需要检测的可写文件描述符的集合
- exset 对应于需要检测的异常文件描述符的集合
- 参数 timeout 为结构 timeval ,用来设置select()的等待时间
关于timeval结构
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
参数timeout取值
- NULL:则表示select()没有timeout, select将一直被阻塞,直到某个文件描述符上发生了事件
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
- 特定的时间值:如果在指定的时间段里没有事件发生, select将超时返回
fd_set结构
fd_set是操作系统提供的文件描述符集,其这个结构就是一个整型数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符。
fd_set是一个输入输出型函数,它的功能如下
- 所有关心读事件的文件描述符,都应该添加在这个集合中。
- 输入:用户告诉内核,OS你要帮我检测一下在这个集合中的fd的读事件。
- 输出:内核告诉用户,你关心的fd,有那些文件描述符已经就绪了,可以读取了!
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的全部位
函数返回值
- 执行成功则返回文件描述符状态已就绪的个数
- 如果返回0代表在描述词状态改变前已超过timeout时间
- 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds, writefds, exceptfds和timeout的值变成不可预测
错误值可能为:
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数n为负值
- ENOMEM 核心内存不足
常见的程序片段如下
fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset))......
理解select执行过程
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd
- 执行 fd_set set; FD_ZERO(&set) ;则set用位表示是0000,0000。
- 若 fd=5 ,执行 FD_SET(fd,&set) ;后set变为0001,0000(第5位置为1)
- 若再加入fd=2,fd=1,则set变为0001,0011.
- 执行 select(6,&set,0,0,0) 阻塞等待
- 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生时fd=5被清空
socket就绪条件
读就绪
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0。
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0
- 监听的socket上有新的连接请求
- socket上有未处理的错误
写就绪
- socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0
- socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号
- socket使用非阻塞connect连接成功或失败之后
- socket上有未读取的错误
select的特点
- 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096
- 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd。一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断;二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数
select缺点
- 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说非常不便
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小
select使用示例
套接字封装 sock.hpp
#pragma once
#include<iostream>
#include<string>
#include<cstdlib>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<strings.h>
namespace ns_sock
enum
SOCKET_ERR=2,
BIND_ERR,
LISTEN_ERR
;
const int g_backlog=5;
class Sock
public:
static int Socket()
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
std::cerr<<"socket error!"<<std::endl;
exit(SOCKET_ERR);
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 设置套接口选项值
return sock;
static void Bind(const int &sock,const u_int16_t &port)
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
std::cerr<<"bind error!"<<std::endl;
exit(BIND_ERR);
static void Listen(const int &sock)
if(listen(sock,g_backlog)<0)
std::cerr<<"bind error!"<<std::endl;
exit(LISTEN_ERR);
;
select 服务端头文件 select_server.hpp
#pragma once
#include"sock.hpp"
#include<sys/select.h>
#include<sys/types.h>
#include<sys/socket.h>
namespace ns_select
using namespace ns_sock;
#define NUM (sizeof(fd_set)*8)
const int g_default = 8080;
class SelectServer
private:
u_int16_t port_;
int listen_sock_;
int fd_arrar_[NUM];
// EndPoint fd_arrar_[NUM];
public:
SelectServer(int port=g_default):port_(port),listen_sock_(-1)
for(int i=0;i<NUM;i++)
fd_arrar_[i]=-1;
void InitSelectServer()
listen_sock_=Sock::Socket();
Sock::Bind(listen_sock_,port_);
Sock::Listen(listen_sock_);
fd_arrar_[0]=listen_sock_;
std::ostream& PrintFd()
for(int i=0;i<NUM;i++)
if(fd_arrar_[i]!=-1) std::cout<<fd_arrar_[i]<<' ';
return std::cout;
void HandlerEvent(fd_set &rfds)
//判断我的有效sock,是否在rfds中
for(int i=0;i<NUM;i++)
if(-1==fd_arrar_[i])
continue;
//区分新链接和数据
if(FD_ISSET(fd_arrar_[i],&rfds))// 测试描述词组set中相关fd的位是否为真
if(fd_arrar_[i]==listen_sock_)
//新链接
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(listen_sock_,(struct sockaddr*)&peer,&len);
if(sock<0)
std::cout<<"accept error"<<std::endl;
else
//将新的sock添加到文件描述符中
int j=0;
for(;j<NUM;j++)
if(fd_arrar_[j]==-1)
break;
if(j==NUM)
std::cout<<"fd_arrar 已经满了"<<std::endl;
close(sock);
else
fd_arrar_[j]=sock;
std::cout<<"获取链接成功,sock: "<<sock<<" 已经添加到数组中了,当前:"<<std::endl;
PrintFd() << " [当前]" << std::endl;
else
//数据
// 1.网络通信,定制协议,和业务场景有关
// 2.是不是每一个sock,都必须有自己独立的buffer
char buffer[1024];
ssize_t s=recv(fd_arrar_[i],buffer,sizeof(buffer),0);
if(s>0)
buffer[s]='\\0';
std::cout<<"clint say#"<<buffer<<std::endl;
else if(s==0)
std::cout<<"client quit------sock: "<<fd_arrar_[i]<<std::endl;
// 对端链接关闭
close(fd_arrar_[i]);
// 从rfds中,去掉sock
fd_arrar_[i]=-1;
PrintFd()<<"[当前]"<<std::endl;
else
// 读取异常
std::cerr<<"recv error"<<std::endl;
void Loop()
//在服务器最开始的时候,我们只有一个sock,listen_sock, 有读事件就绪,读文件描述符看待的!
fd_set rfds;
//FD_SET(listen_sock_,&rfds);
while(true)
//struct timeval timeout=3,0;
// 对位图结构进行清空
FD_ZERO(&rfds);
int max_fd=-1;
for(int i=0;i<NUM;i++)
if(-1==fd_arrar_[i]) continue;
FD_SET(fd_arrar_[i],&rfds);
if(max_fd<fd_arrar_[i]) max_fd=fd_arrar_[i];
int n=select(max_fd+1,&rfds,nullptr,nullptr,nullptr);
switch(n)
case 0:
std::cout<<"timeout ... "<<std::endl;
break;
case -1:
std::cout<<"select error"<<std::endl;
break;
default:
// select成功,至少有一个fd是就绪的
HandlerEvent(rfds);
//select成功,至少有一个fd是就绪的
//std::cout<<"有事件发生"<<std::endl;
break;
~SelectServer()
if(listen_sock_>=0)
close(listen_sock_);
;
select 服务端文件
#Linux 高级IO
I/O多路转接复用机制---select,poll,epoll