全面剖析UNIX网络编程的五种I/O模式

在Linux系统中,内核将所有外部设备都视为文件来操作。对一个文件的读写操作会调用内核提供的系统命令,并返回一个文件描述符(file descriptor,简称fd )。同样地,对一个套接字的读写操作也有对应的描述符,称为socket描述符(socketfd)。描述符实际上是一个数字,指向内核中的一个结构体,该结构体包含文件路径、数据区等属性。

《UNIX网络编程》将I/O模型分为五种类型:

1. 阻塞I/O模型

阻塞I/O模型是最常用的I/O模型。在默认情况下,所有文件操作都是阻塞的。当在进程空间中调用recvfrom时,该系统调用会一直阻塞,直到数据包到达并被复制到应用进程的缓冲区中,或者发生错误。这段时间内,进程会一直等待,因此被称为阻塞I/O模型。

工作流程

  1. 应用程序调用recvfrom
  2. recvfrom阻塞等待数据包到达或错误发生
  3. 数据包到达或发生错误时,recvfrom返回

示例代码

c 复制代码
char buffer[1024]; // 定义一个缓冲区用于存储接收到的数据
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL); // 调用recvfrom阻塞等待数据

代码讲解

  • char buffer[1024]:定义一个大小为1024字节的缓冲区,用于存储接收到的数据。
  • int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL,NULL) :调用recvfrom 函数从套接字sockfd 读取数据。该调用会阻塞,直到数据包到达并被复制到缓冲区buffer 中。返回值n是读取到的字节数。

2. 非阻塞I/O模型

在非阻塞I/O模型中,当调用recvfrom 时,如果缓冲区没有数据,它会立即返回一个EWOULDBLOCK错误。应用程序通常会通过轮询不断检查这个状态,以查看内核是否有数据到达。

工作流程

  1. 应用程序调用recvfrom
  2. 如果没有数据,recvfrom 立即返回EWOULDBLOCK
  3. 应用程序轮询检查数据到达状态
  4. 数据到达时,recvfrom成功返回
c 复制代码
// 设置套接字为非阻塞模式
fcntl(sockfd, F_SETFL, O_NONBLOCK); // 设置非阻塞模式

char buffer[1024]; // 定义缓冲区
int n;
while ((n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL)) < 0) {
    if (errno == EWOULDBLOCK) {
        // 无数据,继续轮询
    } else {
        // 处理其他错误
    }
}

代码讲解

  • fcntl(sockfd, F_SETFL, O_NONBLOCK) :将套接字设置为非阻塞模式,这样recvfrom调用不会阻塞。
  • while ((n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL))< 0) :循环调用recvfrom 读取数据。如果没有数据,recvfrom 会立即返回EWOULDBLOCK错误。
  • if (errno == EWOULDBLOCK) {} :检查错误类型是否为EWOULDBLOCK,表示当前没有数据到达,继续轮询。
  • else {}:处理其他可能的错误情况。

3. I/O复用模型

I/O复用模型利用select/poll 系统调用来监控多个文件描述符。进程将一个或多个fd 传递给select/poll ,并阻塞在这些操作上。当任何一个fd 就绪时,select/poll 会返回。这种方式允许我们同时监控多个I/O事件,select/poll 是顺序扫描fd 是否就绪,而且支持的fd 有限,因此它的使用受到了一些制约。epoll 是Linux特有的更高效的I/O复用方式,它采用事件驱动机制代替了顺序扫描,比传统的selectpoll 更高效。当有fd 就绪时,立即回调函数rollback

工作流程

  1. 应用程序调用select/poll
  2. 阻塞等待任一fd就绪
  3. fd 就绪时,select/poll返回
  4. 处理I/O事件

示例代码

c 复制代码
fd_set readfds; // 定义文件描述符集合
FD_ZERO(&readfds); // 初始化文件描述符集合
FD_SET(sockfd, &readfds); // 将套接字添加到集合中

int nfds = select(sockfd + 1, &readfds, NULL, NULL, NULL); // 调用select等待数据
if (nfds > 0) {
    if (FD_ISSET(sockfd, &readfds)) { // 检查套接字是否有数据
        char buffer[1024];
        int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL); // 读取数据
        // 处理数据
    }
}

代码讲解

  • fd_set readfds :定义一个文件描述符集合readfds
  • FD_ZERO(&readfds) :将文件描述符集合readfds初始化为空。
  • FD_SET(sockfd, &readfds) :将套接字sockfd添加到文件描述符集合中。
  • int nfds = select(sockfd + 1, &readfds, NULL, NULL, NULL) :调用select,等待集合中的任何一个文件描述符变为就绪状态。如果有文件描述符就绪,select返回。
  • if (FD_ISSET(sockfd, &readfds)) {} :检查套接字sockfd是否有数据可读。
  • int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL) :从套接字读取数据并存储到缓冲区buffer中。

4. 信号驱动I/O模型

信号驱动I/O模型首先需要开启套接字的信号驱动I/O功能,并通过sigaction 系统调用设置一个信号处理函数。当数据准备就绪时,内核会向进程发送一个SIGIO 信号,通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。

工作流程

  1. 设置信号处理函数并启用信号驱动I/O
  2. 继续处理其他任务
  3. 数据准备就绪时,内核发送SIGIO信号
  4. 信号处理函数调用recvfrom读取数据

示例代码

c 复制代码
// 信号处理函数
void sigio_handler(int signo) {
    char buffer[1024];
    int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL); // 读取数据
    // 处理数据
}

// 设置信号处理函数
struct sigaction sa;
sa.sa_handler = sigio_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGIO, &sa, NULL);

// 启用信号驱动I/O
fcntl(sockfd, F_SETOWN, getpid()); // 将当前进程设为套接字的所有者,以便接收信号
int flags = fcntl(sockfd, F_GETFL);
fcntl(sockfd, F_SETFL, flags | O_ASYNC); // 启用异步I/O

代码讲解

  • void sigio_handler(int signo) {} :定义一个信号处理函数,当接收到SIGIO信号时被调用。
  • struct sigaction sa :定义一个sigaction 结构体sa
  • sa.sa_handler = sigio_handler :将信号处理函数设置为sigo_handler
  • sigemptyset(&sa.sa_mask):初始化信号掩码。
  • sa.sa_flags = 0:设置标志位。
  • sigaction(SIGIO, &sa, NULL) :为SIGIO信号安装信号处理函数。
  • fcntl(sockfd, F_SETOWN, getpid()) :将当前进程设置为套接字的所有者,以便接收SIGIO信号。
  • int flags = fcntl(sockfd, F_GETFL):获取套接字的当前标志。
  • fcntl(sockfd, F_SETFL, flags | O_ASYNC):启用异步I/O(信号驱动I/O)。

5. 异步I/O模型

异步I/O模型与信号驱动I/O模型类似,但它不仅通知我们何时可以开始I/O操作,还会通知我们操作已经完成。应用程序通过系统调用告诉内核启动某个操作,当操作完成(包括将数据从内核复制到用户缓冲区)后,内核会通知应用程序。

工作流程

  1. 告知内核启动I/O操作
  2. 继续处理其他任务
  3. 内核通知I/O操作完成
  4. 处理完成的数据

示例代码

c 复制代码
// 定义异步I/O控制块
struct aiocb cb;
memset(&cb, 0, sizeof(struct aiocb)); // 清零控制块
cb.aio_fildes = sockfd; // 设置文件描述符
cb.aio_buf = buffer; // 设置缓冲区
cb.aio_nbytes = sizeof(buffer); // 设置读取字节数
cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL; // 设置信号通知方式
cb.aio_sigevent.sigev_signo = SIGIO; // 设置通知信号

// 启动异步读操作
aio_read(&cb); // 启动异步读取

// 信号处理函数
void aio_completion_handler(int signo) {
    // 检查操作是否完成
    if (aio_error(&cb) == 0) {
        int n = aio_return(&cb); // 获取读取的字节数
        // 处理数据
    }
}

代码讲解

  • struct aiocb cb :定义一个异步I/O控制块cb
  • memset(&cb, 0, sizeof(struct aiocb)):将控制块清零。
  • cb.aio_fildes = sockfd :设置文件描述符sockfd
  • cb.aio_buf = buffer:设置缓冲区buffer。
  • cb.aio_nbytes = sizeof(buffer):设置要读取的字节数。
  • cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL:设置信号通知方式。
  • cb.aio_sigevent.sigev_signo = SIGIO :设置通知信号为SIGIO
  • aio_read(&cb):启动异步读操作,立即返回,内核将在操作完成时通知。
  • void aio_completion_handler(int signo) {}:定义一个信号处理函数,当异步操作完成时被调用。
  • if (aio_error(&cb) == 0) {}:检查异步操作是否出错。
  • int n = aio_return(&cb):获取读取的字节数。

总结

本文介绍了UNIX网络编程中五种主要的I/O模型:阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O和异步I/O。每种模型都有其独特的应用场景和优缺点,理解这些模型对于优化网络编程性能至关重要。通过实际的代码示例和详细讲解,您可以更好地掌握每种模型的工作原理和实现方法。

参考文献

  • 《UNIX网络编程(第1卷):套接字联网API》 - W. Richard Stevens, Bill Fenner, Andrew M.
    Rudoff
  • 《高级编程:UNIX环境》 - W. Richard Stevens, Stephen A. Rago
  • Linux手册页(man pages)

希望这篇文章对您了解和使用不同的I/O模型有所帮助。如果您有任何问题或需要进一步的解释,请随时留言。

相关推荐
Heart_to_Yang25 分钟前
华为OceanStor磁盘阵列存储恢复出厂设置命令 LUN不处于在线状态,不能执行此操作解决方案
网络·经验分享
千殃sama38 分钟前
Linux高并发服务器开发(十一)UDP通信和本地socket通信
linux·服务器·网络·笔记·学习·udp
聪明的小脑袋瓜儿44 分钟前
网络安全设备——探针
网络·网络安全·探针·安全设备
今天你fpga了嘛1 小时前
以太网协议介绍——UDP
网络·网络协议·udp
hgdlip1 小时前
ip地址是固定的还是经常变换的
网络·网络协议·tcp/ip
起个别名2 小时前
必须掌握的Linux的九大命令
linux·服务器·网络
为几何欢2 小时前
【内网安全】组策略同步-不出网隧道上线-TCP转ICMP
网络·tcp/ip·安全·网络安全
Danica~2 小时前
RpcChannel的调用过程
网络·c++·rpc
季截2 小时前
rpc的仅有通信的功能,在网断的情况下,比网通情况下,内存增长会是什么原因
网络·网络协议·rpc
炫酷的伊莉娜3 小时前
【计算机网络】数据链路层(作业)
网络·计算机网络·数据链路层