Linux下的IO

阻塞IO和非阻塞IO

  • IO 本质是基于操作系统接口来控制底层的硬件之间数据传输,并且在操作系统中实现了多种不同的IO的方式(模型)
  • IO 模型描述的是不同的 IO 方式,比较常用的几种
    • 阻塞型 IO 模型9
    • 非阻塞型 IO 模型
    • 多路复用 IO 模型
  • 阻塞型 IO
    • 当进程发出 IO 请求后,阻塞进程(让进程进入睡眠状态),资源就绪后唤醒进程继续执行
    • 一般默认的 IO 操作都是阻塞型 IO
  • 特点
    • 会一直等待,直到数据就绪

非阻塞型 IO

  • 当进程发出 IO 请求后,无论资源是否就绪都会立即返回,相应的模型如下:
  • 实现非阻塞 IO,需要设置 O_NONBLOCK 标志,设置有两种方式
    • 可以通过调用 fcntl 函数来进行设置
    • 通过 open 函数来进行设置,一般在打开文件时就需要设置
  • fcntl 函数
    • 函数头文件
      • #include <unistd.h>
      • #include <fcntl.h>
    • 函数原型
      • int fcntl(int fd, int cmd, ... /* arg */ );
    • 函数功能
      • 通过命令字(cmd)来设置文件描述符
    • 函数参数
      • fd : 文件描述符
      • cmd : 控制命令字
        • F_GETFD: 获取文件描述符标志
        • F_SETFD: 设置文件描述符标志
        • F_GETFL: 获取文件状态标志
        • F_SETFL : 设置文件状态标志
  • 注意
    • fcntl 函数是一个可变参数列表函数
#include<stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

int main(void){
    int flags;
    char buffer[16] = {0};
    flags = fcntl(0,F_GETFL);
    flags |= O_NONBLOCK;

    int ret = fcntl(0,F_SETFL,flags);
    if(ret == -1){
        perror("[ERRROR] fcntl");
        exit(EXIT_FAILURE);
    }
    while(1){
        fgets(buffer,sizeof(buffer),stdin);
        printf("buffer: %s\n",buffer);
    }
    return 0;
}

IO多路复用

进程处理多路IO请求

  • 在没有多路复用IO之前,对于多路IO请求,一般只有阻塞与非阻塞IO两种方式
    • 阻塞IO
      • 需要结合多进程与多线程,每个进程/线程处理一路I0
  • 缺点:
    • 客户端越多,需要创建的进程/线程越多,相对占用内存资源较多
  • 非阻塞IO
    • 单进程可以处理,但是需要不断检测客户端是否发出IO请求,需要不断占用cpu,消耗 cpu 资源

      多路复用 IO 简介
  • 本质上就是通过复用一个进程来处理多个 IO 请求,基本思想如下
    • 由内核来监控多个文件描述符是否可以进行I/0操作,如果有就绪的文件描述符,将结果告知给用户进程,则用户进程在进行相应的 I/0 操作

三、多路复用方案

  • 目前在 Linux 系统有三种 多路复用 I/0 的方案,具体如下:
    • select 方案
    • poll 方案
    • epoll 方案

四、select 多路复用方案

  • select 是多路复用I/0 的一种解决方案,具体的设计思想如下:
    • 通过单进程创建一个文件描述符集合,将需要监控的文件描述符添加到这个集合中
    • 由内核负责监控文件描述符是否可以进行读写,一旦可以读写,则通知相应的进程可以进行相应的IO操作

select的应用

  • select 多路复用io在实现时主要是以调用 select 函数来实现
  • select 函数的基本信息如下:
    • 函数头文件
      • #include <sys/select.h>
    • 函数原型
      • int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
    • 函数功能
      • 监控一组文件描述符,阻塞当前进程,由内核检测相应的文件描述符是否就绪,一旦有文件描述符就绪,将就绪的文件描述符拷贝给进程,唤醒进程处理
    • 函数参数
      • nfds: 最大文件描述符加1
      • readfds : 读文件描述符集合的指针
      • writefds : 写文件描述符集合的指针
      • exceptfds: 其他文件描述符集合的指针
      • timeout: 超时时间结构体变量的指针
    • 函数返回值
      • 成功 : 返回已经就绪的文件描述符的个数如果设置 timeout,超时就会返回 0
      • 失败:-1,并设置 errno
  • 操作文件描述符集合函数如下:
    • void FD_CLR(int fd,fd set *set);
    • 将 fd 从文件描述符集合中删除
    • fd : 文件描述符
    • set: 文件描述符集合的指针

FD_ISSET

  • int FD_ISSET(int fd,fd set *set)
  • 判断 fd 是否在文件描述符集合中
  • fd : 文件描述符
  • set: 文件描述符集合的指针

FD_SET

  • void FD SET(int fd,fd set *set)
  • 将文件描述符添加到文件描述符集合中
  • fd: 文件描述符
  • set : 文件描述符集合的指针

FD_ZERO

  • void FD_ZERO(fd_set *set)

  • 将文件描述符集合清空

  • set: 文件描述符集合的指针

  • 使用 select 函数监控标准输入,如果有输入,则打印相应的信息

#include<stdio.h>
#include <stdlib.h>
#include <sys/select.h>

int main(void){
    int ret;
    int maxfd = 0;
    fd_set readfds, tmpfds;
    struct timeval tv = {3,0},tmp_tv;
    char buffer[64] = {0};

    FD_ZERO(&readfds);
    FD_SET(0,&readfds);

    for(;;){
        tmp_tv = tv;
        tmpfds = readfds;

        ret = select(maxfd + 1,&tmpfds,NULL,NULL,&tmp_tv);
        if(ret == -1){
            perror("[ERROR] select()");
            exit(EXIT_FAILURE);
        }
        else if(ret == 0){
            printf("Timeout\n");
        }
        else if(ret > 0){
            if(FD_ISSET(0,&tmpfds)){
                fgets(buffer,sizeof(buffer),stdin);
                printf("buffer %s ",buffer);
            }
        }
    }
    return 0;

}
  • 使用 select 监听有名管道,当有名管道有数据时,读取数据并打印
#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <stdlib.h>
#define FIFO_NAME "./fifo"

int main(){
    int fd;
    int ret;
    char rbuffer[64];
    fd = open(FIFO_NAME,O_RDWR);
    if(fd == -1){
        perror("[ERROR] open()");
        exit(EXIT_FAILURE);
    }

    fd_set readfds,tmpfds;
    int maxfd = fd;
    struct timeval tv = {3,0},tmptv;
    FD_ZERO(&readfds);
    FD_SET(fd,&readfds);
    //read(fd,rbuffer,sizeof(rbuffer));
    //printf("recv: %s\n",rbuffer);
    //exit(EXIT_SUCCESS);
    for(;;){
        tmptv = tv;
        tmpfds = readfds;
        ret = select(maxfd+1,&tmpfds,NULL,NULL,&tmptv);
        if(ret == -1){
            perror("[ERROR] select()");
            exit(EXIT_FAILURE);
        }
        else if(ret == 0){
            printf("Timeout\n");
        }
        else if(ret > 0){
            if(FD_ISSET(fd,&tmpfds)){
                read(fd, rbuffer,sizeof(rbuffer));
                printf("recv: %s\n",rbuffer);
            }
            else{
                printf("....");
            }
        }
    }
    return 0;
}

select原理

文件描述符集合

  • 文件描述符集合在定义时的类型为 fd_set,在内核中的定义如下
  • 数组的类型为 long int 类型 __fd_mask 为 typedef 产生的类型,在 64 位系统中 long int 的大小为 8 个字节
    • typedef long int __fd_mask;
  • NFDBITS 的大小为 64,FD_SETSIZE 为 1024,经过计算之后,大小为 16
  • 文件描述符集合的数组最终在存储时,是使用了位图的方式来记录相应的文件描述符,具体原理如下:
    • 数组中没有直接存储文件描述符,而是使用某一位来表示该文件描述符是否需要监控
    • 需要监控的文件描述符需要转成数组的某一个元素的某一位,然后将对应的位设置为 1

  • 比如当 fd = 60 的成员需要监控,则需要将数组的第0个成员的第[60] bit 设置为 1,
  • 当 fd = 64 时,则需要将数组的第1个成员的第[0] bit 设置为 10
  • 总结:
    • 从上面的文件描述符集合内存管理可以分析出,select 最终只能存储 1024 个文件描述符

select底层分析

  • select 基本的原理如下:
  • 在 select()函数中一共需要使用三个文件描述符集合,分别是
    • in: 读文件描述符集合,主要包含 需要进行读的文件描述符的集合,反映在底层实际可以从设备中读取数据
    • out:写文件描述符集合,主要包含 需要进行写的文件描述符的集合,反映在底层实际可以将数据写入到设备
    • exp: 其他文件描述符集合,主要包含其他类型的操作的文件描述符集合
  • 一旦调用了 select() 函数,内核则做了如下事情:
    • 从用户空间将集合的文件描述符拷贝到内核空间
    • 循环遍历 fd set 中所有的文件描述符,来检测是否有文件描述符可进行I/0操作
      • 如果有文件描述符可进行I/0操作,则设置返回的文件描述符集对应位为1(res_in,res_out,res_exp),表示可以进行I/0操作跳出循环,直接返回,最终会赋值给 in,out,exp 文件描述符集合
      • 如果没有文件描述符可进行I/0操作,则继续循环检测,如果设置 timeout,则在超时后返回,此时 select()函数返回0
  • select() 函数 减少了多进程/多线程的开销,但仍然有很多缺点:
    • 每次调用 select0)函数都需要将 fd 集合拷贝到内核空间,这个开销在 fd 很多时越大
    • 每次都需要遍历所有的文件描述符集合,这个开销在 fd 很多时越大
    • 支持的文件描述符只有1024

select 系统调用分析

sys select 系统调用

  • selelct 系统调用在内核中的实现如下
  • 在 select 系统调用中对应的形式参数的值都是由 应用层 select 函数传递过来的

core_sys_select 函数

  • sys_select 主要调用的核心函数为 core_sys_select,具体定义如下:
  • 在 select 系统调用中对应的形式参数的值都是由 应用层 select 函数传递过来的

详细信息

posted @ 2023-04-09 19:29  shubin  阅读(72)  评论(0编辑  收藏  举报