第四章 Linux套接字通信:异步通信小结

1. 异步通信的基本概念

在 Linux 系统开发中,异步通信是指程序在执行某些操作(如 I/O 操作)时,不需要等待操作完成即可继续执行其他任务。异步通信的主要目的是提高程序的效率,特别是在需要处理大量 I/O 操作时,避免阻塞和浪费 CPU 时间。

异步编程的重要性

异步编程允许程序在等待某些操作(通常是 I/O 操作)完成的同时,继续执行其他任务。这种方式提高了程序的响应性和性能。在 Linux 中,异步编程不仅用于文件 I/O,还广泛应用于网络编程、进程间通信等领域。

1.2 Linux 中的异步通知机制概览

Note

Linux 提供了多种异步通知机制,包括但不限于信号(Signals)、异步 I/O(AIO)、I/O 多路复用(如 select、poll 和 epoll)以及 inotify 文件系统事件通知。每种机制都有其特定的应用场景和优势。

1.2.1 信号(Signals)

信号是一种进程间通信机制,它能够通知程序某个事件的发生。例如,当一个子进程退出时,父进程会收到一个 SIGCHLD 信号。信号处理函数可以被用来响应这些信号,执行特定的操作。

1.2.2 异步 I/O(AIO)

异步 I/O 允许程序发起一个 I/O 操作后立即返回,不会阻塞调用线程。当 I/O 操作实际完成时,程序会收到一个通知。这种机制在处理大量并发 I/O 操作时非常有用。

1.2.3 I/O 多路复用(I/O Multiplexing)

I/O 多路复用允许程序监视多个文件描述符,等待它们中的任何一个准备好进行 I/O 操作。这是通过 selectpoll 或 epoll 系统调用实现的。

1.2.4 inotify 文件系统事件通知

inotify 是 Linux 特有的文件系统事件通知机制。它允许应用程序监视文件系统事件,如文件的创建、修改、删除等,并在这些事件发生时接收通知。

在 Linux 内核源码中,我们可以在 fs/notify/inotify/ 目录下找到 inotify 的实现。其中,inotify_user.c 文件包含了用户空间与 inotify 交互的接口实现。

1.2.5 异步通信的基本理念

基本理念

  • 同步通信:程序在进行 I/O 操作时,必须等操作完成后才能继续执行其他任务,造成 CPU 时间浪费。
  • 异步通信:程序发起一个 I/O 操作后,可以继续执行其他任务,直到 I/O 操作完成,操作系统会通过回调函数、信号或事件机制告知程序何时可以处理 I/O 操作的结果。

异步通信通过回调、信号或事件机制,避免了程序等待 I/O 操作完成的时间,提升了程序的并发性和资源利用效率。


2. Linux 中的异步 I/O 模型

在 Linux 系统中,异步 I/O 通常有几种实现方式。接下来,我们将详细介绍这些常见的异步 I/O 模型及其实现方法。

例如,信号是一种轻量级的通信机制,常用于通知程序某个事件的发生,如进程终止、子进程状态改变等。而异步 I/O 则允许程序在 I/O 操作完成时接收通知,从而不必阻塞等待 I/O 操作的完成。

2.1 非阻塞 I/O

非阻塞 I/O模型中,应用程序可以发起 I/O 操作,但如果数据不可用,操作系统会立即返回错误(如 EAGAIN 或 EWOULDBLOCK)。此时,应用程序可以继续执行其他任务,之后再尝试进行 I/O 操作。

如何设置非阻塞 I/O

使用 fcntl() 函数将文件描述符设置为非阻塞模式。这样,在 I/O 操作无法立即完成时,程序不会被阻塞。

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int setNonBlocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);  // 获取当前文件描述符的标志
    if (flags == -1) {
        perror("fcntl");
        return -1;
    }
    flags |= O_NONBLOCK;  // 设置非阻塞标志
    return fcntl(fd, F_SETFL, flags);  // 设置新的标志
}

通过上述代码,我们将文件描述符 fd 设置为非阻塞模式。当 read() 或 write() 无法立即完成时,操作系统返回错误,程序继续执行其他任务。


2.2 select() 和 poll() 模型

select() 和 poll() 是两种常见的 I/O 复用机制,它们允许程序监视多个文件描述符的状态,检测哪些文件描述符准备好进行读写操作。通过这些系统调用,程序可以进行非阻塞操作,而不会被阻塞。

  • select():检测文件描述符是否准备好读写或发生异常。
  • poll():类似 select(),但是 poll() 支持更多的文件描述符。
示例:使用 select() 进行异步通信
#include <sys/select.h>
#include <unistd.h>
#include <stdio.h>

int selectExample() {
    fd_set readfds;
    struct timeval timeout;

    FD_ZERO(&readfds);  // 清空文件描述符集合
    FD_SET(STDIN_FILENO, &readfds);  // 监视标准输入

    timeout.tv_sec = 5;  // 设置超时时间为 5 秒
    timeout.tv_usec = 0;

    int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
    if (ret > 0) {
        if (FD_ISSET(STDIN_FILENO, &readfds)) {
            printf("There is data to read from stdin\n");
        }
    } else if (ret == 0) {
        printf("Timeout occurred!\n");
    } else {
        perror("select");
    }

    return 0;
}

在上面的代码中,select() 监视标准输入是否准备好可以读取数据。当数据可用时,程序执行读取操作,否则等待超时。


2.3 epoll 模型

epoll 是 Linux 特有的高效 I/O 复用机制,特别适用于处理大量并发连接,能够高效地监听和处理多个文件描述符。相比于 select() 和 poll()epoll 在大规模连接时具有更高的性能,避免了频繁轮询所有文件描述符。

使用的函数:
  • epoll_create():创建一个 epoll 实例。
  • epoll_ctl():注册、修改或删除监控的文件描述符。
  • epoll_wait():等待并返回就绪的文件描述符。
示例:使用 epoll 进行异步 I/O
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

#define MAX_EVENTS 10

int epollExample() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1");
        return -1;
    }

    struct epoll_event event;
    event.events = EPOLLIN;  // 监听可读事件
    event.data.fd = STDIN_FILENO;  // 监视标准输入

    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) {
        perror("epoll_ctl");
        return -1;
    }

    struct epoll_event events[MAX_EVENTS];
    int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);  // 等待事件
    if (nfds == -1) {
        perror("epoll_wait");
        return -1;
    }

    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == STDIN_FILENO) {
            printf("Data available to read from stdin\n");
        }
    }

    close(epoll_fd);
    return 0;
}

在这个例子中,epoll 用于监视标准输入文件描述符的可读事件。当标准输入数据可用时,程序会执行读取操作。


2.4 异步 I/O (AIO)

AIO(异步 I/O)是 POSIX 标准的一部分,允许程序在不阻塞的情况下发起 I/O 操作,并通过信号或回调函数通知程序 I/O 操作的结果。

在 Linux 中,AIO 是通过一系列系统调用来实现的,如 io_setupio_submitio_getevents 等。这些系统调用在 Linux 的内核源码中有具体的实现,例如在 fs/aio.c 文件中。来自于libaio库。专门用于提交异步 I/O 操作,适用于高性能 Linux 系统中的 I/O 密集型应用。

POSIX AIO 提供了标准的异步 I/O 接口,是 POSIX 标准中的一部分。它通常在 Linux 中通过 librt 库实现,适用于跨平台开发。

  • aio_read():异步读取文件内容。
  • aio_write():异步写入文件内容。
  • aio_error():检查异步操作是否完成。
  • aio_return():获取异步操作的结果。
 AIO 的工作原理

AIO 的核心是允许程序在 I/O 操作完成之前继续执行其他任务。当程序需要读取或写入数据时,它仅仅是发起这个请求,然后立即返回,继续执行其他代码。一旦 I/O 操作完成,操作系统会以某种方式通知程序,通常是通过信号或回调函数。

示例1: 使用 AIO 进行文件读写
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <libaio.h>

#define FILE_PATH "example.txt"  // 要读取的文件路径
#define BUFFER_SIZE 1024         // 缓冲区大小

int main() {
    io_context_t ctx = 0;  // 异步 I/O 上下文,初始化为 0
    struct iocb cb;        // I/O 控制块(异步 I/O 操作描述符)
    struct iocb *cbs[1];   // I/O 控制块数组,用于提交多个异步 I/O 请求
    unsigned char *buf;    // 用于存储读取数据的缓冲区
    struct io_event events[1];  // 存储异步 I/O 操作完成后的事件
    int ret;
    int fd;

    // 打开文件 example.txt 以只读方式打开
    fd = open(FILE_PATH, O_RDONLY);
    if (fd < 0) {  // 如果文件打开失败
        perror("open error");  // 打印错误信息
        return -1;  // 返回失败
    }

    // 初始化异步 I/O 上下文,128 表示提交队列的大小
    ret = io_setup(128, &ctx);
    if (ret < 0) {  // 如果初始化失败
        perror("io_setup error");
        return -1;  // 返回失败
    }

    // 分配内存用于缓冲区,大小为 BUFFER_SIZE
    buf = malloc(BUFFER_SIZE);
    if (!buf) {  // 如果分配内存失败
        perror("malloc error");
        return -1;  // 返回失败
    }

    // 准备异步读取操作
    io_prep_pread(&cb, fd, buf, BUFFER_SIZE, 0);  // 准备从文件 fd 中的偏移量 0 处开始读取 BUFFER_SIZE 字节到 buf
    cbs[0] = &cb;  // 将准备好的 I/O 操作添加到控制块数组中

    // 提交异步 I/O 请求
    ret = io_submit(ctx, 1, cbs);
    if (ret != 1) {  // 如果提交失败
        io_destroy(ctx);  // 销毁异步 I/O 上下文,释放资源
        perror("io_submit error");
        return -1;  // 返回失败
    }

    // 获取异步 I/O 请求的结果
    ret = io_getevents(ctx, 1, 1, events, NULL);
    if (ret != 1) {  // 如果获取事件失败
        io_destroy(ctx);  // 销毁异步 I/O 上下文,释放资源
        perror("io_getevents error");
        return -1;  // 返回失败
    }

    // 将读取到的数据输出到标准输出(通常是终端)
    write(1, buf, BUFFER_SIZE);  // `1` 是标准输出文件描述符
    free(buf);  // 释放缓冲区内存

    // 销毁异步 I/O 上下文,释放资源
    io_destroy(ctx);

    return 0;  // 程序成功结束
}

示例2:使用POSIX AIO 进行异步 I/O
#include <aio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int aioExample() {
    struct aiocb aio;
    char buf[100];
    int fd = open("file.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    aio.aio_fildes = fd;
    aio.aio_buf = buf;
    aio.aio_nbytes = sizeof(buf);
    aio.aio_offset = 0;
    aio.aio_lio_opcode = LIO_READ;
    if (aio_read(&aio) == -1) {
        perror("aio_read");
        return -1;
    }
    // 等待异步操作完成
    while (aio_error(&aio) == EINPROGRESS) {
        // 异步操作仍在进行中
    }
    int n = aio_return(&aio);
    printf("Read %d bytes: %s\n", n, buf);
    close(fd);
    return 0;
}

在这个示例中,aio_read() 被用来异步地读取文件数据,程序不会被阻塞,而是继续执行其他操作,直到 I/O 完成时才通过 aio_return() 获取操作结果。

实例:使用 AIO 读写大文件

假设我们需要读取一个非常大的文件,并对其中的数据进行处理。如果使用传统的阻塞 I/O,我们的程序会在读取数据时被阻塞,无法执行其他任务。但如果使用 AIO,我们可以在读取数据的同时执行其他任务,例如处理已经读取的数据。异步 I/O 可以帮助我们更高效地利用系统资源,提高程序的性能和响应速度。

方式优势劣势
阻塞 I/O简单、直观CPU 效率低,程序在等待 I/O 时不能执行其他任务
异步 I/O (AIO)CPU 效率高,程序可以在等待 I/O 时执行其他任务编程复杂度较高,需要处理异步通知

在这个表格中,我们可以看到阻塞 I/O 和异步 I/O 的主要区别。虽然异步 I/O 的编程复杂度较高,但它能带来更高的 CPU 效率和程序性能。


2.5 信号机制

Linux 系统中的信号机制允许程序在异步操作完成时接收到通知。可以使用信号(如 SIGIO)来告知程序某个文件描述符的 I/O 操作已完成。信号处理函数可以被用来响应这些信号,执行特定的操作。

在 Linux 源码中,信号的处理逻辑主要在 kernel/signal.c 文件中实现。

  • SIGINT:当用户按下 Ctrl+C 时发送,通常用于终止程序。
  • SIGKILL:用于立即终止程序,该信号不能被捕获或忽略。
  • SIGALRM:由 alarm 系统调用设置的定时器超时时发送。

例如,当你运行一个程序时,可以使用Ctrl+C来终止程序。在这个过程中,终端会发送一个 SIGINT 信号给程序,程序收到信号后,会执行相应的处理函数,通常是终止程序。

使用的函数:
  • fcntl():用于设置文件描述符进行异步操作。
  • signal():注册信号处理函数。
2.5.1 如何使用信号处理异步事件

在 Linux 中,可以使用系统调用如 kill 来发送信号,使用 signal 或 sigaction 来捕获和处理信号。

2.5.2 发送信号

一个进程可以使用 kill 系统调用发送信号给另一个进程。例如,发送 SIGKILL 信号终止一个进程:

#include <signal.h>
#include <stdio.h>

int main() {
    // 发送 SIGKILL 信号到进程,进程 ID 为 1234
    int ret = kill(1234, SIGKILL);
    if (ret == 0) {
        printf("信号发送成功\n");
    } else {
        perror("信号发送失败");
    }
    return 0;
}
2.5.3 捕获和处理信号

进程可以使用 signal 或 sigaction 函数来指定信号的处理函数。例如,捕获 SIGINT 信号:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handle_sigint(int sig) {
    printf("收到信号 %d\n", sig);
}

int main() {
    signal(SIGINT, handle_sigint);
    while (1) {
        printf("运行中...\n");
        sleep(1);
    }
    return 0;
}

在这个示例中,当用户按下 Ctrl+C 时,程序会捕获 SIGINT 信号,并调用 handle_sigint 函数处理该信号,而不是终止程序。

2.5.4 实例1:使用信号处理子进程结束事件

在多进程编程中,父进程通过捕获 SIGCHLD 信号来实现知道子进程什么时候结束。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void handle_sigchld(int sig) {
    int status;
    wait(&status);  // 清理已终止子进程
    printf("子进程结束,状态码 %d\n", status);
}

int main() {
//父进程通过调用 `signal(SIGCHLD, handle_sigchld)` 注册了一个信号处理函数 `handle_sigchld`。
//当子进程结束时,操作系统会向父进程发送 `SIGCHLD` 信号。父进程在接收到该信号时,会执行 `handle_sigchld` 函数。
    signal(SIGCHLD, handle_sigchld);
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        printf("子进程开始执行\n");
        sleep(5);  // 模拟子进程执行任务
        printf("子进程结束执行\n");
        exit(0);
    } else if (pid > 0) {
        // 父进程
        printf("父进程继续执行\n");
        while (1) {
            sleep(1);//父进程保持运行
        }
    } else {
        perror("fail to fork");
        return 1;
    }
    return 0;
}

在这个示例中,父进程通过捕获 SIGCHLD 信号来知道子进程何时结束。当子进程结束时,内核会发送 SIGCHLD 信号给父进程,父进程在 handle_sigchld 函数中调用 wait 函数来清理已终止的子进程,并打印子进程的结束状态。
这种机制允许父进程异步地处理子进程的结束事件,而不需要阻塞地等待子进程结束。

示例2:使用信号处理异步通信
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

void signal_handler(int signo) {
    if (signo == SIGIO) {
        printf("文件描述符可读\n");
    }
}

int signalExample() {
    signal(SIGIO, signal_handler);  // 注册信号处理函数
    fcntl(STDIN_FILENO, F_SETFL, O_ASYNC);  // 设置标准输入为异步操作
    fcntl(STDIN_FILENO, F_SETOWN, getpid()); // 将 SIGIO 信号发送给当前进程

    while (1) {
        sleep(1);  // 等待 SIGIO 信号
    }

    return 0;
}

该代码通过 fcntl() 设置标准输入为异步操作,并在数据可读时通过 SIGIO 信号通知程序执行相应操作。


3. 异步通信的优势

  • 高性能:异步通信可以有效减少 I/O 操作的等待时间,使程序能够在等待 I/O 完成时继续执行其他任务。
  • 并发处理:特别适用于需要处理大量并发连接或多任务的情况,能够避免因阻塞等待而浪费资源。
  • 节省系统资源:减少线程/进程切换和阻塞等待,提高系统的资源利用率。

4. 异步通信的挑战

  • 编程复杂度:异步编程比同步编程更复杂,程序员需要处理回调函数、状态管理和错误处理等。
  • 错误处理:异步操作的完成时机不同,可能需要复杂的错误处理机制。
  • 调试和测试:异步程序的行为往往难以预测,调试和测试时需要特别注意并发和线程同步问题。

部分参考【Linux 异步操作】深入理解 Linux 异步通知机制:原理、应用与实例解析 - 知乎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值