INTRODUCTION TO MULTITASKING AND ASSOCIATED CONCEPTS Multitasking In the real world, things usually happen in a concurrent manner , and a computer that has to cope with a real world environment must behave as if it could process several inputs and outputs at the same time. Since most computers can only do one thing at a given time, the software has to simulate this kind of concurrent processing by assigning computer (CPU) time sequentially to all the external needs. That kind of simulation can be made by application code itself, but this produces "spaghetti code", very difficult to understand and maintain. A better solution is taking out the simulation to a separate module that hides the implementation details. In this case, we are using multitasking services. Programmers structure their system as a group of tasks , also called processes and threads .The words code and program refer to a static sequence of instructions, whereas the words task, process or thread refer to the dynamic execution state of that sequence. These tasks can be seen as independent programs running concurrently, and each task has its own independent control flow. The module described above, providing multitasking and related services is referred to as a multitasking kernel. Each time the kernel has control, it has to decide which of the concurrent tasks have to get control next time. The selection of a task to get the CPU is called scheduling , and the action of giving control to that task is referred to as dispatching. The complete sequence of actions is known as task switch. Usually, importance of each task is different , so the kernel has to provide a way to specify which tasks are the most important in order to give them CPU time as soon as possible. This is achieved by means of a priority mechanism. A task may need to pass some data to another task, this is called interprocess communication (IPC) . Sometimes, several tasks may need to orderly access a shared resource, requiring interprocess synchronization . These are also important services of the kernel. In order to get its job done, the kernel has to spend some time in its own operations. The (kernel execution time)/(total execution time) ratio is called kernel overhead, usually expressed as a percentage. Since the time spent by the kernel , though necessary, decreases the time available for application code, overhead must be kept as short as possible. Interrupts Interrupts are asynchronous hardware signals, that is, signals that enter the system at any time, no matter which is the current execution state. Interrupts are used to signal hardware events, such as the reception of a byte in a serial port. Interrupts are ordered by priority. When an interrupt enters the system, the execution jumps to the associated Interrupt Service Routine (ISR). For not to disturb the execution of the interrupted program, the state of the machine has to be saved before servicing the interrupt , and restored before returning from the ISR. These actions are referred to as context switching. Interrupts can be disabled , and then incoming hardware signals are not serviced until interrupts are re-enabled. The time elapsed between the moment an interrupt signal enters the system and its corresponding ISR begins its execution is known as interrupt latency. Interrupt latency should be as small as possible. Microkernel-based architecture Classic monolithic kernels include every conceivable kernel service within a single module, wasting resources if any service is not used. Another problem with monolithic kernels is that they disable interrupts for a long time while executing kernel calls, increasing interrupt latency. Microkernel approach is used to overcome these problems.Basic functions, such as scheduling and message passing are performed by a tiny module, referred to as microkernel, and other kernel services, such as networking, peripheral management, etc., are implemented by kernel daemons (system tasks) which run on top of that small microkernel, and that can be added or not depending on the actual needs. This feature is referred to as scalability. So, when using APDMX modular approach, you build a kernel for your application according with your own needs, adding just the modules you really need. Both the APDMX Microkernel and kernel daemons are intended to be embedded into your own applications, that is, you link them with your code, and while the underlying operating system sees your application as a single program, it internally manages several concurrent tasks. These kind of kernels are sometimes referred to as multitasking executives. Scheduling Using APDMX Microkernel, tasks run in a common shared memory space. In technical literature this is called lightweight multitasking or multithreading. Depending on when the scheduling decisions are done, kernels can be classified in preemptive and non preemptive or cooperative. In preemptive kernels , scheduling decisions are made as result of interrupts entering the system, whereas in cooperative ones, they are made only in explicit system calls. APDMX Microkernel uses cooperative multitasking. This provides a lower kernel overhead and a shorter interrupt latency, because interrupts are enabled most of the time, even when executing kernel code. Third-party libraries can be used whether they are reentrant or not, and the same is true for DOS services. The scheduler policy is very simple: it chooses for execution the ready task with the highest priority. There are several queues for ready tasks, one for each priority, and within a given priority, tasks are managed on a First In First Out basis. Synchronization There are situations in which access to shared resources have to be carefully controlled in order to avoid race conditions. Let's think, as an example, in two tasks of the same priority trying to output each one a page of text to a shared printer. Each task could print a single character and then relinquish control in order to let other tasks get CPU time. It is not too difficult to see that the two outputs will be mixed, forming a babble with no sense at all . These possibly conflicting sections of code are known as critical sections. An option would be for a task retaining control for the entire printing time, but in that case, not just the other task but every task in the system, is prevented to run until the page is finished. This situation is described as starvation. The best solution is to use a semaphore. A semaphore is a synchronization mechanism that is accessed through two complementary operations: wait and signal (also called p and v). Any semaphore has an integer variable associated to it. When a task performs a wait operation on a semaphore, the current value of the variable is checked. If it is greater than zero, then the variable is decremented by one and execution continues. If not, the calling task is suspended until a signal operation is performed on this semaphore. Waiting tasks are queued on a FIFO basis. When a signal operation is performed, the microkernel checks whether there are tasks waiting in the queue. If so, the first task in the queue becomes ready, otherwise the variable is incremented. The variable has an upper bound. These kind of semaphores are sometimes called Dijkstra semaphores, for the name of its creator. A special case is when the maximum value is 1 ; a binary semaphore like this, with only two possible values, 0 and 1, is used to implement mutual exclusion and thus it is known as mutex . Using a mutex in our previous example enables serializing printer accesses without starving other tasks. The resulting pseudocode is as follows: Task A ------- wait(mutex); while(page is not finished) { print a character; yield control; } signal(mutex); Task B ------ wait(mutex); while(page is not finished) { print a character; yield control; } signal(mutex); The first task in accessing the semaphore sets its value to 0; when the other task performs its own wait , it encounters this value and is suspended until the first task reaches its signal call. In the previous case, the critical sections are time-consuming. However, for very short critical sections it may be preferable to simply avoid yielding control inside the critical section. In some cases, race conditions may appear when a resource (e.g. a buffer) is shared by a task and an ISR. In these situations, to avoid yielding control is not enough, due to the asynchronous nature of interrupt signals,and critical sections within the task code must be protected by disabling interrupts. Intertask communication Since APDMX Microkernel has lightweight tasks (also called threads), you can easily share memory between different tasks as long as you guard against race conditions. However there are many situations in which you need to pass discrete data from a task to another . APDMX Microkernel provides convenient message-passing mechanisms to perform intertask communication. A task can send a message to any other task in two different ways: * Tightly coupled : Sending task waits until it receives a reply from recipient. * Loosely coupled: Sending task continues execution without expecting any reply. Each task has an associated message queue for incoming messages and a mailbox for incoming replies. The primitives only carry a pointer to the message body and the message size. It is up to communicating tasks to agree on the message meanings and allocating and freeing memory for the message contents. When a task makes a system call to receive a message it can choose one of the following options: * Not to wait for anything and returning whether a message is in the queue or not. * To wait a specified amount of time for a message arrival. * Waiting indefinitely for the first message arrival. Suggested Reading We cannot describe here all the topics related to multitasking. If you want to learn more, there are several books with an in-depth coverage of that topics. Two of them are: Peterson, J.L, and Silberschatz, A. Operating System Concepts (Addison-Wesley) Tanenbaum, A.S. Operating Systems: Design and Implementation (Prentice Hall) ARCHITECTURE AND SERVICES OVERVIEW System Architecture As prevously stated, APDMX Microkernel is an executive and it is embedded within your application. However, we can make a conceptual distinction between them. As nearly all real-mode/protected-mode interactions are managed by the DOS Extender ( 32RTM or DOS 4G/W , depending on the compiler used) , a simplified layered view of the system would be: Fig. 1 General Architecture Although there are some overlaps not depicted here , it is a good approximation to the System Architecture. The microkernel is splitted in two parts. The first one is 32-bit code running in protected mode. This part is responsible for all the main services of the kernel. In addition to the Protected Mode part there is a TSR , the Real Mode Interrupt Manager, that hooks some interrupts and acts as a "translator", making it possible for the P.M. part to communicate with 16-bit drivers and TSRs in an efficient way. Your code sees the microkernel as a normal library, with services implemented as conventional function calls, though behind the scenes the microkernel is switching contexts, juggling with registers and so on. One of the most important things we can observe in fig. 1 is that no fundamental distinction exists between user tasks and system tasks. This lack of distinction means you can add services whenever they are needed, even if the system is already running. Furthermore, you can even write your own daemons for non-standard devices, and the microkernel makes no difference between your daemons and standard ones. Task states A task can be in one of these states: READY The task is ready to execute. The scheduler can choose the task for CPU allocation. When the task gets control, it goes to RUNNING state. RUNNING Task code is executing. PENDING The task is waiting on a semaphore. When another task performs a signal operation on the semaphore, the first waiting task becomes READY . RECEIVING The task is waiting for a message. When a message arrives at the reception queue of this task, or an optional timeout expires, the task becomes READY. EXPECTING The task has used a blocking send service and is waiting for a reply. When a message arrives at the mailbox, the task becomes READY. FROZEN Task execution is suspended until it is unfrozen by another task . The scheduler don't care about frozen tasks. This state, and its associated services, only exists in the debugging version in order to make debugging easier. A transition diagram is shown in fig. 2. Besides each transition is the corresponding microkernel service or action. Fig.2 Transition diagram Throughout all the present Manual we use the terms suspended and blocked to collectively refer to those states in which the task cannot be scheduled until something happens. APDMX Microkernel API services list by category Some services make calling task relinquish control if there is a equal or higher priority task ready to execute, even if the result of the call itself does not suspend the task execution. If the task is not suspended, then it becomes READY. General mK_init() Initializes microkernel's internal data structures. mK_start() Switches to multitasking mode and starts executing the available task with the highest priority. mK_end() Ends execution, returns control to the Operating System command interpreter. mK_yield() Relinquishes control to the highest priority ready task. Task management mK_create_task() Task creation. A task is coded in a C function, typically containing a while(1) or for(;;) loop. mK_self_pid() * Returns the calling task identifier. mK_get_task_info() * Retrieves actual information about the specified task (a subset of the Task Control Block). mK_set_task_info() * Sets values for some fields of the Task Control Block . mK_kill_task() * Destroys calling task, releasing all the resources allocated to that task. Synchronization mK_enter_critical() Execution enters a short critical section. Disables interrupts. mK_exit_critical() Execution exits from a short critical section. Enables interrupts. mK_create_semaphore() * Creation of a Dijkstra semaphore. It allocates memory for the data associated with the semaphore, sets the initial value and the upper bound for the associated counter. mK_wait_semaphore() * Waits on a semaphore. If semaphore state is >0, it decrements counter by one and continues execution. Otherwise, the calling task is suspended. mK_signal_semaphore() * Sets to READY the state of the first task waiting on a Dijkstra semaphore. If there is no task waiting, it increments by one the semaphore counter and continues execution. mK_delete_semaphore() Deletes the semaphore if there are no tasks waiting on it. Timing mK_time_of_day() Returns the date and time of day. mK_get_systicks() Returns the time (in interrupt tick units) elapsed since the system started. Memory management mK_malloc() * Allocates memory blocks from the microkernel heap. mK_mfree() * Releases a memory block previously allocated by a call to mK_malloc() Intertask communication mK_non_blocking_send() * Transmits a message to another task. The calling task remains ready for execution. mK_blocking_send() * Transmits a message to another task. The calling task is suspended until the recipient sends it the reply. mK_receive() * Calling task is suspended until it receives a message or a specified time has elapsed. Alternatively , calling task may specify either no waiting at all or an indefinite waiting. mK_respond() * The recipient of a message sends a reply to the transmitter, allowing it to resume its execution. Debugging calls These calls are available, but have no effect in the production version of the microkernel (See the DEBUG VERSION chapter for additional debug features). mK_freeze() Suspends calling task execution. The scheduler does not consider that task until it is "unfrozen" mK_unfreeze() Makes a previously frozen task schedulable , setting its state to READY . mK_get_task_snapshot() Gets information about the current state of a task. mK_get_semaphore_snapshot() Gets information about the current state of a semaphore. mK_profiler_control() Controls the task profiler's data gathering. mK_heap_view() Provides information about the blocks allocated by the memory manager. APPLICATION PROGRAM INTERFACE REFERENCE mK_init() Description: This call is used to initialize internal data structures of the microkernel. It must be done before any other microkernel service is requested. Function prototype: void mK_init(void); Arguments: None. Returned value: None. Notes/Warnings: mK_start() Description: This call switches to multitasking mode and starts executing the available task with the highest priority. Function prototype: void mK_start(void); Arguments: None. Returned value: None. Notes/Warnings: mK_end() Description: This call ends multitasking mode. It returns control to the operating system command interpreter. Function prototype: void mK_end(void); Arguments: None. Returned value: None. Notes/Warnings: mK_yield() Description: This call explicitly relinquishes control to the ready task with the highest priority. Function prototype: void mK_yield(void); Arguments: None. Returned value: None. Notes/Warnings: If a task makes regular calls to other microkernel services, explicit calls to mK_yield() may not be necessary. mK_create_task() Description: It creates a task on the specified code (usually a C function) and sets its state to READY. Function prototype: int mK_create_task(TASK_INFO *settings); Arguments: settings : address of the structure of TASK_INFO type containing the desired values for the task. TASK_INFO is defined as follows: typedef struct { void *code; //address of task code starting point unsigned stack_size; // size of the stack area unsigned char default_priority; unsigned char current_priority; char *name; // task symbolic name }TASK_INFO; Returned value: On success, it returns an identifier for the task just created (positive value). mKCREATPROC_NOMEM : Not enough memory available to create the task. Notes/Warnings: mK_self_pid() Description: It returns the identifier of the calling task. Function prototype: int mK_self_pid(void); Arguments: None. Returned value: Caller's PID . Notes/Warnings: mK_get_task_info() Description: This function retrieves information associated with the specified task. Actually, this information is a subset of the Task Control Block inside the microkernel. Function prototype: int mK_get_task_info(int pid, TASK_INFO *info); Arguments: pid : identifier of the task of which information is requested info : address where the information is to be written [see TASK_INFO definition in mK_create_task()] Returned value: mKGETINFO_OK : successful termination mKGETINFO_BADPID : the pid does not belong to a living task Notes/Warnings: mK_set_task_info() Description: This function sets some of the values within the Task Control Block . Function prototype: int mK_set_task_info(int pid, TASK_INFO *info); Arguments: pid : identifier of the task of which information is requested info : address where the information is to be read [see TASK_INFO definition in mK_create_task()] Returned value: mKSETINFO_OK : successful termination mKSETINFO_BADPID : the pid does not belong to a living task Notes/Warnings: Neither the code start address nor the stack size can be modified by this call. mK_kill_task() Description: This service is used to kill the calling task, freeing all its allocated resources. Function prototype: int mK_kill_task(void); Arguments: None. Returned value: mKKILL_OK : successful execution Notes/Warnings: mK_enter_critical() Description: This call disables interrupts when entering a short critical section. Function prototype: void mK_enter_critical(void); Arguments: None. Returned value: None. Notes/Warnings: This call enables protecting critical sections with a smaller overhead than that produced by using semaphores. It is important to note that in time-consuming critical sections it may be preferable to use semaphores to avoid an excessive interrupt latency. mK_exit_critical() Description: Re-enables interrupts previously disabled by a call to mK_enter_critical(). Function prototype: void mK_exit_critical(void); Arguments: None. Returned value: None. Notes/Warnings: See mK_enter_critical(). mK_create_semaphore() Description: This call allocates memory for a Dijkstra semaphore, sets the initial value for its counter and the upper limit for the counter value. A mutual exclusion semaphore (mutex) can be obtained by setting this upper limit to 1. Function prototype: int mK_create_semaphore(short value, short maximum); Arguments: value : value to be assigned to the semaphore's counter maximum: upper limit for the semaphore value Returned value: On success, the call returns an identifier for the semaphore just created. otherwise, it returns one of the following error codes: mKCREATSM_NOMEM : not enough memory to create semaphore mKCREATSM_BADVALUE : value not allowed mKCREATSM_BADCEILING : maximum value is less than 1 . Notes/Warnings: mK_wait_semaphore() Description: This call performs a WAIT (P) operation on a semaphore. If the value of the semaphore is >0 , it is decremented by one and execution continues. Otherwise, the calling task is suspended until another one signals that semaphore. Suspended tasks are queued on a FIFO basis. Function prototype: int mK_wait_semaphore(int semaphore); Arguments: semaphore : semaphore identifier Returned value: mKWAITSM_SIGNAL : another task has signaled the semaphore mKWAITSM_BADNUM : wrong semaphore identifier Notes/Warnings: mK_signal_semaphore() Description: This call makes a signaling operation on a semaphore. If there are tasks waiting on the semaphore, it sets to READY the state of the first one in the semaphore queue. Otherwise, it increments by one the semaphore's value and execution continues. Function prototype: int mK_signal_semaphore(int semaphore); Arguments: semaphore : semaphore number Returned value: mKSIGNALSM_OK : successful termination mKSIGNALSM_BADNUM : wrong semaphore number mKSIGNALSM_OVERFLOW : semaphore counter has already its maximum value. Notes/Warnings: mKSIGNALSM_OVERFLOW may not indicate an error, if we are using the semaphore as a mutual exclusion one mK_delete_semaphore() Description: Deletes a semaphore, releasing its associated resources. Function prototype: int mK_delete_semaphore(int semaphore); Arguments: semaphore : semaphore identifier Returned value: mKDELSM_OK : successful termination mKDELSM_BADNUM : wrong semaphore number mKDELSM_BUSY : semaphore has tasks waiting on it. Notes/Warnings: mK_time_of_day() Description: It gives the date and time of the day obtained from the CMOS clock. Function prototype: void mK_time_of_day(mKTIME *buffer); Arguments: buffer : address where the results are to be written. The type mKTIME is defined as follows: typedef struct { int seconds; int minutes; int hours; int monthday; int month; int year; } mKTIME; Returned value: None. Notes/Warnings: mK_get_systicks() Description: This call gives out the total clock ticks count since system initialization, in a 32 bit value. Function prototype: DWORD mK_getsysticks(void); Arguments: None. Returned value: Clock ticks count. Notes/Warnings: The hardware is programmed to produce a clock interrupt every 5 milliseconds, so this is the value for a system tick. However, BIOS routines receive "fake" clock ticks every 55 milliseconds, so that BIOS internal counters are properly updated. mK_malloc() Description: Requests a block of memory from the microkernel memory manager. Function prototype: void *mK_malloc(DWORD size); Arguments: size : size of the memory block being requested. Returned value: On success, it returns a pointer to the block just allocated. On failure, it returns NULL . Notes/Warnings: mK_mfree() Description: Releases a memory block previously allocated by a call to mK_malloc(). Function prototype: int mK_mfree(void *block); Arguments: block : pointer to the memory block to be released. Returned value: mKMFREE_OK : the block has been released mKMFREE_BADPTR : pointer is not a valid one. Notes/Warnings: mK_non_blocking_send() Description: Sends a message to another task. Calling task remains ready for execution, no matter whether the message is read or not. Function prototype: int mK_non_blocking_send(int machine, int receiver, void *contents, int length); Arguments: machine : Number of the machine where the recipient resides. It must be mKLOCAL_MACHINE if network extensions are not present. receiver : identifier of the recipient. contents : address of the message body length : size of the message body Returned value: mKNBSEND_OK : message was sent (queued) mKNBSEND_BADPID : the pid does not belong to a living task mKNBSEND_OVERFLOW : there is no room for the message in the input queue of the recipient. Notes/Warnings: * There are no semantics associated with neither the message type nor the contents, at the microkernel level. It is up to the upper layers assigning any meaning to messages. * The reason for including here references to remote machines is to unify local and remote comms. Likely, most of the messages are sent to another task in the same machine, so it is more efficient to include this service within the microkernel. * If remote messaging is requested, the microkernel assumes that the first task created (the task whose pid == 0) is responsible of network communications. Thus, every non-local message is forwarded to task 0. mK_blocking_send() Description: Sends a message to another task., blocking the calling task until a reply is received. Function prototype: int mK_blocking_send(int machine, int receiver, void *message_contents, int message_length, void *reply_contents, int *reply_length); Arguments: machine : Number of the machine where the recipient resides. Unless network extensions are installed, it must be mKLOCAL_MACHINE . receiver : identifier of the recipient. message_contents : address of the message body. message_length : size of the message . reply_contents : address of the reply body. reply_length : size of the reply . Returned value: mKBKSEND_OK : A reply has been received. mKBKSEND_BADPID : the pid does not belong to a living task mKBKSEND_OVERFLOW : there is no room for the message in the input queue of the recipient. Notes/Warnings: see mK_non_blocking_send(). mK_receive() Description: The calling task is blocked until a message is received from another task or the specified time has elapsed. Function prototype: int mK_receive(int *machine int *pid, void *contents, int *length, int *reply_requested, DWORD delay); Arguments: machine : Number of the machine where the recipient resides. Unless network extensions are installed, it must be mKLOCAL_MACHINE . pid : identifier of the task sending the message. contents : address where the contents of the message have to be written. length : size of the message body. reply_requested : If (*reply_requested != 0) then the task that sent the message is blocked waiting for a reply. Otherwise, no reply is expected. delay : timeout for the reception , in clock ticks. There are two special values: mKWAIT_FOREVER : The task waits indefinitely mKNOWAIT : The call returns inmediately Returned value: mKRECEIVE_ARRIVAL : a message is available mKRECEIVE_ELAPSED : specified time has elapsed mKRECEIVE_NOTHING : there's no message in the queue. This return value can only appear when using mKNOWAIT . Notes/Warnings: This call may be used to obtain a timed sleep with a possible wakeup by another task. mK_respond() Description: It sends a reply to a task blocked by a call to mK_blocking_send, getting it ready to execute. Function prototype: int mK_respond(int machine, int receiver, void *contents, int length); Arguments: pid : task identifier of the expecting task contents : address of the message body Returned value: mKRESPOND_OK : message was sent and the recipient was unblocked. mKRESPOND_BADPID : wrong task identifier, recipient does not exist. mKRESPOND_BADSTATE : recipient was not waiting for a reply Notes/Warnings: When the recipient is not waiting for a reply, the message is discarded. mK_freeze() Description: It freezes specified task, suspending its execution until it is unfrozen by another one. Function prototype: int mK_freeze(int pid); Arguments: pid : identifier of the task that is to be frozen Returned value: mKFREEZE_OK : successful execution mKFREEZE_BADPID : wrong task identifier Notes/Warnings: This call, along with mK_unfreeze(), are intended only for debugging purposes. For synchronization, it is far better to use messages or semaphores. These calls are inactive in the production version of the kernel. mK_unfreeze() Description: Unfreezes a previously frozen task . Function prototype: int mK_unfreeze(int pid); Arguments: pid : identifier of the task to be unfrozen Returned value: mKUNFREEZE_OK : succesful termination mKUNFREEZE_BADPID : wrong task identifier mKUNFREEZE_BADSTATE : specified task was not in FROZEN state Notes/Warnings: See mK_freeze(). mK_get_task_snapshot() Description: Gets information about the current state of a task. Function prototype: int mK_get_task_snapshot(int task, mKTASK_INFO *buffer); Arguments: task : pid of the task of which information is requested buffer : address where the task information is to be put. The structure for the task information is as follows: typedef struct { // general information int how_many; // total number of tasks in the system // task profiler info DWORD times_entered; DWORD time_running; // other status info int process_state // current state of the task unsigned stack_size; unsigned char default_priority; unsigned char current_priority; char *symbolic_name; }mKTASK_INFO; Returned value: mKGTASKINFO_OK : Successful completion mKGTASKINFO_BADPID : Wrong task identifier Notes/Warnings: The following constants are defined for task states: READY, RUNNING, PENDING, RECEIVING, EXPECTING, FROZEN . mK_get_semaphore_snapshot() Description: This call returns information about a semaphore. Function prototype: int mK_get_semaphore_snapshot(int semaphore, mKSEM_INFO *buffer); Arguments: semaphore : semaphore identifier buffer : address where the information is to be put. The structure for the semaphore information is as follows: typedef struct { short value; short maximum; int *waiting_tasks; // a large enough space must be previously allocated } mKSEM_INFO; Returned value: mKGSEMINFO_OK : Successful completion mKGSEMINFO_BADPID : Wrong task identifier Notes/Warnings: The limit for the number of waiting tasks on a single semaphore is the same as the total number of tasks in the whole system. mK_profiler_control() Description: This function controls the task profiler data gathering. Function prototype: int mK_profiler_control(int command, int task); Arguments: command : action to perform. Must be one of the following constants: mKPROF_ON start/restart data collection mKPROF_OFF stop data collection mKPROF_RESET clears profiler data task : Any valid task identifier . Returned value: mKPROFCNT_OK : successful termination mKPROFCNT_BADCOMMAND : wrong command mKPROFCNT_BADPID : wrong task identifier Notes/Warnings: As default, profiling is active for every task from its first CPU burst. mK_heap_view() Description: Gets information on the memory allocation. Function prototype: mKBLOCK_INFO * mK_heap_view(void); Arguments: None. Returned value: Address for the memory map. The entries of the memory map are of the mKBLOCK_INFO type , defined as follows: typedef struct { void *block_address; int block_size; int owner_task; }mKBLOCK_INFO; The end of the memory array is marked by means of a NULL in the field block_address of the corresponding structure. Notes/Warnings: DEBUG VERSION Introduction If you have purchased the registered version of APDMX Microkernel, you have some additional debug features. You get : * State dumps for tasks, semaphores and memory blocks. * Parameter checking in kernel calls. * A microkernel exception is raised in case of error, giving you a clue on what's wrong. * Block stuffing and other actions against memory-related bugs. * Specific kernel calls for your own debug code. As previously stated, the following functions are only active in the debug version: * mK_freeze() * mK_unfreeze() * mK_get_task_snapshot() * mK_get_semaphore_snapshot() * mK_profiler_control() * mK_heap_view() Differences in memory management To gain speed, the production version of the memory manager performs just the essential actions to allocate and release memory blocks. Since memory requests are often one of the main error-prone parts of a program, additional measures are taken inside the debug versions in order to make these bugs easier to catch: * Blocks are initialized, so that if execution jumps into an unused part of a block, a breakpoint interrupt is triggered. * A checksum is computed for each internal block header, both in mK_malloc() and mK_mfree(). This eases catching dangling or misplaced pointers. * When a block is released by calling mK_mfree(), any further attempt of referencing the associated pointer triggers a processor exception. * Also, when a block is released, its contents are re-initialized. Dump Options Although you can use debug calls to make your own state displays, some special features are provided to see what is happening inside your system without any special code. If you press the PAUSE key while your system is running, the following options are displayed: F1.- Task information. First option (F1) produces a listing of the tasks present in the system at this moment, giving for each task: * pid (task identifier) * name (symbolic name) * whether profiling is active (represented by an asterisk) or not, for that task * total CPU time used by that task * total number of CPU bursts * default priority and current priority (DP/CP) * current task state ( READY, RUNNING, etc.) You can scroll this and the following listings by pressing PGUP and PGDN keys. In the bottom of the screen, the total number of living tasks is shown. F2.- Semaphore information For each semaphore the following information is displayed: * semaphore identifier (num.) * current value of the associated variable * maximum value * list of tasks waiting within the semaphore queue. Up to 20 pids are shown F3.- Memory blocks. For each block requested through mK_malloc(), the following information is displayed: * address of the block * size in bytes * owner of that block (pid); if a block is owned by the system itself, this field is -1 In the last line of the screen, the global heap state is shown. Two different algorithms are used inside the memory manager. The tail is the size of the memory available for the primary algorithm and never allocated. The "hits" field shows the percentage of block requests satisfied using the primary algorithm. F12.- Terminate execution. Pressing this key ends execution, returning to the DOS prompt. Microkernel exceptions When an internal assertion fails, a microkernel exception is raised. If you are executing your application on a debugger, such as Turbo-debugger, a breakpoint interrupt is triggered, so that you can examine the call nesting, variable contents and so on. This breakpoint has no effect if the code is running directly from the operating system. Whether you are using a debugger or not, the following information appears on your screen: * Error code. * Error message, including between square brackets the kernel call where the error is detected. * Task identifier (pid) . If the error appears before starting multitasking, a message is displayed instead of the pid. * Task symbolic name. * Address where task code starts. * Default and current priorities of the task. * Address and size of the stack zone for that task. * Total number of CPU bursts for that task. Exception Messages by service mK_create_task() * Not enough memory for task name * No free entry for task * Not enough memory for stack space * No room in queue for task pid * Incorrect default priority * Incorrect current priority * Incorrect stack size mK_get_task_info() * Task pid out of range * Task pid doesn't correspond to any living task mK_set_task_info() * Task pid out of range * Task pid doesn't correspond to any living task * Incorrect current priority mK_start() * Second call not allowed * No tasks created mK_malloc() * Allocation failure * Incorrect block size mK_mfree() * Corrupted header detected * Attempt to free a NULL pointer mK_create_semaphore() * Bad limit value * Bad initial value * No free entry * Not enough memory mK_wait_semaphore() * Semaphore number out of range * Number doesn't correspond to any existing semaphore mK_signal_semaphore() * Semaphore number out of range * Number doesn't correspond to any existing semaphore mK_delete_semaphore() * Semaphore number out of range * Number doesn't correspond to any existing semaphore mK_non_blocking_send() * Recipient pid out of range * Recipient doesn't exist * No room in queue for message * Incorrect machine number mK_blocking_send() * Recipient pid out of range * Recipient doesn't exist * No room in queue for message * Incorrect machine number mK_respond() * Recipient pid out of range * Recipient doesn't exist in * Recipient was not expecting a response mK_freeze() * Task pid out of range * Task pid doesn't correspond to any living task mK_unfreeze() * Task pid out of range * Task pid doesn't correspond to any living task * Referenced task was not frozen mK_get_task_snapshot() * Task pid out of range * Task pid doesn't correspond to any living task * Semaphore number out of range mK_get_semaphore_snapshot() * Number doesn't correspond to any existing semaphore mK_profiler_control() * Task pid out of range * Task pid doesn't correspond to any living task * Incorrect command issued GLOSSARY Aperiodic task A task that is activated on demand. [= asynchronous task] Asynchronous Device I/O task A task that interfaces to an I/O device and is activated by interrupts from that device. Asynchronous I/O device An I/O device that generates an interrupt when it has produced some input or when it has finished processing an output operation. Asynchronous task A task that is activated on demand. Binary semaphore A boolean variable that is only accessed by means of two atomic operations, wait and signal, usually used to enforce mutual exclusion. Clock tick Time unit, it is the time elapsed between two consecutive clock interrupts. Cohesion A criterion for identifying the strength or unity within a module. Component In analysis, an object or function. In design, a task or information hiding module. Concurrent task [See Task] Context Switch Action of saving the processor state (the contents of its registers) in memory and then restoring another set of register contents to either start or resume another line of execution. A context switch may be part of a task switch. [See task switch]. Control Specification A specification that describes the behavior of the system in terms of decision tables, state transition tables, state transition diagrams and/or process activation tables. Control task A task that executes a state transition diagram (mapped to a state transition table). Coupling A criterion for determining the degree of connectivity between modules. CPU Burst Time elapsed between the moment when a task gets control and the moment when control is given to a different task. Critical Section A section of code that may lead to race conditions if not protected by means of intertask synchronization. Daemon Word borrowed from UNIX terminology. A daemon is a task which provides an operating system service. Deadlock A case where two or more tasks are suspended indefinitely because each task is waiting for a resource acquired by another task. Device Interface Module (DIM) An information hiding module that hides the characteristics of an I/O device and presents a virtual device interface to its users. Discrete Data Data that arrives at specific time points. Distributed Processing Environment A system configuration where several geographically dispersed nodes are interconnected by means of a local area or wide area network. Environmental Model A model that defines the external entities that the system has to interface to, and the inputs to and outputs from the system. Finite State Machine (FSM) A conceptual machine with a given number of states and transitions, which are caused by input signals. A FSM is usually represented by a state transition diagram or state transition table. FIFO Acronym for First In First Out [see Queue]. FSM Finite State Machine Identifier An unique number used to specify an individual entity (task, semaphore, etc). IPC [See Interprocess Communication] Interprocess Communication Action of passing data and/or control signals between concurrent tasks. ISR Acronym for Interrupt Service Routine. Loosely-coupled Message Communication A producer task sends a message to a consumer task and does not wait for a response; a message queue could potentially build up between the tasks. Message An item of data transmitted using explicit addressing. Microkernel Component of some operating systems that performs the lowest level operations, such as context switching, dispatching and interrupt management. Usually, the higher level operation are performed by daemons. The microkernel approach is the opposite to the monolithic approach in operating system design. Modularity The extent to which software is composed of discrete components so that a change to one component has minimal impact on the other components. Multitasking The technique of concurrently executing a number of related tasks in the same computer. Mutual exclusion Only allowing one task to have access to shared data at a time, often enforced by means of binary semaphores. Overhead Relative cost imposed to a system by a service or a component. Usually it is measured as the relation between the amount of a resource used by that service and the total amount of that resource. Passive I/O device A passive (synchronous) I/O device does not generate an interrupt on completion of an input or output operation. The input from a passive input device needs to be read on a polled basis. Periodic Function A function that is activated at regular intervals to perform some action. Periodic I/O task A task that interfaces to a passive I/O device and polls the device on a regular basis. PID A unique number identifying a task within a machine. Polling Action of sequentially testing each possible source of input until one is found with some data ready to send. Preemption The kernel take away control from the running task in order to give it to a higher priority one. Running task does not voluntarily relinquish control but the kernel uses an interrupt to make the task switch. Priority An assigned level of importance given to various entities that determines the order in which they will be serviced. Process Same as task. Profiling Measuring the relative amount of CPU time used by different software components (tasks, functions, etc.). Usually it is used to determine which components have to be optimized. Queue A list that allows insertions at one end and deletions at the opposite end. Real-time Pertaining to the processing of data by a computer in connection with another process outside the computer according to time requirements imposed by the outside process. Scheduler Software component that chooses which is the next task to run. Usually it is embedded in a kernel. Semaphore Synchronization mechanism used to avoid race conditions and data corruption when two processes are trying to access a shared resource. Starvation Situation where one or more tasks never get control, even if they are ready for execution. State Transition Diagram (STD) A graphical representation of a Finite State Machine in which the nodes represent states and the arcs represent transitions between them. State Transition Table (STT) A tabular representation of a Finite State Machine. Symbolic name Human-understandable name for an entity (process, queue, etc.), represented as a character string. Usually, software uses also a numeric identifier for that entity. Task [also concurrent task, process] A task represents the execution of a sequential program or a sequential component of a concurrent program. Each task deals with a sequential thread of execution; there is no concurrence within a task. Task Control Block Data structure internally used by the kernel to store all the data concerning a specific task. Task Priority Number associated with each task that indicates to the scheduler the order in which the tasks must be chosen to run. Task Switch Action of saving the state of a task, selecting another task to run, and restoring the corresponding state. Usually involves at least two context switches. Throughput The amount of processing performed in a given amount of time by an entity in a computer system. Tightly-coupled message communication with reply The case when a producer task sends a message to a consumer task and then waits for a reply. Contact APD for the availability of such extensions. To gain speed, Protected Mode part sometimes accesses low memory directly. Those marked with * not to be confused with microprocessor exceptions, which are managed by the DOS Extender Moonlight Microkernel - Programmer's Manual *1995 APD S.A. Page 65