IO模型分类

发布于:2024-11-27 ⋅ 阅读:(72) ⋅ 点赞:(0)

1. 阻塞IO

  1. IO阻塞的定义与影响
    IO阻塞是效率最低的程序设计模式之一。如果程序中存在多个阻塞IO操作,每次执行到阻塞IO时,程序将会停在那里,直到该IO操作完成后才继续执行。这样会导致程序的其他部分无法同时执行,造成CPU资源的浪费和处理效率的低下。

  2. 常见的阻塞函数
    我们在编程中已经接触过一些常见的阻塞函数,例如:

    • accept:在网络编程中,等待客户端连接的函数,直到接收到连接请求才返回。
    • recvfrom:接收数据的阻塞函数,直到有数据到达才会返回。
    • scanf:标准输入函数,在等待用户输入时会阻塞。
    • getchar:从标准输入获取一个字符,也会阻塞直到有字符输入。
    • read:读取文件或设备数据的阻塞函数,直到指定的字节数被读取完毕才返回。
    • write:写入数据时,如果缓冲区满或设备忙,则会阻塞直到可以继续写入。
  3. 阻塞函数对程序效率的影响
    阻塞函数会让程序等待某些事件的发生,这些事件可能是用户输入、数据接收或其他IO操作。如果这些事件迟迟没有发生,程序便会一直停滞在阻塞状态,无法继续执行后续代码,导致CPU资源的浪费,整体执行效率变低。因此,程序的响应速度和资源利用率都受到影响。

  4. 示例代码

  1. 以下是一个展示阻塞行为的简单代码示例:

#include <stdio.h>

int main() {
    int n;
    printf("请输入一个数字:");
    // 此处阻塞,直到用户输入完成并按下回车
    scanf("%d", &n);
    printf("你输入的数字是:%d\n", n);

    return 0;
}

进一步优化理解

阻塞 IO 的低效表现主要是因为其会等待操作完成,而不允许程序并发执行其他任务。为了解决这一问题,可以采用以下优化方式:

  1. 非阻塞 IO 模型:允许程序继续执行,不会因为 IO 操作被阻塞。
  2. 多线程或多进程:通过并发处理不同的 IO 请求,提高 CPU 利用率。
  3. IO 多路复用:使用 selectepoll 等系统调用,同时监视多个文件描述符,以减少资源浪费。

2. 非阻塞IO

  1. fcntl函数

fcntl 是一个在文件描述符上进行设置或获取操作的系统调用。它用于获取或设置文件描述符的状态属性,包括文件的阻塞模式、访问模式等。

函数原型
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);

参数说明:
  • fd: 文件描述符,指定操作的文件或设备。
  • cmd: 指定操作类型,常用的有:
    • F_GETFL:获取文件描述符的状态。
    • F_SETFL:设置文件描述符的状态。
  • arg: 可选参数,具体取决于cmd的值:
    • 如果cmdF_GETFL,则该参数可省略。
    • 如果cmdF_SETFL,则arg可以是如下之一(它们通常通过位或运算组合):
      • O_RDONLY:设置为只读状态。
      • O_WRONLY:设置为只写状态。
      • O_RDWR:设置为读写状态。
      • O_NONBLOCK:设置为非阻塞状态。

返回值

  • 如果cmdF_GETFL,成功返回文件描述符的状态值,失败返回 -1 并设置错误码。并设置错误码。
  • 如果cmdF_SETFL,成功时返回0,失败时返回-1,并设置错误码。

2. 示例代码
以下是使用fcntl设置文件描述符为非阻塞状态的示例:
#include <myhead.h>

int main(int argc, const char *argv[])
{
    int n;
    // 获取标准输入的文件描述符状态
    int arg = fcntl(0, F_GETFL, 0);  // 0是标准输入
    printf("标准输入的状态值: %d\n", arg);

    // 将标准输入的文件描述符设置为非阻塞状态
    fcntl(0, F_SETFL, arg | O_NONBLOCK);

    while (1)
    {
        scanf("%d", &n);  // 非阻塞模式下,scanf不会阻塞
        printf("n = %d\n", n);
    }

    return 0;
}

  • 代码解释:

    • 通过 fcntl(0, F_GETFL, 0) 获取标准输入(文件描述符0)的当前状态值。
    • 然后使用 fcntl(0, F_SETFL, arg | O_NONBLOCK) 将标准输入的状态设置为非阻塞状态。
    • while 循环中,scanf 不会阻塞程序执行,如果没有输入,它会立即继续执行而不会等待用户输入。
  • 总结:
    fcntl 函数用于控制文件描述符的状态,可以将文件描述符设置为非阻塞模式。通过合理使用非阻塞IO,可以避免程序因等待IO操作而停滞,提高程序的响应速度和并发处理能力。


2.3 实现非阻塞输入的完整示例

代码

#include "/home/ubuntu/myheader.h" // 替换为实际头文件路径

int main(int argc, const char *argv[]) {
    int n;
    // 获取标准输入描述符状态
    int arg = fcntl(0, F_GETFL, 0); 
    printf("标准输入的初始状态值: %d\n", arg);

    // 设置标准输入为非阻塞状态
    fcntl(0, F_SETFL, arg | O_NONBLOCK);

    // 不断读取用户输入
    while (1) {
        if (scanf("%d", &n) > 0) { // 非阻塞模式下,scanf 不会等待输入
            printf("n = %d\n", n);
        } else {
            printf("未检测到输入,继续尝试...\n");
        }
    }

    return 0;
}

说明

  1. 程序先获取标准输入的当前状态值。
  2. 使用 fcntl 函数将标准输入设置为非阻塞模式。
  3. 非阻塞模式下,scanf 不会一直等待输入,能有效避免程序阻塞。

2.4 非阻塞 IO 的优势与使用场景

优势

  • 避免阻塞造成的资源浪费。
  • 在单线程中实现更高效的任务处理。

使用场景

  • 网络编程中对大量客户端请求的并发处理。
  • 实现流式数据读取时减少程序停顿。
  • 提高实时性要求的程序性能。

3. IO 多路复用

  1. 多路复用简介
    • 没有操作系统的情景:
      如果计算机没有操作系统,就没有进程和线程的概念,无法通过操作系统提供的多任务并发功能。在这种情况下,程序需要自己实现多任务并发。

    • IO多路复用的作用:
      IO多路复用可以让单个进程在多个IO操作上进行“并发”,通过一个程序实现类似多任务处理的效果。即便是在没有操作系统的环境下,程序依然可以模拟并发执行多个IO操作。

    • 原理:
      IO多路复用的实现原理是将所有阻塞的IO操作放入一个集合中,然后通过某种机制监视这些IO操作。如果其中某个或多个IO操作发生了事件(如数据准备好读取或写入),程序会解除这些IO操作的阻塞状态,允许继续处理。其他没有发生事件的IO操作仍然处于阻塞状态。

    • 示意图:
      通常,IO多路复用的原理图会展示一个进程如何同时监控多个文件描述符(或IO流),并在一个或多个IO事件发生时解除阻塞,避免一个IO操作阻塞整个程序。

  2. select 函数介绍
函数原型:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

功能:

  • select 函数用于监视多个文件描述符,判断它们是否有读、写或错误事件发生。当一个或多个文件描述符的状态发生变化时,select 会解除它们的阻塞。通过这种方式,程序可以在一个线程中并发处理多个IO操作。

  • 参数

    • nfds:最大文件描述符+1,表示要监视的文件描述符的个数。
    • readfds:用于监视可读事件的文件描述符集合。
    • writefds:用于监视可写事件的文件描述符集合。
    • exceptfds:用于监视异常事件的文件描述符集合。
    • timeout:指定超时时间,超过指定时间会自动解除阻塞。
  • timeout的结构体定义:

    struct timeval {
        long tv_sec;  /* 秒 */
        long tv_usec; /* 微秒 */
    };
    
    • 返回值

      • 成功时,返回解除阻塞的描述符个数。
      • 如果发生错误,返回-1,并设置errno
  • 常用宏函数:

    • FD_CLR(int fd, fd_set *set):从集合中清除指定的描述符。
    • FD_ISSET(int fd, fd_set *set):判断指定描述符是否在集合中。
    • FD_SET(int fd, fd_set *set):将指定描述符添加到集合中。
    • FD_ZERO(fd_set *set):清空集合中的所有描述符。
4. 示例:使用select解除阻塞

以下是使用select函数来验证阻塞解除的示例代码:

#include <myhead.h>

int main(int argc, const char *argv[])
{
    int n;
    fd_set readfds;  // 定义一个集合
    FD_ZERO(&readfds);  // 清空整个集合
    FD_SET(0, &readfds);  // 将标准输入描述符(0)放入集合中,进行监视
    
    struct timeval time = {5, 0};  // 设置5秒的阻塞时间
    int res = select(0 + 1, &readfds, NULL, NULL, &time);

    scanf("%d", &n);  // 发生写操作

    if (FD_ISSET(0, &readfds)) {
        printf("输入事件发生了,解除阻塞\n");
    }

    if (res == 0) {
        printf("超时\n");
    }

    return 0;
}

代码解释:

  • 使用 FD_ZERO 清空文件描述符集合,使用 FD_SET 将标准输入(文件描述符 0)添加到集合中。
  • select 监视标准输入的读操作,超时时间设为 5 秒。如果在 5 秒内有输入,select 会解除阻塞。
  • 如果 FD_ISSET(0, &readfds) 为真,说明标准输入发生了读事件,解除阻塞,程序继续执行。
5. IO多路复用的优势
  • 提高效率:通过避免在多个阻塞操作之间进行切换,IO多路复用可以使得程序更加高效地处理多个IO操作,减少了系统开销。
  • 简化线程管理:IO多路复用可以在单个线程内处理多个IO操作,而不需要创建和管理多个线程,从而降低了并发操作的复杂性。 

补充说明:
  • select适用于文件描述符数量较小的情况,因为它的性能受限于fd_set集合的大小,通常最大为1024个文件描述符。如果需要处理更多的文件描述符,可能需要考虑使用poll或者epoll等其他方式。

4. 使用IO多路复用完成TCP并发服务器

在基于IO多路复用的TCP并发服务器模型中,我们通过使用select等IO多路复用技术,同时管理多个客户端连接并处理它们的请求。服务器使用一个大的文件描述符集合来监视多个客户端的请求,能够高效地处理大量并发连接。下面是这一模型的实现思路和伪代码:

模型描述
  • 定义两个容器:用于存储活动的文件描述符。
  • 清空容器:在每次循环开始时,清空上一次的状态。
  • 旧的套接字放入容器:将原始的监听套接字放入集合中。
  • 记录最大文件描述符maxfd,用于确定select的参数。
伪代码实现
#include <myhead.h>

int main(int argc, const char *argv[]) {
    int listenfd, connfd, maxfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    fd_set readfds, tempfds;
    int client_fds[FD_SETSIZE];  // 客户端连接文件描述符集合
    int i, n;

    // 创建监听套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
        perror("socket");
        return -1;
    }

    // 初始化服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(8080);

    // 绑定套接字
    if (bind(listenfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        return -1;
    }

    // 开始监听
    if (listen(listenfd, 10) == -1) {
        perror("listen");
        return -1;
    }

    // 初始化文件描述符集合
    FD_ZERO(&readfds);
    FD_SET(listenfd, &readfds);  // 将监听套接字加入集合

    maxfd = listenfd;  // 初始时最大描述符是监听套接字

    // 客户端连接文件描述符集合初始化
    for (i = 0; i < FD_SETSIZE; i++) {
        client_fds[i] = -1;
    }

    while (1) {
        // 复制一份readfds容器,用于select阻塞
        tempfds = readfds;

        // 使用select阻塞,等待某些描述符准备好
        n = select(maxfd + 1, &tempfds, NULL, NULL, NULL);
        if (n == -1) {
            perror("select");
            return -1;
        }

        // 遍历所有文件描述符,处理事件
        for (i = 0; i <= maxfd; i++) {
            if (FD_ISSET(i, &tempfds)) {  // 如果描述符i准备好了
                if (i == listenfd) {  // 如果是监听套接字准备好,接受新连接
                    client_len = sizeof(client_addr);
                    connfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_len);
                    if (connfd == -1) {
                        perror("accept");
                        continue;
                    }

                    // 将新连接的描述符加入集合
                    FD_SET(connfd, &readfds);
                    client_fds[connfd] = connfd;

                    // 更新最大描述符
                    maxfd = (connfd > maxfd) ? connfd : maxfd;
                    printf("New connection from %s:%d\n",
                           inet_ntoa(client_addr.sin_addr),
                           ntohs(client_addr.sin_port));

                } else {  // 如果是客户端描述符准备好了,接收数据
                    char buffer[1024];
                    int len = recv(i, buffer, sizeof(buffer), 0);
                    if (len == 0) {  // 客户端关闭连接
                        close(i);  // 关闭客户端连接
                        FD_CLR(i, &readfds);  // 从集合中移除
                        client_fds[i] = -1;  // 清空客户端描述符
                        if (i == maxfd) {  // 如果关闭的是最大描述符,更新最大描述符
                            while (FD_ISSET(maxfd, &readfds) == 0) {
                                maxfd--;
                            }
                        }
                        printf("Client disconnected\n");
                    } else if (len > 0) {
                        buffer[len] = '\0';
                        printf("Received: %s\n", buffer);

                        // 回复客户端数据
                        send(i, buffer, len, 0);
                    }
                }
            }
        }
    }

    close(listenfd);
    return 0;
}
代码解释:
  1. 初始化

    • 使用socket()函数创建监听套接字。
    • 使用bind()绑定服务器地址和端口。
    • 调用listen()开启监听。
  2. 文件描述符集合

    • FD_SET()将监听套接字加入readfds集合。
    • 使用select()阻塞,等待readfds中的文件描述符准备好。
  3. 处理连接

    • 如果select()返回的描述符是监听套接字(listenfd),说明有新连接请求,使用accept()接受连接,并将新连接的文件描述符加入readfds集合。
    • 如果select()返回的是某个客户端的连接,使用recv()接收数据,并根据接收到的数据决定是继续接收还是关闭连接。
  4. 客户端连接管理

    • 每个连接的客户端都维护在readfds集合中,如果客户端关闭连接,移除相应的文件描述符。
    • maxfd用于动态更新当前最大的文件描述符,确保select()调用时能够覆盖所有有效的文件描述符。

IO多路复用实现并发服务器

该程序展示了如何使用IO多路复用技术(select)来实现一个能够同时处理多个客户端连接的并发服务器。程序使用了TCP协议,并通过select()来监视多个文件描述符,从而处理多个客户端的请求。

代码解析:

#include <myhead.h>
#define IP "192.168.60.59"
#define PORT 6666
#define BACKLOG 20

int main(int argc, const char *argv[])
{
    // 1. 创建套接字
    int oldfd = socket(AF_INET, SOCK_STREAM, 0);  // AF_INET: IPV4通信;SOCK_STREAM: TCP协议
    if (oldfd == -1)
    {
        perror("socket");
        return -1;
    }

    // 2. 绑定IP和端口号
    struct sockaddr_in server = {
        .sin_family = AF_INET,  // IPV4
        .sin_port = htons(PORT),  // 端口号转为网络字节序
        .sin_addr.s_addr = inet_addr(IP),  // IP地址
    };
    if (bind(oldfd, (struct sockaddr *)&server, sizeof(server)) == -1)
    {
        perror("bind");
        return -1;
    }

    // 3. 监听
    if (listen(oldfd, BACKLOG) == -1)
    {
        perror("listen");
        return -1;
    }

    // 4. 初始化客户端信息结构体和容器
    struct sockaddr_in client;  // 客户端信息结构体
    socklen_t client_len = sizeof(client);  // 计算出结构体大小

    fd_set readfds, temp;  // 定义两个集合存放描述符
    FD_ZERO(&readfds);  // 清空集合内的内容
    FD_SET(0, &readfds);  // 添加标准输入(0号描述符)
    FD_SET(oldfd, &readfds);  // 添加监听套接字描述符

    int maxfd = oldfd;  // 初始化最大描述符
    int newfd;

    // 5. 循环接收多个客户端连接
    while (1)
    {
        // 5-1 监视所有描述符
        temp = readfds;
        int res = select(maxfd + 1, &temp, NULL, NULL, NULL);  // 永久阻塞
        if (res == -1)
        {
            perror("select");
            return -1;
        }

        if (res == 0)
        {
            printf("超时\n");
            continue;
        }

        // 程序执行到此,说明某些描述符解除了阻塞
        // 5-2 循环检测发生IO操作的描述符
        for (int i = 0; i <= maxfd; i++)
        {
            if (!FD_ISSET(i, &temp))  // 如果描述符没有解除阻塞,继续下次循环
            {
                continue;
            }

            if (i == oldfd)  // 如果是监听套接字解除阻塞
            {
                // 接受新客户端连接并创建新的描述符
                newfd = accept(i, (struct sockaddr *)&client, &client_len);
                FD_SET(newfd, &readfds);  // 将新描述符加入集合
                maxfd = (newfd > maxfd) ? newfd : maxfd;  // 更新最大描述符
                if (newfd == -1)
                {
                    perror("accept");
                    return -1;
                }
                printf("%s发来连接请求\n", inet_ntoa(client.sin_addr));
            }
            else  // 如果是客户端描述符解除阻塞,进行数据收发
            {
                // 5-3 循环收发信息
                char buff[1024];
                memset(buff, 0, sizeof(buff));
                int len = recv(i, buff, sizeof(buff), 0);  // 阻塞接收
                if (len == 0)  // 如果客户端关闭连接
                {
                    close(i);  // 关闭客户端连接
                    FD_CLR(i, &readfds);  // 从集合中移除
                    if (maxfd == i)  // 如果关闭的是最大描述符,更新最大描述符
                    {
                        maxfd--;
                    }
                    printf("客户端下线\n");
                    break;
                }
                printf("%s\n", buff);
                fgets(buff, sizeof(buff), stdin);  // 从标准输入获取信息
                send(i, buff, sizeof(buff), 0);  // 回复客户端
            }
        }
    }

    // 6. 关闭监听套接字
    close(oldfd);
    return 0;
}


网站公告

今日签到

点亮在社区的每一天
去签到