2022-06-13
In general Unix system calls are not cancellable, just abortable
One of the common wishes in environments and languages that support concurrency is for (Unix) system calls to be cancellable in the way that other operations often are. Unfortunately this is not practical, which is part of why a lot of such environments don't try to support it (Go is famously one of them, which makes people unhappy since it does have a 'context' package that can cancel other things).
All or almost all Unix system calls can be aborted, which is to
say that you can interrupt them before they complete and force
control to return to the program. However, when you abort a system
call this way the effects of the system call may be either incomplete
or indeterminate, leaving you with either broken state or unusable
state (or at least a peculiar state that you have to sort out). For
example, if a close()
is aborted, the state of the file descriptor
involved is explicitly unknown. Only some Unix system
calls can be cancelled, which is to say stopped with things in
some orderly and known state. Often these system calls are the least
interesting ones because all they do is inquire about the state of
things, such as what file descriptors are ready or whether you have
dead or stopped children.
Some interesting system calls can be cancelled under some but not all situations using special mechanisms that may have side effects. You may be able to relatively cleanly cancel certain network IO by setting the file descriptor to non-blocking, for example, but this will probably have done some IO and might affect other threads if they immediately try to do IO on the file descriptor before you can set it back to blocking.
Some languages and environments actually turn certain normally
synchronous 'system call' operations into asynchronous actions that
can be cancelled (to some degree). For example, Go's runtime and
network IO subsystem cooperate to turn what looks like blocking
read()
or write()
operations into non-blocking ones combined
with waiting for network sockets to be ready (along with other
things, such as periodic timer ticks). This allows these operations
to be cancellable (although Go doesn't expose a clean way to do
it). But this can only be applied to certain sorts of read()
s and
write()
s; very few Unixes support cleanly cancelling filesystem
IO, for example.
The distinction I'm drawing between cancelling a system call and aborting it may seem picky, but I think it's an important one. If you think of it as aborting a system call, it means means that you have to carefully consider and deal with partially done operations and potentially uncertain state. If you could cleanly cancel system calls without worrying about all of that, life would be much nicer. But you can't.
(This was sort of sparked by some comments here.)