C语言-IO模型
IO模型
在UNIX/Linux下主要有4种I/O模型
- 阻塞I/O(最常用)
- 非阻塞I/O(可防止进程阻塞在I/O操作上,需要轮询)
- I/O多路复用(允许同时对多个I/O进行控制)
- 信号驱动I/O(一种异步通讯模型)
阻塞I/O模式
- 阻塞I/O模式是最普遍使用的I/O模式,大部分程序使用的都是阻塞模式的I/O
- 缺省情况下,套接字建立后所处于的模式就是阻塞I/O模式
- 很多读写函数在调用过程中会发生阻塞
- 读操作-read、recv、recvfrom
- 写操作-write、send
- 其他操作-accept、connect
读阻塞-这里以read函数为例
- 进程调用read函数从套接字上读取数据,当套接字的接受缓冲区中还没有数据可读,函数read将会发生阻塞
- 它会一直阻塞下去,等待套接字的接受缓冲区中有数据可读
- 经过一段时间后,缓冲区内接受到数据,于是内核便去唤醒该进程,通过read访问这些数据
- 如果在进程阻塞的过程中,对方发生故障,那这个进程将永远阻塞下去
写阻塞
- 在写操作时发生阻塞的情况要比读操作少,主要发生要写入的缓冲区的大小小于要写入的数据量的情况下
- 这时,写操作不进行任何拷贝工作,将发生阻塞
- 一旦发送缓冲区内有足够的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区
- UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区慢的情况;也就是说UDP套接字上执行的写操作永远不会阻塞
非阻塞I/O模型
- 当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我
- 当一个应用程序使用了非阻塞的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称作polling)
- 应用程序不停的polling 内核来检查是否I/O操作已经就绪。这是一个极浪费CPU资源的操作
- 这种模式使用中不普遍
非阻塞模式的实现-fcntl() 函数
- 当你一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞I/O模式
- 可以使用fcntl()设置一个套接字的标志位O_NONBLOCK 来实现非阻塞
- 代码实现
#include <fcntl.h> int flag = 0; // 获取到当前的设置 flag = fcntl(sockfd,F_SETFL,0); flag |=O_NONBLOCK; // 设置回去 fcntl(sockfd,F_SETFL,flag);
或者使用 ioctl()函数
#include <sys/ioctl.h> int b_on = 1; ioctl(sockfd, FIONBIO, &b_on);
多路复用
- 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的
- 若采用非阻塞模式,对多个输入输出进行轮询,但又太浪费CPU时间
- 若设置多进程,分别处理一条数据通路,将新产生进程的同步于通讯问题,使程序变得更加复杂
- 比较好的方式是使用I/O多路复用,基本思想是
- 先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个以及准备好进行I/O时函数才返回。
- 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作
#include <sys/select.h> int fd_num = -1; // 一般使用一个计数器计算集合中的文件描述符数量 void FD_ZERO(fd_set *fdset); // 将集合清零 void FD_SET(int fd, fd_set *fdset); // 将关心的文件描述符加入到集合中 void FD_CLR(int fd, fd_set *fdset); // 将某个文件描述符从集合中清除 int FD_ISSET(int fd, fd_set *fdset); // 判断fd是否在set集合中 int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout); // nfds 传入 fd_num+1 表示最大集合数 // 这三个集合如果需要传入 不需要则可以传NULL // readfds 读集合 // writefds 写集合 // errorfds 异常集合 // timeout 超时等待 // struct timeval /* _STRUCT_TIMEVAL { __darwin_time_t tv_sec; /* seconds */ 秒 __darwin_suseconds_t tv_usec; /* and microseconds */ 毫秒 }; */
Demo:多路复用模型
#include <sys/select.h> #include <stdio.h> int main(int argc, char *argv[]) { fd_set rset; // 创建读集合 int max = -1; int fd = -1; // 句柄 struct timeval timeout; // socket 连接省略... // bind 省略... // listen 省略... // 如果循环是要一直执行如下操作 FD_ZERO(&rset); // 将集合清零 FD_SET(fd, &rset); // 将创建好的fd加入到读集合中 max++; // 计数器+1 // 依次将其他连接的fd加入... 省略 // 设置超时时间 为5s timeout.tv_sec = 5; timeout.tv_usec = 0; // 使用select监控 select(max + 1, &rset, NULL, NULL, &timeout); // 依次判断select监控后的rset // 例如:fd // 现在的rset集合中存放的是已经就绪的描述符 所以需要依次判断 手上的描述符是否存在于集合中 if (FD_ISSET(fd, &rset)) { // 已经准备就绪 做一些该做的操作 } // if (FD_ISSET(fd2,&rset)){ // // 已经准备就绪 做一些该做的操作 // } // ... }
Songzhibin