Welcome to one of the last parts of this “process abstraction” series. In this article, we will consider the various insulators which control communication with external hardware through interrupts, I/O ports, and memory-mapped registers.
Interrupt manager (class name : InterruptManager, instance name : interrupt_manager)
This kernel component manages interrupt sources that are directly connected to the CPU or to interrupt indirection layers that must be managed by the kernel for architectural reasons (see this post for more details). Its primary role is to “connect” interrupts to suitable RPC calls, which take no parameter, so that these RPC calls are triggered in a safe and asynchronous fashion when the relevant interrupt is fired.
The interrupt manager offers a way to lookup specific interrupt sources using filepath-like string names, so that drivers can do their job. As an example, on an x86_64 system, “Vector13” could designate the tenth CPU interrupt vector (#GP), whereas “PIC/IRQ0” could designate the first IRQ of the legacy 8259 PIC (PIT). A limited form of the “*” wildcard character may also be used, in order to lookup all interrupt sources whose name starts with a certain string, such as “PIC/*”. This may be necessary for database-based “plug and play” hardware detection on some architectures.
The interrupt manager manages an unknown number of interrupts, so it depends on the memory allocator to work. It manages a part of the process abstraction, so it depends on the process manager. Interrupt are translated to RPC calls, so it depends on the RPC manager. Finally, the interrupt manager requires saving the state of the previously running thread before processing the current interrupt in kernel space, which makes it depend on the Scheduler.
- As part of its initialization, the interrupt manager runs the Scheduler’s init_interrupt() method, in order to activate its interrupt-bound functionality such as preemptive multitasking.
- PID add_process(PID, ProcessProperties) : Part of the standard insulator interface, this function is called by the process manager when a process is created. The first parameter is the process’ PID, the second is the series of parameters which describes the process from an interrupt management point of view, such as the interrupt sources which it’s given access to.
- void remove_process(PID) : Another part of the standard insulator interface, removes a process from the interrupt manager’s database.
- PID update_process(PID, PID) : Called during live updating of a process, in order to make the new process take over the work of the old one and swap PID with it. From the point of view of the interrupt manager, interrupt connections of the old process are either closed or transferred to the new process, so that the old process stops receiving new work through interrupts. First parameter is the PID of the old process, second parameter is the PID of the new process. Returns the new PID of the old process if successful, PID_INVALID otherwise.
- CID connect_interrupt(InterruptID, PID, SID) : Connects an interrupt, as specified by a unique numeric identifier, to an RPC entry point. Returns the RPC connection’s CID if successful, CID_INVALID otherwise. Any intermediary step which may be required in order to connect the interrupt to one of the CPU’s interrupt vectors is transparently handled by the kernel.
- bool connect_interrupt(InterruptID, void (*entry)()) : Connects an interrupt to a local entry point inside of the kernel. This entry point will be run synchronously as interrupts are disabled, so a bug in this function has the potential to freeze the entire kernel unlike regular interrupt handlers. Mainly used by the scheduler to handle clock interrupts, returns true if successful and false otherwise.
- void disconnect_interrupt(InterruptID, CID) : Disconnects an interrupt from its running RPC connection.
- void disconnect_interrupt(InterruptID, void(*entry)()) : Same but for local entry points.
- InterruptID find_interrupt_source(KString) : Provides the numeric interrupt identifier that is associated to an interrupt name.
- KStringArray* probe_interrupt_sources(PID, KString) : Looks up all interrupt sources matching a wildcard-inclusive name, and returns their textual names as an array of strings. The PID of the process doing the request must be specified, as the array on which the strings will be stored is to be allocated in its private RAM. Returns NULL if something fails.
IO port manager (class name : IOPortManager, instance name : io_port_manager)
This kernel component manages, as the name says, the IO ports of the CPU. On architectures where the CPU has no IO ports, this component is not implemented. The IO port manager allocates access to IO ports on a per-process and per-port basis, and enforces that this access is mutually exclusive except on explicit (and privileged) port sharing requests. Hardware protection schemes are used when available, but not emulated if not available.
The IO port manager relies on the availability of the process manager and the memory allocator.
- PID add_process(PID, ProcessProperties) : Part of the standard insulator interface, this function is called by the process manager when a process is created. The first parameter is the process’ PID, the second is the series of parameters which describes the process from an IO port management point of view, such as the IO ports which the process has access to.
- void remove_process(PID) : Another part of the standard insulator interface, removes a process from the IO port manager’s database.
- PID update_process(PID, PID) : Called during live updating of a process, makes the old process (first parameter) and the new process (second parameter) swap PIDs and returns the new PID of the old process if successful, PID_INVALID otherwise.
- bool connect_io_port_range(int, int, PID) : Gives the specified PID access to all the IO ports of a given range (from first integer identifier to last integer identifier) if they are available. Returns true if successful, false otherwise.
- void disconnect_io_port_range(int, int, PID) : Revocates the access of the specified PID to all the IO ports of a given range, using same syntax as before. Caution : this function may be called for a different range of ports than previous connect_io_port_range, which means that port access must be managed independently and not on a per-range fashion.
- bool share_io_port_range(int, int, PID) : Identical to connect_io_port_range in semantics and overall purpose, but does not check for mutually exclusive port access. Requires higher process privileges.
Memory-mapped IO manager (class name : MMIOManager, instance name : mmio_manager)
The kernel component manages memory-mapped IO resources, typically registers from external hardware. It offers a way to map them in the address space of user-mode processes, and to manage the caching issues that are inherent to any memory-mapped IO mechanism. Like the IO port manager, the memory-mapped IO manager enforces mutually exclusive driver access to memory-mapped regions as a default, though it is possible to bypass it.
The memory-mapped IO manager relies on the availability of the process manager, the memory allocator, and the virtual memory manager.
- PID add_process(PID, ProcessProperties) : Part of the standard insulator interface, this function is called by the process manager when a process is created. The first parameter is the process’ PID, the second is the series of parameters which describes the process from a memory-mapped IO management point of view, such as the physical address ranges which the process may be given access to.
- void remove_process(PID) : Another part of the standard insulator interface, removes a process from the MMIO manager’s database.
- PID update_process(PID, PID) : Called during live updating of a process, this function makes the old process (first parameter) and the new process (second parameter) swap PIDs and returns the new PID of the old process if successful, PID_INVALID otherwise.
- CachingPolicies probe_caching_policies(PID) : Allows a process to know which caching policies are available on a given architecture. I currently consider implementing it as a simple bitmap, but should more complex descriptors be needed later, the requestor’s PID would be needed in order to allocate the required RAM.
- VirMemMap* map_mmio_region(PID, size_t, size_t, VirMemFlags, CachingPolicy) : Maps a chunk of memory-mapped IO registers, as specified by a position and size, in the virtual address space of a specified PID. Extra parameters provide the virtual memory flags which the mapped chunk should have (RW as a default) and the caching policy that should be applied to this chunk of memory (same as regular RAM as a default). Returns a pointer to the management structures of this virtual address space chunk if successful, NULL otherwise.
- void free_mmio_region(PID, size_t) : Releases a MMIO region from a process’ virtual address space, making it available for mutually exclusive use by other processes.
- VirMemMap* share_mmio_region(PID, size_t, size_t, VirMemFlags, CachingPolicy) : Similar to map_mmio_region in semantics and purpose, but does not perform mutual exclusion checks. In case of conflicting caching policies, the most restrictive policy is applied.
- void flush_caches() : On architectures where the CPU’s caching policies cannot be well controlled, this instruction offers the option to flush any form of software-driven CPU cache to RAM, and must be run after any memory-mapped IO instruction. The actual implementation may be coded as a user-mode library if unprivileged code is able to perform the required operations. On architectures where sufficient caching policies are available, this instruction is a NOP.
Questions and answers
- Why are these three components implemented as separate classes ? It is true that I initially planned to implement the IO port manager and memory-mapped IO manager as one single “IO manager” class. Then I thought about the idea some more, and concluded that the distinction between interrupts and other forms of IO which that would enforce in the kernel interface was quite artificial. What’s more, separating these components in two different classes allowed me to avoid implementing port management on architectures that do not have IO ports and memory-mapped IO management on architectures that do not have MMIO. The main drawback, namely duplicate management structures, seems worth it to me.
This is it for IO-driving kernel components ! At this point, the design of kernel components is almost done, but there is one step left : I would like to clean up the memory management components, in both interface and implementation, in the light of what has been done on their younger kernel component cousins. The new effort should aim at making the memory management components follow the general insulator conventions, and allow them to use full memory allocation once it is available in order to provide extra functionality. Stay tuned !