9 ms·
Practical Libc-free threading on Linux
- lanstin 4y agoCool, that was simpler to understand than I expected. Still unsure of the utility aside from understanding how things work.
- rwmj 4y agoSeveral reasons why this is a terrible idea, aside from learning how things work: (1) It only works on one architecture. Even this architecture may change optimal alignment requirements in future, which glibc would transparently handle for you. (2) It's very complex and error-prone compared to using the pthread_* APIs (and they themselves are not exactly easy to use correctly). This article is a good overview of why using the futex syscall directly is hard: https://lwn.net/Articles/823513/ https://lwn.net/Articles/823513/ (3) You're missing future bug fixes and optimizations. (4) The kernel may move to a different API in future and glibc will handle that for you, but this code will still be using the old API. The kernel has changed its threading APIs, the clone API, and how syscalls are made several times in the past, and you cannot predict how they will change in future.
- doodlesdev 4y agoYup. Additionally, if you need portability (i.e. don't want to depend on the system libc) you may use Gnulib [0]. [0]: https://www.gnu.org/software/gnulib/manual/html_node/Multithreading-APIs.html https://www.gnu.org/software/gnulib/manual/html_node/Multith...
- dataflow 4y agoTo make it explicit, #4 is basically making the case for why the Linux setting the syscall interface as the stable API boundary is a poor approach.
- nine_k 4y agoWhat approach would be better?
- ben0x539 4y agoI think competing products define their interface in terms of calling a provided C library, and leave the entirely of the system call mechanics as an implementation detail.
- jmillikin 4y agoFor context, this is how it worked in old-school UNIX. If you look at (for example) SunOS, the kernel ABI is undocumented and everything in userspace depends on libc. The OSes and userspace languages of the day were deeply coupled -- C was the language of UNIX. You can still see the remnants of this design in macOS, except instead of libc it's libSystem. Apple has decoupled their OS from C (in favor of Objective C, then Swift) but the idea of a userspace entry point to system functionality survived. Linux is different from traditional UNIX because it's a kernel-only OS. There's no "Linux libc", and third parties have written multiple implementations of the C standard library that run on Linux. The only way this structure can work is for the kernel itself to provide stable ABI, and the standard approach at the time (mid 90s) was numeric syscall codes with positional parameters.
- __turbobrew__ 4y agoOpenBSD does (did?) require all syscalls to originate from libc as well.
- ben0x539 4y agoYeah, and the particular old-school UNIX I had in mind was Win32. :)
- jmillikin 4y agoWhat alternative is there? Linux is a more minimalist OS than old-school UNIX, and libc as a userspace construct allows more evolution in that space (see glibc vs musl vs uclibc). I could see an argument for small shims injected by a vDSO (AIUI this is what Fuchsia does), but clearly the 80s-style "libc is the OS boundary" is obsolete.
- dataflow 4y agoA user-mode library bundled with the kernel (like libc, but doesn't have to be libc) is the alternative to the syscall boundary. You maintain the library as the stable interface for the user, and let the syscalls change as you please.
- jmillikin 4y agoYes, I understand the mechanism, but why would you want to structure your OS like that? You either have to maintain a stable syscall ABI, or a stable userspace function ABI, and it's not like one is materially more difficult than the other. If anything, the Linux approach has proven to be superior -- glibc has had multiple breaking changes over the years and to this day users will resort to Docker to run binaries that depend on "old" glibc, but programs written against the Linux syscall ABI twenty years ago will run fine to this day. Even if you decide to have a userspace trampoline/shim, it doesn't make sense for that to be libc. The C standard library is huge and requiring userspace programs to link it regardless of implementation language does nothing except add forty years of bad ideas into your process's address space.
- userbinator 4y agoIf anything, the Linux approach has proven to be superior -- glibc has had multiple breaking changes over the years and to this day users will resort to Docker to run binaries that depend on "old" glibc, but programs written against the Linux syscall ABI twenty years ago will run fine to this day. Windows takes the opposite approach (except that the libraries are gdi32/kernel32/ntdll/user32/etc.), and it's definitely an example of backwards-compatibility done well.
- userbinator 4y agoThe kernel has changed its threading APIs, the clone API, and how syscalls are made several times in the past, and you cannot predict how they will change in future. I thought "don't break userspace" was one of the key principles of Linux design?
- rwmj 4y agoThe old APIs continue to be supported, but they may be slower or have other limitations. eg You can continue to make syscalls through int $0x80 if you want, even on the latest x86, but performance will suck. You can keep using clone instead of clone3, but you won't get to use all the newer features.
- vanviegen 4y agoSo your code will keep on working as good as it ever did. I fail to see the problem with that.
- pedrocr 4y agoNothing guarantees that. clone() may still be provided by the kernel but nothing guarantees it will still be fast and not just a compatibility retrofit. Particularly since most users will move on automatically through libc.
- wahern 4y ago> (1) It only works on one architecture. Even this architecture may change optimal alignment requirements in future, which glibc would transparently handle for you. A portable solution (on any POSIX system, actually) for the stack switching would be to jump to the new stack using sigaltstack and initialize a sigjmp_t context (i.e. setjmp) from the signal handler. After calling clone you conditionally invoke longjmp based on the return value. This works because setjmp and longjmp are compiler builtins; but if not, use __builtin_setjmp, __builtin_longjmp. (You can pass the sigjmp_t context using the same trick mentioned in the writeup. And of course this also assumes the compiler won't emit stack-dependent code between the syscall and longjmp.) You still have the problem of implementing the syscalls, though. But maybe if we scrounge around we can find solutions. On some architectures the vdso Linux maps into the process address space has a syscall implementation. And maybe there are similar compiler builtins for other architectures, or macros in the Linux header files with the necessary inline assembly. As a learning exercise this type of stuff is fun ;)
- manwe150 4y agosetjmp and longjmp are not normally compiler builtins, just some library functions. They are quite different also from the __builtins that you mention, sometimes in quite surprising ways. Actually compilers are very buggy when you use these functions, even if you do follow their quite strict rules for what is allowed. Calling longjmp in a signal handler is technically undefined behavior in posix and trigger issues on some platforms that I have had the displeasure to run into. Recently various hardening options in the definitions of setjmp now make it detect these sorts of attempts and abort the program. So I wouldn’t call all of that very portable, but I agree it is fun!
- deleted 4y ago[deleted]
- vlovich123 4y ago> Even this architecture may change optimal alignment requirements in future, which glibc would transparently handle for you. Funny. “Optimality” is not a criticism I’ve ever heard levied often about glibc.
- rwmj 4y agoIf you think glibc pthreads are not optimal then you could supply a patch and transparently fix literally thousands of applications and tens of millions of users. Otherwise I trust the glibc developers I work with who are extremely smart and focused on performance.
- vlovich123 4y agoI highly recommend you take a look at https://webkit.org/blog/6161/locking-in-webkit/ https://webkit.org/blog/6161/locking-in-webkit/ then. It’s been known for a long time that pthread mutexes are very expensive and terrible. Unfortunately you can’t fix it AND maintain back compat (and you might lose technical compliance with POSIX). But that doesn’t mean that there aren’t drastically better locking primitives and it’s the epitomy of hubirus to think glibc is a great codebase. It’s just widely used and most people really don’t want to dive into writing their own libc. Literally every high performance application I’ve seen uses their own locking primitives, DNS alternatives etc. The only exception is when the locking isn’t a critical bottleneck. Examples of the same concept in different contexts: WTF lock Folly locks ABSEIL locks Rust mutexes I have more interesting projects to work on than glibc maintenance.
- rwmj 4y agoSounds like a problem with POSIX rather than glibc. Nothing about glibc has stopped Webkit from using their own locking implementation alongside, or it could be supplied as a library (libraries after all provide a vast array of Linux features which operate above or alongside libc). You could even work through the POSIX committee to get lighter weight locks approved and then have it added to glibc and then work to modify existing programs to use them. So I don't see how this relates to the supposed inefficiency of glibc's pthreads implementation.
- iudqnolq 4y agoThe recently published book Rust Atomics and Locks by Mara Bos has an interesting section on why Rust uses the futex syscall directly. > ... However, there are a few issues with that, as this pthread type was designed for C, not for Rust. > In Rust, we move objects around all the time. ... The pthread types we discussed do not guarantee they are movable, which becomes quite a problem in Rust. Even a simple idiomatic Mutex::new() function is a problem: it would return a mutex object, which would move it into a new place in memory. > A solution to this problem is to wrap the mutex in a Box. By putting the pthread mutex in its own allocation, it stays in the same location in memory, even if its owner is moved around. This is how std::sync::Mutex was implemented on all Unix platforms before Rust 1.62. > The downside of this approach is the overhead: every mutex now gets its own allocation, adding significant overhead to creating, destroying, and using the mutex. Another downside is that it prevents the new function from being const, which gets in the way of having a static mutex. Even if pthread_mutex_t was movable, a const fn new could only initialize it with default settings, which results in undefined behavior when locking recursively. There is no way to design a safe interface that prevents locking recursively, so this means we’d need to make the lock function unsafe to make the user promise they won’t do that. > A problem that remains with our Box approach occurs when dropping a locked mutex. ... pthread specifies that calling pthread_mutex_destroy() on a locked mutex is not guaranteed to work and might result in undefined behavior. One work-around is to first attempt to lock (and unlock) the pthread mutex when dropping our Mutex, and panic (or leak the Box) when it is already locked, but that adds even more overhead. These issues don’t just apply to pthread_mutex_t, but to the other types we discussed as well. Overall, the design of the pthread synchronization primitives is fine for C, but just not a great fit for Rust.
- kromych 4y agoHere is with support for aarch64 and more: https://github.com/kromych/tls-curious/blob/master/lib.c https://github.com/kromych/tls-curious/blob/master/lib.c
- rurban 4y agoBut using non-standard C case-ranges: https://stackoverflow.com/questions/7043788/are-triple-dots-inside-a-case-case-0-9-valid-c-language-switch-syntax https://stackoverflow.com/questions/7043788/are-triple-dots-... case '0' ... '9':
- kromych 4y agoStandards are much like religions imo: they promise to give the eternal life but the details are tbd. I chose to be practical.
- eqvinox 4y agoIf this weren't horribly mislabeled, it'd be a great behind-the-scenes article. > Practical Libc-free threading on Linux > This article will demonstrate a simple, practical, and robust approach to spawning and managing threads using only raw system calls. This is neither practical nor simple nor robust. It is a good write-up on what happens behind the scenes when you set up threads in your language of choice with your library of choice. Great if you have that understanding. Bring this into any kind of production code base (that isn't itself a threading library) and it's grounds for dismissal, because it makes very evident a severe lack of insight and judgement on what tools to use for a given problem at hand.
- bheadmaster 4y agoIf you're writing applications exclusively for Linux, what's the practical difference between using libc and system calls? Linux system calls are one of the most stable interfaces out there, maybe even more so than glibc. "WE DO NOT BREAK USERSPACE" and all that jazz.
- j16sdiz 4y agoCustom threads does not mix well with libc. Unless you can avoid the whole libc ecosystem, this won't end well
- gpderetta 4y agoIt is hard to interface with other libraries if you avoid libc, unless you reimplement the whole libc ABI (thread locals for example). Still, this stuff is very cool and I would love to have time to play with it.
- tmtvl 4y agoInterfacing with other libraries is fine as long as those other libraries also avoid libc... though of course then you get the whole problem of everyone reinventing the various wheels libc offers. Hypothetically that would then lead to various libraries implementing all the basics springing up from the primordial soup of code and the community eventually deciding on some libraries that are "good enough" which become the de facto new standard.
- jart 4y agoExplain how to do locks without using the lock instruction!
- jnordwick 4y agodoes xchg count
- jart 4y agoThat's an implicit lock.
- monocasa 4y agofutex will do lock instructions for you in kernel space. It's non optimal to just jump to kernel space every time, but it should still at least be correct.
- jart 4y agoA system call has even more overhead than the lock prefix!
- monocasa 4y agoTrue, but arguably this article constrains itself to the kernel-user boundary, where atomic instructions could be considered out of scope considering how the Linux syscall ABI is constructed.
- jart 4y agohttps://www.reddit.com/r/C_Programming/comments/120momf/a_lockfree_single_element_generic_queue/ https://www.reddit.com/r/C_Programming/comments/120momf/a_lo...
- sylware 4y agoThe manic abuse by the glibc devs of the gnu symbol/module versioning makes linux ABI stability extremely attractive (actually even ISO work has a smell of planned obsolescence nowdays). This is a very good idea... since I decided to do exactly that for many of my personal projects and I started years ago. That said, "clone" syscall is the easy part. For newcomer, a gentle introduction and explanation of the volontary cpu yielding "futex" synchronization primitive is kind of much more important, or we'll end up with spinlocks everywhere. Often, you need 2 "futexes" to build higher level synchronization primitives. One of the good things here is educational: it shows how much expensive a cpu yielding synchronization primitive is since usually it is "hidden" by the POSIX API.