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.