Hello everyone, this is a follow up to a discussion from 2012 about EPOLL_CTL_DISABLE, which ended up NOT being accepted in the kernel.
I believe however that this issue wasn't addressed. Michael Kerrisk did an excellent job writing a summary of the problem: https://lkml.org/lkml/2012/10/16/302 (the first four listed points). In the past two years I've been working on "yet another" event driven I/O library, exclusively for linux however, with the general aim to be highly scalable for many cores. The I/O part (around epoll) is only part of that library - but it is what I spent the last two years on. I ran independently into the same problem as described by Paton Lewis and only afterwards, when I couldn't think of a working solution and started to do heavy research, found related articles about the problem. Reading through the thread on lkml.org that given above, it still isn't clear to me that this would be a non-issue that can be solved in user space (without severe (unnecessary) performance penalties). Let me summarize the problem in my own way (probably less general, but more related to my personal case): 1. The epoll interest list stores a pointer in epoll_event::data.ptr that points to a specific file descriptor related object, which indeed can be seen as a user-space cache of data related to that file descriptor. In my case this includes one mutex that protects an unsigned int whose bits keep track of things like whether or not that object (pointer) is in the epoll interest list (was added with epoll_ctl) and which events are being watched specifically, as well as whether or not the fd was closed (after removing it from the interest list with EPOLL_CTL_DEL) etc. 2. At least one thread (only one, in my case) calls epoll_wait (epoll_pwait) in a loop. I use no timeout because that seems too inaccurate, my timers are signal based. When epoll_wait returns, the pointer epoll_event::data.ptr is used to call a member function on the dereferenced FileDescriptor class. For non-C++ coders, that means a function is passed as first parameter the value of epoll_event::data.ptr (the 'this' pointer) which is subsequently dereferenced inside that function. The exact function depends on the actual event. In most cases the handling of the event is passed on to a thread pool queue for handling by another thread - but before that happens the pointer was already dereferenced (which, among others, allows me to increment a reference count for that object). This obviously means that epoll_event::data.ptr may never point to freed data. 3. As almost all processing happens by worker threads of a thread pool - it will be some other thread that decides that an object is done and needs to be freed. This thread first removes the object from the epoll interest list, and then deletes the object. Unfortunately this does not work -- and apart from adding a delay before the object is really deleted (which isn't a real solution as several of you already pointed out in 2012), I do not see any possible alternative. The reason it can't work is because the Event Loop Thread (that loops around epoll_wait) may already be in the process of returning from epoll_wait, lets say -- it already returned from epoll_wait, but wasn't able to execute ANY code following it. And because this is ALWAYS possible there is NEVER a safe moment to delete the object. The scenario is, for example: Event Loop Thread Worker thread Returns from epoll_wait() passing back to user space a data.ptr pointing to a user allocated object. Removes the object from the epoll interest list with EPOLL_CTL_DEL <arbitrary delay here, for example, no delay> Free the object pointed to by data.ptr. Dereferences data.ptr and crashes. The solution proposed by Andy Lutomirski (https://lkml.org/lkml/2012/10/18/434) does not work here: In order use RCU the ptr must be removed from the protected "list" before the grace period is started, which must start before a read-side critical area ends. But the "list" here is the epoll interest list - and removing it from that list requires the call to epoll_ctl(..., EPOLL_CTL_DEL, ...) to finish. In other words, the Worker thread is the RCU "updater" and the "arbitrary delay" must be the rcu grace period. No problems there. The event loop thread however must call rcu_read_lock() before accessing the epoll_event structure, which is not possible because that happens inside epoll_wait(), which doesn't provide a hook to add such call. As far as the Worker Thread is concerned there are no readers, and the grace period can finish instantly, simply because there is no way to register that data.ptr was already copied. If one tries to begin a read-side critical area after epoll_wait() returns, then that won't work: in that case you should not be able to access that ptr when it was already removed from the interest list. The only way that RCU would work here is when a reader subscribes *before* the kernel copies the corresponding epoll_event structure to user space, in a way that that will never happen when the EPOLL_CTL_DEL finished before it got to that point. I believe that the only safe solution is to let the Event Loop Thread do the deleting. So, if all else fails I'll have to add objects that a Worker Thread thinks need to be deleted to a FIFO that is processed by the Event Loop Thread before entering epoll_wait(). But that is a lot of extra code for something that could be achieved with just a small change to epoll: I propose to add a new EPOLL event EPOLLCLOSED that will cause epoll_wait to return (for that event) whenever a file descriptor is closed. The Worker Thread then does not remove an object from the interest list, but either adds (if it was removed before) or modifies the event (using EPOLL_CTL_MOD) to watch that fd for closing, and then closes it. Aka, Working Thread: epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event); close(fd); where event contains the new EPOLLCLOSED (compare EPOLLOUT, EPOLLIN etc). This must then guarantee the event EPOLLCLOSED to be reported by exactly one epoll_wait(), the caller thread of which can then proceed with deleting the resources. Note that close(fd) must cause the removal from the interest list of any epoll struct before causing the event - and that the EPOLLCLOSED event may only be reported after all other events for that fd have already been reported (although it would be ok to report them at the same time: simply handle the other events first). I am not sure how this will work when more than one thread calls epoll_wait and more than one watch the same fd: in that case it is less trivial because then it seems always possible that the EPOLLCLOSED event will be reported before another event in another thread. What are your thoughts? Is the addition of EPOLLCLOSED event feasible? -- Carlo Wood <ca...@alinoe.com>