Richard Tobin said:
Again, true - but I'm very much in the "make it portable unless
you have a darn good reason not to" camp, and this case doesn't
strike me as having a darn good reason not to.
Indeed, and apparently this was cleaned up at some point anyway,
so the linux kernel development folks appear to agree with you
(Richard Heathfield I mean).
Okay, and I realise that nobody is about to rewrite the entire
kernel to my spec, but whoever designed mmap needs a good kicking.
It should return NULL on error.
Indeed -- although in this case, the "broken" interface is now
enshrined in a standard (not Standard C, but POSIX). It might help
to illustrate a bit how the situation came about, and how it could
have been avoided, though.
In Ye Aulde Daze, er, Days, "Unix" was "whatever Ken wrote for the
PDP-11". If it ran on the 11, that was good enough -- and if you
had more than 32K or so of RAM on your machine, you had a really
big machine, so conserving memory space, including code space, was
quite important.
At that time, which was also before C had <stdio.h> and "FILE"s --
maybe even before the syntax for "struct" used "{", and back when
"i += 2" was spelled "i =+ 2" -- there was one kind of "system
call", and it was not the system() function: it consisted of putting
a syscall number into one of the eight registers and trapping to
the Unix kernel, which would then pick out which "system operation"
you wanted, read your (user) space to obtain any necessary arguments,
and do whatever system-level privileged operation was requested,
or fail the request. To "fail" a request, the kernel would set
the carry flag in the PSW -- remember, the only machine that *exists*
(as far as we are concerned) is the PDP-11, and its carry flag is
in the PSW -- and return; to indicate success, the kernel would
clear the carry flag and return. The return value went into register
r0; on failure, this value was the error number to store in "errno".
So, now we take a short trip back in time, to the mid-1970s...
In C code, there is no way to test "the carry flag" directly. As
a result, the C-to-kernel interface is written in assembly. Because
code space is at a premium, and there is no need for portability
(your code only runs on the PDP-11), one short assembly stub could
handle pretty much every kernel call. It starts with a system-call
trap instruction, then a test of the carry flag. If the carry flag
is set, it stores r0 in the global variable "_errno" ("the" C
compiler -- there is only one of those, remember, as it the mid
1970s right now -- prepends an underscore) and sets r0 to -1. Then
in any case it returns to the caller.
(I glossed over a few details here. The actual syscall stubs were
four instructions long: constant into r0; sys; branch on carry;
return. The branch-on-carry went to the shared "store errno and
set r0 to -1" code. The principal still applies, though.)
Now, this is all well and good for the system calls that return an
"int", because successful return values are normally 0 or small
positive numbers. No successful int-valued system call returns
-1. The one call that does not quite fit is the "brk" ("set break")
system call, which returns a pointer -- but, luckily for us, we
are only using the PDP-11, and "-1" is the address of the very last
byte of memory, which is never a valid address for the "break".
So we need not come up with a special system-call handler for brk();
we can just use (char *)-1. (We are still in the 1970s and "void"
does not exist, much less "void *".)
Now we begin to return back to the present. Time accelerates: the
PDP-11 Unix kernel acquires new versions, being ported to other
models of PDP-11 (the various 40s and the split-I&D 11/70), then
to newer machines like the VAX. C, too, begins to be ported, to
machines like the Interdata 8/32, Honeywell mainframes, IBM
mainframes, and even "weird" 9-bit-byte, 36-bit-word machines.
Many things change, with C compilers growing new features like a
"long" datatype, multiple flavors of "unsigned" integers, and
standardized file I/O via <stdio.h> and company. But the "brk"
system call remains unchanged throughout. This is not a big deal,
as "normal user code" uses malloc() anyway, which hides the funny
return value from "normal users". (The malloc() implementations
on "non-Unixy" machines use various other system-specific mechanisms
to obtain memory, so portable code *never* calls brk(), which often
does not even exist.)
As we zip through the 1980s, UC Berkeley (and others) work with
DARPA on a grant for an "improved" Unix-like system that will
function well on the "ARPAnet". They decide that there should be
a file-to-memory-mapping interface similar to what was in Multics.
This is called "mmap", and an initial interface is hammered out.
For some reason, no one on the steering committee notices that
mmap() probably should return NULL on failure; instead, they let
the old code-space-constrained model, i.e., the one that had "brk"
return (char *)-1, persist. And when 4.2BSD is released, even
though mmap() is not actually supported, the documentation for it
claims that it returns (char *)-1 on failure.
Meanwhile, ANSI committee X3J11 works on a standard for C. They
add "void *" and change malloc()'s return type. The Unix brk()
call is not part of the proposed standard, nor is mmap(), which is
just as well since no one actually has an mmap() that works yet.
Sun Microsystems releases SunOS, and at some point, SunOS implements
mmap(), sticking with the "wrong" failure-return-value. 4.3-net-n-BSD
adopts (and adapts) the CMU Mach VM system, and also implements
mmap(), in a way that differs from SunOS, but also still has the
"wrong" failure-return-value.
As we fly through the 1990s, POSIX picks up the effort, and
standardizes mmap(), with some attempt to resolve the various
differences between implementations. Since they all return
(char *)-1 on failure, however, POSIX -- which picks up ANSI C as
a base item -- has mmap() return (void *)-1 on failure.
If, at any point along this path, someone had said: "hang on a
moment, returning -1 is silly, mmap() should return NULL on failure",
we would have had to do one extra thing: modify the mmap() system
call stub, to stop sharing its error-path with that for "int"-valued
system-call stubs. Since code space was no longer at such a great
premium -- systems with 32K of RAM were no longer the norm; indeed,
many machines came with as much as 16 entire *mega*bytes of RAM by
1990 or so -- throwing a dozen bytes at this "special" syscall
error path would not have been a big deal. (We could even have
fixed the brk() return value at the same time, although that would
have entailed changing existing code that tests the return value.
Since mmap() had not yet been implemented, fixing it then would
have been cheaper. Fixing it in POSIX would have been a bit more
expensive, but we already had the problem of resolving incompatible
mmap() implementations anyway.)
But it never happened, probably because it never rose above the
noise level. Instead, a trick for saving space on machines with
32K of RAM is now firmly embedded in the POSIX standard, used on
machines that routinely come with a minimum of over 100 thousand
times as much memory.