【在Linux世界中追寻伟大的One Piece】五种IO模型和阻塞IO

服务器 0

目录

1 -> 五种IO模型

1.1 -> 阻塞IO(Blocking IO)

1.2 -> 非阻塞IO(Non-blocking IO)

1.3 -> 信号驱动IO(Signal-Driven IO)

1.4 -> IO多路转接(IO Multiplexing)

1.5 -> 异步IO(Asynchronous IO)

2 -> 高级IO概念

2.1 -> 同步通信VS异步通信(synchronous communication/asynchronous communication)

2.2 -> 阻塞VS非阻塞

2.3 -> 非阻塞IO

2.3.1 -> fcntl函数

3 -> 实现函数SetNoBlock

4 -> 轮询方式读取标准输入


1 -> 五种IO模型

1.1 -> 阻塞IO(Blocking IO)

阻塞IO是一种同步IO模型,它是最基本的IO模型。在这种模型中,当一个线程调用read()或write()进行IO操作时,该线程被阻塞,即线程会暂停执行直到有数据被读取或数据完全写入。在此期间,线程不能执行其他操作。阻塞IO的特点是实现简单,但在高并发场景下,每个线程都可能长时间处于阻塞状态,导致系统资源利用率低下。 

在阻塞IO模型中,如果应用程序尝试读取数据,而内核的缓冲区中没有数据可读,那么应用程序会阻塞等待,直到数据准备好。同样,如果应用程序尝试写入数据,但内核的缓冲区已满,写入操作也会阻塞,直到有空间可用。 

阻塞IO通常用于连接数较少且每个连接都有大量数据交换的应用场景。在这种情况下,每个连接都可以由一个单独的线程处理,从而简化编程模型。

总的来说,阻塞IO是在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。

阻塞IO是最常见的IO模型。 

1.2 -> 非阻塞IO(Non-blocking IO)

非阻塞IO允许线程发起IO请求后立即返回,即使数据还没有准备好。在这种模式下,线程可以进行其他任务,而不是等待IO操作完成。如果数据准备好,线程可以继续执行IO操作;如果没有,线程可以轮询或执行其他任务。非阻塞IO适用于需要处理大量并发连接但每个连接的数据量不大的场景。 

总的来说,如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这种过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用。

1.3 -> 信号驱动IO(Signal-Driven IO)

信号驱动IO(Signal-Driven IO)是一种IO模型,它允许应用程序在数据准备好时通过信号来处理IO事件。在这种模型中,当用户线程发起一个IO请求操作时,会给对应的socket注册一个信号函数,然后用户线程会继续执行。当内核数据就绪时,会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。 

信号驱动IO特别适合于UDP套接字,因为在UDP中,SIGIO信号会在数据报到达套接字或套接字上发生错误时产生。这样,应用程序可以在信号处理函数中读取数据,而不需要不断轮询。 

在TCP套接字中,SIGIO信号的使用并不常见,因为TCP是双工的,信号产生得过于频繁,并且信号的出现并没有告诉我们发生了什么事情。因此,对于TCP套接字,SIGIO信号的使用是有限的。

总的来说,信号驱动IO是内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。

1.4 -> IO多路转接(IO Multiplexing)

IO多路转接是一种网络编程技术,它允许一个或多个进程使用单个或少量的线程高效地管理多个网络连接。这种技术通过内核提供的系统调用来实现,内核会监视多个文件描述符,当这些文件描述符上的IO事件(如可读、可写)准备就绪时,内核会通知应用程序。这样,应用程序就可以在同一个线程中同时处理多个网络连接,而不是为每个连接创建一个新的线程,从而提高了程序的并发处理能力和系统资源的利用率。

在Linux系统中,常见的IO多路转接技术包括select、poll和epoll。这些技术在处理大量并发连接时非常有用,尤其是在构建高性能的服务器应用程序时。

虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

1.5 -> 异步IO(Asynchronous IO)

异步IO(Asynchronous IO)是一种IO处理模式,它允许应用程序在发起一个IO操作后立即继续执行其他任务,而不需要等待IO操作的完成。在异步IO模型中,当应用程序发起一个IO请求时,它会立即返回,应用程序可以继续处理其他任务。一旦IO操作完成,系统会通过某种机制通知应用程序,应用程序可以在适当的时候处理IO操作的结果。这种模型可以显著提高程序的并发处理能力和系统资源的利用率,尤其是在处理大量并发IO请求时。

总的来说,异步IO是由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。

任何IO过程中,都包含两个步骤。第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让IO更高效,最核心的办法就是让等待的时间尽量少。

2 -> 高级IO概念

高级IO涉及到文件的IO操作,它包括多种技术,如非阻塞IO、记录锁、IO多路转接、异步IO和存储映射等。这些技术通常依赖于文件描述符(fd)和fcntl函数的支持,用于提高文件IO操作的效率和灵活性。

2.1 -> 同步通信VS异步通信(synchronous communication/asynchronous communication)

同步和异步关注的是消息通信机制。

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果。
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

这里的同步通信和进程之间的同步是完全不相干的概念。

  • 进程/线程同步也是进程/线程之间直接的制约关系。
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。尤其是在访问临界资源的时候。

2.2 -> 阻塞VS非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态。

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

2.3 -> 非阻塞IO

2.3.1 -> fcntl函数

fcntl(file control)是一个在UNIX和类UNIX操作系统中用于文件描述符控制的系统调用。它提供了多种功能,包括复制文件描述符、设置文件状态标志、管理文件锁定以及设置文件描述符的异步I/O通知等。fcntl函数的原型通常如下:

#include <unistd.h>#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ );

其中,fd是要操作的文件描述符,cmd是一个指示要执行的操作的命令,arg是一个可选参数,具体取决于cmd的值。fcntl函数可以接受不同的命令来执行不同的操作,例如:

  • F_DUPFD:复制文件描述符。
  • F_GETFD 和 F_SETFD:获取和解析文件描述符的标志。
  • F_GETFL 和 F_SETFL:获取和解析文件状态标志,如O_NONBLOCK(非阻塞模式)。
  • F_GETLK、F_SETLK 和 F_SETLKW:获取、设置和尝试设置记录锁。
  • F_GETOWN 和 F_SETOWN:获取和解析接收SIGIO信号的进程ID或进程组ID。

fcntl函数在文件IO操作中非常有用,尤其是在需要对文件描述符进行精细控制时,如设置文件为非阻塞模式或管理多进程对同一文件的并发访问。在网络编程中,fcntl也常用于设置文件描述符的异步通知,允许进程在IO事件发生时接收信号,而不是轮询检查。

3 -> 实现函数SetNoBlock

基于fcntl,我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞。

void SetNoBlock(int fd) {	int fl = fcntl(fd, F_GETFL);	if (fl < 0) 	{		perror("fcntl");		return;	}	fcntl(fd, F_SETFL, fl | O_NONBLOCK);}
  • 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)。
  • 然后再使用F_SETFL将文件描述符设置回去。设置回去的同时,加上一个O_NONBLOCK参数。

4 -> 轮询方式读取标准输入

#include <stdio.h>#include <unistd.h>#include <fcntl.h>void SetNoBlock(int fd) {	int fl = fcntl(fd, F_GETFL);	if (fl < 0) 	{		perror("fcntl");		return;	}	fcntl(fd, F_SETFL, fl | O_NONBLOCK);}int main() {	SetNoBlock(0);	while (1) 	{		char buf[1024] = { 0 };		ssize_t read_size = read(0, buf, sizeof(buf) - 1);		if (read_size < 0) 		{			perror("read");			sleep(1);			continue;		}		printf("input:%s/n", buf);	}	return 0;}

感谢各位大佬支持!!!

互三啦!!!

也许您对下面的内容还感兴趣: