IO模型(阻塞,非阻塞,多路复用)

在了解IO模型前,先了解什么叫IO,IO得操作是怎么样的?

        IO既输入输出,指的是一切操作程序或设备与计算机之间发生的数据传输的过程。它分为IO设备和IO接口两个部分。

  • IO设备:就是指可以与计算机进行数据传输的硬件。最常见的I/O设备有打印机、硬盘、键盘和鼠标。从严格意义上来讲,它们中有一些只能算是输入设备(比如说键盘和鼠标);有一些只是输出设备(如打印机)。
  • IO接口:就是是主机和外设之间的交接界面,通过接口可以实现主机和外设之间的信息交换。

那IO是怎么操作这些进行数据的传输呢?

IO模型(阻塞,非阻塞,多路复用)_第1张图片

而用户进程中的一个完整IO分为两个阶段:

  • 用户空间与内核空间交互
  • 内核空间与设备空间交互

 Linux中的五种IO模型有阻塞IO模型、非阻塞IO模型、信号驱动IO模型、IO多路复用模型、异步IO模型。通常有同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式。IO模型(阻塞,非阻塞,多路复用)_第2张图片

同步与异步

  • 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。(死等结果)
  • 所谓异步,就是当一个异步过程调用发出后,调用者不能立刻得到结果,调用者不用等待这件事完成,可以继续做其他的事情。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。(回调通知)

阻塞与非阻塞

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,CPU不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
  • 非阻塞调用是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

总的来说就是:同步与异步是 两个对象之间的关系,而阻塞与非阻塞是一个对象的状态

阻塞IO模型(blocking I/O)

场景描述

        点完餐后,不知道什么时候能做好,只好坐在餐厅里面等,直到做好,然后吃完才离开。但是不知道饭能什么时候做好,只好在餐厅等,而不能去逛街,直到吃完饭才能去逛街,中间等待做饭的时间浪费掉了。这就是典型的阻塞。

优点:

  1. 能够及时返回数据,无延迟;
  2. 对内核开发者来说这是省事了; 缺点:
  3. 对用户来说处于等待就要付出性能的代价了;

非阻塞IO模型(nonblocking I/O)

场景描述

        我女友不甘心白白在这等,又想去逛商场,又担心饭好了。所以我们逛一会,回来询问服务员饭好了没有,来来回回好多次,饭都还没吃都快累死了啦。这就是非阻塞。需要不断的询问,是否准备好了。

同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。所以,非阻塞 IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。

缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

IO多路复用模型(I/O multiplexing)

场景描述

        与第二个方案差不多,餐厅安装了电子屏幕用来显示点餐的状态,这样我和女友逛街一会,回来就不用去询问服务员了,直接看电子屏幕就可以了。这样每个人的餐是否好了,都直接看电子屏幕就可以了,这就是典型的IO多路复用

I/O多路复用的主要应用场景如下:

  1. 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
  2. 服务器需要同时处理多种网络协议的套接字。

在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既谁开始发信号那么就对那个文件描述符进行操作。

你可能感兴趣的:(Linux,java,服务器,前端)