linux c ipc eventfd


原文链接: linux c ipc eventfd

1.新内核版本为什么要增加eventfd?

首先说明的一点是eventfd是用来实现多进程或多线程的之间的事件通知的,那么我们在没接触eventfd之前用到的事件通知机制都有那些?

1.条件变量
2.管道

我们来逐一比较此俩中机制与eventfd的效果方面的好坏,首先,条件变量必须和互斥锁结合使用,使用起来麻烦,而且性能未必比eventfd好,其次条件变量不能像eventfd一样为I/O事件驱动,因此不能和服务器的I/O模式很好的融合,所以在某些时候不如eventfd好用

接着是管道,虽然管道能与I/O复用很好的融合,但很明显管道相比eventfd多用了一个文件描述符,而且管道的话内核还得给其管理的缓冲区,eventfd则不需要,所以单纯作为事件通知的话还是管道好用

2.event的主要接口

eventfd只有一个接口,形式如下

int eventfd(unsigned int initval, int flags); //成功返回事件驱动的文件描述符fd
说明:initval的范围是0~0xfffffffffffffffe;flags的值可以是如下枚举值:

enum
{

EFD_SEMAPHORE = 00000001,       //since Linux 2.6.30

#define EFD_SEMAPHORE EFD_SEMAPHORE

EFD_CLOEXEC = 02000000,         //since Linux 2.6.27

#define EFD_CLOEXEC EFD_CLOEXEC

EFD_NONBLOCK = 00004000         //since Linux 2.6.27

#define EFD_NONBLOCK EFD_NONBLOCK
};

如果设置了EFD_SEMAPHORE,则不会出现粘包的情况,即write多少次,就需要read多少次;

如果设置了0(内核2.6.27版本之前必须设置为0),则会出现粘包的可能,write多次,可能read到的是若干次的值的和;

另外两种一般不需要,所以并未进行测试。

eventfd()创建一个文件描述符,这个文件描述符用户可以通过等待其可读来实现事件通知,该通知靠内核来响应用户空间的应用事件。上述接口的第一个参数是一个由内核来保持的64位计数器,这个计数器有参数initval来初始化,关于此计数器的影响我在下文中的具体实例中给大家演示,一般我们可将其设为0

第二个参数flags可以为EFD_NONBLOCK或EFD_CLOEXEC,其含义分别为阻塞文件描述符,和与普通文件的CLOEXEC标志一样的功能
一般来说进程往往会调用fork函数来执行一个子进程,调用execve()执行其他程序,这时候可能就导致子进程中存在一些无用的文件描述符问题。
父进程在fork函数的时候,子进程会拷贝跟父进程一样的地址空间,包括寄存器,文件描述符,堆,栈等。在一开始,父进程与子进程的文件描述符指向的是系统文件表中的同一项(包括文件状态,和文件偏移量)。
当我们用execve执行其他程序的时候,全新的程序会替换子进程中的地址空间,数据段,堆栈,此时保存与父进程文件描述符也就不存在了,也无法进行关闭,这时候就需要FD_CLOEXEC, 表示子进程在执行exec的时候,该文件描述符就需要进行关闭。

通过fcntl设置FD_CLOEXEC标志有什么用?
close on exec, not on-fork, 意为如果对描述符设置了FD_CLOEXEC,使用execl执行的程序里,此描述符被关闭,不能再使用它,但是在使用fork调用的子进程中,此描述符并不关闭,仍可使用。

实现代码如下:

int fd=open("1",O_RDONLY);
//获取当前文件描述符的相关标志位
int flags=fcntl(fd,F_GETFD,NULL);
flags|=FD_CLOEXEC;
fcntl(fd,F_SETFD,flags);

3.具体实例

#include <sys/eventfd.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int fd;
uint64_t buffer;

void threadFunc(void)   //线程函数
{
int t;
while(1)
{
     t = read(fd,&buffer,sizeof(buffer));       //阻塞等待fd可读,及通知事件发生

     if(sizeof(buffer) < 8)
     {
        printf("buffer错误\n");
     }
     printf("t = %llu   buffer = %llu\n",t,buffer);
     if(t == 8)
     {
        printf("唤醒成功\n");
     }

}    
}

int main(void)
{
    uint64_t buf = 1;
    int ret;
    pthread_t tid;
    int init_val =3;
    if((fd = eventfd(init_val,0)) == -1)   //创建事件驱动的文件描述符
    {
        printf("创建失败\n");
    }

    //创建线程
    if(pthread_create(&tid,NULL,threadFunc,NULL) < 0)
    {
        printf("线程创建失败\n");
    }

    while(1)
    {
        ret = write(fd,&buf,sizeof(buf));  //通过往fd里写东西来进行事件通知
        if(ret != 8)
        {
            printf("写错误\n");
        }
        sleep(2);                           //没2s通知一次
    }

    return 0;
}

上述代码中,我们创建一个线程,通过主线程往fd里写数据,来通知另一个线程。
代码执行结果如下
这里写图片描述

关于eventfd中的initval参数的作用,我的测试结果如下,前提把上述代码eventfd函数中的0改为3

这里写图片描述
可以看出这个由内核管理的计数器,我们的初始化值,只会影响程序第一次buffer的值,后续buffer中的值依然为1.

`