One is that fork() is by definition a very costly operation (a copy of the entire address space of the current process), and the kernel has to do a lot of work to implement it efficiently (implementing copy-on-write clones of all of the pages of a process). And that all that work is done for nothing in the very very very common case of doing fork() + exec().
The other problem is that the semantics of fork() just fundamentally can't work properly for a multi-threaded process. In any multi-threaded process, if you do fork(), the only thing you can safely do in the child process is to call exec(). Any other call, even a printf() or some path logic, has a very good chance to lead to a deadlock, quite possibly inside malloc() itself.
So fork() as a standalone operatikn is actually an extremely niche utility (duplictaing single-threaded processes) that has been made the main way of spawning new processes. Similarly, exec() by itself is an even more niche utility, sometimes useful for "launcher" style processes.
So, instead of achieving an extremely common task (launch some binary file as a new process) using a dedicated system call, Unix has chosen to define two extremely niche syscalls that you should almost never use individually, but that together can implement this common process, but only with a lot of behind the scenes work to make it efficient.
The way the shell works is an essential part of Unix design, and the fork/exec pair suits it very well. In the forked child process before the exec, the shell can execute arbitrary code that inherits all file descriptors and manipulates/redirects them in a certain way.
I didn't say it's useless, I said it's a niche utility. There are, what, 10 even slightly commonly used shells?
Compared to the thousands of other programs that spawn processes, I think shells count as a niche use.
If fork() and exec() had been kept as the niche utilities they are in addition to a more commonly used spawn() syscall, fork() and exec() could have remained simple and small, and not needed all the CoW magic and many other complex features at all.
It is, but there are many other differences between Unix and Windows processes than just fork() VS CreateProcess().
Even disregarding this, the actual fork() syscall has had many hundreds of dev hours poured into it to implement the costly semantics (copy all resources) in an efficient way for the common use case (copy-on-write semantics).
The first is to implement POSIX shells, and that's less because this is a good design and more because shells are a wrapper around the original Unix system calls. Note that if you're designing a scripting language that isn't beholden to compatibility with /bin/sh (especially one that can be portable to OSes that don't have fork()!), then you're liable to not design it in such a way that requires you to use fork().
The second use case is an alternative to threads for parallel processing. And there are some reasons that processes can work better than threads for parallel processing. But fork() has such a bad interaction with multithreaded code [1] that you end up having to choose fork() xor threads. And as threading has become an increasingly important part of modern environments, well, given that xor choice, almost everybody is going to come down on the threads side of the equation.
The final use case, and by far the most common, is to be able to spawn a new process. This means you break up one logical system call (spawn) into two (fork + exec), the first of which semantically requires you to do a lot of work (clone memory state) that you're immediately going to throw away. Even in the case where you want to do more expansive process-twiddling magic before spawning the process, there are better designs (especially if you're willing to commit to a handle-based operating system).
Of the three use cases, one amounts to "backwards compatibility", and the other two amount to "fork() is actively fighting you". That is not the hallmark of a good API.
[1] Think things like "locks are held by threads that don't exist."
that's some 1000+ comments for light background reading about fork(). I think you can make some aggregate sentiment analysis from that and conclude that fork() is not great.
One is that fork() is by definition a very costly operation (a copy of the entire address space of the current process), and the kernel has to do a lot of work to implement it efficiently (implementing copy-on-write clones of all of the pages of a process). And that all that work is done for nothing in the very very very common case of doing fork() + exec().
The other problem is that the semantics of fork() just fundamentally can't work properly for a multi-threaded process. In any multi-threaded process, if you do fork(), the only thing you can safely do in the child process is to call exec(). Any other call, even a printf() or some path logic, has a very good chance to lead to a deadlock, quite possibly inside malloc() itself.
So fork() as a standalone operatikn is actually an extremely niche utility (duplictaing single-threaded processes) that has been made the main way of spawning new processes. Similarly, exec() by itself is an even more niche utility, sometimes useful for "launcher" style processes.
So, instead of achieving an extremely common task (launch some binary file as a new process) using a dedicated system call, Unix has chosen to define two extremely niche syscalls that you should almost never use individually, but that together can implement this common process, but only with a lot of behind the scenes work to make it efficient.