7 comments

[ 2.8 ms ] story [ 29.2 ms ] thread
What if lib.B had another dependency? To deal with this dlclose would have to query every loaded module for its dependencies and then decide if lib.B could safely be unloaded.

What if lib.B had been loaded explicitly somewhere else such that it did not appear in any other module's dependency list?

Do you mean, what if lib.B had another dependent? Every loaded library has a reference count, which counts both dependents and explicit dlopen() calls. Unless the caller is the last user (and the other conditions are satisfied), dlclose() has no effect except to decrement the reference count.
There is no safe, reliable, cross-environment way to deal with closing a DLL. A DLL initialization function can allocate arbitrary resources, some of which may be in use by clients of the DLL when it is closed.

The only safe, consistent, reliable approach is not to close DLLs.

I'm curious what design led them to split code between two shared libraries but also require the state of them to be synchronized across calls to dlopen and dlclose. It sounds like that state should be in one library and not two.
Not only is dlclose(3) not guaranteed to close/unload the library, close(2) isn't guaranteed to close a file!.

If the same file descriptor is open in another process (e.g. one that has it via fork(2) FD sharing either without child processes exec(2)ing or with CLOEXEC not set, or some older and esoteric abuses of fdpassing over UNIX sockets), then close(2) just decrements a refcount. The actual file isn't closed until the last holder of a reference to its file descriptor calls close(2).

This is rarely relevant, but when it comes up, it sure is wild. Common sources of confusion and pain due to this behavior are: files opened for "global library reasons" (e.g. /dev/shm buffers) in preforking servers, signalfd descriptors in preforking servers, processes that fork off and daemonize but leave their spawner around for e.g. CoW memory sharing efficiencies (looking at you, Python multiprocessing backends--the docs make it sound like they all act more or less the same, but in this regard they very much do not), libraries that usually swap STDIN/OUT/ERR file descriptors around with CLOEXEC disabled for a manually fork+exec'd child (e.g. situations where posix_spawn doesn't support needed pre-exec setup) but that are then used as part of larger applications that fork/spawn for other reasons and don't realize that file descriptor allocation/forking needs care and synchronization with the manually-fork/execing library in question: mixing forks and threads is one of those things that everyone says is a fast-track ticket to nasal demons, but that everyone also does regularly, I've found--if this describes you, be careful!

If you end up in one of those situations, suddenly invariants like "I called unlink on this path and then close(2)'d the descriptor to it, so that (maybe large) chunk of allocated space isn't taking up space on the filesystem/buffers any more" and "this externally-observable lockfile/directory is now unlocked due to its absence, now processes coordinating using that file on NFS will work as expected" no longer hold.

https://www.ibm.com/docs/en/aix/7.1.0?topic=domains-unix-dom...

I know that close(2)'s weirdness isn't a superset of dlopen(3)'s and there are different reasons for both behaviors. But it's still interesting that they "rhyme" as it were.

Call the library's shutdown procedure before you dlclose. A dynamically linked library is a resource like any other. Ya gotta close it properly.

Of course, if the library doesn't probably attempt to close libraries it's responsible for during its shutdown procedure, that's another can of worms.

There needs to be a way to forcibly run the thread destructors for a library that is going away.

Since the library is going away, it means it's not used any more. Not being used means that no thread that is currently running will call into that library any more.

If no thread that is currently running will use the code of that library, it has no business hanging on to the data belonging to the library; that data should only be manipulable via that code. Therefore, it should be forcibly taken away.

The reason they flunk on this issue is that there isn't a nice way to enumerate through all the threads and snipe away a particular class of thread local storage from each. The architecture is oriented around the POSIX-induced brain-damaged idea that a thread must clean up all thread-specific storage after itself.

Fight me!