Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO

背景

整理之前学习socket编程的时候复习到了多路复用,搜索了有关资料,了解到多路复用也有局限性,本着打破砂锅问到底的精神,最终找到了关于IO模型的知识点。

在《Unix网络编程》一书中提到了五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。

我们在这里就介绍并实现这5种模型。介绍之前,请允许我引用某段比喻

阻塞IO, 给女神发一条短信, 说我来找你了, 然后就默默的一直等着女神下楼, 这个期间除了等待你不会做其他事情, 属于备胎做法.
非阻塞IO, 给女神发短信, 如果不回, 接着再发, 一直发到女神下楼, 这个期间你除了发短信等待不会做其他事情, 属于专一做法.
IO多路复用, 是找一个宿管大妈来帮你监视下楼的女生, 这个期间你可以些其他的事情. 例如可以顺便看看其他妹子,玩玩王者荣耀, 上个厕所等等. IO复用又包括 select, poll, epoll 模式. 那么它们的区别是什么?

  • 1) select大妈 每一个女生下楼, select大妈都不知道这个是不是你的女神, 她需要一个一个询问, 并且select大妈能力还有限, 最多一次帮你监视1024个妹子

  • 2) poll大妈不限制盯着女生的数量, 只要是经过宿舍楼门口的女生, 都会帮你去问是不是你女神

  • 3) epoll大妈不限制盯着女生的数量, 并且也不需要一个一个去问. 那么如何做呢? epoll大妈会为每个进宿舍楼的女生脸上贴上一个大字条,上面写上女生自己的名字, 只要女生下楼了, epoll大妈就知道这个是不是你女神了, 然后大妈再通知你.
    上面这些同步IO有一个共同点就是, 当女神走出宿舍门口的时候, 你已经站在宿舍门口等着女神的, 此时你属于阻塞状态

    接下来是异步IO的情况:
    你告诉女神我来了, 然后你就去打游戏了, 一直到女神下楼了, 发现找不见你了, 女神再给你打电话通知你, 说我下楼了, 你在哪呢? 这时候你才来到宿舍门口。 此时属于逆袭做法

阻塞IO 与 非阻塞IO

Linux 系统编程中,我们 在 有关概念 章节中介绍了阻塞的概念。那么也很容易理解什么是阻塞IO与非阻塞IO。直接看图

阻塞IO

最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。

当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。

%% 时序图 sequenceDiagram title : 阻塞IO participant application participant kernel Note right of application: 应用程序调用系统调用 application ->> kernel: recv() kernel ->> kernel: 判断数据是否就绪(否则等待) kernel ->> kernel: 准备好数据,拷贝到用户空间 kernel ->> application: 拷贝完成,唤醒应用程序

代码如下:

	printf("Calling recv(). \n");
	ret =  recv(socket, recv_buf, sizeof(recv_buf), 0); 
	printf("Had called recv(). \n");

也许有人会说,可以采用多线程+ 阻塞IO 来解决效率问题,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。

非阻塞IO

当用户线程发起一个IO操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送IO操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。

在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。

对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。

%% 时序图 sequenceDiagram title : 非阻塞IO participant application participant kernel Note right of application: 应用程序调用系统调用 application ->> kernel: fread kernel ->> kernel: 数据未就绪 kernel ->> application: 返回EAGAIN application ->> application: 判断返回值 application ->> kernel: fread kernel ->> kernel: 准备好数据,拷贝到用户空间 kernel ->> application: 拷贝完成,返回成功

代码如下:

while(1)
{
	printf("Calling recv(). \n");
	ret =  recv(socket, recv_buf, sizeof(recv_buf), 0); 
	if (EAGAIN == ret) {continue;}
    else if(ret > -1) { break;}
	printf("Had called recv(), retry.\n");
}
posted @ 2020-03-22 00:37  schips  阅读(3231)  评论(0编辑  收藏  举报