Richard Carver is a software engineer with six years experience programming in C on iRMX systems. He received his B.S. in computer science from Akron University (Akron, Ohio). He has worked on projects involving financial transaction processing, security systems and medical diagnostics equipment. He may be reached on CompuServe (71061,1754) at the Real-Time Forum (GO REALTIME) or by phone at (708) 249-0633.
iRMX is a "layered real-time object-oriented multi-user multitasking interrupted-driven priority-based preemptive operating system." iRMX I, iRMX II and iRMX III are a family of operating systems designed for the Intel 80x86 family of microprocessors. iRMX I uses "Real Mode" addressing, which accesses up to 1 megabyte of system memory. iRMX II uses the "Protected Virtual Address Mode" (PVAM) of the 80286 and 80386/486 processors, which permits addressing up to 16 megabytes. In addition, the protected mode supports segment boundary protection, stack-overflow detection, invalid segment detection and segment access rights (read, write, or execute). With iRMX III, the 32-bit addressing of the 80386/486 allows accessing up to 4 gigabytes using PVAM.
The iRMX operating systems are supported for three major industry-standard 80x86-based bus architectures: Intel's MULTIBUS I & II and IBM's PC/AT architecture.
In almost all cases, applications written for a lower member of the OS family can be easily ported to a higher member (e.g., iRMX I to iRMX II). In fact, by using the features of the 80386/486 processor for supporting 16-bit and 32-bit code, almost all iRMX II applications (executable code) can be run directly under iRMX III.
At the heart of the iRMX OS is the Nucleus Layer. This layer of the operating system supports scheduling, controls access to system resources, provides communication between processes and processors, and enables the system to respond to external events. It provides the basic "objects" for all other layers (Basic I/O, Extended I/O, Application Loader, and Human Interface) and applications. The objects provided by the Nucleus include: Tasks, Jobs, Segments, Mailboxes, Semaphores, Regions, Extension Objects, and Composite Objects. These objects may be created, destroyed and manipulated by programs. When an object is created, a token for the object is returned. This object token is used for all references to the object.
Within the iRMX OS, work is performed by tasks. The tasks operate within the environment provided by the job. This includes the tasks, objects used by the tasks, a directory for tasks to catalog objects and a memory pool. Every job has at least one task to refer to as the "initial" task. This task may create other tasks to accomplish the work of the program. Tasks may also create new jobs (child jobs) with their own environments. Creating new jobs results in a Job Tree with parent/child connections. Tasks creating other tasks do not result in parent/child relationships. Tasks within a single job are part of the job as a whole.
Tasks may communicate with each other using the iRMX "Exchange Objects." The three types of exchange objects are mailboxes, semaphores, and regions. Mailboxes can be either data mailboxes, used for sending and receiving data, or object mailboxes, which are used for sending and receiving objects. Mailboxes are created and deleted using the rqcreatemailbox and rqdeletemailbox system calls. Data mailboxes are used with the rqsenddata and rqreceivedata system calls to transfer data (up to 128 bytes) from one task to another. Data is physically copied from the sender to the receiver using the mailbox as a transfer buffer. Object mailboxes are used with the rqsendmessage and rqreceivemessage system call to pass object tokens between tasks. The object token may be for any valid iRMX object (e.g., tasks, jobs, segments, mailboxes, semaphores, regions).
Semaphores are the "keepers of units." A unit is an abstract object that can be sent to or received from a semaphore. Semaphores are often used for controlling access to shared system resources (e.g., data, devices) or for synchronizing tasks. iRMX semaphores can also be used as "counting" semaphores, which means that they are capable of holding more than a single unit. Semaphores are supported through the rqcreatesemaphore, rqdeletesemaphore, rqsendunits, and rqreceiveunits system calls.
Regions are special forms of semaphores. Tasks may receive or release control of regions. In this manner, regions work like "single unit" semaphores. However, when a task receives control of a region, its priority is increased to allow the task to run until it releases the region. Also, the task cannot be suspended or deleted until the region is released. Because of this, regions are only used in situations where semaphores do not provide enough control. Regions are supported through the rqcreateregion, rqdeleteregion, rqsendcontrol, and rqreceivecontrol system calls.
You can accomplish access to shared resources, such as data or devices, by using semaphores. However, sometimes it makes more sense to implement resource control through tasks. For a given resource, such as a terminal, a task is written to provide the services of this resource. The task accepts requests from other tasks for operations to be performed, such as printing a message to the display.
The example program consists of five tasks. The first task is the initial task which it responsible for creating all other tasks and program termination. The clock and crt tasks act as resource managers for the clock and crt, respectively. The count and timer tasks are the resource consumers.
This program uses only semaphores and object mailboxes for communication. Regions are rarely used and are specifically not recommended for use in operator-executed programs. Data mailboxes are not used because they only provide for one-way communications. Typically, two-way communication, such as that provided by rqsendmessage and rqreceivemessage with object mailboxes, provides task synchronization.
The initial task is the main() program. It creates many of the objects used during the program and catalogs these objects so that the other tasks may use them. This task then creates one task at a time, each time waiting for a task to indicate that it has completed initialization by sending a unit, using rqsendunit, to the initialization semaphore (init_sem). When the last task, timer_task(), signals its completion, the shutdown process begins. Each remaining task is sent a unit to its respective shutdown semaphores. When all tasks indicate they have shutdown, all objects are cleaned up (uncatalogued and deleted) and the program is terminated.
The clock_task() provides access to the hardware clock and implements the software timers. All timers are maintained using the single hardware interrupt from the clock board. This task also maintains the display time/date by communicating with the crt_task().
The clock_task has been set-up to service an interrupt. It became an interrupt task when it called the rqsetinterrupt system call. This call specifies the interrupt to be serviced and the address of an interrupt handler. The handler is not the task, it is a function that receives control when the interrupt occurs. The interrupt handler passes control to the interrupt task by using rqsignal interrupt. The interrupt task is waiting to receive control through rqetimedinterrupt. The main difference between the handler and the task is that all interrupts are disabled when the handler is in control. Only the interrupt being serviced is disabled when the task in control. Also, interrupt handlers are restricted to interrupt-related system calls. Since this implementation needs to access semaphores and mailboxes, it must use an interrupt task.
When an interrupt occurs, the task first requests access to the shared time/date data segment. It then checks to see if the hardware clock needs to be changed (supported but not used in example). If no change is required, the current time/date is advanced by one second. Next, all software timers, if any, are advanced by one second. If a timer's timeout value is reached, a unit is sent to the timer semaphore and the timer restarts. Finally, the time/date is formatted and sent to the crt_task() for display. This task communication is synchronous, but the send and receive do not occur one after the other. A display message is not sent unless the response to the last request has been received. This prevents the task from being delayed during interrupt processing and prevents a build up of request messages when the crt_task() is busy (which could occur since the crt_task() has a lower priority). The minor side effect is that the time/date display may skip a second when then crt_task() is busy.
When the task receives the shutdown signal, it waits for the outstanding display response and calls rqresetinterrupt to disable itself as an interrupt processing task. It will then suspend itself to await deletion.
The crt_task() provides shared access to the video display. This is a basic implementation, providing only a means of printing a string at a specific row and column of the display. The task receives request messages that indicate a message to be displayed and the location to display the message. After displaying the message, the task determines if a response is expected. If the calling task indicated a "response mailbox" in the rqsendmessage call, then it will be expecting a response. This facility is used for synchronizing the task communication. The response will be the original message segment. If the task did not expect a response, the "response mailbox" value will be the null token (NUL_TKN). In this case, this task will free the request message segment.
When the task receives the shutdown signal, it will go into an infinite loop of receiving requests and returning response messages (if a response is required). Since many tasks send a request and then wait for the response, you don't want tasks getting stuck waiting for responses. This could happen if the crt_task() quits responding after a shutdown signal. Requests messages will pile up in the request mailbox and will never be processed. Minimum processing to send the response is needed.
The count_task() continuously counts from 0-65535 and displays the value in the middle of the screen by communicating with the crt_task(). This task's purpose is to help demonstrate how multiple tasks can use the crt_task() to safely share the video display.
Upon receiving a unit at its shutdown semaphore, the task suspends itself to await deletion.
The timer_task() uses a software timer, maintained by the clock_task(), to determine when the program should be terminated. The task creates a one-second timer and waits for the timer to be signalled 20 times (20 seconds). The timer is actually a semaphore that has been set up so that the clock_task() sends a unit to the semaphore every second. The timer_task() calls rqreceiveunits to wait for 20 units to be sent to the semaphore. (A single unit from a 20-second timer could also be used.) When 20 seconds have elapsed, the task sends a unit to the initialization semaphore (init_sem). In this case, receiving a unit at init_sem not only indicates that the task has initialized but that it is also time to terminate the program. Since it initiates the shutdown signal process, it suspends itself.
The clock_task() is a fairly complete task. There isn't much more useful functionality that would be added to service a hardware clock. On the other hand, the crt_task() is only the core of what could be a sophisticated display servicing task supporting many features (e.g., video attributes, hotkeys, windows).
Remember, the example program is just that an example of task communications. It is by no means the only way to implement and organize tasks within an application program. Because multitasking operating systems are usually very complex, you should use good design and modular programming techniques. But don't let this scare you, once you become familiar with a multitasking operating system, such as iRMX, you'll wonder know how you ever got along without it.