Why is the RTOS in a single file?
Usually, in large projects, the code is separated into small compilation units. Then, with a suitable Makefile, only changed portions need to be re-compiled and linked. On very large projects this practice saves much time.
Breaking the project into several files also embodies the principle of encapsulation. Variables and functions can be local to a file, and won't conflict with similarly named variables and functions in other files.
From a maintenance point of view, smaller files are easier to deal with. It is much easier to find items in an editor, and programmers can work on separate files simultaneously.
So why do we choose to have virtually all of our OS code in a single file?
To answer this, we need to compare our OS to other operating systems, and consider the extent and linkage of names of variables and functions.
Other Operating Systems
In an operating system such as Windows or Unix, the kernel operates in its own protected address and name space; a process (task) exists in a self contained executable file, which is compiled and linked independently of the operating system.
But in our OS, the kernel and application code are compiled and linked together. They share the same address space and symbol table. This will require special handling.
Linkage
If our OS was separated into more than one file, and these pieces need to communicate, then
they will likely need to share a variable between them. This is accomplished by defining the variable
to have external linkage. In C, this is accomplished by defining the variable in one of the files at the top level,
such as with int x;
, and declaring
the same variable in the other file with an extern
storage class specifier, as extern int x;
.
The linker will provide storage for x
only once, but will reference this same storage in both files.
To prevent a top-level variable or function name being exported to the linker, the name needs to be prefixed with
a static
storage class specifier, as the default linkage is extern
when none is specified.
Statically declared variables have extent throughout the file, but won't conflict with
variables in other files.
Single File
We want to provide the application writer who uses our OS with the flexibility to choose any names and storage
specifiers he wants for variables and functions, except for the system calls declared in os.h
. To avoid
conflicts, we could choose obscurity, and choose long names for our global names that the user is unlikely to
use himself. But that only reduces the probability of name collision. The only way C allows to achieve complete
separation is to declare all our private globals as static
. Necessarily, any function that references
these variables must be in the same file. Since the variable cur_task
is referenced by virtually all of our
functions, we must put almost all the functions in the file with its definition.
We apologize to any readers who find the code difficult to navigate. We tried to help by organizing the contents of the file systematically. All the variables are at the top, all the public functions are at the end, and helper functions are placed near to the function they help.
Get Naked
The "single-file" design decision even necessitated re-writing the context switching
code that was provided to us.
The provided file was written in assembly code, but the names of the functions in the file had to be
accessible to the rest of our kernel, which was written in C. In order to declare these functions as
static
, they had to be in the same file. Rather than write the kernel in assembler,
we wrote the context switching code in C.
Mostly, that involved liberal use of the
asm()
macro used by gcc to insert assembly statements into C programs. But there was another
problem to overcome which was caused with the way gcc translates function calls into assembly instructions.
By convention in gcc, general purpose registers are either caller-saved or callee-saved. That is, the responsibility for saving values in registers to make sure the values are not clobbered is divided. The reason for this is to prevent unnecessary saving. Most functions only use a few of the registers. It would be wasteful to spend time saving extra unused data.
In gcc, as in most compilers, any callee-saved register that is modified by the function will be saved at the start of the function by pushing its contents onto the stack. gcc calculates which registers are used and inserts instructions at the beginning of the function code to do the pushes. Similar instructions are inserted before the return statement to pop the stack and restore the register values.
But our context switching code relies on manipulating the stack. We can't allow an unknown number
of registers being pushed on top of the return address. We need to know exactly where that address
is in the stack. The solution to prevent this behavior is to use the function attribute
naked
in the function declaration, as in
static void enter_kernel(void) __attribute((naked));
This attribute directs the compiler to forego saving registers. As we save all the registers anyway
in these functions, this does not cause any problems. The other thing the naked
attribute
prevents is the automatic insertion of a return statement, so we have to call it explicitly.
See the context switching code in os.c
for details.