【网络】高级IO——阻塞IO和非阻塞IO的实现
目录
一.文件描述符的默认行为——阻塞IO
二.非阻塞IO
2.1.在打开文件或创建套接字时设置非阻塞模式:
2.2.在使用网络I/O接口时请求非阻塞行为:
2.3.fcntl函数
一.文件描述符的默认行为——阻塞IO
在Linux系统中,无论是通过open系统调用打开的文件(包括系统文件、设备文件等),还是通过socket创建的网络套接字(sock),它们对应的文件描述符(fd)默认都是阻塞的。这种阻塞行为是操作系统为了简化同步I/O操作而设计的。
当进程尝试对一个文件描述符执行读或写操作时,如果所需的数据当前不可用(例如,读操作而缓冲区为空)或无法立即写入数据(例如,写操作而缓冲区已满或磁盘I/O繁忙),则进程将被挂起(阻塞),直到以下条件之一发生:
- 对于读操作:有数据可供读取,或者到达文件末尾(EOF)。
- 对于写操作:数据已经被成功写入到内核的缓冲区中,即使这些数据还没有被实际写入到磁盘上。
在阻塞模式下,进程需要等待这些条件成立才能继续执行,这可能会导致进程在I/O操作上花费大量时间,从而降低程序的响应性和吞吐量。
下面是一个简单的例子。
#include <iostream>
#include <unistd.h>
#include <string.h> int main() { std::cout << "This program echoes input in blocking mode. Try typing something and pressing enter.\n"; char buffer[100]; while (true) { ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1); if (bytes_read > 0) { buffer[bytes_read] = '\0'; // 确保字符串以null字符结束 std::cout << "Echo: " << buffer << std::endl; } else if (bytes_read == 0) { // 对于文件描述符如stdin,在阻塞模式下,read返回0通常意味着EOF(文件结束) // 但对于stdin,这通常不会发生,除非输入被重定向自一个文件或管道,并且该文件或管道已经到达末尾 std::cout << "Unexpected end of input (this should not normally happen with stdin)." << std::endl; break; } else { // read返回-1时表示发生错误 std::cerr << "Error reading input: " << strerror(errno) << std::endl; break; } } return 0;
}
上面这就是典型的阻塞式IO,当程序运行起来时,执行流会在read处阻塞,因为read今天读取的是0号文件描述符,也就是键盘文件上的数据,只要我不从键盘上输入数据的话,read就会一直阻塞,此时进程会被操作系统挂起,直到硬件设备键盘上有数据时,进程才会重新投入CPU的运行队列,当我们输入数据后,可以立马看到进程显示出了echo回应的结果,同时进程又立马陷入阻塞,等待我进行下一次的输入数据,这样的IO方式就是典型的阻塞式,同时也是最常用,最简单的IO方式。
为了解决这个问题,Linux提供了非阻塞I/O(Non-blocking I/O)和异步I/O(Asynchronous I/O)等机制。通过设置文件描述符为非阻塞模式,进程可以在I/O操作无法立即完成时立即返回一个错误(通常是EAGAIN
或EWOULDBLOCK
),而不是被挂起。这样,进程就可以继续执行其他任务,而不会因等待I/O操作而阻塞。
对于网络套接字,除了设置非阻塞模式外,还可以使用I/O多路复用技术(如select、poll、epoll)来同时监视多个文件描述符的状态,从而在不增加线程或进程数量的情况下处理多个并发连接。这些技术允许进程在单个线程中高效地管理多个I/O操作,提高了程序的性能和可扩展性。
二.非阻塞IO
在Linux操作系统中,关于设置文件描述符(fd)为非阻塞模式的方式主要有两种。下面是对这两种主要方式的准确描述:
2.1.在打开文件或创建套接字时设置非阻塞模式:
- 对于文件(包括设备文件),通常不建议也不常见将其设置为非阻塞模式,因为文件I/O操作(如读、写)通常是同步完成的,且没有像网络I/O那样的等待状态。但是,如果您确实需要对文件描述符设置非阻塞模式(例如,对于管道、FIFO或某些特殊类型的文件),可以在使用open系统调用时,在flags参数中包含O_NONBLOCK标志。
- 对于套接字,由于套接字是通过socket系统调用创建的,而不是open,因此您不能在socket调用时直接设置O_NONBLOCK。但是,您可以在socket调用之后,使用fcntl函数和F_SETFL命令来修改套接字文件描述符的标志,以包含O_NONBLOCK。
// 创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 设置套接字为非阻塞模式 fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);
注意:像fcntl这样的方式,无论对于系统文件还是网络套接字都是完全适用的。
2.2.在使用网络I/O接口时请求非阻塞行为:
对于网络套接字,虽然您已经在套接字级别设置了非阻塞模式,但在某些情况下,您可能还想在特定的send、recv等I/O调用中请求非阻塞行为,以确保该调用不会因等待数据而阻塞。这可以通过在调用这些函数时包含MSG_DONTWAIT标志来实现。但是,请注意,如果套接字已经设置为非阻塞模式,那么即使没有指定MSG_DONTWAIT,这些调用也不会阻塞。
MSG_DONTWAIT主要用于在套接字处于阻塞模式时,临时请求非阻塞行为。
// 套接字已经设置为非阻塞模式,但显式使用MSG_DONTWAIT也可以 ssize_t n = recv(sockfd, buf, len, MSG_DONTWAIT); if (n == -1 && errno == EAGAIN) { // 处理非阻塞情况下没有数据可读的情况 }
然而,在实际应用中,当您希望套接字以非阻塞方式工作时,通常会在套接字创建后立即使用fcntl设置其非阻塞模式,并在后续的网络I/O调用中不再需要显式地指定MSG_DONTWAIT(除非您有特定的理由需要在某些调用中临时恢复阻塞行为)。
因此,总结来说,设置文件描述符为非阻塞模式的主要方式是通过fcntl和O_NONBLOCK标志(对于套接字和某些特殊类型的文件),而不是通过open的O_NONBLOCK选项(因为套接字不是通过open创建的),并且MSG_DONTWAIT是在网络I/O调用中请求非阻塞行为的额外选项,但它不是设置文件描述符非阻塞模式的独立方法。
2.3.fcntl函数
当涉及到在Linux中对文件进行控制和管理时,fcntl(file control)函数是一个强大的工具。它提供了一种灵活的方式来执行各种文件操作,从修改文件属性到锁定文件,甚至是改变文件的行为。本文将深入探讨fcntl函数的用法、参数和示例,帮助读者更好地了解如何利用这个功能强大的API来操作文件。
fcntl函数是Linux系统中用于执行各种文件控制操作的系统调用之一。它可以用于修改文件描述符的属性,如文件状态标志(file status flags)、文件描述符标志(file descriptor flags)、文件锁(file locks)以及其他的一些操作。fcntl函数提供了对文件或文件描述符进行底层控制的接口,使得开发者可以更精细地管理文件的行为。
fcntl函数的原型和参数
在C语言中,fcntl函数的原型如下:
#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */);
- fd 是要操作的文件描述符。
- cmd 是控制操作的命令。
- arg 是与命令相关联的可选参数。
fcntl函数的cmd参数决定了具体执行的操作类型,常见的一些操作包括:
- F_GETFL:获取文件描述符的状态标志。
- F_SETFL:设置文件描述符的状态标志。
- F_GETLK:获取文件锁。
- F_SETLK:设置或释放文件锁。
- F_SETLKW:阻塞地设置或释放文件锁。
文件状态标志(File status flags)
- O_RDONLY:只读打开。
- O_WRONLY:只写打开。
- O_RDWR:读写打开。
- O_APPEND:追加写入。
- O_CREAT:如果文件不存在则创建文件。
- O_EXCL:与O_CREAT一起使用,如果文件存在则报错。
- O_TRUNC:如果文件存在且为只写或读写,则将其长度截断为0。
文件描述符标志(File descriptor flags):
- FD_CLOEXEC:在exec执行期间关闭文件描述符。
其他标志:
- O_NONBLOCK:非阻塞模式,用于文件描述符,使得对文件的读写操作不会阻塞进程。
- O_SYNC:使得每次write都等到物理 I/O 操作完成后才返回。
- O_DIRECTORY:如果文件名是目录,则打开失败。
- O_DSYNC:等待物理 I/O 数据完成,不等待文件属性更新。
- O_NOATIME:不更新访问时间戳。
- O_NOCTTY:如果设备是终端,不将其分配为控制终端。
下面是一个简单的将文件描述符设置为非阻塞的例子
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h> int main() { std::cout << "This program echoes input in non-blocking mode. Try typing something and pressing enter.\n"; char buffer[100]; while (true) { ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1); if (bytes_read > 0) { buffer[bytes_read] = '\0'; // 确保字符串以null字符结束 std::cout << "Echo: " << buffer << std::endl; } else if (bytes_read == 0) { // 对于文件描述符如stdin,read返回0通常不表示结束,但在某些特殊情况下(如管道关闭)可能会发生 std::cout << "Unexpected end of input (this should not happen with stdin)." << std::endl; break; } else { // read返回-1且errno被设置时表示发生错误 if (errno == EAGAIN || errno == EWOULDBLOCK) { // 这是非阻塞模式下期望的行为,表示没有数据可读 std::cout << "No data available. Waiting...\n"; sleep(1); // 等待一秒后再次尝试 } else { // 处理其他类型的错误 std::cerr << "Error reading input: " << strerror(errno) << std::endl; break; } } } return 0;
}
我们看到它阻塞了。
接下来我将使用fcntl函数来将fd的属性改为非阻塞
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h> /** * 将指定的文件描述符设置为非阻塞模式。 * * @param fd 需要设置为非阻塞模式的文件描述符。 */
void set_no_block(int fd) { // 尝试获取文件描述符的当前标志(flags) int flags = fcntl(fd, F_GETFL, 0); // 检查fcntl调用是否失败 if (flags == -1) { // 如果失败,则打印错误消息并返回 perror("fcntl"); // 使用perror打印错误信息,它会自动添加"fcntl: "前缀 return; } // 使用OR操作将O_NONBLOCK标志添加到现有的flags中 // 这样做的目的是保留文件描述符的其他标志不变,只添加非阻塞特性 fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 注意:这里没有检查fcntl的返回值,因为即使设置失败,也可能不会立即影响程序的行为 // 在实际使用中,可能需要根据需要添加错误处理逻辑
} int main() { std::cout << "This program echoes input in non-blocking mode. Try typing something and pressing enter.\n"; // 将标准输入设置为非阻塞模式 set_no_block(STDIN_FILENO); // 使用STDIN_FILENO代替0,更具可读性 char buffer[100]; while (true) { ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1); if (bytes_read > 0) { buffer[bytes_read] = '\0'; // 确保字符串以null字符结束 std::cout << "Echo: " << buffer << std::endl; } else if (bytes_read == 0) { // 对于文件描述符如stdin,read返回0通常不表示结束,但在某些特殊情况下(如管道关闭)可能会发生 std::cout << "Unexpected end of input (this should not happen with stdin)." << std::endl; break; } else { // read返回-1且errno被设置时表示发生错误 if (errno == EAGAIN || errno == EWOULDBLOCK) { // 这是非阻塞模式下期望的行为,表示没有数据可读 std::cout << "No data available. Waiting...\n"; sleep(1); // 等待一秒后再次尝试 } else { // 处理其他类型的错误 std::cerr << "Error reading input: " << strerror(errno) << std::endl; break; } } } return 0;
}
注意:
F_GETFL
操作用于获取文件描述符的当前标志,这些标志包括文件的访问模式(如只读、只写、读写)和文件状态标志(如同步、异步、非阻塞等)。F_SETFL
操作用于设置文件描述符的标志。O_NONBLOCK
是一个标志,用于指示文件描述符应处于非阻塞模式。在非阻塞模式下,如果操作(如读取或写入)不能立即完成,则调用将返回一个错误,而不是阻塞等待操作完成。这对于需要处理多个输入源或避免阻塞的应用程序非常有用。- 先通过F_GETFL选项获取原有文件描述符的标志位,然后再通过F_SETFL选项将原有的标志位与O_NONBLOCK按位或之后,再重新设置回文件中。这样就可以将文件描述符设置为非阻塞了。
- 非阻塞IO时,read的返回结果是-1,这样合理吗?
当在非阻塞模式下调用
read
函数,并且底层没有数据时,read
会返回-1
,并设置errno
为EAGAIN
或EWOULDBLOCK
(这两个错误码在大多数系统上可互换,表示资源暂时不可用)。这是非阻塞IO的一种标准行为,用于告诉调用者当前没有数据可读,而不是表示read
函数本身出现了错误。底层没有数据,这算错误吗?其实这并不算错误,只不过当底层没有数据时,read以错误的方式返回了,但我们该如何区分read接口是真的调用失败了(比如read读取了一个不存在的fd),还是仅仅底层没有数据罢了,当然通过read的返回值我们是无法区分的,因为read在这两种情况下都返回-1,但可以通过错误码来区分,当非阻塞IO返回时,如果是底层没有数据,错误码会是EWOULDBLOCK或EAGAIN,如果read是真的出错调用了,会有相对应的错误码。
因此,在处理非阻塞IO时,一种常见的做法是在
read
返回-1
时检查errno
,并根据errno
的值来决定下一步的操作。如果errno
表示资源暂时不可用(EAGAIN
或EWOULDBLOCK
),则程序可能会选择等待一段时间后再次尝试读取,或者执行其他任务。如果errno
表示真正的错误(如EBADF
),则程序应该采取适当的错误处理措施。
在Linux系统中,
EAGAIN
和EWOULDBLOCK
是两个常见的错误码,它们通常用于指示资源暂时不可用的情况。具体来说:
EAGAIN:这个错误码代表“Try again”(再试一次),意味着请求的操作暂时无法完成,但之后可能会成功。在网络编程中,
EAGAIN
经常出现在非阻塞套接字的读写操作中,表示暂时没有数据可读或数据无法立即写入。在文件操作中,如果文件描述符被设置为非阻塞模式,并且请求的操作不能立即完成(例如,读取时文件指针已经到达文件末尾,或者写入时磁盘空间不足但系统决定不等待),那么也会返回EAGAIN
。EWOULDBLOCK:这个错误码在某些情况下与
EAGAIN
可互换,同样表示资源暂时不可用。然而,在某些系统或特定的上下文中,EWOULDBLOCK
可能更具体地用于指示某个操作被阻塞了,因为它会等待某个条件(如数据到达)变为真,但在非阻塞模式下,这个等待被阻止了。在大多数情况下,EAGAIN
和EWOULDBLOCK
可以视为同义词,尤其是在处理非阻塞I/O时。
怎么样?是不是很神奇?