文章目录
IO一般分为两步进行的:
- 等待IO就绪。
- 拷贝IO数据到内核或外设。
1. 五种IO模型
-
阻塞IO:内核数据准备好之前,系统调用一直等待。所有套接字系统默认是阻塞模式。
-
非阻塞IO:数据未准备好,系统调用直接返回,并返回EWOULDBLOCK错误码。
注意:非阻塞IO需要程序员循环的方式(轮询)读取文件描述符,需要耗费大量的CPU资源。阻塞的本质是进程被挂起。 -
信号驱动IO:内核将数据准备好后,使用SIGIO信号(29号信号)通知应用,由应用进程进行IO拷贝操作
提前注册信号函数,当收到信号时进入信号处理函数中处理。
Linux进程信号 -
IO多路转接:select、poll、epoll
recv,read,write,send这些IO接口进行IO时,都先要等待数据就绪,再进行拷贝操作。
将IO等待的时间交给多路转接函数(select、poll、epoll),当数据就绪时,再执行对应recv/read等函数。在进行对应的recv等操作,让上面这些函数能够专注于拷贝而不是等待。
多路转接函数可以等待多个文件描述符,将很多等待的时间进行压缩。(一次等待多个文件描述符,任意一个就绪的概率增大,IO效率提高),详情看下面的介绍。
需要注意的是:
多路转接适合于有大量链接,但每个连接都不活跃的情况(聊天软件)。
连接很活跃不适合多路转接。直接非阻塞轮询IO效果更好。
- 异步IO:内核将数据拷贝完成后,通知应用程序(注意:与信号驱动拷贝相比,异步IO数据从内核拷贝到用户区是由系统进行的,用户空间直接进行IO操作即可。)
综上:
- 前四种IO方式都属于同步IO,最后一种属于异步IO。
- 同步:进程需要拷贝内核数据到用户区。异步:系统帮进程将内核数据拷贝到用户区
- IO分为两步,大部分情况等待数据就绪的事件要大于拷贝IO的时间,缩短IO等待时间是提高IO效率的核心方式。
2. 非阻塞IO接口(fcntl)
可以在open函数打开文件时设置为非阻塞,还可以将已经打开的文件描述符设置为非阻塞
fcntl所有功能如下
- 复制一个现有的描述符(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)
将已经打开的文件描述符设置为非阻塞。是第三个功能
fd:设置的文件描述符。
cmd:文件描述符的属性。可以选择具体的功能。
arg:可变参数
使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数。
这样就把一个已经打开的文件描述符设置非阻塞。
eg:设置0号文件描述符属性,让标准输出变成非阻塞,观察现象。
阻塞情况:
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
int main(){
while(true){
char buff[1024]={0};
ssize_t size=read(0,buff,sizeof(buff)-1);
if(size<0){
std::cerr<<"read error "<<size<<std::endl;
break;
}
buff[size]='\0';
std::cout<<"echo: "<<buff<<std::endl;
}
return 0;
}
设置非阻塞情况:
注意非阻塞需要轮询检测是否就绪,如果因为没有就绪而返回,errno会被设置为EAGAIN或EWOULDBLOCK。
注意:IO操作可能被某些信号终断,这时进程会收到EINTR信号,也需要考虑这种情况
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
bool SetNoBlock(int fd){
int tmp_fd=fcntl(fd,F_GETFL);//获取文件描述符属性
if(tmp_fd<0){
std::cerr<<"fcntl error"<<std::endl;
return false;
}
else{
fcntl(fd,F_SETFL,tmp_fd|O_NONBLOCK);//使用F_SETFL将文件描述符设置回去
return true;
}
}
int main(){
SetNoBlock(0);
while(true){
char buff[1024]={0};
ssize_t size=read(0,buff,sizeof(buff)-1);
if(size<0){
if(errno==EWOULDBLOCK||errno==EAGAIN){
std::cout<<"errno: "<<errno<<std::endl;
sleep(1);
continue;
}
else if(errno==EINTR){
//数据被信号中断
std::cout<<"break of"<<std::endl;
sleep(1);
continue;
}
std::cerr<<"read error "<<size<<" errno: "<<errno<<std::endl;
break;
}
buff[size]='\0';
std::cout<<"echo: "<<buff<<std::endl;
}
return 0;
}
3. IO多路转接select接口分析(sys/select.h)
select函数功能是等待多个文件描述符,有一个数据等待完成时就通知进程。进程调用read、recv等IO接口时不会被阻塞。
程序会在select这里等待,直到被监视的文件描述符有一个就绪时。
- nfds:select在等待的多个描述符中,最大的文件描述符+1。对每个文件描述符进行检测。
- fd_set:是一个位图,可以将特定的文件描述符添加到位图中。
FD_SET:将文件描述符设置到fd_set位图中。
FD_ISSET:判断文件描述符是否在fd_set位图中。
FD_ZERO:清除fd_set位图的所有位。
FD_CLR:将特定的文件描述符清从fd_set中清除 - select是系统调用函数,参数传递给系统。这里以readfds读文件描述符这个位图为例
fd_set是输入输出型参数,输入要系统监测的文件描述符。函数返回后fd_set保存的是就绪的文件描述符。
用户调用select:传入readfds位图,让系统检测所有在位图中的文件描述符,是否是读就绪状态。有任意文件描述符就绪就返回。
函数返回,系统到用户区:fd_set位图被系统修改,需要检测的文件描述符中,就绪的文件描述符被设置到这个位图中返回给用户区 - readfds,writefds,exceptfds:分别代表让操作系统监测读事件,写事件,异常(重点监测文件描述符出错)事件。
- seclect中的timeout是一个设置等待时间的结构体
timeout=nullptr时select会阻塞等待
timeout={0};非阻塞轮询
timeout={a,b} a秒 b毫秒之后返回,无论是否有事件就绪
- select返回值:正常返回就绪时文件描述符个数,返回0代表超时,出错返回-1错误原因存于errno。可能错误如下:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足
select工作流程
所以套接字下的select多路转接的伪代码格式(以监测读事件为例)为下图:
int fds[sizeof(fd_set)*8];//select中fd_set最大可以监测的文件描述符
fd_set readfds;
int listen_sock=sock(...);
//套接字链接事件到来,在多路转接中都统一当作读事件就绪,如果没有accept,就认为listen_sock没有就绪
listen_sock add fds;
listen_sock set readfds;
int maxfd=0;
for(int i=0;i<fds.size();i++){
//将有效的文件描述符添加到readfds位图上
fds[i] set readfds;
更新maxfd;
}
int ret=select(maxfd+1,&readfds,NULL,NULL,NULL);//阻塞等待
if(ret>0){
//有事件就绪
for(int i=0;i<fds.size();i++){
if(fds[i] in readfds && fds[i]==listen_sock){
//链接事件就绪
int sock_fd=accept(...);
sock_fd add fds;
}
else if(fds[i] in readfds){
//内核告诉用户这个文件描述符是就绪的,读事件就绪
read(...);
}
}
}
demo select回显服务器
简单套接字封装:sock.h
#pragma once
#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<string.h>
namespace NetWork_Sorket{
class Sork{
public:
static int Socket(){
//创建监听套接字
int listenSock=socket(AF_INET,SOCK_STREAM,0);
if(listenSock<0){
std::cout<<"socket error"<<std::endl;
exit(-1);
}
int opt=1;
//设置套接字属性
setsockopt(opt,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
return listenSock;
}
static bool Bind(int listenSock,int port){
//绑定,IP=INADDR_ANY
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(listenSock,(struct sockaddr*)&local,sizeof(local))<0){
std::cout<<"bind error"<<std::endl;
exit(-2);
}
return true;
}
static int Listen(int listenSock,int Len){//全连接队列长度
//监听
if(listen(listenSock,Len)<0){
std::cout<<"listen error"<<std::endl;
exit(-3);
}
return true;
}
};
}
sever:
#pragma once
#include"sock.h"
#define LISTEN_SIZE 5
#define RFDS_SIZE (sizeof(fd_set)*8) //最大可以等待的套接字个数1024
#define DEF_FD -1 //默认无效套接字
#include<sys/select.h>
#include<vector>
#include<algorithm>
namespace Select{
class SelectSever{
private:
int listenSork;
int port;
public:
SelectSever(int _port):port(_port){
listenSork=NetWork_Sorket::Sork::Socket();
}
void InitSever(){
NetWork_Sorket::Sork::Bind(listenSork,port);
NetWork_Sorket::Sork::Listen(listenSork,LISTEN_SIZE);
}
void Start(){
fd_set rfds;//读文件描述符集
std::vector<int>fd_Array(RFDS_SIZE,DEF_FD);//保存所有文件描述符,DEF_FD代表没有文件描述符。
fd_Array[0]=listenSork;//将监听套接字写入数组第一个元素,之后将其写入到rfds让select等待链接就绪
while(true){
//对所有合法的文件描述符,每次循环重新设置到rfds
FD_ZERO(&rfds);//每次处理后将rfds位图清空
//遍历数组,将有效文件描述符设置到rfds
for(auto& fd:fd_Array){
if(fd==DEF_FD){
continue;
}
else{
//合法fd,添加到文件描述符集中
FD_SET(fd,&rfds);
}
}
int MaxFd=*(std::max_element(fd_Array.begin(),fd_Array.end()));//获取数组最大的文件描述符值
//设定select时间参数(输入,输出参数),每次循环需要重新设定
//struct timeval timeout={5,0};//每隔5秒一次
/*
* seclect中的timeout=nullptr时select会阻塞等待
* timeout={0};非阻塞轮询
* timeout={a,b}as bms之后返回,无论是否有事件就绪
* */
switch(select(MaxFd+1,&rfds,nullptr,nullptr,/*&timeout*/nullptr)){
case 0://超时
std::cout<<"over time"<<std::endl;
break;
case -1://等待出错
std::cout<<"select error"<<std::endl;
break;
default://正常事件处理
//std::cout<<"select!"<<std::endl;
//事件处理,所有事件就绪情况在rfds中
EventProc(rfds,fd_Array);
break;
}//end switch
}//end sever
}
~SelectSever(){
}
private:
void EventProc(const fd_set& rfds,std::vector<int>&fd_Array){
//判定特定的fd是否在rfds中,证明fd文件描述符已经就绪。
for(auto&fd:fd_Array ){
if(fd==DEF_FD){
continue;
}
if(FD_ISSET(fd,&rfds)&&fd==listenSork){
//监听套接字已经就绪
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(fd,(struct sockaddr*)&peer,&len);//不会阻塞
if(sock<0){
std::cout<<"accept error"<<std::endl;
continue;
}
//链接建立后,还要判断sock文件描述符是否就绪,将sock放入select中让select等待数据就绪,服务器不需要阻塞
//将文件描述符添加到fd_Array中,找到数组未使用的位置
int peer_port=htons(peer.sin_port);
std::string peer_ip=inet_ntoa(peer.sin_addr);
std::cout<<"accept! "<<peer_ip<<" : "<<peer_port<<std::endl;
std::vector<int>::iterator pos=find(fd_Array.begin(),fd_Array.end(),DEF_FD);
if(pos==fd_Array.end()){//数组已满
close(sock);//无法处理,直接关闭接受的sock
std::cout<<"select sever is full ! close sock:"<<sock<<std::endl;
}
else{
*pos=sock;
}
}
else{
//处理正常的fd,先判断fd是否就绪
if(FD_ISSET(fd,&rfds)){
//读事件就绪,实现不阻塞的读
char buff[1024]={0};
ssize_t size=recv(fd,buff,sizeof(buff)-1,0);
if(size>0){
buff[size]='\0';
std::cout<<"echo# "<<buff<<std::endl;
}
else if(size==0){
std::cout<<"client quit!"<<std::endl;
//数组对应位置设置为DEF_FD,关闭文件描述符
close(fd);
fd=DEF_FD;
}
else{
std::cerr<<"recv error"<<std::endl;
close(fd);
fd=DEF_FD;
}
}
else{//fd未就绪
//...
}
}
}//end for
}//end fuction
};
}
#include"sever.h"
#include<string>
#include<iostream>
#include<stdlib.h>
// ./sever port
static void usrHelp(char*name){
std::cout<<"UsrHelp: "<<name<<"+port "<<std::endl;
}
int main(int argc,char*argv[]){
if(argc!=2){
usrHelp(argv[0]);
exit(-3);
}
Select::SelectSever*sever=new Select::SelectSever(atoi(argv[1]));
sever->InitSever();
sever->Start();
return 0;
}
运行结果:
注意: 上述服务器存在很严重的问题。
- 因为上述服务器一次读取1024个字节,但是无法确定对端发送的数据是否被完全读完。
- 对端连续发送多条数据,服务器每次读取相同的大小,可能读不到完整的数据出现粘包问题。
解决方法:
- 需要定制客户端和服务器协议,TCP基于字节流,定制对应的读取规则。
- 给每一个文件描述符创建缓冲区。
这里为了练习select,不讨论这些复杂情况。
select函数的优缺点
缺点:
- select函数等待的文件描述符有上限,最大sizeof(fd_set*8)=1024个
- select需要和内核交互数据,涉及到很多数据的来回拷贝。当select管理的链接较多时,会因为拷贝导致效率降低。
- 每次调用select都需要重新添加文件描述符,select会多次遍历保存文件描述符的数组,效率下降。
- 系统监测文件描述符就绪时,系统需要遍历文件描述符表。当链接数目变多时,系统遍历成本变高。
优点:
- select可以同时等待多个文件描述符,只负责等待,由具体的accept,read,recv等函数完成具体的IO操作且不会被阻塞。IO效率提高