Linux 【C编程】IO进阶— 阻塞IO、非阻塞IO、 多路复用IO、 异步IO

文章目录

  • 1.阻塞IO与非阻塞IO
    • 1.1为什么有阻塞式?
    • 1.2非阻塞
  • 2.阻塞式IO的困境
  • 3.并发IO的解决方案
    • 3.1非阻塞式IO
    • 3.2多路复用IO
      • 3.2.1什么是多路复用IO?
      • 3.2.1多路复用IO select原理
      • 3.2.1多路复用IO poll原理
    • 3.3异步IO

1.阻塞IO与非阻塞IO

1.1为什么有阻塞式?

1.常见的阻塞:wait pause sleep 等函数 ; read或write某些文件
2.阻塞式的好处:在某些情况下,阻塞式 I/O 可以更有效地利用系统资源。在一些高负载场景下,阻塞式 I/O 可以避免频繁的上下文切换,降低系统开销。 阻塞式 I/O 的编程模型通常比较直观和易于理解。代码顺序执行,不需要太多复杂的逻辑来处理异步操作和事件回调。

1.2非阻塞

1.为什么要实现非阻塞?
非阻塞 I/O 允许程序在进行 I/O 操作时不被阻塞,可以继续执行其他任务。这对于高并发的应用程序尤其重要,可以充分利用系统资源,提高系统的吞吐量和性能。 在一些场景下,程序需要快速响应并处理多个客户端请求。如果使用阻塞 I/O,一个慢速的 I/O 操作可能会导致整个程序被阻塞,无法及时响应其他请求,而非阻塞 I/O 可以避免这种情况。非阻塞 I/O 可以与异步 I/O 结合,使得程序可以发起一个 I/O 操作后继续执行其他任务,当 I/O 完成时,系统通知程序并处理完成的数据。这种模式可以提高系统的并发性和性能。
2.如何实现非阻塞IO访问: O_NONBLOCK和fcntl

2.阻塞式IO的困境

以在程序中读取键盘为例

#include
#include
#include
#include 
int main(){
    char buf[199];
    memset(buf,0,sizeof(buf));
    //读取键盘
    // 键盘就是标准输入, stdin
    printf("before read.\n");
    read (0,buf,2);  //从键盘读取两个字节 就是0号文件标识符 read 本身就是阻塞式的
    printf("读出来的内容是 :【%s】\n",buf);
    return 0;
}

此时运行程序后,发现已经堵塞住了 正在等待输入
Linux 【C编程】IO进阶— 阻塞IO、非阻塞IO、 多路复用IO、 异步IO_第1张图片
在程序中读取鼠标
鼠标设备本质上也是字符设备 在/dev/input中,使用cat 读取 mouse 可能有多个 哪个晃动鼠标能得到数据就说明是鼠标设备,至于乱码是因为读取的是二进制数据,不是普通的字符。
Linux 【C编程】IO进阶— 阻塞IO、非阻塞IO、 多路复用IO、 异步IO_第2张图片
代码演示:

#include
#include
#include
#include 
#include 
int main(){
    char buf[199];
    memset(buf,0,sizeof(buf));
    //读取鼠标
    // 鼠标不是标准输入 需要open 打开
    int fd = -1;
    fd = open("/dev/input/mouse0",O_RDONLY);
    if(fd<0){
        perror("open:");
        return -1;
    }

    printf("before read mouse.\n");
    read (fd,buf,2);  //从鼠标读取两位
    printf("mouse读出来的内容是 :【%s】\n",buf);




    return 0;
}

如果程序同时读取键盘和鼠标

#include
#include
#include
#include 
#include 
int main(){
    char buf[199];
    memset(buf,0,sizeof(buf));
    //读取鼠标
    // 鼠标不是标准输入 需要open 打开
    int fd = -1;
    fd = open("/dev/input/mouse0",O_RDONLY);
    if(fd<0){
        perror("open:");
        return -1;
    }

    printf("before read mouse.\n");
    read (fd,buf,2);  //从鼠标读取两位
    printf("mouse读出来的内容是 :【%s】\n",buf);

    memset(buf,0,sizeof(buf));
    printf("before keyboard read.\n");
    read (0,buf,2);  //从键盘读取两个字节 就是0号文件标识符 read 本身就是阻塞式的
    printf("keyboard 读出来的内容是 :【%s】\n",buf);


    return 0;
}

如果用户使用这个程序,先使用鼠标,再使用键盘输入,程序没有问题
Linux 【C编程】IO进阶— 阻塞IO、非阻塞IO、 多路复用IO、 异步IO_第3张图片
当用户先输入键盘的时候,阻塞IO就出现问题了,程序必须先等待鼠标事件,当前程序已经被阻塞住了,无论怎么输入键盘,程序也不会有响应。例如下图
Linux 【C编程】IO进阶— 阻塞IO、非阻塞IO、 多路复用IO、 异步IO_第4张图片

3.并发IO的解决方案

3.1非阻塞式IO

使用fcntl 修改0号文件标识符的属性,添加非阻塞属性。
鼠标是通过open 打开的,直接添加非阻塞属性即可。

#include
#include
#include
#include 
#include 
int main(){
    char buf[199];
    memset(buf,0,sizeof(buf));
    int ret = -1;
    // 鼠标不是标准输入 需要open 打开
    int fd = -1;
    fd = open("/dev/input/mouse0",O_RDONLY|O_NONBLOCK);  //添加非阻塞属性
    if(fd<0){
        perror("open:");
        return -1;
    }
    int flag=  -1;
    //把0号描述符变成非阻塞式的
    flag = fcntl(0,F_GETFL); //获取原来的flag
    flag |= O_NONBLOCK ; //添加非阻塞属性
    fcntl(0,F_SETFL,flag); //更新flag
    while (1){
        ret  = read (fd,buf,2);  //从鼠标读取
        if(ret>0){
            printf("mouse读出来的内容是 :【%s】\n",buf);
            memset(buf,0,sizeof(buf));
        }
        ret  = read (0,buf,10);  //从键盘读取 
        if(ret>0){
            printf("keyboard 读出来的内容是 :【%s】\n",buf);
            memset(buf,0,sizeof(buf));
        }
    }
   


    return 0;
}

与阻塞相比,想读入键盘就读入键盘,想读入鼠标就读入鼠标,提高并发性
Linux 【C编程】IO进阶— 阻塞IO、非阻塞IO、 多路复用IO、 异步IO_第5张图片

3.2多路复用IO

3.2.1什么是多路复用IO?

多路复用 I/O 是一种机制,允许一个进程能够同时监视和处理多个 I/O 源,例如文件描述符、sockets 或其他文件 I/O。这些多路复用的系统调用允许程序等待多个 I/O 事件中的任何一个就绪,从而避免了阻塞等待单个 I/O 完成的情况,提高了程序的效率和并发处理能力。对外部还是阻塞式的,内部非阻塞式自动轮询多路IO看看哪个有数据,就先用哪个。

3.2.1多路复用IO select原理

在Linux系统上常见的多路复用IO技术包括 select poll

1.文件描述符集合:
select 使用三个文件描述符集合来表示待检查的文件描述符。这三个集合分别是读文件描述符集合(readfds)、写文件描述符集合(writefds)和异常文件描述符集合(exceptfds)。
2.超时设置:
select 允许设置一个超时时间,表示最长等待时间。当超时时间达到时,select 将返回,不再等待事件的发生。
3.轮询:
select 通过轮询检查文件描述符的状态,判断是否有可读、可写或异常事件发生。它会遍历指定的文件描述符集合,检查每个文件描述符的状态。
4.阻塞:
当没有任何文件描述符就绪时,select 可以阻塞程序,等待文件描述符变得可读、可写或发生异常。在这种情况下,它会一直等待,直到有文件描述符就绪或超时发生。
5.就绪文件描述符集合:
select 在返回时会修改传入的文件描述符集合,标识出哪些文件描述符已经就绪,可以进行相应的IO操作。

在使用 select 时,需要注意其效率问题,特别是在大规模的文件描述符集合中。因为 select 是线性扫描所有文件描述符的,当文件描述符数量增加时,性能可能会下降。在一些操作系统上,存在文件描述符数量的限制。
使用select实现同时读取键盘和鼠标数据,并且设置了3s超时 如果一直没有等到IO到达,就直接退出程序。

#include 
#include 
#include 
#include 
#include 
#include 

int main() {
    char buf[199];
    memset(buf, 0, sizeof(buf));

    // 打开鼠标设备
    int mouse_fd = open("/dev/input/mouse0", O_RDONLY);
    if (mouse_fd < 0) {
        perror("open mouse:");
        return -1;
    }
    struct timeval timeout;
    timeout.tv_sec = 3;   // 设置秒数
    timeout.tv_usec = 0;  // 设置微秒数
    printf("before select.\n");

    while (1) {
        fd_set read_fds;
        FD_ZERO(&read_fds);
        FD_SET(0, &read_fds);   // 标准输入(键盘)
        FD_SET(mouse_fd, &read_fds);  // 鼠标

        // 使用select监听多个文件描述符
        int result = select(mouse_fd + 1, &read_fds, NULL, NULL, &timeout);

        if (result > 0) {
            //判断出来是键盘IO到了
            if (FD_ISSET(0, &read_fds)) {
                // 从键盘读取
                read(0, buf, sizeof(buf));
                printf("keyboard 读出来的内容是:%s\n", buf);
                memset(buf, 0, sizeof(buf));
            }
            //判断出是鼠标到了
            if (FD_ISSET(mouse_fd, &read_fds)) {
                // 从鼠标读取
                read(mouse_fd, buf, 2);  // 从鼠标读取两位
                printf("mouse 读出来的内容是:%s\n", buf);
                memset(buf, 0, sizeof(buf));
            }
        }
        if(result == 0){
            printf("select 等待超时\n");
            return -1;
        }
    }

    // 关闭鼠标设备
    close(mouse_fd);

    return 0;
}

3.2.1多路复用IO poll原理

poll 是 Linux 中用于多路复用 I/O 操作的系统调用之一,它允许一个进程等待多个文件描述符上的事件发生。poll 的原理如下:
1.准备文件描述符集合和事件关注列表: 在调用 poll 之前,应用程序需要创建一个 struct pollfd 数组,该数组包含了要监听的文件描述符以及对每个文件描述符关注的事件。每个 struct pollfd 结构体包括以下字段:

  • fd
  • events
  • revents

2.调用 poll 函数: 应用程序调用 poll 函数,传递准备好的 struct pollfd 数组及数组的长度。

#include 
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:指向struct pollfd数组的指针
  • nfds: 数组中结构体的数量
  • timeout:设置超时时间,-1代表一直等待

代码演示:

#include 
#include 
#include 
#include 
#include 
#include 

int main() {
    char buf[199];
    memset(buf, 0, sizeof(buf));

    // 打开鼠标设备文件
    int mouse_fd = open("/dev/input/mouse0", O_RDONLY);
    if (mouse_fd < 0) {
        perror("open mouse:");
        return -1;
    }

    // 使用poll监听标准输入和鼠标输入
    struct pollfd fds[2];
    fds[0].fd = STDIN_FILENO;  // 标准输入
    fds[0].events = POLLIN ;  //事件为可读

    fds[1].fd = mouse_fd;  // 鼠标输入
    fds[1].events = POLLIN ;//事件为可读

    printf("Waiting for input...\n");

    while (1) {
        //这里的timeout 设置-1 代表一直等待
        int result = poll(fds, 2, -1);  // 阻塞等待事件发生

        if (result > 0) {
            if (fds[0].revents & (POLLIN | POLLPRI)) {
                // 从标准输入读取数据
                read(STDIN_FILENO, buf, sizeof(buf));
                printf("Keyboard input: %s\n", buf);
            }

            if (fds[1].revents & (POLLIN | POLLPRI)) {
                // 从鼠标设备读取数据
                read(mouse_fd, buf, sizeof(buf));
                printf("Mouse input: %s\n", buf);
            }
        } else if (result < 0) {
            perror("poll:");
            break;
        }
    }

    // 关闭文件描述符
    close(mouse_fd);

    return 0;
}

与select效果一致
Linux 【C编程】IO进阶— 阻塞IO、非阻塞IO、 多路复用IO、 异步IO_第6张图片

3.3异步IO

在Linux中,异步IO(Asynchronous I/O)是一种文件IO操作的模型,与传统的同步IO模型(例如使用read和write函数)不同。在异步IO模型中,IO操作的请求被提交后,程序可以继续执行其他任务,而无需等待IO操作完成。
异步IO的关键特点包括:

  • 非阻塞: 异步IO允许程序在等待IO操作完成的同时继续执行其他任务,不会阻塞整个进程。

  • 回调机制: 异步IO通常通过回调机制来处理IO操作完成的通知。当IO操作完成时,系统会调用预先注册的回调函数,以便程序可以处理IO的结果。

  • 提高并发性: 异步IO适用于需要同时处理大量IO操作的场景,能够提高程序的并发性能。
    在Linux中,异步IO可以通过以下几种机制来实现:

  • fcntl+signal:使用fcntl中的F_SETOWN和O_ASYNC选项来设置异步IO的所有者,然后结合signal来注册信号处理函数,以便在IO事件发生时得到通知。

  • aio_ 函数族:* 提供了一组异步IO相关的系统调用,例如aio_read、aio_write等。这些函数使用结构体struct aiocb来描述IO操作,并可以设置回调函数。

  • epoll: epoll本身是一个多路复用机制,但也可以与异步IO结合使用,通过epoll监听IO事件,当IO操作完成时,通过回调机制处理。

  • libuv: 是一个跨平台的异步IO库,它封装了不同操作系统的异步IO机制,使得在不同平台上能够使用相似的异步IO接口。

使用fcntl+signa完成异步IO操作

#include
#include
#include
#include 
#include 
#include
int mouse_fd = -1;
//设置信号回调函数  鼠标时间设置为一个异步IO
void func(int sig){
    char buf[100] = {0};
    if (sig !=SIGIO)
    read (mouse_fd,buf,2);  //从鼠标读取两位
    printf("mouse读出来的内容是 :【%s】\n",buf);
}
int main(){
    char buf[100] = {0};
    //读取鼠标
    int flag = -1;
    mouse_fd = open("/dev/input/mouse0",O_RDONLY);
    if(mouse_fd<0){
        perror("open:");
        return -1;
    }
    //注册异步通知 把鼠标设置为异步IO事件
    flag  = fcntl(mouse_fd,F_GETFL);
    flag |= O_ASYNC;
    fcntl(mouse_fd,F_SETFL,flag);
    //把当前进程设置异步IO接收的进程
    fcntl(mouse_fd,F_SETOWN,getpid());
    // 注册信号处理函数
    signal(SIGIO, func);
    memset(buf,0,sizeof(buf));
    printf("before keyboard read.\n");
    read (0,buf,2);  //从键盘读取两个字节 就是0号文件标识符 read 本身就是阻塞式的
    printf("keyboard 读出来的内容是 :【%s】\n",buf);
    return 0;
}

你可能感兴趣的:(Linux,笔记,linux,c语言,运维)