The programs in this appendix illustrate the use of some of the features discussed in the book. The following programs are included:
“Mapping and Reading the Cycle Counter” illustrates the use of the cycle-counter.
“Getting the Time of Day Stamp” illustrates the use of gettimeofday() and shows how to test its precision.
“Interprocess Communication” illustrates some uses of arenas, semaphores, and interval timers.
“Probing the Address Space” displays the addresses assigned to a process address space, and illustrates some uses of mmap().
“Deadline Scheduling Subroutines” illustrates the use of schedctl(2) to set a deadline scheduling policy.
“Frame Scheduler Examples” describes the sample programs distributed with the REACT/Pro Frame Scheduler.
“Asynchronous I/O Example” illustrates the use of asynchronous I/O including four different methods of testing for I/O completion, and also shows process creation with sproc() and the use of semaphores and barriers.
“Guaranteed-Rate Request” demonstrates how to request a guaranteed rate of I/O transfer.
This program shows how to map the high-precision cycle counter into memory and sample it. By default, the file compiles to a library of the following functions:
getIPNumber() | Returns the model number of the CPU board in this system. For example, an Indy contains an IP22 board, so in an Indy, this function returns the number 22. |
mapTheTimer() | Uses mmap() to map the cycle counter into the address space. Returns the unit-value of the timer in picoseconds; for example returns 21000 in a Challenge where the timer unit value is 21 nanoseconds. |
timerBitCount() | Returns the number of bits of precision in the timer, which varies with the CPU board type, either 24, 32 or 64 bits. |
readTimer32() | Returns the least-significant (or only) word of the timer value. |
readTimer64() | Returns the timer value as a 64-bit unsigned integer (extended with 0-bits when necessary). |
When you define the compiler variable UNIT_TEST, it also compiles a main() that exercises the functions.
The following program test the precision of the time of day stamp returned by gettimeofday(). The function getTODdiff() contains an example call to gettimeofday().
The following sample program illustrates the use of some of the interprocess communication features of IRIX.
The program models a real-time data-collection program. The main process establishes an arena. Within the arena it creates a data structure that defines and manages a ring buffer. Then the main process uses sproc() to create three processes:
inputProcess() generates random-integer “input data” and stores it in the ring buffer. To simulate an unpredictable and varying input rate, the process “receives” bursts of from 1 to 16 data items. The average input rate is calculable (see the commentary in the code).
After generating a certain number of items, inputProcess() terminates. The number of items can be specified on the command line.
outputProcess()—of which two instances are created—takes data from the ring buffer. To simulate a steady average output rate, each process sets a repeating itimer and takes one data item each time the timer expires. The itimer interval represents the simulated “processing time” of a data item. It can be specified on the command line.
After starting the three processes, the main process waits for one to terminate (if there are no errors, inputProcess() finished first). The main process dumps the metering information from the lock and semaphores, and terminates the program.
The three simulated real-time processes communicate through two semaphores and a lock.
Semaphore semRBdata represents the number of data items now in the ring buffer. inputProcess() does the V operation, increasing the semaphore count with each input datum; outputProcess() does the P operation, decreasing the count with each output.
Semaphore semRBspace represents the number of empty slots in the ring buffer. inputProcess() does the P operation and outputProcess() does the V operation.
Lock lockRBupdate represents the right to alter the ring buffer index values. All processes set this lock before modifying the ring buffer, and clear it afterward.
The displayed metering data at the end of the program shows whether the output processes could keep up with the input process. The following example shows a case in which they did not keep up:
# /usr/sbin/npri -h 30 rbtest -t 3000 Lock information : LOCKDUMP at 0x4413a0 (last owner = 0) lock free lock meter: tries 7781 spins 0 hits 7781 lock debug: owner pid -1 semRBdata information : SEMADUMP of 0x4411c0 count 159 semaphore meter: value 159 nwait 0 maxnwait 1 psemas 3811 phits 3808 vsemas 3970 vnowait 3967 semRBspace information : SEMADUMP of 0x4412b0 count 0 semaphore meter: value 0 nwait 0 maxnwait 2 psemas 3972 phits 2275 vsemas 3812 vnowait 2115 detaching arena file |
When inputProcess() executed a P operation on semRBspace, it “hit” (passed through without waiting) only 2275 of 3972 times. In other words, more than a third of the time, the input process was blocked, waiting for an empty slot in the ring buffer. Similarly, outputProcess() performed 3812 V operations on semRBspace to release ring buffer slots. On 2115 of those times there was no process waiting. That means that 1697 times, there was a process (which must have been the input process) waiting on the semaphore. So on this particular machine, the simulated processing time of 3000 microseconds (specified on the command line as -t 3000) was too long to keep up with the input data rate.
The following sample program uses some generally unsafe coding tricks to get the addresses of segments for text, stack, library DSO and mapped data. It demonstrates the use of mmap() with /dev/zero, with default and with absolute segment addresses.
The following example contains two subroutines that simplify the interface to the schedctl() function for deadline scheduling. If the code is compiled with variable UNIT_TEST defined, it compiles a main() procedure that runs a test. Otherwise it compiles only the functions.
A number of example programs are distributed with the REACT/Pro Frame Scheduler. This section describes them. Only one is reproduced here; the others are found on disk.
The following example programs are distributed with the Frame Scheduler:
| simple | Several processes scheduled on a single CPU. | |
| mprogs | Processes loaded as independent programs dispatched by a Frame Scheduler. | |
| multi | Three synchronous Frame Schedulers running lightweight processes on three processors. | |
| driver | A pseudo-driver that demonstrates the Frame Scheduler device driver interface. | |
| sixtyhz | One process scheduled at a 60 Hz frame rate. | |
| memlock | The sixtyhz example with added code to lock memory and to skip the first minor frame to account for an initial cold cache. |
The code for each example is found in a subdirectory named for the example.
This example shows how to map a real-time application specification into a Frame Scheduler specification and also how to write the corresponding program using the Frame Scheduler API. The code is reproduced starting on “Code of Basic Example”.
The application consists of two processes that have to periodically execute a specific sequence of code. The period for the first process, process A, is 600 milliseconds. The period for the other process, process B, is 2400 ms.
![]() | Note: Such long periods are unrealistic for real-time applications. However, they allow the use of printf() calls within the “real-time” loops in this sample program. |
The two periods and their ratio determine the selection of the minor frame period—600 ms—and the number of minor frames per major frame—4, for a total of 2400 ms.
The discipline for process A is strict real-time (FRS_DISC_RT). Underrun and overrrun errors should cause signals.
Process B should run only once in 2400 ms, so it operates as Continuable over as many as 4 minor frames. For the first 3 frames, its discipline is Overrunnable and Continuable. For the last frame it is strict real-time. The Overrunnable discipline allows process B to run without yielding past the end of each minor frame. The Continuable discipline ensures that once process B does yield, it is not resumed until the fourth minor frame has passed. The combination allows process B to extend its execution to the allowable period of 2400 ms, and the strict real-time discipline at the end makes certain that it yields by the end of the major frame.
There is a single Frame Scheduler so a single processor is used by both processes. Process A runs within a minor frame until yielding or until the expiration of the minor frame period. In the latter case the frame scheduler generates an overrun error signaling that process A is misbehaving.
When process A yields, the frame scheduler immediately activates process B. It runs until yielding, or until the end of the minor frame at which point it is preempted. This is not an error since process B is Overrunable.
Starting the next minor frame, the Frame Scheduler allows process A to execute again. After it yields, process B is allowed to resume running, if it has not yet yielded. Again in the third and fourth minor frame, A is started, followed by B if it has not yet yielded. At the interrupt that signals the end of the fourth frame (and the end of the major frame), process B must have yielded, or an overrun error is signalled.
The code in directory mprogs is based on the code for the basic example (see “Basic Example”). However, the scheduled processes A and B are physically loaded as separate commands. The main program establishes the single Frame Scheduler. The real-time processes are started as separate programs. They communicate with the main program using SVR4-compatible interprocess communication messages (see the intro(2) and msgget(2) reference pages).
There are three separate executables in the mprogs example. The master program, in master.c, is a command that has the following syntax:
master [-p cpu-number] [-s slave-count] |
The cpu-number specifies which processor to use for the one Frame Scheduler this program creates. The default is processor 1. The slave-count tells the master how many subordinate programs will be enqueued to the Frame Scheduler. The default is two programs.
The problems that need to be solved in this example are as follows:
The frs-master program must enqueue the subordinate processes. However, since they are started as separate programs, the master has no direct way of knowing their process IDs.
The subordinates need to specify upon which minor frames they should be enqueued, and with what discipline.
The master needs to enqueue the subordinates in the proper order on their minor frames, so they will be dispatched in the proper sequence. Therefore the master has to distinguish the subordinates in some way; it cannot treat them as interchangeable.
The subordinate programs must join the Frame Scheduler, so they need the handle of the Frame Scheduler to use as an argument to frs_join(). However, this information is in the master's address space.
If an error occurs when enqueueing, the master needs to tell the subordinate so they can terminate in an orderly way.
There are many ways in which these objectives could be met. In this example, the master and subordinates communicate using a simple protocol of messages exchanged using msgget() and msgput(). The sequence of operations is as follows:
The master program creates a Frame Scheduler.
The master sends a message inviting the most important subordinate to reply. (All the message queue handling is in module ipc.c, which is linked by all three programs.)
The subordinate compiled from the file processA.c replies to this message, sending its process ID and requesting the FRS handle.
The subordinate process A sends a series of messages, one for each minor queue on which it should enqueue. The master enqueues it as requested.
The subordinate process A sends a “ready” message.
The master sends a message inviting the next most important process to reply.
The program compiled from processB.c will reply to this request, and steps 3-6 are repeated for as many slaves as the slave-count parameter to the master program. (Only two slaves are provided. However, you can easily create more using processB.c as a pattern.)
The master issues frs_start(), and waits for the termination signal.
The subordinates independently issue frs_join() and the real-time dispatching begins.
The example multi demonstrates the creation of three synchronized Frame Schedulers. The three use the cycle counter to establish a minor frame interval of 50 ms. All three Frame Schedulers use 20 minor frames per major frame, for a major frame rate of 1 Hz.
The following processes are scheduled in this example:
Processes A and D require a frequency of 20 Hz
Process B requires a frequency of 10 Hz and can consume up to 100 ms of execution time each time
Process C requires a frequence of 5 Hz and can consume up to 200 ms of execution time each time
Process E requires a frequency of 4 Hz and can consume up to 250 ms of execution time each time
Process F requires a frequency of 2 Hz and can consume up to 500 ms of execution time each time
Processes K1, K2 and K3 are background processes that should run as often as possible, when time is available.
The processes are assigned to processors as follows:
Scheduler 1 runs processes A (20 Hz) and K1 (background).
Scheduler 2 runs processes B (10 Hz), C (5 Hz), and K2 (background).
Scheduler 3 runs processes D (20Hz), E (4 Hz), F (2 Hz), and K3.
In order to simplify the coding of the example, all real-time processes use the same function body, process_skeleton(), which is parameterized with the process name, the number of the Frame Scheduler it is to join, and the “real-time” function it is to execute. In the sample code, all real-time functions are empty function bodies.
The code in directory driver contains a skeletal test-bed for a kernel-level device driver that interacts with the Frame Scheduler. Most of the driver functions consist of minimal or empty stubs. However, the ioctl() entry point to the driver (see the ioctl(2) reference page) simulates a hardware interrupt and calls the Frame Scheduler entry point, frs_handle_driverintr() (see “Generating Interrupts”). This allows you to test the driver. Calling its ioctl() entry is equivalent to using frs_usrintr() (see “The Frame Scheduler API”).
The example in directory sixtyhz demonstrates the ability to schedule a process at a frame rate of 60 Hz, a common target rate in visual simulators. A single Frame Scheduler is created. It uses the cycle counter with an interval of 16,666 microseconds (16.66 ms, approximately 60 Hz). There is one minor frame per major frame.
One real-time process is enqueued to the Frame Scheduler. By changing the compiler constant LOGLOOPS you can change the amount of work it attempts to do in each frame.
The example in directory memlock is almost identical to that in directory sixtyhz. (Use the diff command to display the altered code.)
The functional difference between the two examples is that the real-time process in memlock.c locks its own virtual address space so as to avoid page faults. Also, it executes one major frame's worth of frs_yield() calls immediately after return from frs_join(). The purpose of this is to “warm up” the processor cache with copies of the process code and data. (An actual application process could access its major data structures prior to this yield in order to speed up the caching process.)
The following program demonstrates the use some asynchronous I/O functions. The basic purpose of the program is to read a list of input files and write their concatenated contents as its output. However, it reads the input files using aio_read(), and writes the output files using aio_write() and aio_fsync(). In addition, it can be compiled in either of two ways,
to read and copy the input files one at a time, by calling a subroutine
to read and copy the input files concurrently, using a separate process for each input file
There is no functional advantage to using multiple processes. Doing so merely makes the example more interesting. It also demonstrates that, even though multiple processes ask for output at different points in the same file at the same time, the output is written to the requested offsets.
The reading and writing is done in one of four functions. The functions all have the following structure:
Initialize the aiocb for the type of notification desired. The type of notification is the principal difference between the functions: some use signals, some callback functions, some no notification.
Until the input file is exhausted,
Call aio_read() for up to one BLOCKSIZE amount from the next offset in the input file
Wait for the read to complete
Call aio_write() to write the data read to the next offset in the output file
Wait for the write to complete
Use aio_fsync() to ensure that output is complete and wait for it to complete.
The four functions, inProc0() through inProc3(), differ only in the method they use to wait for completion.
inProc0() alternates calling aio_error() with sginap() until the status is other than EINPROGRESS.
inProc1() calls aio_suspend() to wait for the current operation.
inProc2() sets the aiocb to request a signal on completion. When the signal handler is entered, it posts a semaphore.
inProc3() waits on a semaphore which is posted from a callback function.
You select which of the four function to use with the -a argument to the program. If you compile the program with the variable DO_SPROCS defined as 0, the chosen function is called as a subroutine once for each input file. If you compile with DO_SPROCS defined as 1, the chosen function is launched by sprocsp() once for each input file.
The following subroutine simplifies the task of requesting a guaranteed rate of I/O transfer. The file descriptor passed to function requestRate() must describe a file located in the real-time subvolume of a volume managed by XLV and XFS.