epoll与fork
阅读原文时间:2023年07月10日阅读:1

使用epoll时,如果在调用epoll_create之后,调用了fork创建子进程,那么父子进程虽然有各自epoll实例的副本,但是在内核中,它们引用的是同一个实例。子进程向自己的epoll实例添加、修改和删除文件描述符时,是可以影响到父进程的epoll_wait的。所以会发生意想不到的问题,分情况看一下:

1:向子进程中的epoll实例添加描述符,描述符事件触发后,也会影响到父进程的epoll实例,代码如下:

#define MAXEVENTS 20

int listenfd;
struct epoll_event events[MAXEVENTS];

int epfd = epoll_create(MAXEVENTS);

if((pid = fork()) < 0) return;

if(pid == 0)
{
listenfd = socketfd();
struct epoll_event lisevent;
lisevent.events = EPOLLIN;
lisevent.data.fd = listenfd;

res = epoll\_ctl(epfd, EPOLL\_CTL\_ADD, listenfd, &lisevent);  

}

while(1)
{
res = epoll_wait(epfd, events, MAXEVENTS, -1);

for(i = 0; i < res; i++)  
{  
    connectfd = accept(events\[i\].data.fd, (struct sockaddr \*)&clientaddr, (socklen\_t \*)&addrlen);  
    if(connectfd < 0)  
    {  
        perror("accept error");  
        continue;  
    }  
    printf("connect from %s\\n", inet\_ntop(AF\_INET, &(clientaddr.sin\_addr), addrbuf, 20));  
    close(connectfd);  
}  

}

上述代码中,在fork之前创建epoll实例,然后在子进程中,创建监听socket,并且加入到epoll实例中。父子进程同时在epoll实例上调用epoll_wait等待连接的到来。如果此时客户端建链,则打印如下:

accept error: Bad file descriptor
accept error: Bad file descriptor
……
accept error: Bad file descriptor
connect from 127.0.0.1

也就是说,连接到来时,尽管是在子进程中创建的监听套接字,加入到子进程中的epoll实例中。但是父子进程中epoll实例都会收到触发的事件,二者的epoll_wait都会停止阻塞,开始调用accept。

父进程调用accept失败,打印出Bad file descriptor错误,是因为在父进程中,根本没有监听套接字。所以,只要子进程没有调用accept成功,则该连接事件就会一直触发,从而父进程一直打印accept错误信息,直到子进程调用accept成功,打印出connect from 127.0.0.1。

2:在fork之前,创建epoll实例、监听套接字listenfd,并将listenfd加入到epoll实例中。然后父子进程一起等待事件的触发,代码如下:

#define MAXEVENTS 20

int listenfd;
struct epoll_event events[MAXEVENTS];

int epfd = epoll_create(MAXEVENTS);

listenfd = socketfd();
struct epoll_event lisevent;
lisevent.events = EPOLLIN;
lisevent.data.fd = listenfd;

res = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &lisevent);

if((pid = fork()) < 0) return;

while(1)
{
res = epoll_wait(epfd, events, MAXEVENTS, -1);

for(i = 0; i < res; i++)  
{  
    printf("\[%s\]before accept\\n", pid?"father":"child");  
    connectfd = accept(events\[i\].data.fd, (struct sockaddr \*)&clientaddr, (socklen\_t \*)&addrlen);  
    printf("\[%s\]after accept\\n", pid?"father":"child");

    if(connectfd < 0)  
    {  
        perror("accept error");  
        continue;  
    }  
    printf("connect from %s\\n", inet\_ntop(AF\_INET, &(clientaddr.sin\_addr), addrbuf, 20));  
    close(connectfd);  
}  

}

上述代码在fork之前创建好epoll实例和监听套接字,然后调用fork,父子进程在各自的epoll实例上等待事件的发生,如果此时到来了一个客户端连接,则打印如下:

[father]before accept
[father]after accept
connect from 127.0.0.1
[child]before accept

可见,到来的连接触发的事件,会同时通告给被父子进程的epoll实例。父进程调用accept得到该连接,而子进程调用accept时,连接已经被取走了,所以子进程中的accept阻塞。

总结:在fork之前创建的epoll实例,尽管分别处于父子进程各自的空间中,但是它们在底层引用的同一个内核结构。所以,当事件发生时,会同时通告给父子进程中的epoll实例。这其实算是epoll设计上的一个缺陷,应该避免在fork之前创建epoll实例,或者在fork之后,关闭原epoll实例,重新创建本进程的epoll实例。这一点在libev的文档中有所提及:

The biggest issue is fork races, however - if a program forks then both parent and child process have to recreate the epoll set, which can take considerable time (one syscall per file descriptor) and is of course hard
to detect.

参考:http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod