1
0
mirror of git://projects.qi-hardware.com/iris.git synced 2024-11-17 01:24:39 +02:00
iris/report/kernel.tex
Bas Wijnen 12637f5695 more
2009-05-22 22:48:49 +02:00

219 lines
11 KiB
TeX

\documentclass{shevek}
\begin{document}
\title{Overview of my kernel}
\author{Bas Wijnen}
\date{\today}
\maketitle
\begin{abstract}
This document briefly describes the inner workings of my kernel, including the
reasons for the choices that were made. It is meant to be understandable (with
effort) for people who know nothing of operating systems. On the other hand,
it should also be readable for people who know about computer architecture, but
want to know about this kernel.
\end{abstract}
\tableofcontents
\section{Operating systems}
This section describes what the purpose of an operating system is, and defines
what I call an ``operating system''\footnote{Different people use very
different definitions, so this is not as trivial as it sounds.}. It also goes
into some detail about microkernels and capabilities. If you already know, you
can safely skip this section. It contains no information about my kernel.
\subsection{The goal of an operating system}
In the 1980s, a computer could only run one program at a time. When the
program had finished, the next one could be started. This follows the
processor itself: it runs a program, from the beginning until the end, and
can't run more than one program simultaneously\footnote{Multi-core processors
technically can run multiple programs simultaneously, but I'm not talking about
those here.}. In those days, an \textit{operating system} was the program that
allowed other programs to be started. The best known operating systems were
called \textit{Disk operating system}, or \textit{DOS} (of which there were
several).
At some point, there was a need for programs that would ``help'' other programs
in some way. For example, they could provide a calculator which would pop up
when the user pressed a certain key combination. Such programs were called
\textit{terminate and stay resident} programs, or TSRs. This name came from
the fact that they terminated, in the sense that they would allow the next
program to be run, but they would stay resident and do their job in the
background.
At some point, people wanted to de \textit{multitasking}. That is, multiple
``real'' programs should run concurrently, not just some helpers. The easiest
way to implement this is with \textit{cooperative multitasking}. Every program
returns control to the system every now and then. The system switches between
all the running programs. The result is that every program runs for a short
time, several times per second. For the user, this looks like the programs are
all running simultaneously, while in reality it is similar to a chess master
playing simultaneously on many boards: he really plays on one board at a time,
but switches a lot. On such a system, the \textit{kernel} is the program that
chooses which program to run next. The \textit{operating system} is the kernel
plus some support programs which allow the user to control the system.
On a system where multiple programs all think they ``own'' the computer, there
is another problem: if more than one program tries to access the same device,
it is very likely that at least one of them, and probably both, will fail. For
this reason, \textit{device drivers} on a multitasking system must not only
allow the device to be controlled, but they must also make sure that concurrent
access doesn't fail. The simplest way to achieve this is simply to disallow
it (let all operations fail that don't come from the first program using the
driver). A better way, if the device can handle it, is to somehow make sure
that both work.
There is one problem with cooperative multitasking: when one program crashes,
or for some other reason doesn't return control to the system, the other
programs stop running as well. The solution to this is \textit{preemptive
multitasking}. This means that every program is interrupted every now and
then, without asking for it, and the system switches to a different program.
This makes the kernel slightly more complex, because it must take care to store
every aspect of the running programs. After all, the program doesn't expect to
be interrupted, so it can't expect its state to change either. This shouldn't
be a problem though. It's just something to remember when writing the kernel.
Concluding, every modern desktop kernel uses preemptive multitasking. This
requires a timer interrupt. The operating system consists of this kernel, plus
the support programs that allow the user to control the system.
\subsection{Microkernel}
Most modern kernels are so-called \textit{monolithic} kernels: they include
most of the operating system. In particular, they include the device drivers.
This is useful, because the device drivers need special attention anyway, and
they are very kernel-specific. Modern processors allow the kernel to protect
access to the hardware, so that programs can't interfere with each other. A
device driver which doesn't properly ask the kernel will simply not be allowed
to control the device.
However, adding device drivers and everything that comes with them
(filesystems, for example) to the kernel makes it a very large program.
Furthermore, it makes it an ever-changing program: as new devices are built,
new drivers must be added. Such a program can never become stable and
bug-free.
Conceptually much nicer is the microkernel. It includes the minimum that is
needed for a kernel, and nothing more. It does include task switching and some
mehtod for tasks to communicate with each other. It also ``handles'' hardware
interrupts, but all it really does is passing them to the device driver, which
is mostly a normal program. Some microkernels don't do memory manangement
(deciding which programs get how much and which memory), while others do.
The drawback of a microkernel is that it requires much more communication
between tasks. Where a monolithic kernel can serve a driver request from a
task directly, a microkernel must pass it to a device driver. Usually there
will be an answer, which must be passed back to the task. This means more task
switches. This doesn't need to be a big problem, if task switching is
optimized: because of the simpler structure of the microkernel, it can be much
faster at this than a monolithic kernel. And even if the end result is
slightly slower, in my opinion the stability is still enough reason to prefer a
microkernel over a monolitic one.
Summarizing, a microkernel needs to do task switching and inter-process
communication. Because mapping memory into an address space is closely related
to task switching, it is possible to include memory management as well. The
kernel must accept hardware interrupts, but doesn't handle them (except the
timer interrupt).
\subsection{Capabilities}
Above I explained that the kernel must allow processes to communicate. Many
systems allow communication through the filesystem: one process writes to a
file, and an other process reads from it. This implies that any process can
communicate with any other process, if they only have a place to write in the
filesystem, where the other can read.
This is a problem because of security. If a process cannot communicate with
any part of the system, except the parts that it really needs to perform its
operation, it cannot leak or damage the other parts of the system either. The
reason that this is relevant is not that users will run programs that try to
ruin their system (although this may happen as well), but that programs may
break and damage random parts of the system, or be taken over by crackers. If
the broken or malicious process has fewer rights, it will also do less damage
to the system.
This leads to the goal of giving each process as little rights as possible.
For this, it is best to have rights in a very fine-grained way. Every
operation of a driver (be it a hardware device driver, or just a shared program
such as a file system) should have its own key, which can be given out without
giving keys to the entire driver (or even multiple drivers). Such a key is
called a capability.
Some operations are performed directly on the kernel itself. For those, the
kernel can provide its own capabilities. Processes can create their own
objects which can receive capability calls, and capabilities for those can be
generated by them. Processes can copy capabilities to other processes, if they
have a channel to send them (using an existing capability). This way, any
operation of the process with the external world goes through a capability, and
only one system call is needed, namely \textit{invoke}.
This has a very nice side-effect, namely that it becomes very easy to tap
communication of a task you control. This means that a user can redirect
certain requests from programs which don't do exactly what is desired to do
nicer things. For example, a program can be prevented from opening pop-up
windows. In other words, it puts control of the computer from the programmer
into the hands of the user (as far as allowed by the system administrator).
This is a very good thing.
\section{Kernel objects}
This section describes all the kernel objects, and the operations that can be
performed on them.
\subsection{Memory}
A memory object is a container for storing things. All objects live inside a
memory object. A memory object can contain other memory objects, capabilities,
receivers, threads and pages.
A memory object is also an address space. Pages can be mapped (and unmapped).
Any Thread in a memory object uses this address space while it is running.
Every memory object has a limit. When this limit is reached, no more pages can
be allocated for it (including pages which it uses to store other objects).
Using a new page in a memory object implies using it in all ancestor memory
objects. This means that setting a limit which is higher than the parent's
limit means that the parent's limit applies anyway.
Operations on memory objects:
\begin{itemize}
\item
\end{itemize}
\subsection{Page}
A page can be used to store user data. It can be mapped into an address space (a memory object). Threads can then use the data directly.
A page has no operations of itself; mapping a page is achieved using an
operation on a memory object.
\subsection{Receiver}
A receiver object is used for inter-process communication. Capabilities can be
created from it. When those are invoked, the receiver can be used to retrieve
the message.
Operations on receiver objects:
\begin{itemize}
\item
\end{itemize}
\subsection{Capability}
A capability object can be invoked to send a message to a receiver or the
kernel. The owner cannot see from the capability where it points. This is
important, because the user must be able to substitute the capability for a
different one, without the program noticing.
Operations or capability objects:
\begin{itemize}
\item
\end{itemize}
\subsection{Thread}
Thread objects hold the information about the current state of a thread. This
state is used to continue running the thread. The address space is used to map
the memory for the thread. Different threads in the same address space have
the same memory mapping. All threads in one address space (often just one)
together are called a process.
Operations on thread objects:
\begin{itemize}
\item
\end{itemize}
\end{document}