4 ms·
While tempting, you can’t generally fix this by simply patching close() with some function that converts it to an unchecked asynchronous operation. If that were
by CyberRabbi 5y ago
While tempting, you can’t generally fix this by simply patching close() with some function that converts it to an unchecked asynchronous operation. If that were the case, you could just do that in the kernel. Close() is expected to complete synchronously. This matters because posix guarantees that open()/pipe() etc. will return the lowest file descriptor not in use[1]. I.e. this should work:
close(0);
fd = open(“/foo/bar”, …);
// fd is guaranteed to be 0
If you made close() just dispatch an asynchronous operation and not wait on the result, then the code above would break. Any code that uses dup() likely has code that expects close() to behave that way.
The other issue is that close() can return errors. Most applications ignore close errors but to be a robust solution you’d need to ensure the target application ignores those errors as well.
[1]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14 https://pubs.opengroup.org/onlinepubs/9699919799/functions/V...
- deleted 5y ago[deleted]
- treis 5y ago>This matters because posix guarantees that open()/pipe() etc. will return the lowest file descriptor not in use[1]. I.e. this should work: close(0); fd = open(“/foo/bar”, …); // fd is guaranteed to be 0 On a multi threaded system that isn't guaranteed is it? Meaning, another thread could call open in-between your close & open.
- CyberRabbi 5y agoIt is guaranteed whether multi-threaded or not. It’s a process level guarantee. If your application is designed such that you don’t know what your other threads are doing then POSIX cannot help you.
- loeg 5y agoWhat you’re getting at is that an individual thread cannot really use this property without some form of synchronization with other threads in the process. Eg, to use this property, other threads either do not allocate fds, or you take some central lock around all fd allocations. Most well-written programs do not rely on it.
- infogulch 5y agoI did not know this, and for some reason it really annoys me. Why are our process contexts littered with useless little synchronous properties? How many other tedious and slow bookkeeping tasks does the OS have to do just to meet some outdated spec that was probably just an ossified implementation detail in the first place? I feel compelled to make it so that new fds are explicitly randomized just so you can't do this, like how Go randomizes map iteration order.
- kennylevinsen 5y agoThis argument does not make sense - the kernel already needs to track per-process file descriptors. It just looks for the first hole instead of giving the "next" value. Go's random map iteration does not apply here. Not only is this not an iterable map, the kernel has no problem providing this insertion guarantee so adding additional costly randomization has no benefit and just burns additional cycles. Go would also be better off without, but they are catering to a different audience and different degree of specification, and apparently need to actively deter developers from ignoring documentation.
- loeg 5y agoIf we ignore POSIX for a moment, the kernel could avoid contending on the one-per-process fd map by sharding the integers into distinct allocation ranges per thread. This would eliminate a source of contention between threads. In addition to violating POSIX’ lowest hole rule, it would break select(2) (more than it’s already broken).
- kennylevinsen 5y agoThis sounds like premature optimization. FD availability is tracked in a bitmask, and finding the next available slot is a matter of scanning for the first unset bit under a spinlock. This is going to be extremely fast. While you could shard the file descriptor tables for CLONE_FILES processes such as threads, you would likely complicate file descriptor table management and harm the much more important read performance (which is currently just a plain array index and pretty hard to beat). You could also juts create your processes (or threads) without CLONE_FILES so that they get their own file descriptor table. ------ The fdtable can be seen here: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/fdtable.h?h=v5.12#n27 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin..., and alloc_fd and __fget can be found here: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/file.c?h=v5.12#n470 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin....
- R0b0t1 5y agoAre you sure such code exists? Doesn't the standard tell you to always treat the fd type as opaque anyway? Referring to exactly the point you cite, the standard seems to be making no strong statement at all. It says to allocate from the lowest fd but that calls which may return multiple fds do not need to guarantee they are adjacent. I always took this to mean the values should pack downward and should not be e.g. allocated randomly, though it never seemed clear to me why, as the standard seems to be planning for multithreaded code. So you are interpreting it one way, but the same statement seems to imply that fds are not meant to be introspected and should always be taken at face value from a call that generates a valid fd.
- toast0 5y ago> says to allocate from the lowest fd but that calls which may return multiple fds do not need to guarantee they are adjacent. If the program has fd 0-3 and 5 open, socketpair should return 4 and 6, which are not adjacent. If socketpair is called again, while close(N) (N < 7) is being called in a separate thread, you could get {7, 8}, {N, 7}, or {7, N}, depending on kernel and timing details. All of those returns fit the requirement that the fds be allocated lowest first, but may or may not be adjacent or in absolute order.
- CyberRabbi 5y ago> I always took this to mean the values should pack downward and should not be e.g. allocated randomly, though it never seemed clear to me why, The reason for this requirement is that early versions of Unix did not have dup2(), only dup(). It has nothing to do with multi threading as this predates pthreads by more than two decades. The shell (sh) makes use of the lowest numbered property to redirect standard in/out/error when setting up pipelines: int pipes[2]; /* ignore errors */ (void) pipe(&pipes); if (fork()) { close(0); /* guaranteed to return 0 */ (void) dup(pipes[0]); close(pipes[0]); close(pipes[1]); exec_child(); } else { close(1); /* guaranteed to return 1, we know 0 is taken */ (void) dup(pipes[1]); close(pipes[0]); close(pipes[1]); exec_parent(); } Code like this exists in literally every POSIX shell. Anyone saying code like this isn’t common has no idea what they’re talking about.
- touisteur 5y agoOooh yes, thanks you're right. That, would make for tricky shadow fd accounting... Ugh.