Time management consists in reading time, and assigning execution time to threads through an event-based model. Hence, time management is performed using events: an event permits to execute some code (the event handler) at a specified time (the event rising time). When an event raises the event handler is called (with interrupt disabled).
Interrupts are managed in a similar way, allowing the programmer to specify the event handler for a special event that will raise when the hardware interrupt arrives.
Using the event mechanism it is easy to implement temporal protection (enforcing that a thread will never require too much execution time), while spatial protection is provided by OSLib through Address Spaces. An address space is a very basic abstraction encapsulating user data and code. Address Spaces are implemented using x86 segments: each Address Space is a different segment. If the user code uses only the default data and code selectors, the code running in an address space can not access other address spaces. As a default, OSLib provides a ``flat'' address space, mapping all the physical memory.
Here is a list of the functions provided by kl:
void *ll_init(void)
This library function is used to initialize the Kernel Library: it detects the CPU, initializes the FPU, and sets the interrupt and exception handlers to default values.
As output, ll_init returns informations about the environment through a modified version of the MultiBoot Information (MBI) structure (the returned value is a pointer to such a structure). The MultiBoot Info structure is defined as shown in Figure 2.2.
Refer to the MultiBoot documentation for more informations about the fields behaviour. The only difference with the standard MultiBoot Info structure is that a new flag MB_INFO_USEGDT in the flags field is provided for informing that the program has been loaded through a DOS Extender, and two new fields are added. If the MB_INFO_USEGDT is set, the two new fields mem_lowbase and mem_upbase indicates the low memory and high memory starting addresses, otherwise the standard values (respectively and ) have to be assumed.
CONTEXT ll_context_create(void (*entrypoint)(void *p), BYTE *stack, void *parm, void (*killer)(void), WORD control)
This library function is used to create a new thread, allocating a CPU context for it. A thread is defined as an independent flow of execution and is characterized by the register set values, a private stack (used to initialize the SS and ESP registers), and an address space in which it executes. The code executed by a thread is defined by a function called thread body that takes as input a void pointer (passed at thread creation time).
The entrypoint parameter is a pointer to the thread body, the stack parameter is a pointer to a preallocated chunk of memory to be used as a stack for the new thread, while the parm parameter is a void pointer passed as parameter to the thread body when the thread is created. The killer parameter is a pointer to a function that will be called on thread correct termination (a thread terminates correctly when the execution arrives to the end of the body function). The control parameters defines some control flags associated to the thread.
The function allocates a free CPU context and initializes the register values using the passed parameters. The EIP register is initialized to entrypoint, and ESP is initialized to stack, whereas DS, GS, and FS are initialized to the default data segment and CS is initialized to the default code segment. As explained introducing Address Spaces, the default code and data segments remap one-to-one all the system memory (``flat'' Address Space). All the other registers are initialized to standard values.
The return value is the identifier of the initialized CPU context.
void ll_context_delete(CONTEXT c);
This library function is used to free a CPU context when a thread is terminated. The c parameter is the identifier of the context to be freed. Note that the stack memory has to be explicitly freed, since ll_context_delete() does not free it.
CONTEXT ll_context_save(void);
This library function saves the CPU registers' values in the current CPU context and returns its identifier. In other words, the context associated to the thread executing when ll_context_save() is called is saved and its identifier is returned. It can be used to implement context switches in OS primitives, as shown in the following code:
SYSCALL(mysyscall(...)) |
Warning: if the virtual context switch mechanism is used, this function cannot be used (use ll_context_from() instead).
void ll_context_load(CONTEXT c);
This library call is used to load a new CPU context in the CPU, for performing context switches, as shown in the example above (see ll_context_save()). Note that ll_context_load() must be called at the end of a system call (immediately before re-enabling interrupts); if a system programmer needs to perform a context switch with interrupt disabled (in an event handler or in the middle of a system call), the virtual context switch mechanism have to be used. When virtual context switch is used, the context switch function only stores the new context ID in a temporary variable and performs the real context switch only when interrupts are enabled (see ll_context_from() and ll_context_to()).
CONTEXT ll_context_from(void);
This library function is similar to ll_context_save(), but can be called when the virtual context switch mechanism is used. In this case it returns the ID of the last context that have been selected to be loaded in the CPU.
void ll_context_to(CONTEXT c);
This library selects a thread to be dispatched: if interrupts are disabled and the context switch cannot be performed immediately, the real context switch will happen as soon as possible (when interrupts will be re-enabled). This is the virtual context switch mechanism.
ll_context_to() is similar to ll_context_load(), but uses the virtual context switch mechanism; if interrupts are enabled, they behave in the same manner.
void ll_end(void);
This function can be used in the shutdown code: if the application was started through the DOS Extender, ll_end() resets the PIT (and the rest of the hardware) to the standard DOS settings and prepares the system to return to MSDOS, otherwise it simply halts the system.
void ll_abort(int code);
This functions acts as safety place to go when any error occurs and the OS does not know which context is active. The function loads a safe context (with a safe stack), displays an error identified by the code parameter, and exits the OS support code (see also ll_end).
void event_init(struct ll_initparms *l)
This function sets the time management services up, by initializing the event queue and programming the Programmable Interval Timer (PIT) in a proper way. The PIT can be programmed in two different modes: the periodic mode and the one-shot mode. In periodic mode, the PIT is programmed to generate a timer interrupt each tick of seconds, specified by the user through the l parameter. In one shot mode, the PIT is dynamically programmed to generate an interrupt only when it is necessary to raise a programmed event. It is worth noting that the PIT mode only influences the error with which an event raises, but is invisible to the OS (the PIT mode can be changed simply changing the event_init() parameter, without any modify to the OS code).
The function takes as input a pointer l to a ll_initparms structure defined as follows:
struct ll_initparms { |
The mode field indicates the PIT mode (LL_PERIODIC or LL_ONESHOT), while the tick field indicates the tick size (for periodic mode only) in seconds.
TIME gettime(int mode, struct timespec *val)
This function can be used to read the current system time. The system time can be read using different levels of precision (and different levels of overhead): currently only the TIME_PTICK and TIME_EXACT modes are implemented. The TIME_PTICK mode works only if the system timer is programmed in periodic mode, and returns the system time in ticks. It is characterized by a low overhead (small execution time). The TIME_EXACT mode reads the exact system time and returns it measured in seconds.
The mode parameter can be TIME_PTICK or TIME_EXACT and specifies the time reading mode; the val parameter can point to a timespec structure that will be filled with the current time ( if val != NULL) .
This function returns the read time in seconds, or if the reading fails.
int event_post(struct timespec *time, void (*handler)(void *p), void *par)
This function is used to create a new event, selecting an handler to be called at the specified time passing an user provided parameter to it. The handler parameter specifies the event handler (the function to be called when the event will raise), the time parameter indicates the time at which the event will raise, while par is a void pointer that will be passed as parameter to the event handler.
The function returns the identifier of the created event, or -1 if an error occurs (it can be due to the lack of free event descriptors, or to some other internal error). The event identifier is used to refer the event for modifying or deleting it (see event_delete()).
The OS support code programs the interrupt controller in a periodic or one-shot mode (see event_init()) so that an interrupt will be generated near to time time to call the event handler. The event handler is called as a response to the timer interrupt, with interrupts disabled, hence it must execute for not too much time (otherwise, interrupts will be left disabled for a long time). The timer mode can affect the error with which the event handler is called, but the code must be independent from the timer mode.
int event_delete(int index)
This library function is used to delete a posted event, identified by the index parameter. It returns 1 in case of success, -1 in case of failure.
int irq_bind(int irq, void (*handler)(void *p), DWORD flags)
This function can be used to associate an handler to an hardware interrupt; each interrupt is converted by the support code in an event, so the interrupt handler is identical to an event handler. The function checks if the requested interrupt is free, and in this case allocates it and assigns the handler to it. If the interrupt is already allocated (is not free), that is, a handler has been already associated to it, irq_bind returns an error and does nothing.
The irq parameter specifies the interrupt number, while handler is a pointer to the interrupt event handler, and the flags parameter defines some flags associated to the interrupt. In particular, the FORCE flag can be used to set a handler for an already allocated interrupt, and the PREEMPTABLE flag specifies that the handler can be called with interrupts enabled (interruptible handler).
FLAGS:
Interruptible handlers are useful to enhance system responsiveness, reducing the time in which interrupts are disabled and allowing to develop a preemptable OS. On the other hand, they must be used with caution, since mutual exclusion is not guaranteed in an interruptible handler.
The FORCE flag can be useful for removing an interrupt handler (use this flag with the handler parameter set to NULL.
int ll_ActiveInt(void)
This function returns the number of pending interrupts or event handlers.
void as_init(void)
This function initializes the Address Space management code. It must be called before using Address Spaces (as_create() or as_bind()).
AS as_create(void)
This library function can be used to create a new Address Space: it searches for a free Address Space descriptor and initializes it to an empty Address Space returning its identifier. The return value is the created Address Space ID in case of success, or 0 in case of failure.
int as_bind(AS as, DWORD ph_addr, DWORD l_addr, DWORD size)
This library function binds a chunk of physical memory to an Address Space. The as parameter is the Address Space identifier, ph_addr is the physical chunk start address, l_addr is the logical address in the as address space where the memory chunk has to be mapped, and size indicate the size of the memory chunk expressed in bytes.
Warning: currently, this function has been only partially implemented. In particular, since paging is not enabled, a single chunk of memory can be bound to an Address Space, starting from logical address 0.
void ll_context_setspace(CONTEXT c, AS as)
This library functions changes the Address Space in which a thread runs. Basically, ll_context_setspace() sets all the context c segment registers to the segment of the as address space. This function can be useful to create a new task: