Chapter 10. Realtime Control Platform: 10.1 General Concepts About Programs
Chapter 10. Realtime Control Platform: 10.1 General Concepts About Programs
Chapter 10. Realtime Control Platform: 10.1 General Concepts About Programs
the aspect of the machine code generated by the compiler. Sequential programming is the most common way of writing programs. The term sequential indicates that the program instructions are given in a fixed sequence, one instruction after the other. The purpose of a sequential program is to transform input data given in a certain form into output data of a different form according to a specified algorithm (i.e. solution method, Figure 10-1). In a sequential program, the only entities are the data and the code to act upon them. No time constraints are given; the result of a run depends only on the input data and the properties of the algorithm. The algorithm of a sequential program can, in principle, be coded in any programming language. A sequential program acts like a filter on the input data. The filter abstraction is carried further in some operating systems (e.g. MS-DOS and UNIX) with device independence in program input and output. In such systems, the input/output of a program can be a file, a terminal screen or another program. In real-time programming, the abstraction of resource independence cannot be made; on the contrary, one has to be constantly aware of the environment in which the program is operating, be it a microwave oven controller or a robot arm positioner.
In real-time systems external signals usually require immediate attention by the processor. In fact, one of the most important features of real-time systems is their reaction time to input signals. The special requirements of real-time programming and in particular the necessity to react quickly to external requests, are not approached adequately with the normal techniques for sequential programming. The forced serial disposition of instruction blocks that should be executed in parallel leads to an unnatural involution of the resulting code and introduces strong ties between functions which should remain separate. We have already seen what problems may arise when two different program modules are bound together. In most cases it is not possible to build real-time systems using the normal methods for
sequential programming. In real-time systems, different program modules or tasks have to be active at the same time, that is, operate in parallel, where each task is assigned to a specific function. This is known as concurrent programming to stress the cooperation among the different program modules. The basic operating entity in real-time systems is the processes. There is a very important distinction between programs and processes. Programs are sets of information on how to operate on and transform the input data, while processes are programs in execution. A process consists of code (the program instructions), a data area where the process variables are stored and, depending on the actual implementation, a free work area (heap) and a stack (Figure 10-2). A program written in the same high-level language and then compiled and executed on different machines will lead to different processes each with its own code, data, heap and stack areas.
Each process is at all instants in a well defined state, unequivocally described by the contents of the CPU registers, the locations of its code, data and stack areas, and a pointer to the next instruction for execution in the code area. This basic information about a running process is called its canonical state or context. The existence of a context is a general fact, whereas what registers, states and pointers are actually part of the context depends on the processor used. The steps needed to transform a program in a process consist of storage on a computerreadable medium such as magnetic tape or floppy disk, compilation, linking, loading and execution. These steps are amply described in books on operating systems and will not be dealt with here. The description used for algorithm definition in the top-down programming method also provides a complete documentation of the system. The importance of good documentation cannot be overestimated; it is enough to consider that maintenance and corrections are the major expenses related to the entire lifetime of a program. Maintenance is much easier if good documentation is available; it might, on the contrary, turn out to be impossible if the documentation is poor or insufficient.
A common term in real-time programming is multitasking. A task is a small program or module and multitasking is the technique of letting them run concurrently. In a sense, multitasking is the practical side of concurrent programming, where the actual aspects of the target machine are taken into consideration. Concurrent programming is more difficult than sequential programming because the human capacity for following the development of interdependent processes and for examining their mutual interactions is limited. (Think about school studies in history: first comes national history and then Western or Eastern history in some chronological order. With this perspective, it is not natural to compare, for instance, what happened in different countries in 1848.) Real-time programming is based on concurrent programming and refers also to techniques to increase the efficiency and execution speed of programs: interrupt management, exception handling and the direct use of operating system resources. Real-time programs also require special testing methods.
systems are also supposed to be 'fair' in some sense, trying not to put any user at a special disadvantage even under heavy machine load conditions. The same does not hold for prioritybased real-time systems, where the processes are strongly differentiated. We will now focus on multiprogramming on a single processor because this is the most common and most important case of digital control applications. Many of the ideas can be transferred to the multiprocessor case. In multiprocessing, the basic entities are the processes or tasks and their contexts. The context of a process in execution can be 'frozen' at any time by saving the content of the CPU registers; while the initial process is suspended, the CPU can run other processes. To achieve multitasking on a single processor, the execution of each task is divided into several short intervals (Figure 10-3). The processor begins executing part of the first task, continues with part of the second, of the third, and so on. A time interval is assigned to each task, so that, for example, for 10 ms the processor is dedicated to the first task, then switches to the second, the third, etc. The macroscopic effect of the CPU time division among the processes is the parallel execution of n processes, each on a fully dedicated CPU of capacity 1/n (that is, n times slower) compared to the original one.
Figure 10-3 the principle of multitasking, (a) Macroscopic effect; (b) CPU time division.
The execution of several tasks on different CPUs or on the same CPU are two different realizations of the same logical principle: in the first case the processes are distributed spatially, in the second they are distributed in time. Apart from overhead due to scheduling and intertask communication, if n processes run on k processors, each process is ideally assigned to a processor of capacity k/n of the original one. A basic multitasking system consists of a procedure, implemented in hardware or software or a combination of both, to save the context of a process on the stack or at defined memory locations, and restore the context of another process to continue its execution where it was halted. A system program called a scheduler selects the next process to execute from among the loaded processes. The process switch operations are time critical and must be realized with maximum efficiency.
In processors not designed for multiprogramming, the process exchange module must save all registers and other context parameters on the stack and then save the pointers to the stack in a protected data area. Processors designed to support multiprogramming have compact instructions to save and recall the content of all the registers. When the context of a process is saved, it is not necessary to also save the process variables. These are located in the process memory area which has to be protected by the operating system against changes by other processes. The same does not hold for the CPU registers, which are shared by all processes and whose content is changed all the time. To be able to halt CPU execution at regular intervals in order for a different process to be executed, a timing device external to the CPU is needed. A system timer sends interrupt signals (ticks) to the processor at defined intervals; typical rates are one tick every 1 ms on very fast processors down to one tick every 50 ms on slower machines. At each tick, the CPU briefly suspends its operations to check whether the current process has to be interrupted and a new one loaded. The action that forces a running task to halt its execution in order to allow another task to run is called preemption. The tick interrupt is not the only way to stop a process and transfer execution to another. A process can stop on its own either because it has reached the end or because it is idle waiting for an event, such as an I/O operation with a physical device which would take several ticks to complete.
Figure 10-4 shows which changes from one state to another are possible. The defined operations are: 1. From 'Removed' to 'Ready'. The process is loaded from disk to RAM memory, with relocation of all the relative addresses and assignment of the work areas (code, data, heap, stack) with the related pointers.
2. From 'Ready' to 'Running'. The process is selected by the scheduler to run and is assigned CPU control via the process switch module. 3. The opposite change, from 'Running' to 'Ready', is controlled by the same process switch module when it is time to let another process run. 4. From 'Running' to 'Waiting'. The process enters an idle state to wait for an external event, often an I/O operation with units much slower than the CPU, or the process must wait for a determined period of time, because of an explicit instruction. 5. From 'Waiting' to 'Ready'. When the awaited event has occurred or the required time has elapsed, the process is not immediately executed but is put instead in 'Ready' state. The scheduler will later determine when the process can be executed again. 6. When the end instruction of a program is reached, the operating system may eliminate a process from central memory.
of the system. If the time slices are short (~ 10-20 ms), the system is quick to react to external events such as interrupts or terminal input, but the process scheduling overhead gets an important share of the total CPU time. With longer time slices, the processes execute more effectively with less overhead, but the reaction time gets appreciably slower. The length of the time slice influences strongly the global performance of a system only when the running processes have similar priorities. Real-time operating systems do not only depend on the time slice. The operating systems are designed to react immediately to situations when changes in the process priorities may have occurred, such as the arrival of an external interrupt or the completion of a disk read operation. For each such event, the relative priorities of the waiting processes are computed anew and, if necessary, a process switch is performed. The scheduling of processes based on priorities works correctly only when the various tasks have different priorities. It does not help to give maximum priority to all processes, as this certainly does not increase the execution speed of the CPU. Each process would still have to wait until all other processes have been executed before it can run again. A system where all tasks have the same priority works in a round-robin fashion. The best results in the operation of a real-time system come when the relative priorities are correctly defined and balanced.
The programmer must explicitly define the program segments. Each program segment will execute and then call the next to continue. This method is straightforward but not particularly efficient because of its dependence on external disk memory. Other memory management schemes are transparent to the processes, in other words, they do not have to be taken into consideration when writing the program. The most straightforward method for memory management is division of the available memory in partitions, each assigned to a process (Figure 10-6). The size of the partitions is defined at the time of system initialization; it may be different for different partitions. When a process is loaded, it is put in the smallest partition large enough to contain it. If no partition is available, the process may have to be broken and loaded in several partitions. Many different algorithms are available to allocate the memory to different processes. With partitions, once the memory space has been allocated at machine start-up, it cannot be reclaimed and reused. To be able to utilize all available memory over and over again, on middle and large-sized computers the virtual memory management technique is commonly used. Virtual memory is based on the assumption that the total size of processes and data may be larger than the RAM space at disposal.
A mass memory unit (e.g. a disk) that allows fast data exchange with central memory is used. The mass memory unit is large enough to hold the total memory space required by all processes. With virtual memory, a process may address a space larger than the one at disposal in central memory. The main reason for which virtual memory is used is economic. The central memory is still much more expensive per unit of stored information than secondary mass memory. The central memory is also volatile and draws electric power, which both costs and heats up the system electronics. In case of a system crash, it is possible to restore operations almost to the point where the crash occurred if a constant copy of the processes is stored on disk. If a crash or a power failure occurs when the whole system is only loaded in RAM, then the whole process state is wiped out. In real-time systems, virtual memory is of interest only when it is fast and efficient. To ensure fast reaction, the most important processes can be permanently stored in central memory partitions and less important ones be called at need. Another important consideration related to the use of secondary memory in real-time applications is whether it can be used in the operating environment. For instance, disk units (hard disks and floppies) cannot be used in environments with vibrations and shocks. One of the major differences between multiuser and real-time operating systems lies in file management. The most important issues in multiuser systems are directory structure and file protection. The management and protection of directories, with the related controls and verifications at each access imposes an overhead seldom acceptable in real-time systems. A
simplified use of mass memory, mainly for storing logs and reports and common ownership of all the tasks, does not warrant the need for a complex file system. There is a strong analogy between CPU control allocation, resource protection and bus master arbitration in a multiprocessor bus system. In all cases we deal with a limited resource (CPU time, memory, the bus) which has to be divided among several requesting units in a safe, efficient and fair manner. The criteria for assigning the resource, be it a simple roundrobin scheme or a more complicated priority-based allocation, must avoid deadlocks and lockouts, assign the resource to all units requesting it and ensure maximum throughput for the whole system. The most sophisticated operating systems allow the tuning of CPU and memory management parameters to achieve optimal performance. Process priorities and time slice length should be chosen and combined in order to increase the general performance, or throughput, of a system.
and then modify it. If one process is interrupted just after the read operation and before it could change its value, the other process may modify the variable while the first is waiting. The first process then resumes execution without knowing that the variable has changed and then proceeds to work on an old value. After all, in a multitasking environment a process does not know when it is interrupted and when it starts again. The problem is that the variable is accessed by both processes without restrictions. It does not help to check the formal correctness of different programs if the effects of the possible interactions among processes are not taken into account. A situation where the result depends on the relative random order of process execution is called a race condition. This problem, known as resource protection, is central to the whole theory of multiprogramming. The variable accessed by both processes has to be considered as a resource to be protected from their concurrent action. In order to avoid race conditions, access to the resource by the processes should not be free and indiscriminate but instead follow determined precedence rules. The problem of resource protection has been studied for a long time and different solution strategies have been devised. These strategies vary with the type, technology and, most of all, access speed of the resource to be protected. Slow units, which tend to be used for quite a long time by the process (e.g. printer or magnetic tape unit) are usually allocated exclusively to the requesting process on the basis of a precedence queue. Alternately, a resource is permanently allocated to a single process (called a spooler, from simultaneous peripheral operations on line) that accepts as input from other processes the names of the files or other data objects, organizes them according to defined precedence criteria and sends them one at a time to the requested unit. Spoolers are commonly used in multiuser operating systems. Other methods are used for the protection of resources with very short access time and are continuously referred to by different processes, for instance, variables in central memory, records in a file or I/O interfaces on a data bus. This section is mainly devoted to such methods and will show different approaches together with their consequences. The principal rule for resource protection is that a process should never change the state of a shared resource while another process has access to it. Or, more generally: a process should never access a resource currently used by another process, independently of whether or not it is going to change its state. The second rule is more restrictive but simplifies practical control operations because it is not necessary to keep track of what operations each process is going to perform on the resource. The major difficulty in resource protection arises from the fact that in multitasking systems all processes can be interrupted at any time to allow other processes to be executed. The exact instants when the interruptions take place are not under the control of the programmer
and cannot be known in advance. A first, elementary, method to guarantee resource protection is to disable interrupts while a resource is accessed. This effect is achieved by blocking the reaction of the processor to the interrupt signals. As process switching is initiated via an interrupt, disabling the interrupt prevents process switching as well. A process is then guaranteed to work without interruptions when it is in a 'protected' area. On the other hand, interrupts should normally be enabled to ensure quick reaction to special conditions requiring immediate attention. In a control system, part of the program modules are controlled by interrupts and disabling them can inhibit the processor from reacting to fully legitimate requests. In some systems, interrupts are not saved after they have occurred so they may go unnoticed if they arise when handling is disabled. Furthermore, interrupt disabling does not work in systems with several CPUs; a task running on a different processor might 'sneak' in the protected resource from behind. Interrupt disabling should thus be used with extreme care and only when no other solution is feasible. It should also be limited to a few code instructions.
Only one process at a time has access to a protected resource. The processes remain mutually independent. Stopping one process should not hinder the other process(es) from continuing their execution.
The above statements relate to two correctness properties, safety and liveness. Safety means that access limits have to be respected, so that a protected resource is not accessed by more than one process at a time. Liveness indicates that a program at some time will do what it is supposed to or, in other words, that it will not hang indefinitely. Safety can always be obtained by giving up some concurrency between tasks; the safest programs are in fact strictly sequential, where no parallel access to a resource from different parts of the program is possible.
When a process intends to enter a critical region, it will receive permission to do it in a finite time. Only one process at a time may enter or stay in a critical region. A process remains in a critical region for a finite amount of time.
It is the responsibility of the compiler to verify that the variables in v are referred only from within s. The run-time operating system should check that only one s module at a time is executed and that no interrupts take place during this execution.
10.3.4 Deadlock
Deadlock is the state when some or all processes in a system are halted waiting for something to happen. If this 'something' can only be initiated by another waiting process, then all processes wait endlessly in a deadlock condition (Figure 10-7). A different case of deadlock is when one or more processes still run but fail to make any progress. This situation is called starvation; this is the case when running processes continuously test the value of a condition variable which is not going to be changed because the other processes are also busy testing. In other words, deadlocked processes are in the 'waiting' queue and starving processes are 'ready' or 'executing', but do not make any progress.
It has been shown that it is necessary that several conditions be true at the same time for deadlock to occur. If any of these conditions does not exist, deadlock cannot happen.
Mutual exclusion. There are system resources which can be used only by one process at a time. Non-preempted allocation. A resource can be released only by the process that allocated it. Successive allocation. A process can allocate the necessary resources one at a time. Inverse-order allocation. The processes can allocate resources in a different order.
These four principles indirectly give the key for avoiding deadlock situations; it is sufficient that one of them is not true to make deadlock impossible. The first principle cannot be changed as mutual exclusion is the principal condition to guarantee the ordered management of shared resources. The second principle requires that the operating system recognize a deadlock situation and react accordingly, for instance, by forcing the release of a resource by a process. But recognition without ambiguity of a deadlock situation is very difficult and the forced release
of some resources may lead to other types of practical problems. The forced release of a resource is interesting only with internal resources (variables stored in RAM memory) and in situations seldom leading to deadlock. Following the third principle, the alternative to allocating one resource at a time is to assign all needed resources at once. This solution is not feasible, as many resources would remain unused for a longer time, or allocated for the whole execution of a process when their actual use may be more limited. Violation of the fourth principle leads easily to deadlock conditions. If two processes need two resources A and B where one allocates them in order A-B and the second in order B-A, it is sufficient that the first process allocates A, is interrupted and control passed to the second process, which allocates resource B, for deadlock to be verified. Each process is now waiting endlessly for the other to release its resource.
10.4.1 Semaphores
We have seen that the introduction of extra variables for resource protection is not free from problems, as the protection variables become common resources themselves. The root of the problem is that the operations of check and change of the value of a variable are separated and can be interrupted at any time. Moreover, continuous tests on the values of the variables waste CPU time. The semaphore was proposed by Dijkstra (1968) as a basic synchronization
principle to overcome the problems related with protection variables. The semaphore is probably the most common method for process synchronization. A semaphore is an integer variable which can only be 0 or take positive values. Its initial value is defined in its first declaration in a program and is usually equal to 0 or 1. Semaphores which can take only values 0 and 1 are called binary semaphores. Two operations are defined on semaphores: signal and wait. The signal operation increases the value of the semaphore by 1; it does not have any other effect on the executing process. The wait operation leads to different results, depending on the current value of the semaphore. If this value is greater than 0, it is decreased by 1 and the process calling the wait function can proceed. If the semaphore has value 0, execution of the process calling wait is halted until the value is increased again by another process with a signal operation. Only then is it possible for wait to decrease the value of the semaphore and proceed with execution. It is very important that the operations of test and decrement of the wait function are executed in one step only. The operating system is not allowed to break the execution of wait after the test on the value and before the decrement operation. The semaphore wait has the same operational significance as the function test_and_set. The names of the functions signal and wait have mnemonic meaning: signal is associated with a 'go' to a process and wait is self-explanatory: if the semaphore has value 0, the process must wait for a signal. If several processes are waiting for the same signal, only one of them may continue execution when signal is given. Depending on the implementation, the processes may wait in an ordered 'First In, First Out' queue or also be selected at random to proceed. The semaphore alone does not imply a given wait and execution order. One semaphore variable is sufficient for access coordination, compared with the different flags of the preceding examples. The processes executing wait are put in the waiting queue and do not have to consume CPU time to test the state of the semaphore. The operating system releases a process when signal is executed. With the use of semaphores, the two processes can access the common resource in an ordered manner. No unnatural bonds are introduced: if one process runs faster than the other one, it will just access the resource more often in a given time interval. A process is forced to wait for the other one only when the latter is in the protected area. Liveness is also guaranteed. If a process should for any reason stop running, provided this happens outside the protected area, the other is not hindered from continuing its execution.
10.4.2 Synchronization
The semaphore can help in synchronizing related activities. For instance, if a process has to operate on data only after this has been read from an external port, the code can have the following aspect:
Process read data while true do begin (* get new data *) signal(data_available); end;
Process change data while true do begin wait(data_available); (* process new data *) end;
This solution has the advantage that, if the data processing algorithm is not ready with execution and new data is available, the presence of the data is indicated with a semaphore value higher than 0. The processing routine can then catch up later with the lost data. Synchronization errors due to incorrect use of semaphores may be difficult to trace. A process not executing a wait instruction can enter a protected region together with another process, leading to unforeseeable results. Of course, it cannot be said that such an error will show up during testing, and it may never even happen during the whole lifetime of a system. It is easier to find the opposite error: a missing signal operation should, at a certain point, lead at least one process to halt, which is promptly detected. A compiler does not usually have the possibility of checking whether semaphores are used correctly, i.e. if wait operations are matched in other points by signals, and if the semaphores in programs is arbitrary in the same way as other instructions, and depends on the algorithm logic. The burden to verify the correctness of the code lies, then, with the programmer. The use of structured programming methods helps considerably in this task.
Buffer
A very important problem with an elegant solution based on event variables is the circular or bounded buffer (Figure 10-8). A circular buffer is a finite memory area used for data exchange between two or more processes. Applications of the circular buffer are found in communication problems, where the relative speeds of the transmitting process, the communication channel and the receiving process are different. The processes operating on the circular buffer are of two kinds: producer and consumer. The producer writes data into the buffer and the consumer reads data out of it. Producer and consumer must work independently from each other. Both have bounds: the producer can operate only when it has sufficient space at its disposal to insert new data and the consumer may read data only if this is present in the buffer. The producer must stop when the buffer is full, the consumer when the buffer is empty. The circular buffer is a clear example of a common resource for which the normal protection rules hold: when a process writes or reads data, the other must wait. The buffer is protected with one semaphore.
When several processes work in mutual cooperation, they need to exchange information. A multitasking operating system must provide an adequate method for this purpose. Data exchange should be transparent for the processes, in other words, it should not influence the data that have to be transferred. The data and the communication format should be defined within the processes and not depend on the particular communication method used.
10.5.2 Monitors
The 'classic' method for resource protection and interprocess communication is the monitor. Monitors are a theoretical construct introduced to simplify the writing of programs that exchange messages with each other or which need to be synchronized. The monitor consists of a reserved data area and associated procedures with exclusive right to operate on the data. External procedures are not allowed to access the monitor data area directly, but have to call monitor procedures; in its turn, the monitor gives service to only one external procedure at a time. It is the responsibility of the operating system to check that execution of a monitor procedure is not interrupted by another procedure from the same monitor. In this way, the monitor can guarantee that execution of one called procedure is completed before another procedure has access to the same data area. Monitors are implemented as critical regions with their reserved access routines. Internally monitors use semaphores to control access to their own data areas. The main advantage of
monitors is that the details of data protection and task synchronization are left to the operating system. Monitors have been proposed in some multitasking languages (e.g. Concurrent Pascal), but they are not implemented in any commonly used programming language. It is often possible to build one's own monitor structures in programming languages like C and Pascal, when a protection and synchronization mechanism is available. In such a case thorough testing of the monitor procedures is imperative before using them.
10.5.3 Mailboxes
A different communication method that allows data exchange and process synchronization at the same time is the mailbox. A mailbox is a data structure oriented to messages which can be deposited and collected (Figure 10-9). Different mailboxes may be defined within the same system to allow the exchange of different types of messages. In many operating systems (e.g. VAX/VMS) mailboxes are considered to be logical files and the access procedures are similar to those that access physical store devices. The allowed operations on mailboxes are: creation, opening, message writing, message reading, closing, deleting. Mailboxes do not have an independent structure. They are located in central memory or on disk, and exist only as long as the system is powered up and operating. If they are physically located on disk, mailboxes are classified as temporary files, to be deleted at system shutdown. Mailboxes do not have generic identifiers or names; they are labelled with logical identifiers (most often numbers) defined when they are created. All processes that use mailboxes address them with their logical identifiers.
The relation among the principles is of practical importance when a system offers only one method and the others have to be derived from it. Message passing and access to common memory areas is slower than the control and update of semaphore and event variables, and involves data processing overhead. For each function, the most straightforward implementation method should be chosen and strange constructs should be avoided as much as possible. When it is possible to choose from among different synchronization and communication functions, the function most apt to solve the specific problem should be used; the resulting code will be clearer and probably even faster. It is very important to consider how efficiently the solutions are implemented in practice in the actual software environment.
The execution flow is not only determined by the processor but also by external events Normal programs act on data; real-time programs act on data and on signals. A real-time program may explicitly refer to the time.
There are timing constraints. Failure to compute a result within a specified time may be just as bad as computing a wrong result (the right data too late is wrong data). A typical predictable response time of 1 ms is generally required, in some cases even 0.1 ms may be necessary.
q
The result of a real-time execution depends on the global state of a system and cannot be predicted beforehand. A run is not terminated when the input data has ended. A real-time process waits for new data to be available.
The particular aspects of real-time programming require the use of special techniques and methods, which are not necessary in sequential programming. These techniques are mainly related to control of program execution flow from the external environment and from time.
The most important of them are interrupt interception, exception handling and the direct use of operating system functions.
performed. Their definition should not be postponed until coding time, otherwise conflicts between program modules and risk of deadlock are unavoidable consequences. The software should be built as for an operating system: in a modular and layered fashion, as this considerably simplifies the construction of complex systems. The main objects of attention are the interfaces rather than the content of single modules. It is very important to define with precision interfaces or interaction points between the modules. These points are used for synchronization and communication between the processes. For the tasks to exchange information in shared memory areas or with the help of messages, the exact area structure and message format must be defined in advance. This does not mean that changes in their definition might not take place after software development has started, only that the later they are done, the more expensive they will be in terms of code rewriting, testing, etc. On the other hand, it is to be expected that some changes will be made anyway in the course of software development, as insight into the problem increases with progress on the work.
Read path data from disk. Compute next position. Read actual position from sensors. Compute appropriate control signal for positioning. Execute control action. Verify that reference and actual positions are within the allowed range.
Accept data from operator. Stop on emergency (asynchronous command, interrupt driven).
The tasks are sequential programs. They have the aspect of closed loops repeating indefinitely, continuously processing the data and the signals at their input. At some point in the code there is usually an instruction to make the loop wait for an external event or for a given time. The code is structured in such a way that the end instruction is never reached: while true do (* repeat forever *) begin (* handling routine *) wait event at ^2,28 (* external interrupt *) (* handling code *) .. end; (* handling routine *) end. (* never reached *) Each module is developed indicating clearly the areas where protected resources are accessed. Entering and exiting those areas is coordinated by some method: semaphores, messages or monitor. In general, a program in a protected area must stay safe there until it leaves the area. Interruptions in the execution of the process should not influence the resources in the protected area. In this way, the chances that the module behaves correctly are increased. Memory allocation by the processes will have to be considered. If all the modules do not fit in the memory together, they will have to be divided into segments to be loaded in at request. A common technique is overlaying, where a main module permanently resides in memory and reads from disk storage overlays with code and/or parameters for some specific operations as described. In real-time systems, the processes might have to access common subroutines. In one solution, the subroutines are linked together with the separate tasks after compiling, but this means that several copies of the same code are loaded in memory.
A different approach is to load in memory only one copy of the subroutines, but still access them from several programs. Such subroutines must be reentrant, that is, they can be interrupted and called several times without interference. Reentrant code operates only on the internal registers and on the stack; it does not address any fixed memory location. During execution, there will be one active stack for each process, so that a reentrant module shared by different processes can be interrupted at any time and restarted from a different position in its code, using a different stack. A reentrant procedure can thus be found in many different process contexts at the same time.
10.6.3 Priorities
Many of the support systems for multiprogramming have the possibility of assigning execution priorities to the different tasks. In many cases, priority assignment is dynamic, which means that the priorities may be modified by the processes as well as by the operating system. Other systems place restrictions on the definition and the later change of process priorities. The most important modules, or the ones which should have fast execution response, get higher priority. It is necessary to pay attention to the conventions in the system used, whether highest priority is associated with a higher or lower numerical value. Priorities have a relative meaning and make sense only if they are different from one module to the other.
When the CPU transfers control to an interrupt handler, it might save only the pointers to the code area of the running process. It is the duty of the interrupt handler to save, in temporary buffers or on stack, all registers it is going to use and restore them at the end. This is a critical operation, and it might be necessary to disable interrupt servicing under execution of the first instructions of the handler in order to avoid the handler itself being interrupted in turn. In interrupt management a very important factor is the response time, which should obviously be as little as possible. The response time is the sum of the interrupt latency (how long it takes for the interrupt to get attention) and the time needed for a context switch, until the interrupt handler actually runs. The typical system load also plays a role. If the workload is so distributed that the CPU has to service many interrupts at the same time, new ones will have to be queued until the CPU is available. Interrupt service routines should be as compact and short as possible. If a complex action is needed following an interrupt, it is better if the action is performed by a regular process. The interrupt service routine should do only the minimum necessary, for example, get an input value and then pass a message to the other routine, signalling that an interrupt has occurred and service is requested. It is always good practice to write reentrant code for system routines and for interrupt handlers. In this way, conflicts are avoided in case a handler is interrupted and called again before it has terminated its execution in the first context. A problem similar to interrupt servicing is reaction to exceptions, i.e. unusual conditions that result when the CPU cannot properly handle the execution of an instruction and that hinder the normal continuation of a process. Examples of exceptions are division by zero and addressing a non-existing memory location. Names for different kinds of exceptions are also traps, faults and aborts.
The common handling of exceptions by an operating system is the termination of process execution and indication of the error situation with messages written in clear text on the device used for the output messages. While acceptable in interactive multiuser sequential processing, in real-time systems the abrupt halt of a process must be avoided. Think about the possible consequences if a microprocessor-controlled fly-by-wire or car automatic braking system (ABS) should halt because of an unexpected divide-by-zero exception. In real-time systems all possible exceptions should be analysed beforehand and appropriately handled. A very tricky aspect of exception handling is the verification that an exception does not arise again. Put another way, exception handling must address the cause and not the symptoms of the abnormal situation. If an exception is not handled correctly, it may arise again prompting the processor to jump to its specific handling module. For instance, the divide-by-zero exception handler must check and modify the operands and not just resume operations to the point before the fault took place. This would lead to an indefinite loop. The effective memory address of any program module is known only at loading time. At system start-up, a module writes the memory addresses where the interrupt handlers are loaded in the interrupt service table. The interrupt routines are then accessed by referencing this table. Exception handling modules are written in a fashion similar to interrupt handlers. Their addresses are put in the interrupt address table at predefined locations. The possible exceptions and the pointer storage locations depend on the actual system.
When one of these functions is executed, the operating system puts the process in a waiting queue. After the requested time has elapsed, the process is moved from the waiting queue to the ready queue. The worst, but not uncommon, method to solve a 'time-waiting' problem is to introduce a
closed loop to check the system time variable in the so-called busy-wait: repeat (*do nothing*) until (time = 12:00:00);
In general, these active waiting loops are nothing else but a waste of CPU time and should be avoided. But there are cases where reality looks different. In a system where an A/D conversion takes 20 s and a process switching operating 10s, it is more economic to run as busy waiting for the 20s before new input data is fetched than to start the task exchange procedure implicit in a 'well-behaved' wait operation. Each case is judged on its own; obviously they require advanced system knowledge and the correct feel. An important aspect of processes started periodically (such as filtering or regulation algorithms) is the accumulated time error. This depends on the fact that a process is not executed immediately after it is moved out of the waiting queue but has to wait for different time intervals in the queue of executable processes until its execution turn arrives (Figure 7.12). Requested and real execution time are not the same.
Accumulated time errors can take place if the running time for a new activity is computed as: new execution time == end of old execution time + interval
The latter is an example of an instruction like wait 10 seconds written at the end of a loop. The correct solution is obtained by using the equation: new execution time == old reference execution time + interval
The principle appears from Figure 10-12, where the nominal times are drawn on the x axis. As absolute time is taken as reference, accumulated time errors are avoided. Running time efficiency is one of the most important aspects in real-time systems. The processes must execute quickly and compromises between good and structured versus time-
efficient code often have to be made. It is a fact of life that if short-cuts are needed to achieve some result, they will be taken anyway. If they can not be avoided, good documentation on what is done and why is imperative.
Figure 10-12 (a) The wrong way to execute periodic tasks (it leads to accumulated time errors); (b) the correct solution (it does not lead to accumulated time errors).
End