C/C++编程:eventfd 的分析与具体例子

本文详细介绍了Linux 2.6.27引入的eventfd特性,用于进程间事件通知,包括接口说明、操作方法、阻塞与非阻塞模式、信号量行为以及父子进程和线程间的通信示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

理论

Linux 2.6.27后添加了一个新的特性,就是eventfd,是用来实现多进程或多线程的之间的事件通知的,也可以由内核通知用户空间应用程序事件。

接口

eventfd的创建是通过eventfd函数实现的,返回值即是该eventfd所对应的文件描述符,函数的原型如下所示:

#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);

eventfd在内核里的核心是一个计数器counter,它是一个uint64_t的整形变量counter,初始值为initval。

  • initval:创建eventfd时它所对应的64位计数器的初始值;
  • flags: eventfd文件描述符的标志,用以改变 eventfd 的行为
    • 如果是2.6.26或之前版本的内核,flags 必须设置为0
    • 2.6.27以上内核有效
      • EFD_CLOEXEC (since Linux 2.6.27)
        • 文件被设置成 O_CLOEXEC,创建子进程 (fork) 时不继承父进程的文件描述符。
        • FD_CLOEXEC 标志,即 close-on-exec,这样在调用 exec 后会自动关闭文件描述符。因为通常执行另一个程序后,会用全新的程序替换子进程的正文,数据,堆和栈等,原来的文件描述符变量也不存在了,这样就没法关闭没用的文件描述符了。
      • EFD_NONBLOCK(since Linux 2.6.27)
        • 设置文件描述符为非阻塞
        • 设置了这个标志后,如果没有数据可读,就返回一个 EAGAIN 错误,不会一直阻塞。
      • EFD_SEMAPHORE (since Linux 2.6.30)
        • 提供类似信号量语义的 read 操作,简单说就是计数值 count 递减 1。可以多次read

操作方法

一切皆为文件是 Linux 内核设计的一种高度抽象,eventfd 的实现也不例外,我们可以使用操作文件的方法操作 eventfd。

read

消费者需要对信号量进行down操作时,调用read从eventfd读即可。

  • read函数会从eventfd对应的64位计数器中读取一个8字节的整型变量;
  • read函数设置的接收buf的大小不能低于8个字节,否则read函数会出错,errno为EINVAL;
  • read函数返回的值是按小端字节序的;

read返回值:

  • 如果eventfd设置了EFD_SEMAPHORE,那么每次read就会返回1,并且让eventfd对应的计数器减一
  • 如果eventfd没有设置EFD_SEMAPHORE,那么每次read就会直接返回计数器中的数值,read之后计数器就会置0。
  • 不管是哪一种,当计数器为0时,如果继续read,那么read就会阻塞(如果eventfd没有设置EFD_NONBLOCK)或者返回EAGAIN错误(如果eventfd设置了EFD_NONBLOCK)。
    在这里插入图片描述

write

  • 在没有设置EFD_SEMAPHORE的情况下,write函数会将发送buf中的数据写入到eventfd对应的计数器中,最大只能写入0xffffffffffffffff,否则返回EINVAL错误;

  • 在设置了EFD_SEMAPHORE的情况下,write函数相当于是向计数器中进行“添加”,比如说计数器中的值原本是2,如果write了一个3,那么计数器中的值就变成了5。如果某一次write后,计数器中的值超过了0xfffffffffffffffe(64位最大值-1),那么write就会阻塞直到另一个进程/线程从eventfd中进行了read(如果write没有设置EFD_NONBLOCK),或者返回EAGAIN错误(如果write设置了EFD_NONBLOCK)。

    除此之外,eventfd还支持select和poll,与一般的读写描述符相类似,这里就不多说了,如下所示:
    在这里插入图片描述

实践

测试标志

阻塞标志

#include <malloc.h>
#include <pthread.h>
#include <zconf.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <wait.h>
#include <sys/eventfd.h>

#define handle_error(msg)   \
    do { perror(msg); exit(EXIT_FAILURE);  } while(0)
int main(int argc, char** argv)
{
    int  efd;

    ssize_t rc;
    efd = eventfd(0, 0);
    if(-1 == efd){
        handle_error("eventfd");
    }

    uint64_t buf;
    printf("wait for read:\n");
    rc = read(efd, &buf, sizeof(uint64_t));
    printf("read finished: %zd\n", rc);
    printf("Parent read %llu from efd\n",(unsigned long long)buf);
    return 0;
}

从下图中可以看出,默认是阻塞的,也就是read没有数据可读时会一直停在read处

在这里插入图片描述

如果将其设置为非阻塞标志EFD_NONBLOCK,就不会阻塞了,如果没有数据就直接过EFD_NONBLOCK

#include <malloc.h>
#include <pthread.h>
#include <zconf.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <wait.h>
#include <sys/eventfd.h>

#define handle_error(msg)   \
    do { perror(msg); exit(EXIT_FAILURE);  } while(0)
int main(int argc, char** argv)
{
    int  efd;

    ssize_t rc;
    efd = eventfd(0, EFD_NONBLOCK);
    if(-1 == efd){
        handle_error("eventfd");
    }

    uint64_t buf = 0;
    printf("wait for read:\n");
    rc = read(efd, &buf, sizeof(uint64_t));
    if(rc == -1){
        printf("read finished: %zd, error: %s\n", rc, strerror(errno));
    }else{
        printf("Parent read %llu from efd\n",(unsigned long long)buf);
    }

    return 0;
}

在这里插入图片描述

EFD_SEMAPHORE

在没有设置EFD_SEMAPHORE 之前:

#include <malloc.h>
#include <pthread.h>
#include <zconf.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <wait.h>
#include <sys/eventfd.h>

#define handle_error(msg)   \
    do { perror(msg); exit(EXIT_FAILURE);  } while(0)
int main(int argc, char** argv)
{
    int  efd;

    ssize_t rc;
    efd = eventfd(0, 0);
    if(-1 == efd){
        handle_error("eventfd");
    }

    uint64_t buf = 11;
    rc = write(efd, &buf, sizeof(uint64_t));
    if(rc != sizeof(uint64_t)){
        handle_error("write");
    }


    printf("Line[%d] wait for read:\n", __LINE__);
    rc = read(efd, &buf, sizeof(uint64_t));
    if(rc == -1){
        printf("Line[%d] read finished: %zd, error: %s\n",__LINE__, rc, strerror(errno));
    }else{
        printf("Line[%d] read finished: %zd, from efd get: %llu\n",__LINE__, rc,(unsigned long long)buf);
    }

    printf("Line[%d] wait for read:\n", __LINE__);
    rc = read(efd, &buf, sizeof(uint64_t));
    if(rc == -1){
        printf("Line[%d] read finished: %zd, error: %s\n",__LINE__, rc, strerror(errno));
    }else{
        printf("Line[%d] read finished: %zd, from efd get: %llu\n",__LINE__, rc,(unsigned long long)buf);
    }


    return 0;
}

从下面可以看出,不设置EFD_SEMAPHORE ,如果有数据可读,read会一次读完。第二次read时因为没有数据可读,就一直阻塞住
在这里插入图片描述

#include <malloc.h>
#include <pthread.h>
#include <zconf.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <wait.h>
#include <sys/eventfd.h>

#define handle_error(msg)   \
    do { perror(msg); exit(EXIT_FAILURE);  } while(0)
int main(int argc, char** argv)
{
    int  efd;

    ssize_t rc;
    efd = eventfd(0, EFD_SEMAPHORE );
    if(-1 == efd){
        handle_error("eventfd");
    }

    uint64_t buf = 3;
    rc = write(efd, &buf, sizeof(uint64_t));
    if(rc != sizeof(uint64_t)){
        handle_error("write");
    }

    int i = 0;
    while (1){
        printf(" Line[%d] order [%d] wait for read:\n", __LINE__, i);
        rc = read(efd, &buf, sizeof(uint64_t));
        if(rc == -1){
            printf("Line[%d] read finished: %zd, error: %s\n",__LINE__, rc, strerror(errno));
            break;
        }else{
            printf("Line[%d] read finished: %zd, from efd get: %llu\n",__LINE__, rc,(unsigned long long)buf);
        }
        i++;
    }
    
    return 0;
}

在这里插入图片描述
如上面可以看出,设置 EFD_SEMAPHORE ,如果有数据可读, read每次都会读出1来,直到读取完才阻塞。

当然,如果想没有数据读取就退出,那么可以设置非阻塞标志EFD_NONBLOCK

#include <malloc.h>
#include <pthread.h>
#include <zconf.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <wait.h>
#include <sys/eventfd.h>

#define handle_error(msg)   \
    do { perror(msg); exit(EXIT_FAILURE);  } while(0)
int main(int argc, char** argv)
{
    int  efd;

    ssize_t rc;
    efd = eventfd(0, EFD_SEMAPHORE | EFD_NONBLOCK );
    if(-1 == efd){
        handle_error("eventfd");
    }

    uint64_t buf = 3;
    rc = write(efd, &buf, sizeof(uint64_t));
    if(rc != sizeof(uint64_t)){
        handle_error("write");
    }

    int i = 0;
    while (1){
        printf(" Line[%d] order [%d] wait for read:\n", __LINE__, i);
        rc = read(efd, &buf, sizeof(uint64_t));
        if(rc == -1){

            printf("Line[%d] read finished: %zd, error [%d]: %s ; EAGAIN =  [%d]\n",__LINE__, rc,errno, strerror(errno), EAGAIN);
            break;
        }else{
            printf("Line[%d] read finished: %zd, from efd get: %llu\n",__LINE__, rc,(unsigned long long)buf);
        }
        i++;
    }

    return 0;
}

在这里插入图片描述

父子进程间通信

#include <malloc.h>
#include <pthread.h>
#include <zconf.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <wait.h>
#include <sys/eventfd.h>

#define handle_error(msg)   \
    do { perror(msg); exit(EXIT_FAILURE);  } while(0)
int main(int argc, char** argv)
{
    int  efd, i;
    uint64_t buf;
    ssize_t rc;

    if(argc < 2){
        fprintf(stderr, "Usage: %s <num>...\n",argv[0]);
        exit(EXIT_FAILURE);
    }

    efd = eventfd(0, 0);
    if(-1 == efd){
        handle_error("eventfd");
    }

    switch(fork()){
        case 0:
            for(i = 1;  i < argc; i++){
                printf("Child writing %s to efd\n",argv[i]);
                buf = atoll(argv[i]);
                rc = write(efd, &buf, sizeof(uint64_t));
                if(rc != sizeof(uint64_t)){
                    handle_error("write");
                }
            }
            printf("Child completed write loop\n");
            exit(EXIT_SUCCESS);
        default:
            sleep(2);
            printf("Parent about to read\n");
            rc = read(efd, &buf, sizeof(uint64_t));
            if(rc != sizeof(uint64_t))
                handle_error("read");
            printf("Parent read %llu from efd\n",(unsigned long long)buf);
        case -1:
            handle_error("fork");
    }
    return 0;
}

在这里插入图片描述

线程间唤醒

#include <malloc.h>
#include <pthread.h>
#include <zconf.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <wait.h>
#include <sys/eventfd.h>



void * threadFunc(void *args){
    uint64_t  buf;
    int rc;

    while (1){
        rc = read(*(int *)args, &buf, sizeof(buf));
        if(rc == 8){
            printf("notify success\n");
        }
        printf("rc = %llu, buffer = %lu\n",(unsigned long long)rc, buf);
    }
}


int main(int argc, char** argv)
{
    pthread_t tid;
    int rc;
    uint64_t buf = 1;
    int efd;

    efd = eventfd(0,0);     // blocking
    if(efd == -1){
        perror("eventfd");
    }

    if(pthread_create(&tid, NULL, threadFunc, &efd) != 0){
        perror("pthread_create");
        exit(-1);
    }
    while(1){
        rc = write(efd, &buf, sizeof(buf));

        if(rc != 8){
            perror("write");
        }
        sleep(2);
    }//end while
    close(efd);
}

在这里插入图片描述

#include <malloc.h>
#include <pthread.h>
#include <zconf.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <wait.h>
#include <sys/eventfd.h>




int main(int argc, char** argv)
{
    int efd = eventfd(10, 0);
    uint64_t  wdata = 0, rdata = 0;

    if(read(efd, &rdata, 8) == -1){
        perror(NULL);
        if(errno != EAGAIN){
            return 0;
        }
    }
    printf("Init data %lu\n", rdata);  // 10
        // 读完了就没有了

    wdata = 20;
    if(write(efd,&wdata,8) == -1) //父进程写20
    {
        perror(NULL);
        return 0;
    }
    printf("write data %lu\n", wdata);  // 10

    if(fork() == 0){
        if(read(efd,&rdata,8) == -1) //子进程读计数器
        {
            perror(NULL);
            return 0;
        }
        printf("child read  %lu\n", rdata);
        wdata = 30;
        if(write(efd,&wdata,8) == -1)  //子进程写30
        {
            perror(NULL);
            return 0;
        }
        printf("child write  %lu\n", wdata);
        exit(0);
    }
    wait(NULL);
    if(read(efd,&rdata,8) == -1)   //父进程读计数器
    {
        perror(NULL);
        return 0;
    }
    printf("parant read  %lu\n", wdata);


    close(efd);
    return 0;
}
<think>好的,用户问的是在Windows和Linux下分别使用什么实现异步I/O。我需要先回忆这两个操作系统中的异步I/O机制,然后组织成清晰的中文回答。 首先,Windows方面,我记得有几个主要的模型。早期的有重叠I/O(Overlapped I/O),它允许同时进行多个I/O操作,需要配合WaitForSingleObject或WaitForMultipleObjects来等待完成。然后是IOCP,即I/O完成端口,这是Windows下高性能的异步模型,常用于服务器软件,比如IIS或SQL Server。还有更现代的方案,比如Windows Runtime中的异步API,基于任务和协程,可能在UWP应用中使用较多。另外,.NET里的async/await应该也是基于这些底层机制封装的。 接下来是Linux这边,异步I/O的实现方式可能更多样。首先想到的是POSIX aio,但好像实际应用不多,因为存在一些限制。然后是Linux特有的io_uring,听说性能很好,现在很多项目都在用,比如Redis 6.0之后支持了io_uring。还有基于事件通知的机制,比如epoll,不过epoll主要用于网络I/O的异步,对于文件I/O可能需要配合线程池。此外,libuv这样的跨平台库在Linux下可能使用epoll或者io_uring,而在Windows用IOCP,这样开发者可以统一接口。 需要注意用户可能关心两者的区别和各自的应用场景,比如高性能场景下Windows的IOCP和Linux的io_uring对比。同时,用户可能想知道如何选择,或者有没有跨平台的库推荐,比如libuv、Boost.Asio等。另外,是否需要提到每个技术的优缺点,比如IOCP的高效但复杂,io_uring的新特性等。 可能还要检查是否有遗漏的技术点,比如Windows是否有其他异步API,或者Linux的eventfd之类的机制是否相关。但用户的问题主要是实现异步I/O的方法,所以可能不需要涉及太广泛。 需要确保术语翻译正确,比如“I/O完成端口”是IOCP的标准翻译,“io_uring”一般不用翻译。同时,注意区分网络I/O和文件I/O,因为有些技术可能只适用于其中一种,比如epoll主要用于网络,而aio或io_uring可以处理文件。 最后,结构要清晰,分点列出Windows和Linux下的不同实现方式,并简要说明它们的原理和使用场景,可能再加上跨平台库的例子,帮助用户理解如何在实际开发中选择和应用。</think>在Windows和Linux系统中,异步I/O的实现方式有显著差异。以下是分步对比说明: 一、Windows系统实现异步I/O 1. **重叠I/O (Overlapped I/O)** - 核心机制:通过`OVERLAPPED`结构体记录I/O操作状态 - 配合使用`ReadFileEx/WriteFileEx`等API - 使用完成通知机制: - 事件通知(WaitForSingleObject) - 完成例程(Completion Routines) 2. **I/O完成端口 (IOCP, I/O Completion Port)** - 高性能异步模型 - 工作原理: ```cpp HANDLE hCompletionPort = CreateIoCompletionPort(...); // 绑定文件句柄到完成端口 PostQueuedCompletionStatus(...); // 投递任务 GetQueuedCompletionStatus(...); // 获取完成状态 ``` - 特点:线程池管理、支持负载均衡 3. **现代异步API** - Windows Runtime (WinRT) 异步模式 - C++/WinRT 协程支持: ```cpp IAsyncOperation<uint32_t> ReadDataAsync() { co_await winrt::resume_background(); // 异步操作 } ``` 二、Linux系统实现异步I/O 1. **POSIX AIO** - 标准接口但存在局限: ```c struct aiocb cb; aio_read(&cb); aio_error(&cb); // 检查状态 ``` - 缺点:仅支持有限的操作类型 2. **io_uring (主流方案)** - 高性能环形队列架构: ```c struct io_uring ring; io_uring_queue_init(ENTRIES, &ring, 0); // 初始化队列 // 提交SQE(提交队列条目) io_uring_get_sqe(&ring); // 处理CQE(完成队列条目) io_uring_wait_cqe(&ring, &cqe); ``` - 优势: - 零拷贝操作 - 支持批量提交 - 文件/网络I/O统一接口 3. **事件驱动模型** - epoll + 线程池组合: ```c int epfd = epoll_create1(0); epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); epoll_wait(epfd, events, MAX_EVENTS, -1); ``` - 特点:主要面向网络I/O 三、跨平台解决方案 1. **libuv库**(Node.js底层) - Windows使用IOCP - Linux使用epoll/io_uring - 统一事件循环接口: ```c uv_fs_read(uv_loop_t* loop, uv_fs_t* req,...); ``` 2. **Boost.Asio** - 基于前摄器模式(Proactor) - 底层自动选择最佳实现: ```cpp asio::io_context io; asio::posix::stream_descriptor pipe(io, fd); async_read(pipe, asio::buffer(data), handler); ``` 四、性能对比 | 指标 | Windows IOCP | Linux io_uring | |---------------|----------------|----------------| | 吞吐量 | 1M ops/sec | 5M+ ops/sec | | 延迟 | 10-50μs | 2-10μs | | CPU利用率 | 中等 | 更低 | | 编程复杂度 | 较高 | 中等 | 实际选择建议: 1. Windows平台优先使用IOCP进行高性能服务开发 2. Linux服务器端首选io_uring(需内核≥5.1) 3. 跨平台项目推荐使用libuv或Boost.Asio 4. 简单场景可使用语言级封装(如C# async/await、Python asyncio) 需要注意:文件I/O和网络I/O的异步实现机制在底层存在差异,网络I/O普遍支持更好,而高效的文件异步I/O需要特定系统支持(如Windows IOCP文件操作、Linux io_uring)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值