> platform-neutral language functions like "open/read/write/close/ioctl/dup/dup2"
These are all syscall wrappers. They are present in libc but they are going to be very boring stubs that merely call into the kernel. (Also I don't think you can call them platform neutral or language functions as they are POSIX rather than being from the C standard...)
The more "implementation-heavy" parts of a libc... Things like stdio, string calls, malloc, ...
pthread cancellation was a terrible idea when it was added, and it's even worse today. It's an API that provides no actual value (you can implement it yourself via signals), is a giant minefield if you care about not leaking or deadlocking, and infects the entirety of libc with pointless checks for functionality that no one in their right mind should use.
The problem with pthread cancellation is that it is fundamentally broken when used on a thread that can ever acquire resources (opening an fd, taking a mutex, mallocing a buffer, etc.). If it were just "call pthread_testcancel to figure out if you were cancelled", that would be fine, but no, you either get blown up immediately, or you get blown up at arbitrary function call boundaries (that don't really make any sense; e.g. why is strerror_r allowed to be a cancellation point?)
It's easy to correctly implement the parts that aren't just adding a call to pthread_testcancel to every single syscall wrapper, just reserve an RT signal and do your thread teardown when you receive it, using pthread_sigmask to implement enable/disable. It's just that it's just a terrible idea.
strerror[_r]() is allowed to be cancellation point, because it's implementation may involve calls to read() which is required to be cancellation point.
No, it's very common for them to be written in primarily C. But I'm obligated to mention (as a member of the Rust Evangelism Strike Force) that you don't have to write them in C. ;)
Many implementations do have performance-critical parts be target-dependent and implemented in assembly. IIRC glibc has more of this than musl/newlib/bionic, but it's been a while since I last looked.
The root of the issue is that, while you can write most of the code in C (but not all), it has to be different for different operating system kernels. It will use different syscall numbers, argument size and order and "extra options" will be different, some kernels will have a more general syscall with options that enable it to be used for a handful of similar standard libc wrapper functions ...
On macOS, you're really supposed to always dynamically link to the libSystem.dylib libc implementation, because the macOS kernel has subtle backwards-incompatible changes in the raw syscall interfaces on every macOS release (maybe even in point-release updates, I'm not sure). Go tried to have its own internal static libc replacement for macOS like it does for Linux, but just a year or two ago Go gave that up, and now dynamically links the macOS libc.
Somebody has to write the library to begin with. The typical Unix thing is for the OS to have one everybody links to. Which means, among other things, variation between libcs is a factor in portability of C programs from one OS to another.
The C library provides implementations for the platform-neutral language functions like "open/read/write/close/ioctl/dup/dup2" and more.