在Linux系统中,内核将所有外部设备都视为文件来操作。对一个文件的读写操作会调用内核提供的系统命令,并返回一个文件描述符(file descriptor,简称fd )。同样地,对一个套接字的读写操作也有对应的描述符,称为socket描述符(socketfd)。描述符实际上是一个数字,指向内核中的一个结构体,该结构体包含文件路径、数据区等属性。
《UNIX网络编程》将I/O模型分为五种类型:
1. 阻塞I/O模型
阻塞I/O模型是最常用的I/O模型。在默认情况下,所有文件操作都是阻塞的。当在进程空间中调用recvfrom时,该系统调用会一直阻塞,直到数据包到达并被复制到应用进程的缓冲区中,或者发生错误。这段时间内,进程会一直等待,因此被称为阻塞I/O模型。
工作流程
- 应用程序调用recvfrom
- recvfrom阻塞等待数据包到达或错误发生
- 数据包到达或发生错误时,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错误。应用程序通常会通过轮询不断检查这个状态,以查看内核是否有数据到达。
工作流程
- 应用程序调用recvfrom
- 如果没有数据,recvfrom 立即返回EWOULDBLOCK
- 应用程序轮询检查数据到达状态
- 数据到达时,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复用方式,它采用事件驱动机制代替了顺序扫描,比传统的select 和poll 更高效。当有fd 就绪时,立即回调函数rollback
工作流程
- 应用程序调用select/poll
- 阻塞等待任一fd就绪
- fd 就绪时,select/poll返回
- 处理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来读取数据,并通知主循环函数处理数据。
工作流程
- 设置信号处理函数并启用信号驱动I/O
- 继续处理其他任务
- 数据准备就绪时,内核发送SIGIO信号
- 信号处理函数调用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操作,还会通知我们操作已经完成。应用程序通过系统调用告诉内核启动某个操作,当操作完成(包括将数据从内核复制到用户缓冲区)后,内核会通知应用程序。
工作流程
- 告知内核启动I/O操作
- 继续处理其他任务
- 内核通知I/O操作完成
- 处理完成的数据
示例代码
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模型有所帮助。如果您有任何问题或需要进一步的解释,请随时留言。