IO既输入输出,指的是一切操作程序或设备与计算机之间发生的数据传输的过程。它分为IO设备和IO接口两个部分。
那IO是怎么操作这些进行数据的传输呢?
而用户进程中的一个完整IO分为两个阶段:
Linux中的五种IO模型有阻塞IO模型、非阻塞IO模型、信号驱动IO模型、IO多路复用模型、异步IO模型。通常有同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式。
总的来说就是:同步与异步是 两个对象之间的关系,而阻塞与非阻塞是一个对象的状态。
点完餐后,不知道什么时候能做好,只好坐在餐厅里面等,直到做好,然后吃完才离开。但是不知道饭能什么时候做好,只好在餐厅等,而不能去逛街,直到吃完饭才能去逛街,中间等待做饭的时间浪费掉了。这就是典型的阻塞。
优点:
我女友不甘心白白在这等,又想去逛商场,又担心饭好了。所以我们逛一会,回来询问服务员饭好了没有,来来回回好多次,饭都还没吃都快累死了啦。这就是非阻塞。需要不断的询问,是否准备好了。
同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。所以,非阻塞 IO的特点是用户进程需要不断的主动询问kernel数据好了没有。
优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
与第二个方案差不多,餐厅安装了电子屏幕用来显示点餐的状态,这样我和女友逛街一会,回来就不用去询问服务员了,直接看电子屏幕就可以了。这样每个人的餐是否好了,都直接看电子屏幕就可以了,这就是典型的IO多路复用。
I/O多路复用的主要应用场景如下:
在IO 多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,整个用户的进程其实是一直被block的。只不过进程是被select这个函数block,而不是被socket IO给block。所以IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用上。
然后下面来几个IO操作例子:
阻塞:
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = inet_addr("xxx.xxx.xxx.xxx");
bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
listen(sockfd,10);
int fd[5];
for(int i = 0;i < 5;i++)
{
fd[i] = accept(sockfd,NULL,NULL);
}
printf("accept 5 ok\n");
//在进程中与5个客户端进行通信
char buf[20];
bzero(buf,20);
read(fd[0],buf,20);//阻塞等待fd[0]进行读操作,才能继续执行
printf("buf is %s\n",buf);
bzero(buf,20);
read(fd[1],buf,20);
printf("buf is %s\n",buf);
bzero(buf,20);
read(fd[2],buf,20);
printf("buf is %s\n",buf);
bzero(buf,20);
read(fd[3],buf,20);
printf("buf is %s\n",buf);
bzero(buf,20);
read(fd[4],buf,20);
printf("buf is %s\n",buf);
}
通过例子可以发现,如果fd[0]没有打印,那么后面将都不会打印。既阻塞等待fd[0]的IO操作之后在进行其他fd里面的操作。
非阻塞:
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = inet_addr("xxx.xxx.xxx.xxx");
bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
listen(sockfd,10);
int fd[5];
for(int i = 0;i < 5;i++)
{
fd[i] = accept(sockfd,NULL,NULL);//阻塞非阻塞属性是在文件描述符中设置
int flag = fcntl(fd[i],F_GETFL);
flag = flag | O_NONBLOCK;//添加非阻塞
fcntl(fd[i],F_SETFL,flag);//把修改后的属性设置回fd[i]
}
sleep(10);
while(1)
{
int num = 0;
//在进程中与5个客户端进行通信
char buf[20];
bzero(buf,20);
num = read(fd[0],buf,20);//非阻塞
bzero(buf,20);
read(fd[1],buf,20);
printf("buf is %s\n",buf);
bzero(buf,20);
read(fd[2],buf,20);
printf("buf is %s\n",buf);
bzero(buf,20);
read(fd[3],buf,20);
printf("buf is %s\n",buf);
bzero(buf,20);
read(fd[4],buf,20);
printf("buf is %s\n",buf);
}
}
非阻塞的缺点就是太占用系统资源了,需要反复的去查看是否接收到信息。
多路复用IO:
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = inet_addr("192.168.3.172");
bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
listen(sockfd,10);
int fd[5];
for(int i = 0;i < 5;i++)
{
fd[i] = accept(sockfd,NULL,NULL);
}
//IO多路复用,应该同时管理多个fd
fd_set readfds;//读表
FD_ZERO(&readfds);
FD_SET(fd[0],&readfds);//把文件描述符加入进表中
FD_SET(fd[1],&readfds);
FD_SET(fd[3],&readfds);
FD_SET(fd[4],&readfds);
while(1)
{
fd_set temp = readfds;
int num = 0;
num = select(fd[4]+1,&temp,NULL,NULL,NULL);//只管理文件描述符的读操作,只监视文件描述符可读
if(num > 0)
{
printf("%d\n",num);
//把readfds中只剩下可操作的(别的客户端发送了数据,当前fd可读)的文件描述符
for(int i = 0; i < fd[4]+1;i++)
{
if ( FD_ISSET(i,&temp) )
{
char buf[20];
read(i,buf,20);//别人发送了数据,当前i文件描述符,可读
printf("fd is %d,data is %s\n",i,buf);
}
}
}
}
return 0;
}
多路复用IO既谁开始发信号那么就对那个文件描述符进行操作。