五种I/O模型
Unix下共有五种I/O模型,分别是
(1)阻塞式I/O;
(2)非阻塞I/O;
(3)I/O复用(select和(e)poll);
(4)信号驱动I/O(SIGIO);
(5)异步I/O(Posix.1的aio_系列函数);
阻塞I/O模型
应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。如果数据没有准备好,一直等待。数据准备好了,从内核拷贝到用户空间,表示IO结束,IO函数返回成功指示。非阻塞I/O模型
我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试 数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。
I/O复用模型
该种模型又被称作是多路转接,I/O复用模型会用到select或者poll函数,这两个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。信号驱动I/O模型
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。异步I/O模型
调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。也就是它只需要发起这个读写事件,不要等待与数据搬迁,只需要在结束之后得到成果。举例
(1)张三在钓鱼,当鱼没有上钩时,便一直进行等待;当鱼上钩后,将鱼调出。这便是基本的阻塞式I/O
(2)张三在钓鱼,这次,当鱼没有上钩时,他便翻着看《C语言入门》这本书,也可以玩玩手机;当鱼上钩后,将鱼调出。这是非阻塞I/O
(3)张三在钓鱼,这次他放了一百个鱼竿。依次检查这些鱼竿,当一个鱼竿有鱼上钩后,将鱼调出,然后继续检查所有鱼竿。这是I/O多路复用
(4)张三钓鱼时,将鱼竿上放一个铃铛,当鱼上钩后会响。然后他便可以干自己的事情,当领响时,他知道上钩了,再去收鱼
(5)张三准备钓鱼,这次。。他直接雇了个人给自己钓鱼。这便是异步I/O
相关函数介绍
重定向的dup函数
头文件
#include<unistd.h>
函数原型
int dup(int oldfd);
int dup2(int oldfd, int newfd);
功能
dup
将oldfd文件描述符进行一份拷贝,返回值为最新拷贝生成的文件描述符(最小的未被使用的文件描述符)
dup2
使用newfd对oldfd文件描述符做一份拷贝,必要是可以先关闭newfd文件描述符
调用dup2之后,oldfd文件描述符不变,newfd和oldfd相等。
代码测试
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
int main()
{
umask(0);
int fd = open("test.txt",O_CREAT|O_RDWR,0666);
const char* msg = "hello world";
printf("fd -> %d\n",fd);
write(fd,msg,strlen(msg));
int new_fd = dup(fd);//将fd进行拷贝,保存到new_fd中
printf("new_fd -> %d\n",new_fd);
write(new_fd,msg,strlen(msg));
int cpfd = dup(1);//拷贝标准输出文件描述符
dup2(fd,1);
printf("nice\n");
dup2(cpfd,1);
printf("niec\n");
return 0;
}
运行结果
select函数
头文件
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
函数原型
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *expectfds,struct timeval *timeout);
函数功能
同时等待多个文件描述符
参数
nfds
表示的是等待的文件描述符中的最大的那个+1;
fd_set
表示的是是否需要等待某个文件描述符,所以这里的fd_set底层是用位图实现的,所以我们最多可以等待的文件描述符的个数为sizeof(fd_set)*8;
readfds
表示的是需要等待的读事件的文件描述符集;
writefds
表示的是需要等待的写事件的文件描述符集;
exceptfds
表示的是需要等待的异常事件的文件描述符集;
timeout
表示的是每次select的时间为ty_sec秒
返回值
与fd_set有关的函数
fd_set为一个集合,底层是用位图进行实现的
它的每一位都可以用来表示对应的文件描述符是否收到了事件消息
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
void FD_CLR(int fd, fd_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模型
理解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被清空
也就是说,readfds[]数组来表示连接的客户端,该数组大小表示最多可以连接的数量
而rfds是一个集合,底层用位图实现
表示的是对应的文件描述符有没有收到对方发来的消息
若有,则对应的位上被置为1
代码实现
select服务器
#include<stdio.h>
#include<stdlib.h>
#include<error.h>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/time.h>
#include<string.h>
int readfds[sizeof(fd_set)*8];
int writefds[sizeof(fd_set)*8];
static void Usage()
{
printf("Usage: [ipaddr] [port]\n");
}
int startUp(char* ip, int port)
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)) < 0)
{
perror("bind");
exit(3);
}
if(listen(sockfd,5)<0)
{
perror("listen");
exit(4);
}
return sockfd;
}
int main(int argc, char* argv[])
{
int i = 1;
if(argc != 3)
{
Usage();
exit(1);
}
int listen_sock = startUp(argv[1],atoi(argv[2]));
int num = sizeof(fd_set)*8;
writefds[0] = -1;
readfds[0] = listen_sock;
for(; i<num; i++)
{
writefds[i] = -1;
readfds[i] = -1;
}
fd_set rfds,wfds;
while(1)
{
int maxfd = -1;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
for(i = 0; i<num; ++i)
{
if(readfds[i] != -1)
{
FD_SET(readfds[i],&rfds);
}
if(writefds[i] != -1)
{
FD_SET(writefds[i],&wfds);
}
maxfd = readfds[i] > maxfd ? readfds[i] : maxfd;
maxfd = writefds[i] > maxfd ? writefds[i] : maxfd;
}
struct timeval time = {1,0};
int n = select(maxfd+1,&rfds,&wfds,NULL,&time);
switch(n)
{
case 0:
printf("time out...\n");
break;
case -1:
break;
default:
{
for(i = 0; i<num; ++i)
{
if(FD_ISSET(readfds[i],&rfds))
{
if(i == 0)
{//监听服务器就绪
struct sockaddr_in client;
socklen_t len = sizeof(client);
int client_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(client_sock < 0)
{
perror("accpet");
exit(5);
}
else
{
int tmp = 0;
for(; tmp < num; ++tmp)
{
if(readfds[tmp] == -1)
{
readfds[tmp] = client_sock;
break;
}
}
if(tmp == num)
{
printf("readfds 满了\n");
exit(6);
}
}
}
else
{//等待的普通文件描述符就绪
char buf[1024];
ssize_t s = read(readfds[i],buf,sizeof(buf));
if(s < 0)
{
perror("read");
exit(7);
}
else if(s == 0)
{
printf("客户端退出..\n");
close(readfds[i]);
readfds[i] = -1;
continue;
}
else
{
buf[s] = 0;
printf("#client: %s",buf);
fflush(stdout);
write(readfds[i],buf,strlen(buf));
}
}
}
}
}
}
}
return 0;
}
select客户端
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
static void Usage()
{
printf("Usage: [ipaddr] [port]\n");
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage();
exit(1);
}
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0)
{
perror("connect");
exit(3);
}
printf("连接成功...\n");
char buf[1024];
while(1)
{
//发数据
printf("#client: ");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s <= 0)
{
perror("read");
exit(4);
}
int fd = dup(1);
dup2(sockfd,1);
printf("%s",buf);
fflush(stdout);
dup2(fd,1);
//收数据
s = read(sockfd,buf,sizeof(buf)-1);
if(s == 0)
{
printf("服务器退出...\n");
break;
}
else if(s < 0)
{
perror("read");
exit(5);
}
else
{
buf[s-1] = '\0';
printf("#server: %s\n",buf);
}
}
close(sockfd);
return 0;
}
select服务器优缺点
优点
1、不需要fork或者pthread_create就可以实现一对多的通信,简化了进程线程的使用
2、同时等待多个文件描述符,效率相对较高
缺点
1、每次调用select,都需要把fd集合从用户态拷贝到内核态,在fd很多的情况下,循环次数多;
2、每次调用select都需要在内核遍历传递进来的所有fd,开销比较大
3、select支持的文件描述符数量过小,默认是1024