% Iris: micro-kernel for a capability-based operating system. % kernel.tex: Description of Iris. % Copyright 2009 Bas Wijnen % % This program is free software: you can redistribute it and/or modify % it under the terms of the GNU General Public License as published by % the Free Software Foundation, either version 3 of the License, or % (at your option) any later version. % % This program is distributed in the hope that it will be useful, % but WITHOUT ANY WARRANTY; without even the implied warranty of % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the % GNU General Public License for more details. % % You should have received a copy of the GNU General Public License % along with this program. If not, see . \documentclass{shevek} \begin{document} \title{Overview of Iris} \author{Bas Wijnen} \date{\today} \maketitle \begin{abstract} This document briefly describes the inner workings of my kernel, Iris, 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. It is probably better suited for the latter category. \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 Iris. \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\footnote{Crackers are better known by the public as ``hackers''. However, I use this word to describe people who like to play with software (or sometimes also with other things). Therefore the malicious people who use hacking skills for evil need a different name.}. 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. For example, a capability can allow the holder to access a single file, or to use one specific network connection, or to see what keys are typed by the user. 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: \textit{invoke}. This has a very nice side-effect, which is 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 kernel objects of Iris, and the operations that can be performed on them. One operation is possible on any kernel object (except a message and reply and call Capabilities). This operation is \textit{degrade}. It creates a copy of the capability with some rights removed. This can be useful when giving away a capability. \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, Pages and Cappages. 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 Create a new item of type Receiver, Memory, Thread, Page, or Cappage. \item Destroy an item of any type, which is owned by the Memory. \item List items owned by the Memory, Pages mapped in it, and messages in owned Receiver's queues. \item Map a Page at an address. \item Get the Page which is mapped at a certain address. \item Get and set the limit, which is checked when allocating pages for this Memory or any sub-structure. \item Drop a capability. This can only be done by Threads owned by the Memory, because only they can present capabilities owned by it.\footnote{Iris checks if presented capabilities are owned by the Thread's Memory. If they aren't, no capability is passed instead. The destroy operation destroys an object that a capability points to. Drop destroys the capability itself. If a Thread from an other Memory would try to drop a capability, Iris would refuse to send it in the message, or it would not be dropped because it would be owned by a different Memory.} \end{itemize} \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 Set the owner. This is the Thread that messages will be sent to when they arrive. Messages are stored in the receiver until the owner is ready to accept them. If it is waiting while the message arrives, it is immediately delivered. \item Create a capability. The new capability should be given to Threads who need to send a message to the receiver. \item Create a call capability. This is an optimization. Because \textit{calls} happen a lot, where a capability is created, sent in a message, then a reply is sent over this new capability, and then it is dropped. This can be done using a call capability. The call capability is invoked instead of the target, and the target is specified where the reply capability should be. The message is sent to the call capability (which is handled by the Receiver in the kernel). It creates a new reply capability and sends the message to the target with it. When the reply capability is invoked, the message is sent to the owner, and the capability is dropped. This approach reduces the number of kernel calls from four (create, call, reply, drop) to two (call, reply). \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 there is only one) together are called a process. Because all threads have a capability to their own Thread object (for claiming Receivers), this is also used to make some calls which don't actually need an object. The reason that these are not operations on some fake object which every process implicitly owns, is that for debugging it may be useful to see every action of a process. In that case, all its capabilities must be pointing to the watcher, which will send them through to the actual target (or not). With such an implicit capability, it would be impossible to intercept these calls. Operations on Thread objects: \begin{itemize} \item Get information about the thread. Details of this are architecture-specific. Standard ways are defined for getting and setting some flags (whether the process is running or waiting for a message, setting these flags is a way to control this for other Threads), the program counter and the stack pointer. This call is also used to get the contents of processor registers and possibly other information which is different per Thread. \item Let Iris schedule the next process. This is not thread-specific. \item Get the top Memory object. This is not thread-specific. Most Threads are not allowed to perform this operation. It is given to the initial Threads. They can pass it on to Threads that need it (if any). \item In the same category, register a Receiver for an interrupt. Upon registration, the interrupt is enabled. When the interrupt arrives, the registered Receiver gets a message from Iris and the interrupt is disabled again. After the Thread has handled the interrupt, it must reregister it in order to enable it again. \item Allocate a range of contiguous physical memory. This is only relevant for device drivers whose device will directly access the storage, such as the display driver. The result of this call is that the memory is counted as used by the Thread, and it is reserved, but it is not returned. Instead, the address of physical memory is returned, and the pages need to be retrieved with the next operation. This capability is not present in normally created threads. \item Allocate a page of physical memory. This is used in combination with the previous operation to reserve a block of physical memory, and by device drivers to map I/O memory into their address space. There is a flag indicating whether this memory should be freed (ranges) or not (I/O). Users of this operation are trusted to handle it properly; no checks are done to ensure that no kernel memory is leaked, or that the allocated memory isn't used by other threads or the kernel. Of course, this capability is not present in normally created threads. \item Get the physical address of a page. Only device drivers need to know the physical address of their pages, so this operation is not available on normal threads. \item And similarly, allow these priviledged operations (or some of them) in an other thread. This is a property of the caller, because the target thread normally doesn't have the permission to do this (otherwise the call would not be needed). The result of this operation is a new Thread capability with all specified rights set. Normally this is inserted in a priviledged process's address space during setup, before it is run (instead of the capability which is obtained during Thread creation). \end{itemize} \subsection{Page and Cappage} 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 Cappage is very similar, in that it is owned by the user. However, the user cannot see its contents directly. It contains a frame with Capabilities. They can be invoked like other owned capabilities. The main feature of a Cappage, however, is that they can be shared. It is a fast way to copy many capabilities to a different address space. Capabilities in a Cappage are not directly owned by the Memory, and thus cannot be dropped. Operations on Page and Cappage objects: \begin{itemize} \item Copy or move the frame to a different Page, which is usually in a different Memory. This way, large amounts of data can be copied between address spaces without needing to really copy it. \item Set or get flags, which contain information on whether the page is shared, is writable, has a frame allocated, and is paying for the frame. Not all flags can be set in all cases. \item Cappages can also set a capability in the frame (pointed to with an index). \end{itemize} \subsection{Capability} A capability object can be invoked to send a message to a receiver or to Iris itself. 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. In some cases, it is needed to say things about capabilities. For example, a Memory can list the Capabilities owned by it. In such a case, the list consists of Capabilities which point to other Capabilities. These capabilities can also be used to destroy the target capability (using an operation on the owning Memory object), for example. Operations or capability objects: \begin{itemize} \item Get a copy of the capability. \end{itemize} \section{Interface classes} Around Iris is a system of some programs to create the operating system. These include the device drivers. While Iris itself needs no specific interfaces from them, some interface classes are defined, which are used by the default environment. By defining classes, it is possible to let a program use any device of that type without needing changes to its code. These definitions are in the source. A copy of the information here would only lead to it getting outdated. \end{document}