I/O模型
- 阻塞I/O
- 非阻塞I/O
- I/O复用
- 信号驱动I/O
- 异步I/O
阻塞I/O模型
应用程序调用一个I/O函数,应用程序会一直等待数据准备好。如果数据没有准备好,就会一直等待。只有当数据准备好,从内核拷贝到用户空间IO函数才成功返回。
非阻塞I/O模型
把一个套接口设置成非阻塞告诉内核,当所有的I/O操作无法完成时,不要将进程睡眠,而返回一个错误信息。此时I/O操作函数将不断的测试数据是否准备好,如果没有准备好,继续测试,知道数据准备好为止。不断测试的过程中会占用cpu时间。
I/O复用模型
I/O复用模型会用到select或者poll函数,这两个函数会使进程阻塞,并同时阻塞多个I/O操作,而且同时对多个读写操作的I/O函数进行检测,知道有数据刻毒或可写,才正真I/O操作函数。
信号驱动I/O模型
允许套接字进行信号驱动I/O,并安装信号驱动函数,进程继续运行并不停止,当数据=准备好时,进程会收到SIGIO信号,可以在信号处理函数中调用I/O操作函数进行处理数。
异步I/O模型
调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知方式,然后立即返回。当内核数据拷贝到缓冲区后,再通知应用程序
大部分场景,一个程序或网络服务出现性能问题时,可以优先考虑I/O所带来的影响。
站在用户的角度,I/O分两个阶段完成,第一阶段是等事件就绪,第二阶段才是真正的进行I/O操作。
对于这5种I/O模型,前四种第一二阶段基本相同,都是等数据就绪后将其从内核拷贝到调用者的缓冲区。而异步I/O模型不同。
影响I/O性能主要是第一阶段等待数据就绪的时候。
提高I/O性能,首先要考虑I/O等的比重。
主要讲述下I/O复用模型及select
I/O复用模型
I/O多路复用指内核一旦发现进程指定的一个或多个IO条件准备读取,它就通知进程。
I/O复用一般用于一下场合:
- 客户端程序要同时处理多个socket。
- 客户端程序要同时处理用户输入和网路连接
- TCP服务器要同时处理监听socket和连接socket。这时I/O复用使用最多的场合
- 服务器要同时处理TCP和UDP请求。
- 服务器要同时监听多个端口或者处理多种服务。
I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪的同时,如果不采取额外的措施,程序就只能按照顺序的依次处理其中的每一个文件描述符,这就使得服务器程序看起来是串行工作的。当然如果要实现并发,只能使用多线程或多进程编程。
select
select系统调用的用途是:提供轮询等待的方式从多个文件描述符中获取状态变化后的消息
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
//第一个参数nfds指定要坚挺的文件描述符的总数,测试的范围是在0到nfds-1的文件描述(nfds为这些文件描述符的最大值+1,因为文件描述符是从0开始的)。
//后面4个参数是输入输出型参数,第2、3、4参数类型都是fd_set*(文件描述符集)。readfds、writefds、exceptfds分别指向可读、可写、可写的文件描述符集。当select调用返回时,内核将修改它们通知应用程序那个文件描述符就绪。
//最后一个参数timeout用来设置select调用的超时时间。采用timeval结构类型的指针,内核将修改它来告诉应用程序select等待了多久。
//select成功返回就绪的文件描述符的总数;返回0时,说明还没有文件描述符就绪。失败返回-1并设置errno
select操作函数
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.c
include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/select.h>
#include<unistd.h>
int fds[1024];
static void usage(const char* proc)
{
printf("usuage:%s [local_ip] [local_port]\n",proc);
}
int startup(const char* ip,int port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);//创建套接字
if(sock<0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)//绑定地址信息
{
perror("bind");
exit(2);
}
if(listen(sock,5)<0)//服务器监听网络
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
return 1;
}
int listen_sock=startup(argv[1],atoi(argv[2]));//监听套接字
int nums=sizeof(fds)/sizeof(fds[0]);
int maxfd=-1;
int i=1;
for(i=1;i<nums;i++)
{
fds[i]=-1;//将放文件描述符的数组初始化位-1
}
fds[0]=listen_sock;//用数组的第一个放listen_sock
while(1)
{
struct timeval timeout={5,0};
fd_set rfds;//定义文件描述符集
FD_ZERO(&rfds);//每次进来将清除rfds的全部位
maxfd=-1;
for(i=0;i<nums;i++)//遍历fds数组,看哪个文件描述符有效
{
if(fds[i]>0)
{
FD_SET(fds[i],&rfds);//fds[i]有效,将设置在rfds中fds[i]的相关位
if(maxfd<fds[i])
{
maxfd=fds[i];//找到文件描述符的最大值
}
}
}
switch(select(maxfd+1,&rfds,NULL,NULL,&timeout))
{
case 0:
printf("timeout..\n");//没有文件描述符事件就绪
break;
case -1:
perror("select");//调用失败
break;
default:
{
for(i=0;i<nums;i++)
{
if(fds[i]<0)
{
continue;//找到第一个有效文件描述符
}
if(i==0 && FD_ISSET//listen_sock(listen_sock,&rfds))
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
int new_fd=accept(listen_sock,(struct sockaddr*)&client,&len);//接受客户请求
if(new_fd<0)
{
perror("accept");//失败继续
continue;
}
printf("get a new client[%s,%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
int j=1;
for(j=1;j<nums;j++)//将新的标识符文件描述符放入fds中
{
if(fds[j]==-1)
{
break;
}
}
if(j==nums)
{
printf("server full..\n");
close(new_fd);
}
else
{
fds[j]=new_fd;
}
}//if
else if((i>0)&&FD_ISSET(fds[i],&rfds))//第二次轮询的时候fds[i]=new_fd ready,进行读事件
{
char buf[1024];
while(1)
{
ssize_t s=read(fds[i],buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
printf("client say:%s",buf);
fflush(stdout);
// fds[i]=-1;
}
else if(s==0)
{
printf("client quit!\n");
close(fds[i]);
fds[i]=-1;//断开连接将fds[i]置为1。
break;
}
else
{
perror("read");
break;
}
}
}//else if
else
{}
}//for
}//default
break;
}//switch
}//while
return 0;
}
//client.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
void usage(const char* proc)
{
printf("%s [server_ip] [server_port]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(1);
}
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("sock");
exit(2);
}
struct sockaddr_in remote;
remote.sin_family=AF_INET;
remote.sin_port=htons(atoi(argv[2]));
remote.sin_addr.s_addr=inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&remote,sizeof(remote))<0)
{
perror("connect");
exit(3);
}
char buf[1024];
while(1)
{
printf("please enter:");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
write(sock,buf,strlen(buf));
}
}
return 0;
}
select缺点
- 每次调用select,都需要把fd集合从用户态拷贝到内核态
- 每次调用select都需要在内核中遍历所有的fd
- select支持的文件描述符个数太小,为sizeof(fd_set)*8=1024