C语言-IO模型

qscool 2020-05-10

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)){
    //        // 已经准备就绪 做一些该做的操作
    //    }
    // ...
}

相关推荐