Power management is probably the single most important factor that makes the difference between a desktop system and a portable system:
q Power has to be used efficiently. Battery life — even with rechargeable batteries — makes a difference to how the user thinks of the device. So also does battery weight. EPOC needs to work effectively on lower-speed, lower-power, hardware than that used by desktop PCs.
q The machine should turn off instantly, and turn back on instantly as required — in the same state as it was when turned off.
q Certain parts of the system should still be able to run while the system is apparently off. For example, when an alarm is due, the machine should be turned on so that the alarm can sound.
q System boot should be avoided. Cold boot destroys the c: disk. Warm boot attempts to recover the c: disk, but even a warm boot takes several tens of seconds, and destroys the state of applications.
q Even if all power is removed suddenly, the system should do what it can to save critical information, so that there is a possibility of a warm boot when power returns (rather than a cold boot).
As an application or server programmer, your task is easier than that of the kernel. But you still get involved in power management. You have to write your programs efficiently, to make best use of an EPOC machine's scarce resources — this applies as much to power as to available RAM, CPU speed etc.
You have to be aware that power can be switched on and off at any time. If your display is animated, or if it depends on the time of day, then you have to make sure you request a power-on notification so that you can update your display when power is resumed.
The deeper you delve into the system, the more complicated power management becomes. For instance, as a device driver programmer, you can see that power management is more complex than simply machine on/off. The user thinks the machine is off if the display is turned off. But each hardware component is responsible for its own power management. A communications link driver should turn the physical device off if it's not needed. The kernel scheduler even turns the CPU off if all threads are waiting for an event. Every possible step is taken to save power. I wish I could say the same about my laptop PC and Windows NT!
EPOC's most fundamental component is E32. E32 consists of the kernel, and user library. The kernel is entirely privileged. The user library, euser.dll, is the lowest-level user-mode code. It offers library functions to other user-mode code and controlled access to the kernel.
The kernel itself has two major components:
q The kernel executive runs privileged code in the context of a thread that usually executes in user mode. Executive code can therefore be preempted by higher-priority user-mode threads, or by the kernel server.
q The kernel server is the main thread of its own process and always runs privileged. The kernel server is the highest-priority thread in the system. It allocates and de-allocates kernel-side resources needed by the system and by user programs. It also performs functions on behalf of user-mode programs. The kernel server is a single thread: it handles user requests in sequence, non-preemptively.
We'll be describing euser.dll's most important facilities in detail over the next few chapters. For now let's note that it offers three types of function:
q Functions that execute entirely user-side, such as most functions in the array and descriptor classes (descriptors are EPOC's version of strings)
q Functions that require privilege, and so cross into the executive, such as checking the time or locale settings
q Functions that require the services of the kernel server: these go through the user library, via the executive, to the server
The functions that operate entirely on the user-side can also be used safely by any kernel-side code. Kernel-side code that needs access to kernel facilities can (and must) use these facilities directly, rather than through the user library interface.
In this book, we'll be writing user-side code exclusively. So although the distinction between the types of function in the user library helps you to understand the system design, it's not essential for you to know all the possible circumstances in which kernel-side code might be called.
System devices such as screen, keyboard, digitizer (for the pen), sound codec, status LEDs, power sensors, serial port, CF-card, etc. are all driven by low-level device drivers. It's possible to add devices and write drivers for them. EPOC OEMs usually does this: EPOC machines are not typically user-expandable in the same way that PCs are.
A device driver is implemented in several parts:
The kernel executive contains support for device drivers, so that a user program can issue a request to device driver code running kernel-side, in either the kernel executive or kernel server. Such requests typically initiate a device operation, or tell the driver that the requesting program is waiting for something to happen on the device.
Drivers also process device interrupts and then tell the user (or kernel) program that an earlier request is complete. Interrupt handling works at two levels.
q First-level handling is done by an interrupt service routine (ISR). ISRs must be short, and can't do very much, because they could occur at any time, even in the middle of a kernel server operation. Usually, they simply acknowledge the device that raised the interrupt and then set a flag to request the kernel to run a delayed function call (DFC) for second-level processing.
q The kernel schedules the DFC when it is in a more convenient state — immediately, if user-mode code was executing when the interrupt occurred; otherwise, when the kernel would have otherwise crossed the privilege boundary back to user code. DFCs can use most kernel APIs. DFCs typically do a small amount of processing and then post a user thread to indicate that an I/O request has completed.
If you're programming a device driver — or any part of the kernel — you need to be aware of the kernel environment, and of the interactions between the scheduler, interrupts, MMU etc. It's a specialist art, and beyond the scope of this book.
The user thread environment is much less restrictive, and much easier to work with. That's why EPOC does as much device-related programming as possible in user-mode servers.
The kernel supports a tick interrupt at 64Hz on ARM, and 10Hz on the PC-based emulator.
The tick interrupt is used to drive round robin scheduling of equal highest-priority threads. It can also be accessed (via User::After() and RTimer::After() function calls) by user programs. The tick interrupt suspends during power-off so that, if you request a timer to expire after 5 seconds, and then turn the machine off 2 seconds later, the timer event will occur 3 seconds after you turn the machine back on again — or even later, if you immediately turn the machine back off!
The kernel also supports a date/time clock, which you can access using User::At() and RTimer:At(). This timer expires at exactly the time requested. If the machine was turned off at the time, it is turned on. This is the kind of timer to use for alarms.
System memory is managed by the memory management unit (MMU).
ROM handling is easy. The ROM consists entirely of files, in a directory tree on drive z:, and is mapped to a fixed address, so that the data in every file can be accessed simply by reading it. Programs can be executed in place, and bitmaps and fonts can be used in place for on-screen blitting, without all the data going through the file server.
RAM management is more interesting. Physical RAM is divided into 4k pages by the MMU. Each physical page can be allocated to
q A user process's virtual address space: there may be many of these, as we have seen
q The kernel server process's virtual address space
q The RAM disk used as c:. Such RAM can only be accessed by the file server process
q DLLs loaded from a non-ROM filing system: RAM for DLLs is marked read-only after the DLL has been loaded. Each DLL appears at exactly the same virtual address for all threads that use it.
q Translation tables for the MMU: the MMU is carefully optimized to keep these small. But there is no practical limit on the number of processes and threads allowed in EPOC.
q The free list, of pages not yet allocated for any of the above purposes
There is no virtual memory, backed up by a swap file on a large hard disk. So any page needed for user processes, the kernel or the RAM disk is taken from the free list. When the free list runs out, the next request for memory will cause an out-of-memory error — or a disk full error, if the request came from a file write.
When a .exe is launched, it creates a new process with a single main thread. During the lifetime of a process, other threads may also be created.
The process's address space includes regions for
q System-wide memory, such as the system ROM and RAM-loaded shared DLLs
q Process-wide memory, such as the .exe image and its writable static data
q Memory for each thread, for a very small stack and a default heap (which can grow up to a limit set by the EPOC OEM: it's 2MB on a Psion Series 5MX)
A thread's stack cannot grow after the thread has been launched. The thread will be panicked — terminated abruptly — if it overflows its stack. The usual initial stack size is 12k. The stack is used for C++ automatic variables in each function. So you have to avoid using large automatics. Instead, put all large variables on the heap.
A thread's default heap is used for all allocations using C++ operator new, and user library functions such as User::Alloc(). If possible, memory is allocated from existing pages committed to the heap. If that's not possible, the heap manager requests additional pages from the system free list. If the system free list has insufficient pages, the allocation will fail, giving an out-of-memory error.
Each thread has its own default heap, which is used for allocation by C++ operator new and de-allocation by delete. Because each thread makes allocations on its own, non-shared, heap allocation and de-allocation is very efficient. If an allocation can be satisfied without growing the heap, only a few instructions are required, no privilege boundaries need be crossed, and no synchronization with allocations by other threads is needed.
You can put small objects on the stack, such as integers or rectangles,
TInt x;
TRect region;
but most objects — especially larger ones — should go on the heap:
CEikDialog* dialog=new CGameSettingsDialog;
Objects whose class name begins with C can only go on the heap. Objects whose class name begins with T, however, can be either members of other classes, or automatics on the stack. Don't put them on the stack unless they're quite small. Beware especially of TFileName:
TFileName fileName;
A filename is 256 characters — 512 bytes in EPOC's Unicode build. There isn't room for too many of them on a 12k stack.
You can control the stack size in a .exe. This can apply to console programs, servers, or programs with no GUI — but not to EIKON programs, since they are launched with apprun.exe. You can also control the stack size when you launch a thread explicitly from within your program. If you have an application with an algorithm which requires a large stack, such as a heavily recursive game-tree search, you may have to encapsulate the algorithm in a .exe of its own, or a separate thread.
Since each user heap eats into a scarce system resource — the free page list — and since applications and servers run for months or years without being restarted, it's vital that programs detect heap failure due to a lack of memory, and it's vital that programs release unneeded memory as soon as possible. This is the domain of EPOC's cleanup framework, which will cover in Chapter 6.
Threads have independent default heaps in the sense that each thread always allocates from its own heap. But since all heaps are in the same process's address space, each thread in a process can access objects on other heaps in that process — provided suitable synchronization methods are used.
In addition to the default heap, threads can have other heaps. But these introduce new complications, so you should use them only if you have to. For any non-default heap, you must provide a specific C++ operator new() to allocate objects onto it. For local shared heaps — shared with other threads in the same process — you have to introduce synchronization using mutexes or the like. For global shared heaps — shared with threads in other processes — the heap is mapped to a different address in each process, so you have to introduce a smart reference system rather than straightforward pointers. All these things are possible if necessary — but they're rarely necessary. Usually, it's better to use a server to manage shared resources, rather than a shared heap. I give an overview of servers below, and cover them more thoroughly, including performance optimization, in Chapter 21.
A thread's non-shared heaps are allocated into a 256MB region of a process's virtual address space. By limiting the maximum size to 2MB (as on the Psion Series 5MX), there is an implied maximum of 128 threads per process.
EPOC DLLs do not support writable static data.
DLLs only support read-only data, and program code. Writable static data is supported only by .exes.
This imposes some design disciplines on native EPOC code that, with object orientation, are actually very easy to live with. But it does make life more difficult when porting code, which often assumes the availability of writable static.
The easiest workaround is to use a .exe to contain the ported code. EPOC's spell check engine uses this technique. The .exe is packaged as an EPOC server, which allows it to be shared between multiple programs. By using a separate process, we also gain the benefit of isolation.
Here's why EPOC doesn't support writable static. Every DLL that supports writable static would require a separate chunk of RAM to be allocated, in every process that uses the DLL. There are about 100 DLLs in EPOC R5: perhaps the typical application uses 60 of them. Say I usually have about 20 applications running concurrently in my 12MB RAM machine, and there are about 10 system servers working on behalf of those applications. The smallest unit of physical memory allocation in conventional MMUs is 4k (and smaller wouldn't be at all sensible). If each DLL used even a single word of writable static, it would require 4k x (20 app processes + 10 server processes) x 60 DLLs each = 72MB of RAM just for the writable static!
I quite often have about 20 apps running on my PC too: 72MB on a PC isn't unacceptable — most of it is paged out to disk anyway — but for a handheld system this overhead, or anything approaching it, is out of the question.
You could argue that most DLLs wouldn't use writable static, so these figures are exaggerated. But EPOC's architects' response was that, if the facility were there, most people would use it, without even knowing that they were doing so. We would only find out at system integration time, and by then it would be too late to fix any problems. So writable static was not implemented by the EPOC loader.
EPOC R5 does provide a workaround for the writable static limitation, intended for system components only. Future releases of EPOC may further ease the restriction.
Even if the rules are loosened up, the underlying economics won't change: at least 4k of RAM will be consumed by each process that loads each DLL that requires writable static. Using writable static isn't environment-friendly. Don't do it without being aware of the consequences.
In fact EPOC associates a single machine word of writable static per thread with each DLL. This is thread-local storage or TLS. You can use the TLS word as an anchor for what would have been your writable static. There are no MMU granularities to worry about here — just a small performance implication, since getting the TLS pointer involves a system call which takes perhaps 20 or so instructions, rather than the single instruction required to get a normal pointer. Not all DLLs use TLS, but the system allocates the word anyway. In my scenario above, TLS would account for only 72k — which is perfectly acceptable.
Let's summarize what we've already seen about files.
EPOC machines have no hard disk, as found on PCs. But EPOC always has two disks present, and may have more.
c: |
RAM disk — full read/write file system. Contents are initialized to empty on a cold boot. Data is maintained as long as there is power to refresh the RAM. Data is recovered in a warm boot, unless it has been corrupted beyond recovery. Files can be extended indefinitely, so long as there are RAM pages to allocate to them from the system free list. RAM pages are subdivided into 512 byte sectors, so that small files are managed more efficiently. |
z: |
ROM — read-only file system. Contents are built by the EPOC OEM when building the device. Some machines, such as the Psion netBook or Series 5MX Pro, load a ROM image on cold boot, so that 'ROM' can be updated and replaced — by enterprise IS departments, distributors and so on. |
d: |
CF card — removable read/write media, supported by some EPOC machines. CF is Compact Flash, a non-volatile medium written using a higher-than-usual voltage 'flash'. Careful power management is used by EPOC's CF-card file system to ensure that 512 byte sector writes are atomic — they either complete fully, or don't even start. File formats such as those used by the persistent file store are written and extensively tested to assume, and support, atomic sector writing. These files can be recovered if failure occurs on any sector write. CF cards are slower than the RAM disk, but their non-volatility, and higher capacity — 20-200MB or so — makes them attractive. CF cards are an industry standard, slightly smaller than PC cards used on laptops. You can buy PC card to CF card adapters, to insert a CF card into a laptop and thus share data between your laptop and EPOC machine. CF cards are also used in other devices such as digital cameras. EPOC can share data with any other device that supports CF cards, provided it uses standard DOS partitions and the FAT (or VFAT) filing system. |
The EPOC file server supports installable file systems that can be loaded at runtime without any kind of reboot. Additional drive letters, and additional media types, can also be supported, depending on system and user requirements.
I'll cover data management more extensively in Chapter 7.