The EPOC operating system and its applications can be divided into various types of component, with different types of boundary between them:
The kernel manages the machine's hardware resources such as system RAM and hardware devices. It provides and controls the way all other software component can access these resources. The kernel uses hardware-supported privilege to gain access to the resources. That is, the CPU will only perform certain privileged instructions for the kernel. It runs other programs — so-called user-mode programs — without privilege, so that they can only access system resources through the kernel APIs. The boundary between the kernel and all other components is a privilege boundary.
An application is a program with a user interface. Each application runs in a separate process, with its own virtual address space, so the boundary between one application and another is a process boundary. One application cannot accidentally overwrite another's data, because their address spaces are entirely separate.
A server is a program without a user interface. A server manages one or more resources. It provides an API so that clients can gain access to its services. A server's clients may be applications, or other servers. Each server generally runs in its own process, so that the boundary between a server and its clients is a process boundary. This provides a strong assurance of server integrity.
Actually, for performance reasons, certain closely related servers may run in the same process: we'll cover this in more detail below.
The isolation between a server and its clients is of the same order as the isolation between a kernel and user-mode programs — but servers are much easier to program and work with. EPOC uses servers to provide many services that, on other systems, are provided by the kernel or device drivers.
An engine is the part of an application that manipulates its data, rather than the part that interacts directly with the user. Often, you can easily divide an application into an engine part and a GUI part. Exactly where you draw this line is part of the art of software engineering. Most built-in EPOC applications, and the larger applications I'll be developing in this book, have engines of sufficient complexity that it's worth drawing the boundary explicitly. An application engine may be a separate source module, a separate DLL, or even a number of separate DLLs. The boundary between engine and application is a module or DLL boundary, whose main purpose is to promote good software design — in contrast to a process or privilege boundary, whose main purpose is to prevent unwanted interactions.
That gives us four component types, and three boundary types. DLL or module boundaries are very cheap to cross: they promote system integrity by modularization and encapsulation. The privilege boundary is a little more expensive to cross: it promotes system integrity by hiding the kernel and devices from user-mode code. Process boundaries are most expensive of all to cross: they promote integrity by isolating programs' private RAM from each other.
If you're an application programmer, you'll spend most of your time writing applications and — if your application is big enough — engines.
I use the word 'system programming' to refer to the art of writing server or kernel software. I'll be covering servers in this book, because they're important for communications programming. I'll give an overview of the kernel and what it does in this chapter, but I won't otherwise be covering how to program the kernel side of the privilege boundary.
The process is a fundamental unit of protection in EPOC. Each process has its own address space. The virtual addresses used by programs executing in that process are translated into physical addresses in the machine's ROM and RAM. The translation is managed by a memory management unit or MMU, so that read-only memory is shared, but the writable memory of one process is not accessible to the writable memory of another.
The thread is the fundamental unit of execution in EPOC. A process has one or more threads. Each thread executes independently of the others, but within the same address space. A thread can therefore change memory belonging to another thread in the same process — deliberately, or accidentally. Threads are not as well isolated from each other as processes.
Threads are preemptively scheduled by the EPOC kernel. The highest-priority thread that is eligible to run at a given time is run by the kernel. A thread that is ineligible is described as suspended. Threads may suspend to wait for events to happen and may resume when one of those events does happen. Whenever threads suspend or resume, the kernel checks which one is now the highest thread, and schedules it for execution. This can result in one thread being scheduled even while another one is running. The consequent interruption of the running thread by the higher priority thread is called preemption, and the possibility of preemption gives rise to the term preemptive multitasking.
The process of switching execution between one thread and another is context switching. Like any other system, EPOC's scheduler is written carefully to minimize the overheads involved with context switching.
Nevertheless, context switching is much more expensive than, say, a function call. The most expensive type of context switch is between a thread in one process and a thread in another, because a process switch also involves many changes to the MMU settings, and various hardware caches must be flushed. It's much cheaper to context switch between two threads in the same process.
Typically, each EPOC application uses its own process and just one thread. Each server also uses its own process and just one thread. But an application (or a server) is really a thread rather than a process. The decision to use a separate process for each application and server is merely one of convenience. In cases where servers are designed to cooperate closely together, they are sometimes packaged into a single process, so that context switching between them is cheaper. Thus all the major communications-related servers in EPOC — serial, sockets, and telephony — run in the same process.
How can an application or server run effectively in a single thread? Don't sophisticated applications need to perform background tasks? And shouldn't a server have a single thread for each client? EPOC implements sophisticated applications and servers using only a single thread, because it has a good event-handling system, based on active objects. I'll return to that, in the Event Handling section, below.
As far as the CPU is concerned, a C++ program is just a series of instructions. But if we want to manage software effectively, we have to group code in more convenient packages. The packages EPOC uses are closely based on those used by Windows NT and similar systems. They are:
q A .exe, a program with a single main entry point E32Main(). When the system launches a new .exe, it first creates a new process and a main thread. The entry point is then called in the context of that thread.
q A dynamic link library or DLL, a library of program code with potentially many entry points. The system loads a DLL into the context of an existing thread (and therefore an existing process).
Both of these are executables. I'll use 'executable' when I mean either a .exe or a DLL. I'll never use just 'executable' if I mean a .exe specifically — I'll use '.exe'.
There are two important types of DLL:
q A shared library DLL provides a fixed API that can be used by one or more programs. Most shared library DLLs have the extension .dll. Executables are marked with the shared libraries they require and, when the system loads the executable at runtime, the required shared libraries are loaded automatically. This happens recursively, so any shared libraries needed by the shared libraries are also loaded, until everything required by the executable is ready.
q A polymorphic DLL implements an abstract API such as a printer driver, sockets protocol, or an EIKON application. Such DLLs typically use an extension other than .dll — .prn, .prt, or .app, for instance. In EPOC, polymorphic DLLs usually have a single entry point, which allocates and constructs a derived class of some base class associated with the DLL. Polymorphic DLLs are usually loaded explicitly by the program that requires them.
To be executed, an executable has to be loaded. This means that its program and data areas must be prepared for use. There are two cases here:
q The first case is an executable in ROM (drive z:). ROM-based executables are executed in-place. Loading is trivial for ROM-based executables.
q Executables not in ROM must first be loaded into RAM. This applies to executables on CF card removable media (drive d:), or in the system RAM disk (drive c:). This kind of loading involves more processing for EPOC.
EPOC optimizes the formats used for DLLs in order to make them as compact as possible in ROM and RAM.
q Most systems supporting DLLs or analogous concepts offer two options for identifying the entry points in them. You can refer to the entry points either by name, or by ordinal number. Names are potentially long, and wasteful of ROM and RAM. So EPOC uses link-by-ordinal exclusively.
q Loading into RAM can involve locating the executable at an address that cannot be determined until load time: this means that relocation information has to be included in the executable format. Loading into ROM happens effectively at build time. So EPOC's ROM-building tools perform the relocation and strip the DLLs of their relocation information to make them smaller still.
EPOC's link-by-ordinal scheme affects the disciplines used for binary compatibility (a future release of a DLL must use exactly the same ordinals as the previous release). The pre-loading scheme means among other things that you can't take an executable out of the ROM and deliver it in another package for RAM loading. These are largely matters for EPOC OEMs, and I shan't be describing them further in this book.
Executables contain three types of binary data:
q Program code
q Read-only static data
q Read/write static data
EPOC handles .exes and DLLs differently.
.exes are not shared. If a .exe is loaded into RAM, it has its own areas for code, read-only data, and read/write data. If a second version of the same .exe is launched, new areas will be allocated for each of these. There is a small optimization: ROM-based .exes allocate a RAM area only for read/write data — the program code and read-only data are read directly from ROM.
DLLs are shared. When a DLL is first loaded into RAM, it is relocated to a particular address. When a second thread requires the same DLL, it doesn't have to load it — it merely attaches the copy already there. The DLL appears at the same address in all threads that use it. EPOC maintains reference counts, so that the DLL is only unloaded when no more threads are attached to it. ROM-based DLLs, like ROM-based .exes, are not actually loaded at all — they are simply used in-place in ROM.
Most servers use their own .exe to generate their own process. For instance, ewsrv.exe is the window server, and efsrv.exe is the file server.
As we saw earlier, some servers piggyback into the process of others, to minimize context-switching overheads. The main server in such a group uses its own process — for instance, c32exe.exe launches the serial communications server. Other servers use a DLL and launch their own thread within the main server thread.
A console application, such as hellotext.exe, is built into its own .exe. A console application must create its own console, which it can then use to interact with the user.
Most GUI applications are EIKON applications like helloeik.app. EIKON applications are actually polymorphic DLLs whose main entry point, NewApplication(), creates and returns a CEikApplication-derived object. The application process is created by a small .exe, apprun.exe, to which the .app name is passed as a parameter. If the application wants to edit an embedded document, it can do so without creating a new process by loading the .app for the embedded document directly, in the same thread.
Other applications use similar techniques. Java applets are all run under a single shared JVM, owned by each web browser or AppletViewer application instance. Java applications are each launched under their own java.exe process.