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效果更好。
综上:
可以在open函数打开文件时设置为非阻塞,还可以将已经打开的文件描述符设置为非阻塞
将已经打开的文件描述符设置为非阻塞。是第三个功能
fd:设置的文件描述符。
cmd:文件描述符的属性。可以选择具体的功能。
arg:可变参数
使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数。
这样就把一个已经打开的文件描述符设置非阻塞。
eg:设置0号文件描述符属性,让标准输出变成非阻塞,观察现象。
阻塞情况:
#include
#include
#include
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
#include
#include
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;
}
select函数功能是等待多个文件描述符,有一个数据等待完成时就通知进程。进程调用read、recv等IO接口时不会被阻塞。
程序会在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(...);
}
}
}
简单套接字封装:sock.h
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
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!"<
//事件处理,所有事件就绪情况在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
#include
#include
// ./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;
}
注意: 上述服务器存在很严重的问题。
解决方法:
这里为了练习select,不讨论这些复杂情况。
缺点:
优点: