output); pstricks (most diagrams); hyperref (hyperlinking for Acrobat PDF format).
AMS font ..... ahttp://www.usq.edu.au/users/leis/units/70935/935link.html .....
states are typically labeled with capital letters or short descriptions to represent.
70935 Real-Time Systems Faculty of Engineering Bachelor of Engineering
Study Book
Written by
John Leis (Modules 1, 3-9) BEng, MEngSc, PhD Senior Lecturer The University of Southern Queensland
Mark Phythian (Modules 1, 2) BEng, MEng Lecturer The University of Southern Queensland
Published by
Distance Education Centre The University of Southern Queensland Toowoomba Qld 4350 Australia
http://www.usq.edu.au
Copyrighted materials reproduced herein are used under the provisions of the Copyright Act 1968 as amended, or as a result of application to the copyright owner.
No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by any means electronic, mechanical, photocopying, recording or otherwise without prior permission.
Camera-ready copy produced using LATEX 2" by the author. Style file supplied by Ted Siebuhr, DEC. MATLAB was used for the majority of PostScript graphs. The following LATEX 2" packages were utilized: dvips (PostScript output); pstricks (most diagrams); hyperref (hyperlinking for Acrobat PDF format). AMS font system used for some mathematics.
TABLE OF CONTENTS PAGE
Module 1 Real-Time Systems
0
1.1 Module Overview
1
1.2 Introduction
1
1.3 Real-Time Concepts
2
1.4 What is an “Operating System”?
2
1.5 What is a “Real-Time System”?
4
1.6 Terminology
5
1.6.1 Systems
5
1.6.2 Events
7
1.7 Real-Time Applications
8
1.8 Compilers for Code Examples
9
1.8.1 Platforms 1.8.2 Gnu Compilers
9 10
1.9 Links to Real-Time Systems Information
11
1.10 Module Summary
12
Module 2 Real-Time Software Design
0
2.1 Module Overview
1
2.2 Introduction
1
2.3 The Software Life Cycle
2
2.3.1 The Concept Phase
3
2.3.2 The Specification Phase
3
2.3.3 The Design Phase
4
2.3.4 The Programming Phase
5
2.3.5 The Test Phase
5
2.3.6 The Maintenance Phase
6
2.4 The Real-time System Specification and Design Techniques
7
2.4.1 Descriptive Techniques
7
2.4.2 Mathematical Techniques
8
2.4.3 Procedural Techniques
9
2.4.4 Structural Techniques
12
2.4.5 State-based Techniques
15
2.5 Implementation of Real-Time Kernels
19
2.5.1 The Function of a Real-Time Kernel
19
2.5.2 Polled Systems
19
2.5.3 Phase-Driven and State-Driven Systems
20
2.5.4 Interrupt Driven Systems
25
2.5.5 Types of Multi-tasking Systems
30
2.5.6 Foreground / Background Systems
30
2.6 Module Summary
31
2.7 Self Assessment Questions
31
Module 3 The C and C++ Programming Languages
0
3.1 Module Overview
1
3.2 Introduction
1
3.3 The C Language
2
3.3.1 Starting
2
3.3.2 Compiling C Programs
2
3.3.3 Basic Structure of a C Program
10
3.3.4 Data Types
13
3.3.5 Variable Scope
15
3.3.6 Pointers
18
3.3.7 Command-Line Arguments
19
3.3.8 Loading & Saving Data
19
3.3.9 Data Structures
23
3.3.10 Bitwise Operators
24
3.4 Advanced Topics
24
3.4.1 Portability
24
3.4.2 Makefiles
25
3.4.3 Public-Domain C Resources
27
3.5 The C++ Language
31
3.5.1 Compiling C++ Programs
31
3.5.2 Superficial Differences
32
3.5.3 Abstract Data Types
33
3.5.4 Object-Oriented Principles & Concepts
35
3.5.5 Object Definitions in C++
37
3.5.6 Input/Output
39
3.5.7 Declaring Classes and Creating Objects
43
3.5.8 Inheritance and Derived Classes
44
3.5.9 Setting Attributes
46
3.5.10 Operator Overloading
46
3.5.11 Object Arrays
47
3.5.12 Functions
49
3.5.13 References
50
3.5.14 Object Pointers
51
3.5.15 Stream Input/Output
52
3.6 Module Summary Module 4 Coding Techniques
55 0
4.1 Module Overview
1
4.2 Introduction
1
4.3 Error Handling
2
4.4 Assembly Language
3
4.5 C to Assembler
6
4.6 Calling Assembly Code
9
4.7 Software Optimizations
14
4.7.1 Constant Folding
15
4.7.2 Constant Propagation
17
4.7.3 Strength Reduction
18
4.7.4 Dead Code
19
4.7.5 Use of Registers
19
4.7.6 Loop Unrolling
20
4.7.7 Use of Pointers
22
4.7.8 Loop Invariance
25
4.7.9 Function Inlining
25
4.7.10 Data Alignment
26
4.8 Memory Allocation
27
4.9 Memory Copying and Searching
33
4.10 Module Summary
35
Module 5 Algorithms
0
5.1 Module Overview
1
5.2 Introduction
1
5.3 Arrays and Buffers
1
5.4 Circular Buffers
8
5.5 Linked Lists
16
5.6 Binary Trees
26
5.7 Module Summary
31
Module 6 Multitasking
0
6.1 Module Overview
1
6.2 Multitasking
1
6.2.1 Processes
2
6.2.2 Threads
3
6.2.3 Examining System Processes
4
6.3 Multitasking Examples
6
6.3.1 Processes under Unix
6
6.3.2 Processes under Windows NT
8
6.4 Threads
11
6.4.1 Threads under Unix
14
6.4.2 Threads under Windows NT
17
6.5 Module Summary
21
Module 7 Interprocess Communication
0
7.1 Module Overview
1
7.2 Interprocess Communication
1
7.3 Command Arguments
2
7.4 Pipes
3
7.4.1 Pipes under Unix
3
7.4.2 Pipes under Windows NT
7
7.5 Shared Memory
12
7.5.1 Shared Memory under Unix
12
7.5.2 Shared Memory under Windows NT
15
7.6 Windows Interprocess Communications
19
7.7 Module Summary
26
Module 8 Process Synchronization and Timing
0
8.1 Module Overview
1
8.2 Process Notification
1
8.2.1 Unix Signals
1
8.2.2 Windows NT Events
2
8.3 Atomic Resource Locking 8.3.1 Lock Files 8.4 Semaphores 8.4.1 Unix Semaphores 8.4.2 Windows NT Semaphores 8.5 Module Summary
5 6 7 7 10 21
Module 9 Video Graphics and Animation
0
9.1 Module Overview
1
9.2 Video Subsystem
1
9.3 Animation Effects
5
9.3.1 Direct Repaint Approach
5
9.3.2 Memory-Copy Approach
8
9.4 Module Summary
12
Abbreviations ASCII GUI CPU CISC RISC DSP OS DOS NT DLL POSIX IEEE I/O IP TCP TCP/IP
American Standard Code for Information Interchange Graphical User Interface Central Processing Unit Complex Instruction Set Computer Reduced Instruction Set Computer Digital Signal Processor (or Processing) Operating System Disk Operating System Windows NT (New Technology) Operating System Dynamic Link Library Portable Operating System Interface Institute of Electrical and Electronics Engineers Input/Output Internet Protocol Transmission Control Protocol Generic term for TCP and IP protocols
Module
1
REAL-TIME SYSTEMS
Module 1 – Real-Time Systems
1.1
1.1
Module Overview
This module gives an overview of the field of real-time operating systems. As such, it forms a basis for what is presented in the following modules by introducing many of the important concepts involved in studying operating systems, and in particular real-time operating systems. The examples given in later modules are aimed at giving concrete, working examples of the principles discussed in this module. The first part of this module discusses some more general themes, which are expanded upon in the later modules:
The main concepts which underpin operating systems. The extension of those concepts to real-time operating systems.
The latter sections of this module outline the materials which are necessary to complete the suggested exercises:
Obtaining and installing a C++ compiler. The differences between hardware platforms, operating systems and compiler versions used for the examples.
Note that obtaining and installing a C/C++ compiler is discussed here, with programming aspects deferred until the following module.
1.2
Introduction
Mention the word “computer” and many people will automatically think of the desktop variety, common in offices and the home. However, applications of computerand microprocessor-based systems abound elsewhere — from television remote controllers and washing machines, to cell phones and engine management systems in vehicles, to large corporate databases. At the hardware level, a common characteristic of all these is the “fetch-decode-execute” processing cycles. The hardware is of course controlled by the software sitting “above” it. The software in fact consists of a multitude of functions – monitoring a keypad, accepting network connections, controlling hardware such as disk drives and video displays. It is this “illusion” of a single processor performing many, many tasks which makes software design and implementation — particularly real-time software — a challenging art. Design issues come to the fore – not only the ability of the software to complete the requisite tasks, but the ability to complete them in a timely fashion, usually with minimal resources.
1.2
70935 – Real-Time Systems
1.3
Real-Time Concepts
The term “real-time system” is generally taken to mean “a computer system which must respond within a given amount of time”. Failure to do so means that the particular job at hand cannot be completed, and the overall system fails. The definition of “failure” is a crucial one. For example, a computer controlling the lift motor in a lift in a high-rise building must ensure that the lift cage is stopped at precisely the right level on each floor. If the lift did not stop so that it was exactly level with the floor, it would be difficult (and possibly dangerous) for the passengers to enter or exit. The computer system must monitor the position of the lift at precise intervals and take action according to the present position and the target position. An overshoot of even a few centimeters would be considered unacceptable. A great many embedded control systems fall into the category of requiring precise, guaranteed response times. Medical devices used to administer patient care in hospitals, on-board controllers for aircraft, even fuel injection controllers for passenger cars, could be given as examples. Contrast the lift control example with, say, using a word processor or spreadheet on your desktop Personal Computer (PC). If the response to a request such as selecting a table column, or a formatting menu item, or even scrolling the text, was delayed by a fraction of a second, the overall system is not considered to have “failed”. If the response time to a mouse selection took several seconds, the end result is that the system is not “failed”, although it might be an inconvenience to the user. Many authors (such as [1]) use the terms soft real-time and hard real-time to distinguish these types of situations. It could be argued, for example, that if the response time of a word processor was of the order of minutes then the system would effectively be unusable. However, the degradation in response time is not acompanied by catastrophic failure. In the case of the lift controller, failure could indeed be catastrophic.
1.4
What is an “Operating System”?
An Operating System is often, erroneously, thought to mean the user interface to a Personal Computer (PC). This is not correct. The user interface, whether text-based or a Graphical User Interface (GUI), is a separate issue. It may, however, be tighly integrated into the operating system itself. A minimal operating system comprises: 1. A resource manager, to control access to attached devices. 2. A task manager, to ensure that several jobs or “tasks” may be accomplished together. 3. A memory manager, to give each task the memory it requires for operation.
Module 1 – Real-Time Systems
1.3
4. A system interface: a set of software entry points to request operations to be done on behalf of the application.
Resources might include, for example: a network connection, a keyboard, an output port connected to a motor speed control, or input ports connected to controlling switches. The “model” is to have several tasks or processes running “concurrently”. This means that each is allocated a certain proportion of time in which to execute. After this time has elapsed, called the timeslice, another process is run. The act of switching between tasks is called a task switch or context switch. The core of the operating system – usually called the kernel or executive – is responsible for supervising the overall system. Context switching is managed by the kernel such that each task is given an appropriate amount of time in which to execute. What constitutes an “appropriate amount” of time depends on the task and what duties it has to perform. The “system interface” is the method by which the systems programmer gains access to the underlying subsystem. This may mean hardware or software resources, or a combination of both. Sending some data to a remote computer using a network connection, for example, entails copying the data to the appropriate buffers and sequencing the transmission through the network hardware. For several reasons, the systems programmer does not normally have, or wish to have, direct access to hardware devices. To continue the example, writing code that directly manipulates the registers of a network controller chip is tedious and error-prone. Worse, the location and functions of the registers will differ depending on the manufacturer of the chipset! This is where the device driver is used. This is the appropriate code to control the hardware devices and provide services to the system. These services are accessed from the user-level programs (tasks) via a well-known API, or Applications Programming Interface. So it can be seen that in order for several tasks to co-operate, a common “manager” must be called upon. User-level programs may be several levels removed from the underlying hardware. For example, it is usually convenient to read disk files as a sequence of characters or lines. The underlying storage is quite different from this, as disks use fixed-size blocks called sectors. The operating system pieces together the sectors in the right order for user programs to access. The access may be via the API calls open() and read(). These usually (but not always) appear to the programmer as simple function calls in the C language. Underneath, the kernel is joining sectors together to form a contiguous “byte-stream” file for the application code. The device driver is called to read the correct sector at a certain offset from the start of the disk. It must manipulate the control registers and set up buffers in order to access the physical disk. A different device driver is required for, say, a hard disk compared to a CD-ROM. Above this, the data bytes constituting the file may be viewed as records in a database. This process — user view of database records, application view of a byte-stream, kernel view of disk blocks, and device driver view of read/write control registers — is called layering or abstraction. This is shown diagrammatically in Figure 1.1.
1.4
70935 – Real-Time Systems
User Applications
User system calls
filesystem
process control
Memory
Kernel device drivers
Hardware
Figure 1.1
1.5
System hierarchy. User programs cannot access hardware and resources such as memory directly – only through the appropriate system calls (called the API or Applications Programming Interface).
What is a “Real-Time System”?
An operating system differs in design compared to a real-time operating system. Realtime operating systems usually include methods for precise timing of events, prioritybased scheduling of tasks according to their importance, and methods to ensure reliability. This is not to say that any or all of these features may not be present in a “conventional” operating system. Priority-based scheduling, for example, is standard practice in Unix systems, however Unix is not considered to be a real-time operating system. As an example of priority, the task which controls the lift position in the previous lift control example is more important than the task which monitors the user-request buttons for each floor. The motor control is crucial, and hence must not be “held up” by polling the press-buttons. Sometimes hardware assistance is available to help ensure the reliability of missioncritical systems. So-called “watchdog” or “dead-man” timers are often used. The tasks and/or kernel must poll certain ports or memory locations periodically. If this is not done, it is assumed the system has failed (possibly due to software malfunction) and the hardware is reset. Although not designed to happen in the normal course of events, such a design means that the system recovers automatically. Such a design may be augmented using polling or supervisory tasks, which check on the contents of certain memory areas (for example) at regular intervals. If the contents is not as expected, the task under supervision is assumed to have failed and action must be taken to restore the state of the system.
Module 1 – Real-Time Systems
1.5
It is important to understand that “real-time” does not necessarily mean “fast”. Although some emphasis is placed in subsequent modules on fast and efficient algorithms and coding techniques, speed is not the sole element. The design aspect of considering what is important and what is relatively less important in terms of time is crucial to the overall success in meeting real-time performance constraints.
1.6
Terminology
As with any technical field there are a number of terms significant to real-time systems. In this section we will look at a number of definitions and briefly expand on their application.
1.6.1
Systems
When we consider a real-time system as a single entity the following terms are useful to define system level concepts:
System Terms 1
A system may be considered as any single entity that has a number of inputs and outputs. A response time is the time interval between the presentation of a set of inputs to a system and the appearance of the resulting set of outputs. A set of inputs or outputs is considered bounded if they are always remain within a set of specified limits. A system specification is a document which defines the requirements of a system. A real-time system is one which much meet specified bounded requirements including response time constraints or cause a system failure. A system failure is the result of failing to satisfy one or more of the specified system requirements.
The use of these terms is fairly generic. You will come across them in many fields associated with real-time applications. In this course of study the above terms will be more specifically applied to software systems, but this not exclude the computer hardware on which software is operating. There is often a very close tie between the software system and hardware system.
1.6
70935 – Real-Time Systems
The depth to which the software and hardware systems are integrated broadly falls into three categories:
System Terms 2
Embedded systems - in which the software is completely encompassed by the hardware system on which it runs. Organic systems - in which the software is independent of the hardware system on which it runs. Semi-detached systems - in which the software may run on an alternate hardware system with some hardware related modifications.
Examples of real-time systems falling into each of the three classifications might include:
embedded systems - a cars fuel injection system, a lift control system. organic systems - an airline reservation system, a library catalogue system. semi-detached systems - a production line quality monitoring system, some computer operating systems.
The timeliness of the response of a real-time system is a major issue in its ability to meet an acceptable performance standard. What is considered as acceptable performance however varies widely from one application to another. You might be prepared to wait for five seconds for an automatic teller machine to dispense your cash, but the same response time would render most personal computer software useless. Deadlines in real-time systems are often referred to as soft, hard or firm deadlines. Consequently systems may also be classified as:
System Terms 3
Soft real-time systems - where performance is degraded by missed deadlines, but does not cause a system failure. Hard real-time systems - where failure to meet response time constraints would cause a system failure. Firm real-time systems - where failure to meet response time constraints can be tolerated occasionally.
Module 1 – Real-Time Systems
1.7
When it comes to stating a measure of performance for a real-time system we could simply state that a system either meets its system specification or it does not, but that would really only work for hard real-time systems. A more general measure of real-time performance often used is time-loading, defined as:
Time Loading Time-loading is a measure of the useful processing the computer is doing expressed as a percentage of its full capacity.
Time-loading may be measured using modern logic analysers or estimated by calculating the execution time of all sections of the application software. A satisfactory level of time-loading for a well designed real-time system should be around 70%. Higher values may be acceptable in some systems if no expansion is expected, but very low values < would indicate under utilised hardware.
( 10%)
1.6.2
Events
When we look closer at the operation of a real-time system we come to realise that it is changes in input conditions or changes in the internal state of the system that trigger its dynamics. By dynamics we mean the way the system responds to change. A change in either of these conditions is referred to as an event. In real-time systems the following terms are used to describe event related concepts:
Event Related Terms 1
An event is any occurrence which causes a change in system state or flow of control of a real-time system. A synchronous event is any event which occurs at a predictable time in the flow of control in the system. An asynchronous event is any event whose occurrence cannot be predicted in the flow of control in the system. A system state is any unique condition that a system can attain, as defined by a set of system variables.
The concept of asynchronous and synchronous events may not at first be obvious to all readers. Consider a software controlled real-time system which, like any software, contains decision points. It is at these decision points that a predictable change in
1.8
70935 – Real-Time Systems
system state or flow of control occurs. Other program events can also be classified as synchronous such as errors in arithmetic calculations and software interrupt instructions. Asynchronous events are characterised by their unpredictability with respect to the program execution. Examples include hardware interrupts and changes in input quantities. Even a regular clock source such as a 1mS interval timer input is considered asynchronous as the event occurs at random with respect to the sequence of instructions in the program. There is another concept related to events that is very important to the operation of realtime systems, that of determinism. Determinism is the ability to predict how a system will behave under all possible conditions, which includes all system states and event combinations. Assuming that a real-time system operates with bounded inputs and a finite number of system states, it should be possible to predict all system responses and any resulting change in system state. Consider the following terms related to determinism.
Determinism
A deterministic system is one for which a unique set of responses and the next state can be determined for each possible state and set of inputs.
Event determinism is where a unique set of responses and the next state can be determined for that event. Temporal determinism is where the response times of the system can be determined for each possible state and set of inputs.
1.7
Real-Time Applications
Practical real-time systems are present in much of the high-tech equipment that surrounds us in this modern world. Some applications include:
aircraft - flight control, navigation, environment control automotive - engine management, cruise control military - weapon guidance, enhanced vehicle control space exploration - rocket control, navigation, environment control medical equipment - pace makers, critical care equipment electricity generation - system stability and security, nuclear reactor control
Module 1 – Real-Time Systems
1.9
lift control - direct operation and safety, multi-lift management teller machine - transaction control, money dispensing library catalog - inquiry, inventory support reservation system - inquiry, reservation stock control - stock movement, inventory support video games - user interface, video and audio generation
1.8
Compilers for Code Examples
Throughout this unit of study, many code examples are presented both to illustrate certain language features or API function calls, and to give concrete examples of various principles and algorithms. The student is encouraged to study these by compiling, executing and (where appropriate) changing the source. This section explains what is required in order to be able to do this.
1.8.1
Platforms
C is available in several “flavours”: Unix The “original” C compiler called cc; it comes as standard in most Unix installations. DOS Various commercial versions (for example, Microsoft QuickC and Borland Turbo C), and the free 32-bit Gnu C compiler (called gcc for C and g++1 for C++). Windows Various commercial versions such as Microsoft Visual C/C++ and Borland C++Builder. The GnuC compiler is also able to compile Windows programs but uses a command-line interface. One advantage of C is that it is available on several different computer platforms, notably Windows and Unix. Windows-based compiler offerings from Borland and Microsoft are available in “academic” versions for around $150-$200. An alternative is the free Gnu C compiler software, available for Windows (using a shell) and Unix. Commercial compilers normally come in the form of an Integrated Development Environment (IDE), whereas Unix compilers come in the traditional “command-line” form. The IDE provides an integrated editor with a mouse-based menu system for compiling, debugging and running programs. Command-line systems, such as Unix and the Gnu C compiler for DOS/Windows (to be discussed) requires the user to run a separate editor and compile programs using command-line sequences (these will be summarized shortly). 1
Previously gxx because of filenaming problems.
1.10
70935 – Real-Time Systems
1.8.2
Gnu Compilers
The Gnu C compiler is available for Unix, with a port to DOS called DJGPP and a Windows port called Cygwin. Firstly, DJGPP is available from
http://www.delorie.com or a (faster) mirror site
ftp://mirror.aarnet.edu.au/pc/simtelnet/gnu/djgpp/ Instructions for obtaining and installing GnuC for DOS may be obtained at
http://www.usq.edu.au/users/leis/gnuc/gnuc.html The GnuC port for Windows by Cygnus is freely downloadable, from
http://sourceware.cygnus.com/cygwin/ or a (faster) mirror site
http://mirror.aarnet.edu.au/cygwin/ Instructions for obtaining and installing GnuC for Windows may be obtained at
http://www.usq.edu.au/users/leis/cygnus/cygnus.html The Cygwin compiler includes tool libraries, software libraries for Unix and Win32 system calls. What’s the difference between the Cygnus and DJGPP ports of the GnuC compiler?
DJGPP does not (as of this writing) support long filenames on Windows NT. DJGPP supports numerous hardware-access and graphics facilities – for example access to input/output ports using inportb() and outportb(). Cygwin does not. Cygwin supports Unix and Win32 Application Programming Interfaces (API’s) – for example the Unix fork() to create child processes, together with the network sockets API. Cygwin is easier to install – only one file need be downloaded. The DJGPP C++ compiler is invoked by gxx whereas the Cygwin C++ compiler is invoked by g++. This is due to historical limitations on DOS filenames. For compiling C programs, both use gcc.
Module 1 – Real-Time Systems
1.11
If unsure, it is recommended that you use the Cygwin GnuC package. The GnuC compiler ports mentioned above are both free, and provide a 32-bit environment for C and C++ programming. This means that the memory limitations of other DOS language tools (such as DOS C compilers and QBasic) do not exist. For an easier-to-use integrated code development system, the author uses Borland C++Builder (version 4 as of this writing).
1.9
Links to Real-Time Systems Information
Recently, real-time system failures have been documented — some with very serious consequences. Amongst the more interesting of recent times are those of the Therac medical treatment system, and the Ariane rocket. The exact cause of each — design failure, hardware failure, software failure, operator failure — is debatable. However, the following activity should be completed before continuing.
activity 1.1
Failures in Real-Time Systems.
1. Read one of the articles on the Therac medical accelerator, linked from 70935 Real-Time Systems Further Referencesa . It is only necessary to read one (several links are provided in case one is unavailable). 2. Read one of the articles on the Ariane rocket, linked from 70935 Real-Time Systems Further Referencesb . It is only necessary to read one (again, more than one link is provided in case one is unavailable). a b
http://www.usq.edu.au/users/leis/units/70935/935link.html http://www.usq.edu.au/users/leis/units/70935/935link.html
1.12
70935 – Real-Time Systems
1.10
Module Summary
This module has:
Served to introduce some of the important concepts and terms pertaining to computer operating systems, with the extension to real-time systems. Indicated the requirements for a C compiler for completing the unit, and given instructions on obtaining the free Gnu compiler.
The linked articles on real-time system failures should be read and considered before continuing.
Further Reading 70935 Real-Time Systems Further Referencesa
a
http://www.usq.edu.au/users/leis/units/70935/935link.html
Module
2
REAL-TIME SOFTWARE DESIGN
Module 2 – Real-Time Software Design
2.1
2.1
Module Overview
The objectives of this module include:
To introduce the concept of the software life cycle To explain the importance of using best-practice techniques of software engineering for developing reliable real-time systems To describe the elements which form a Real-Time System Specification To describe several software design techniques and design tools for real-time system design To introduce the concept of the kernel To explain how to implement basic real-time kernels To explain the concept of foreground and background functions in kernels
2.2
Introduction
The best way to ensure that the implementation of a real-time system meets all the requirements of its system specification is to adopt a solid engineering methodology. A traditional engineering design and development process involves the following phases:
conception specification design implementation testing maintenance
Real-time software is such an integral part of most modern real-time systems that it is common to hear software referred to as an engineering material. Hence the phases listed above should equally apply to the creation of software as they do to building a sky-scraper or manufacturing a jet engine, given that we want to produce a quality real-time system. The characteristics we associate with the term quality such as conformity, precision, cost effectiveness, efficiency, maintainability and reliability are often linked with engineered objects we can see, but what about the bits we can’t see? These characteristics
2.2
70935 – Real-Time Systems
should be at least as important to the production of a quality software as they are to our other engineering achievements, perhaps even more so. Would you want to trust your life to a aircraft’s autopilot that you knew to be of suspect quality? In this module we will learn about good software design methodology and how to use several design tools and techniques to produce quality real-time software. We will also be looking at the ways that system specifications can be defined/described in order to unambiguously state the requirements of the real-time system.
2.3
The Software Life Cycle
Good software development is a controlled process, with a clear methodology and predictable outcomes. It is much more than someone just sitting at a computer writing code. As described earlier, applying a solid engineering approach to software design and development can significantly improve the quality of the final product. And we must begin to think of our work as a product to be consumed by a discerning customer, if we intend to produce a system embodying quality characteristics. There are six distinct phases in the software life cycle which parallel the six phases used in engineering. In software engineering these are referred to as:
The Concept phase The Specification phase The Design phase The Programming phase The Test phase The Maintenance phase
While an engineering methodology is often applied rigorously in a pure engineering application, software engineering tends to exhibit a certain flexibility, as software is inherently a more pliable engineering material than steel or concrete. While the malleability of software can be a very beneficial characteristic its misuse can also result in a substandard product. Software engineering for real-time systems can be subdivided into the six phases above with fairly distinct outcomes for each phase. Table 2.3 identifies the processes and outcomes involved in each phase. While all phases in the software life cycle are important it is often the design and test phases that new-comers to the field tend to under-value. In fact it is in the design phase that qualities such as conformity, precision and reliability are ‘built-in’ to realtime software. It is also in the design phase that a test plan is devised from which the final system performance will be determined. The test phase is then used to check the correspondence of the system with respect to the system specification.
Module 2 – Real-Time Software Design
2.3.1
2.3
The Concept Phase
All things begin with an idea. In this phase of a real-time system design ideas are proposed and discussed for new products, enhanced products or solutions to a problem posed. This phase is often initiated by market forces, changes in technology or a request from a client, where an opportunity is identified to fulfill a need. In the conceptual phase of the project the main objectives are to:
identify the opportunity/need for the product identify the features/objectives of the product produce market/feasibility studies
While these tasks may not always fall into the domain of the real-time systems engineer, he/she is often involved in evaluating and preparing the technical aspects of proposals and estimating a product’s potential performance and practicalities.
2.3.2
The Specification Phase
In this phase the operational and contractual details of the project are identified and documented. The operational part of the specification lists and describes the functions, processes and performance requirements of the system including characteristics such as speed, accuracy, stability and response times. The contractual part of the specification details the scheduling, budgetary and legal (if any) elements of the project. These two sets of documentation are sometimes referred to as the functional and non-functional requirements. The non-functional requirements may also include specification of programming language, time-loading, system hardware etc. The specification phase is also the stage at which the test plan is produced. The express purpose of the test plan is to define how the system will be tested to verify conformity and performance with respect to the system specification. In the specification phase of the project the main objectives are to: Phase Concept Specification Design Programming Test Maintenance
Processes Outline project objectives/feasibility Detail system requirements Detail the system structure Implement design methodology Verify system meets specification Maintain code
Table 2.1 Software Engineering phases
Primary Outcome Project outline System specification System design document Program code Test reports Maintenance reports
2.4
70935 – Real-Time Systems
identify the functions/requirements of the product
produce the system specification documentation
produce schedule, budget and contractual documentation
produce a test plan for system verification
The system specification is often written by or in close contact with the client or target user of the product. The client may be a traditional customer, your boss, another section of a large organisation or a particular industry sector. Real-time system engineers are most likely to have direct input to this phase when the objective is to produce an enhanced version of an existing product.
2.3.3
The Design Phase
In the design phase the system specification is transformed from a list of functions and requirements to a detailed statement of implementation referred to as the system design document. The system design document specifies how the system requirements are to be met by partitioning the functions and processes prescribed in the system specification into functional modules, supported by a collection of data structures. Techniques and tools for creating the system design are explained in section 2.4. During the design phase it is possible to identify flaws in the system specification which cannot be worked around, or requirements that cannot be met with current technology. In either case a request must be made to the originators of the system specification for a system change. Design changes should only be implemented as the result of an authorised system change. In the design phase of the project the main objectives are to:
partition the functions and processes into modules
identify problems in the system specification for possible change
produce the system design documentation to a recognised design standard
produce a set of test cases based on the test plan
The system design document is often prepared by a team of design engineers and analysts. This should be done according to a recognised international software design standard such as DOD-STD-2167A or IEEE standard 1016.
Module 2 – Real-Time Software Design
2.3.4
2.5
The Programming Phase
Once a system design document is produced the next phase of the project involves programming the functional modules and creating the data structures to operate as per the system specification. This task may involve one or many software engineers depending on the complexity and diversity of skills required for the implementation. Different approaches can be used to achieve this task but the most common is a bottom-up implementation strategy, where low level modules are developed first following on to more complex interactions of modules. In the programming phase of the project the main objectives are to:
decide on a kernel structure and implement the kernel create the data structures and write the software modules debug the software develop test cases and procedures integrate modules to form a functional system
For the real-time design engineer the most important aspect of this phase is to properly manage the software development process to ensure a level of quality control. This can be accomplished through careful management techniques, sometimes with the aid of software management software.
2.3.5
The Test Phase
The formal testing of real-time software involves the strictly controlled application of the test plan that was produced in the design phase. While testing is performed during the programming phase of the project to verify the operation of various modules, this process tends to be relatively ad-hoc. To show that the complete real-time system meets all requirements as set down in the system specification a rigid test program must be adopted. The importance of the test phase cannot be overstated. In some cases it is only when the system is tested as a complete unit that some errors can manifest. Some performance measures such as response time and time loading can only be fully evaluated when the system is fully operational. Any failure to meet the requirements of the system specification will require corrective action being taken by returning to the programming phase or possibly even the design phase. To thoroughly test a complex system the test phase should include a comprehensive set of loading models. Loading models are sets of operational states and conditions under which the real-time system is expected to operate. The purpose of these tests is to thoroughly exercise or stress the system to prove its stability and performance.
2.6
70935 – Real-Time Systems
The likelihood of various loading models can be estimated from a probabilistic analysis based of the systems operation and structure. In the test phase of the project the main objectives are to:
verify system function and performance according to the test plan perform system stability and performance tests under various loading models produce test reports detailing level of compliance
This phase of system development is performed either by the development team or a third, independent party. Either way the test phase must be carefully supervised and documented. In traditional engineering this phase is referred to as commissioning.
2.3.6
The Maintenance Phase
Beyond the test phase there are invariably ongoing corrections and revisions to software systems in response to reports of errors and suggestions from users. A controlled release program of a Beta version of a system is often used to help identify significant deficiencies in complex systems before the true product launch. Revisions of complete systems typically use a system of regression testing to verify system function after selected sections of a system are modified. While some consider this revision process as part of the test phase it has a distinct maintenance aspect which is bound to customer support issues. It is often essential that product maintenance include the capacity to revise system software, but after some predefined period the product is usually no longer supported. In the maintenance phase of the project the main objectives are to:
deploy the system into practical application provide a customer support system including error reporting maintain the system as a product through a system of revisions
The software engineer may be involved during this phase in ongoing product enhancement or regression testing. In small companies the engineer may also find him/her-self with some direct involvement in marketing and customer support.
Module 2 – Real-Time Software Design
2.4
2.7
The Real-time System Specification and Design Techniques
The two foundations of good real-time software design are a sound system specification and a definitive system design document. The purpose of the concept phase is to produce a collection of features and objectives for the desired product, but this is a far cry from a definitive statement of requirements. The purpose of the Specification Phase is to transform the conceptual into the practical. This task can be proceduralised to some degree, by using specific techniques to define the system requirements. Most system requirements are initially described in words which are later replaced by more specific mathematical, diagramatic or pseudo-code definitions. These techniques attempt to unambiguously define the system requirements, processes and anticipated structures for the data and the program. We use the term attempt as most system specifications use two or more techniques in combination to ensure clarity. For all but the simplest systems any one technique alone is usually inadequate. While the specification and design phases are presented as separate steps in the software life cycle, these two phases are often closely tied through the development of the system design documentation. The system design document is typically a collection of descriptive, mathematical, procedural, structural or state-based models of the required system. In the following sections we will outline several techniques used to create system specifications and design real-time software.
2.4.1
Descriptive Techniques
Natural Language
Often the first step in developing a system design is to write down a description of what is desired. This may take the form of a formal request from a client or your boss, or summarised in point form scribbled down during or after a discussion. Either way the form is descriptive using phrases or sentences outlining the desired result. As one might expect this approach can create a degree of ambiguity when concepts are not clearly presented and often suffers from problems related to cultural perception and non-native languages. This technique is not recommended as a means of detailing a specification, but may be used to complement other techniques where an explanation is beneficial.
2.8
70935 – Real-Time Systems
Psuedo-code or Structured English A more precise descriptive technique is available which greatly reduces the potential for ambiguity. By converting a description into a series of statements and writing them down in a structured way it is possible to create a descriptive form called psuedo-code, or structured english. This technique adopts a procedural form. The following example shows how this can be done.
example 2.1
Example of psuedo-code for a vending machine.
do
display "Add Coins" wait for coin to be inserted identify coin count coin value if coin value exceeds product cost begin display "Select Product" wait for product selection eject product calculate change eject change end until product dispenser empty display "No Stock"
2.4.2
Mathematical Techniques
Most real-time applications are based on the monitoring and/or control of some physical object or process. This presents the design engineer with an ideal opportunity to apply some of that mathematics learnt at university in an attempt to define the physics of the problem as a series of mathematical equations. We use the term attempt because practical systems can be difficult to accurately define as a set of equations, as they are often too complex or exhibit non-linearities. The concept of using mathematics to define the behaviour of a physical system is one of engineering’s fundamentals. It provides a concise, commonly understood technique for defining a set of static and dynamic conditions; offers little chance for ambiguity and is in a form which translates easily into software. In addition to these features mathematical equations can be manipulated to achieve simplifications and optimisations
Module 2 – Real-Time Software Design
2.9
which can greatly improve the performance of real-time software. In many circumstances formal proving of the stability of a system is possible through mathematical analysis. The following example details a mathematical specification for the age old physics problem of projectile motion. The quantities involved include initial velocity (V ), angle of projectile (A), the force of gravity (G), time (t) and the resulting distances traveled horizontally (x) and vertically (y ).
example 2.2
Example of a mathematical specification for projectile motion.
() = y (t) =
x t
Where:
t V,A G x, y
( ) tV sin(A) tV cos A
Gt2
– is time. – are the initial velocity and angle respectively. – is the gravitational constant. – are resulting horizontal and vertical distances.
Mathematical specification is a widely accepted and well understood technique and is recommended for use with real-time systems involved with monitoring and/or control of a physical object or process. Just a word of warning however - as a physical system is approximated through mathematical modeling one must not fool one’s self into believing the model is the system. A strict set of operational limits and checks are usually required to ensure that the system operates within a predefined range over which the model is valid.
2.4.3
Procedural Techniques
In many cases the design engineer is aware of the processes and procedures that are required to be executed in a real-time system, these include: start-up and shutdown sequences, specific algorithm execution, user data entry, a series of functional decisions, sequences of events and others. Several design tools are available that help the software engineer to detail procedures which will form an integral part of the real-time system. Some of these techniques have the distinction of being capable of defining increasingly finer levels of detail as the design develops. The following sections outline the most common of these tools.
2.10
70935 – Real-Time Systems
Flowcharts Perhaps one of the earliest developed and most widely recognised graphical techniques is the flowchart. Figure 2.1 shows an example of the most commonly used subset of flowcharting symbols. There are actually many other symbols available for flowcharting, but most of them relate to file and record handling for administrative program design.
Start
1
Process
SubProcess
Decision
True
Connector
Input / Output
Process
Stop 1 Connector
False Stop
SubProcess
Process flow indicated by arrows.
Figure 2.1 Example of the basic set of flowcharting symbols. The flowchart is designed to show unidirectional program flow, decision points, input/output, processes and sub-processes. The few basic rules to drawing flowcharts include:
Each symbol has a maximum of one entry and exit point, except the decision diamond which has two exit points. Program flow can only be joined arrow to arrow Arrows cannot divide program flow Flow should generally be top to bottom
The application of this type of procedural representation is not restricted to software development and is often used to describe all sorts of procedures from changing a tyre to operating a photocopier. Flow charts are not recommended for use in the specification phase of real-time system design, but can be useful in defining/documenting specific procedures of small parts of a larger system. In multi-tasking systems flowcharts do not easily represent the interaction between the tasks, or between tasks and the operating system. There is also no way of indicating temporal relationships in flowcharting.
Module 2 – Real-Time Software Design
2.11
Dataflow Diagrams A dataflow diagram is a simplified system representation showing major flow of data through a series of processes. The four main elements of data flow diagrams are the data source/sink, data storage, processes and data flow arrows. Figure 2.2 shows the symbols used to represent these elements.
Label
Data source/sink (Hardware)
Label
Data Store (Memory/Disk)
Label
Label
Process (Software)
Data flow
Figure 2.2 Symbols for Dataflow diagram elements. Figure 2.3 shows an example of how these simple symbols can be used to convey information about a process. Data sources/sinks are typically hardware elements such as peripherals and Input/Output devices. The steps used to create a dataflow diagram include:
Identify the major data flows based on the system requirements Starting on the outer edges draw the sources and sinks for data, usually the system hardware elements Draw and label arrows indicating data flow between hardware, memory and software modules Ensure symbols are clearly labeled according to their function Do not show initialisation or flow-of-control and keep detail to a minimum
Dataflow diagrams are highly recommended and widely used in the design of realtime systems. They offer a structured approach for identifying the main data flows in a system and for partitioning software into modules(processes). Interrupts can be shown as an input from a hardware source that triggers an interrupt service routine. Dataflow diagrams can be used to form a hierarchy of system structure with varying levels of detail. Processes in upper layers can be represented by their own dataflow diagram showing any underlying processes. A particular feature of dataflow diagrams is that they provide the designer with the capability to identify concurrent processes, that is processes which can be run at the same time on either multiple processors or as multiple tasks. This can be achieved by locating sections of the dataflow diagram which only connect to other sections only through common data storage. In Figure 2.3 there are three such sections: the sample and control section, the FFT section and the display section. In such cases the designer has the option of considering any section as a separate task, which may significantly influence the structural design of the overall system.
2.12
70935 – Real-Time Systems
Analog to Digital Converter Sample rate Control
Selection
Raw
Graphical Display Graphic Frequency Display Data
Sample
Sample Time data 512 value time buffer Time data
Frequency data 256 value frequency buffer
F.F.T.
User Interface
Frequency data
File System
Figure 2.3 A dataflow diagram for a Fast-Fourier Transform application.
2.4.4
Structural Techniques
When adopting an engineering approach to real-time software design it is important to establish a good structural framework around which to build an application. This is why procedures such as top-down design are so successful, because they encourage modularity and hierarchy in a design. Several design tools are available to help the software engineer create well structured and modular code. The following sections outline a few of these tools.
Structure Charts Structure Charts are widely used for describing the hierarchial structure of a system. They can be used to describe, not only software, but any system where a layered structure exists. In software the layers are related to the depth of subroutine and function calls. In other applications the structure chart may depict the hierarchial structure of a chain of command, a written document or the physical components of a device. The elements which form a structure chart are quite simple: a box represents a process, vertical position indicates hierarchy, lines show links between processes and arrows show major data and control flow. The advantages of structure charts include:
execution sequence is shown as a left-to-right progression across each layer in the diagram they encourage top-down design the help identify the modularity of a system
Module 2 – Real-Time Software Design
2.13
Some variants of the structure chart can be used to illustrate decisions and interrupt processing. Figure 2.4 shows an example of a structure chart including a decision and interrupts. A
Interrupt Source
Main Process
Interrupt Source Initialising / Debugging
Process with Decision
Control SubProcess
Data
SubProcess
B
Process
Either
Interrupt Service B
SubProcess Interrupt Service A
Common SubProcess
Figure 2.4 Example of the basic set of structure chart symbols. The position of processes in a structure chart is significant, as it shows the level of depth of subroutine/function call required to reach that module of code, its relationship with processes above and below it and its relative functional level. In the case of interrupt service processes its position and the dashed line above it can be used to indicate the scope of the interrupt. For example, Figure 2.4 shows interrupt service B is only enabled during the execution of the first layer of processes under the main process, while the interrupt service A is capable of interrupting all but the common sub-process. While structure charts are useful to describe the general structure of a system they lack the ability to depict concurrent processes, significant data storage and temporal relationships. Thus structure charts are only recommended for use in the initial stages of a design to outline the expected modularity and hierarchy of a system. They may also be used as a good documentation tool to summarise a completed system’s structure for later reference.
Warnier-Orr Notation Warnier-Orr notation is a semi-descriptive, semi-structural system of notation which can represent program structure, data and their conditional relationships. It is somewhat like a structure chart on its side with conditional elements indicating options for execution of subprocesses. Warnier-Orr also uses set-theoretic notation to carefully indicate the conditions under which various processes are executed.
2.14
70935 – Real-Time Systems
Warnier-Orr notation is written top-to-bottom in order of execution and is formed from a combination of sets of steps, a little like psuedo-code. Each set can be made of a combination of other sets and steps. Each step can include a conditional decision which indicates alternate sets for execution. As the notation is written left-to-right increasing levels of detail are introduced. Logical operators of exclusive or () and or ( ) are used to indicate mutually exclusive cases and combinational cases respectively. Figure 2.5 illustrates the elements which can be used in Warnier-Orr notation.
+
8 > > > > > > > > > > > > > > > > > > > > > > < label - program > > > > > > > > > > > > > > > > > > > > > > :
label (optional) fstatement
8 > < label - set example > :
statement ( statement set statement
8 > < test condition ftrue action - statement label - condition example > : complementary condition ffalse action - statement 8 option1 fstatement > > > > : option3 fstatement label - while loop (test condition,W) fstatement label - loop until (test condition,U) fstatement label - indexed loop (n) fstatement
Figure 2.5 Example of Warnier-Orr Notation syntax In Figure 2.5 each set is identified by a label. In this example they indicate the type of element, but in a real application they should indicate the function or purpose of the element. The elements, in order of appearance include:
a statement - a statement for execution a set - a set of statements or sets a condition - requiring a test condition and corresponding true and false actions which are statements or sets a case - requiring a series of options and corresponding statements a while loop - requiring a test condition, W for while and statement to be executed while the condition is true
Module 2 – Real-Time Software Design
2.15
a loop until - requiring a test condition, U for until and statement to be executed until the condition becomes true
0
a indexed loop - requiring a counter n and statement to be executed while n > , which will typically include as statement like n n
=
1
Warnier-Orr notation is recommended as an alternative to structure charts as they exhibit the features of modularity, clear sequencing, decision and case capability, looping and counters. Multi-tasking can be incorporated through the inclusion of flags and tests. However Warnier-Orr notation can become laiden with detail and become unclear if the designer is not careful to state elements concisely.
2.4.5
State-based Techniques
Many real-time systems have clearly defined conditions or states in which the system operates. The identification of these states allows the design engineer to logically divide a system into distinct operational components. Not only does this method of analysis help partition the software but it also helps identify the system events that trigger changes in system operation. Several design tools are available that use statebased techniques to develop well structured and modular code. The following sections outline a few of these tools. Finite State Machines Finite State Machines and Finite State Automata are terms used for the technique of defining a system as a fixed number of unique states between which the system moves in response to events. A state is identified as a distinct condition a system may occupy, based on system parameters called state variables. Transitions between states are triggered by system inputs (events) or increments of time. There are actually two types of Finite State Automata, the Moore and Mealy implementations. The difference between the two implementations lay in the way that outputs are defined. The Moore machine can only define system outputs in terms of the state variables, where-as the Mealy machine can use input conditions and state variables. The significance of this distinction will be made clear later when we consider its effect on implementation. Finite state machines can be represented by mathematical notation, graphically as a State Diagram or State Chart, or in a tabular form. As the graphical and tabular techniques are the most easily applied to software design we shall only consider these as tools for real-time system design.
State Diagrams The State Diagram is a graphical representation of a finite state machine in which:
2.16
70935 – Real-Time Systems
Not "C" "C"
"C" Start Out=0
Space
not "C" "A" or space
Not ("T" or "C") or Space Third Out=1
First Out=0
"C"
"A"
Second Out=0 "T"
Figure 2.6 Example of a State Diagram (Moore implementation).
circles are used to represent system states states are typically labeled with capital letters or short descriptions to represent system conditions connecting arrows represent transitions between states inputs/events are used as labels on transistions indicting trigger conditions outputs/actions are either placed inside states or associated with inputs on transistions starting or terminating states may be depicted by double circles
Figure 2.6 shows a Moore machine for a simple task to recognise the word CAT from a string of letters. The input is the next letter of the string, the output is a single bit which is when CAT is recognised. Note that inputs are shown on the transitions and the outputs are shown inside the states. Recall that a Moore machine’s outputs are dependent only on the system state variables and hence are only defined within states. In the Moore machine the outputs are static while the system stays in any individual state.
1
Figure 2.7 shows the Mealy machine implementation for the same task of Figure 2.6. Note the outputs are shown on the transitions along with the input causing the transition, separated by a /. In the Mealy machine the outputs are static if based only on state variables or may be transitional if based on input conditions as well.
Module 2 – Real-Time Software Design
2.17
Not "C" / 0 "C" / 0 Start
space, Not "C" or "A" / 0
"C" / 0 First
"C" / 0 "A" / 0
Not ("T" or "C") or Space / 0 Space / 0 Third
Second "T" / 1
Figure 2.7 Example of a State Diagram (Mealy implementation). State Diagrams are recommended as a good design technique for real-time systems, particularly for state driven tasks which operate equipment in a range of sequences like traffic light control, medical equipment, aircraft flight control, teller machines etc.
Statecharts Statecharts are a combination of Finite State Machines and Data Flow Diagrams which feature the ability to depict not only states of operation, but also states within states and orthogonality. The structure of the statechart allows these and other features to be incorporated in the following way.
States are represented by loops with labels. States within states (depth) are represented as loops within loops. Orthogonality is represented by a dashed line separating concurrent processes. Small letters a; b; : : : z represent events that trigger transitions. Small letters in parentheses represent conditions that must be true for the transition to occur. Simultaneous transitions in orthogonal states, called broadcast communications, are represented by transitions with the same event. Outputs are represented as actions associated with states or transitions.
Cascade events can be attached to triggering events.
2.18
70935 – Real-Time Systems
A function B function a f
b c
function C
D function
d/f e
F function
f(g) c function E
Figure 2.8 Example of a State Chart. Figure 2.8 shows a sample statechart containing each of the above features. This statechart is comprised of six states A to F, with two orthogonal (concurrent) processes - one containing states B and C, the other states D, E and F. Each state shows a default label ”function” which would be substituted with a descriptive statement of the function of that state. The dynamics of the system are as follows:
Transition to state B is triggered by event a. Transition to state D is triggered by event b. Transition to states C and E are triggered simultaneously (synchronously) by event c. Control returns to states B and D respectively, triggered by event f , with the transition to D conditional on g . Transition to state F is triggered by event d which causes a cascade trigger of event f . Control returns to state A from state F on event e.
States B to F are said to be nested states of state A, indicating an increasing depth of detail and function. This concept of states-within-states encourages software designers to utilise top-down design principles and create modular code. The concept of depth is similar to sets containing sets in Warnier-Orr notation. The presence of the dashed line indicates that two processes may be run concurrently, in this case triggered by separate events a and b. Each process can run independently but some transitions can be synchronised by common events called broadcast communications, such as c and f . Statecharts are highly recommended for real-time system design as they offer representations for many of the features required for modern software design, in particular concurrency, modularity and intertask communication. When combined with the state-based decomposition offered by Finite State Machines this design technique is probably the one of the best available.
Module 2 – Real-Time Software Design
2.5
2.19
Implementation of Real-Time Kernels
The programming or implementation phase of real-time software is a critical stage in the development of real-time systems. One of the primary tasks is to decide on a suitable kernel structure and efficiently implement that kernel. The kernel is the underlying structure or core of a real-time system or operating system. Several basic types of kernel are available for use which vary from simple polled loops through to full featured commercial operating systems. For some real-time applications, particularly for embedded systems, commercial operating systems are too big and complex to be efficiently applied. In these cases the system designer is much more likely to write a simple kernel to meet the requirements of the application. In this section we will describe how to implement the basic structures of several real-time kernels and outline the more advanced features of commercial operating systems.
2.5.1
The Function of a Real-Time Kernel
The three primary functions performed by an operating system are
Task Scheduling - which identifies which task runs next Task Dispatcher - which performs necessary housekeeping required to switch from task to task Intertask Communications - which allows for data transfer and synchronisation
The kernel, sometimes referred to as the executive or nucleus, is the smallest portion of an operating system that provides the primary functions listed above. This is not to say that all real-time applications are implemented using multiple tasks, but in one form or another they implement each of these primary functions. Later in this unit we will take a closer look at two of the most widely used commercial operating systems in use today, Unix and WindowsNT. Several examples will be provided to show some of the basic functions of each of these operating systems (kernels). In this module we will be focusing on the fundamentals of implementing purpose written kernels.
2.5.2
Polled Systems
The simplest of all real-time kernels is based on the polled loop structure, where one or more devices are repetitively polled to check for changes (events) upon which to take action. While polled systems can achieve fast response for a small numbers of
2.20
70935 – Real-Time Systems
devices, they offer little other functionality. Each device service needs to be as short as possible to achieve short response times for all events. The C program shown in Figure 2.9 illustrates the concept of a polled system with a polled single key input triggering either of two events.
/* poll.c - a sample polled system * * Mark Phythian * */ #include #include #include void process_event1(); void process_event2(); int main() { char c = 0; while (c != 'q') { while (!_kbhit()); /* while no key press do nothing */ c= _getch(); if (c == '1') process_event1(); if (c == '2') process_event2(); } exit(0); } void process_event1() { printf("Event 1 \n"); } void process_event2() { printf("Event 2 \n"); } Figure 2.9 Sample Polled Application Polled systems are simple to write, easy to debug, response time is easy to determine and they are good for high speed data channel interfacing. However polled loops are inefficient with CPU time, they do not handle bursts of events unless specifically designed with a buffer, and polled systems can not satisfy the requirements of all but the simplest systems.
2.5.3
Phase-Driven and State-Driven Systems
Phase-Driven or State-Driven implementations utilise case statements, nested if statements or tables of function pointers to divide the code into manageable segments. These segments may be phases of a larger process or states defined in a Finite State
Module 2 – Real-Time Software Design
2.21
Machine. The division of the total program function into smaller distinct sections also provides the ability for the program to be suspended at the end of each section’s execution, as would be required in a multitasking application. This technique particularly lends itself to the implementation of real-time systems designed using the Finite State Machine approach. Figure 2.10 shows a State Diagram for a simple parity generator for a bit stream. The program shown in Figures 2.11 and 2.12 show the implementation of the parity function for State-Driven code using the switch/case approach.
0/ EVEN
1/ ODD
0 / ODD B
A 1/ EVEN
Figure 2.10 State Diagram for a parity generator. (Mealy implementation). Note how each of the states are defined by letter to which each is assigned an integer. The state variable state is initialised to the starting state and the program drops into an endless loop. The input is received and control is passed to the case in the switch statement that corresponds to the current state. The input value is tested to determine if a transition is to be made in the state machine. For example: in case (state A) there are two possibilities - if input = 1 then the output is changed to EVEN and the new state becomes B, or input = 0 and no change is required. Note it is good practice to re-affirm the output condition and state variable in this case.
0
An alternative to the switch/case approach is to use a rather elegant solution derived from the tabulated form for the Finite State Machine. In this approach each possible transition is tabulated for current state and input, where each entry indicates the next state and resulting output. The table below shows the tabular form for the parity generator of Figure 2.10.
Input 0 1
Current State A B A / EVEN B / ODD B / ODD A / EVEN
Table 2.2 Tabular Representation for the Parity Generator
2.22
70935 – Real-Time Systems
/* statesw.c - a sample state driven application using switch/case * * Mark Phythian * */ #include #include const int A = 0; const int B = 1; const int EVEN = 0; const int ODD = 1; int output; // rnd(m) produces a random number between 0 and m double rnd(double m) { double r;
}
r = m * (double)rand() / RAND_MAX; return r;
char parity[2][6] = {"EVEN\n" , "ODD\n"}; void main() { int state; int input; // state transition functions output = EVEN; state = A; while(1) { //delay input = (int)(rnd(1.0) + 0.5); printf("%d\t",input); switch (state) { case 0 : if (input == 1) { output = ODD; state = B; } else Figure 2.11
The switch/case implementation for the parity generator - part 1 of 2
Module 2 – Real-Time Software Design
{
}
}
2.23
output = EVEN; state = A;
} break; case 1 : if (input == 1) { output = EVEN; state = A; } else { output = ODD; state = B; } break; } printf("%s\n",parity[output]);
Figure 2.12
The switch/case implementation for the parity generator - part 2 of 2
In the implementation of the tabular form the present state and input are used as indices into an array (table) holding pointers to individual functions representing each transition. In each of these functions the output values are set and the desired next state is specified by the return value of the transition function. This is achieved in a single line in the main program using: state
= (next[input][state])();
The disadvantages of this approach include:
input values must be mapped to a set of consecutive integer indices different inputs may be required for some states many entries in the table may be unused as some states may not use all input conditions
In the last case a default error handling function should be specified in each unused entry to trap illegal combinations of current state and input as appropriate. Figures 2.13 and 2.14 show the implementation of the same parity function for StateDriven code using tabulated function pointers. Note that in the main program the table of addresses for each of the transition functions is defined individually using:
2.24
70935 – Real-Time Systems
/* statetab.c - a sample state driven application * using tabulated function pointers * * Mark Phythian * */ #include #include const int A = 0; const int B = 1; const int EVEN = 0; const int ODD = 1; int output; int AtoB() { output = EVEN; return B; } int BtoB() { output = EVEN; return B; } int AtoA() { output = ODD; return A; } int BtoA() { output = ODD; return A; } // rnd(m) produces a random number between 0 and m double rnd(double m) { double r;
}
r = m * (double)rand() / RAND_MAX; return r;
Figure 2.13 The tabular implementation for the parity generator - part 1 of 2
next
[0][A] = &AtoA;
Module 2 – Real-Time Software Design
2.25
int (*next[2][2])(); char parity[2][6] = {"EVEN\n" , "ODD\n"}; void main() { int state = B; int input; // state transition functions output = EVEN; next[0][A] next[0][B] next[1][A] next[1][B]
}
= = = =
&AtoA; &BtoB; &AtoB; &BtoA;
while(1) { //delay input = (int)(rnd(1.0) + 0.5); printf("%d\t",input); state = (*next[input][state])(); printf("%s\n",parity[output]); }
Figure 2.14 The tabular implementation for the parity generator - part 2 of 2 Each of the transition functions sets the output response and exits with the desired next state as the return value. One particular advantage with this approach is that response times can be easily calculated as the output update can be made to occur in one place only, in the main program after the return from the transition function. Also the tabular approach is very easy to modify and maintain.
2.5.4
Interrupt Driven Systems
In all modern computer systems the hardware supports single or multiple interrupt inputs. These interrupts can be associated with external event triggers, internal or external clock sources, software instructions or both hardware and software error traps. This rich source of asynchronous and synchronous event information is the underlying feature upon which interrupt driven systems are based. Instead of polling for events as previously described, a real-time system can be programmed to respond to interrupt events. The basic concept is that each interrupt utilises its own interrupt service routine (ISR) to service that event. Servicing may
2.26
70935 – Real-Time Systems
include transferring one or many pieces of data, counting events, starting or stopping processes and many other functions. Systems which receive only aperiodic interrupts are called sporadic systems, where-as systems which utilise only periodic interrupts are called fixed-rate systems. Systems that use both types of interrupts are called hybrid systems. One of the difficulties associated with interrupt operation is the restricted means of interaction between these ’separate’ event handlers and the main program. Because interrupts are designed to carefully save and restore the CPU status before and after the ISR is executed almost all communications between ISRs and the main program must be through shared memory. While this is achievable it greatly increases the complexity of the system. One of the main uses for interrupts in real-time systems is to provide a means of switching between processes/tasks in multi-task applications. In this case the saving and restoring of CPU status along with other system parameters can be used to stop one process and start another in a procedure called context switching.
Context switching is the process by which the kernel of a real-time system suspends the operation of one task/process via an interrupt by saving CPU registers, co-processor registers, memory page registers, stack pointers and other significant system information, before restoring an alternate context for the next process to run. The data is typically saved on the run-time stack of the suspended process, and the new context restored from the run-time stack of the next process. This is usually achieved through the use of multiple process stacks. In full featured operating systems the multiple stack model is often replaced by the Task Control Block model. In more advanced operating systems it is advantageous to allocate an area called the task control block to each task in the system. This area not only holds the process’s run-time stack but areas for task specific information, input/output buffers and inter-task communications. Figures 2.15, 2.16 and 2.17 show a program to set up the Intel8253 timer on the IBM PC to generate a regular interrupt at approximately 55ms intervals. The main program is comprised of the initialisation of the interrupt, a simulated three task/state implementation based on state variable intswitch, a sample use of the timer for time measurement and the closing down of the interrupt. The task of the interrupt service routine is to count twenty timer interrupts and change the state variable intswitch to switch between three simulated ’tasks’ approximately once every second. While there is no context switching implemented here for the tasks themselves the example serves to illustrate the concept of an interrupt driven system and task selection.
Module 2 – Real-Time Software Design
/* timer functions for 8253 programmable timer chip * * J. Leis, modified M Phythian */ #include #include #include #include #define CRYSTAL_RATE rate in Mhz */
(unsigned long)(1193180)
/* crystal
void OpenMicroTimer(void); void CloseMicroTimer(void); unsigned long ReadMicroTimer(void);
void (_interrupt _far *oldvect)(void); static unsigned ticks=0; void _interrupt _far inthndlr(void); /* example program to demonstrate use */ int int_switch; void main(void) { int n; unsigned long start, end; intswitch = 0; OpenMicroTimer(); n = 20; while (n > 10) { switch (intswitch) { case 0: printf("IN CASE 0\n"); break; case 1: printf("IN CASE 1\n"); break;
}
case 2: printf("IN CASE 2\n"); break; }
Figure 2.15 A sample timer interrupt application - part 1 of 3
2.27
2.28
70935 – Real-Time Systems
/* just reading the timer alone */ start = ReadMicroTimer(); end = ReadMicroTimer(); printf("Latency time to read timer = %ld ticks.", end-start); printf("Equivalent to %7.0f microseconds.\n", (end-start) * 1000000.0 / (float)CRYSTAL_RATE); /* time the printf() function */ start = ReadMicroTimer(); printf("The time to print this is "); end = ReadMicroTimer(); printf("%ld ticks.", end-start); printf("Equivalent to %7.0f microseconds.\n", (end-start) * 1000000.0 / (float)CRYSTAL_RATE); CloseMicroTimer(); } /* OpenMicroTImer() - must be called to initialize the * high-precision timer. */ void OpenMicroTimer(void) { outp( 0x43, 0x34); /* channel 0, mode 2 */ outp( 0x40, 0); /* lsb = 0, */ outp( 0x40, 0); /* msb = 0 -> count = 65536 */
}
oldvect = _dos_getvect(0x1c); _dos_setvect( 0x1c, inthndlr);
/* CloseMicroTimer() - must be used to de-install the timer */ void CloseMicroTimer(void) { outp( 0x43, 0x36); /* mode 3 */ outp( 0x40, 0); /* lsb = 0 */ outp( 0x40, 0); /* msb = 0 */ _dos_setvect(0x1c, oldvect); }
Figure 2.16 A sample timer interrupt application - part 2 of 3
Module 2 – Real-Time Software Design
/* ReadMicroTimer() - read the current value * of the micro-interval timer. Value returned * is an 'unsigned long', representing a time * value in crystal clock ticks, where one * clock tick is 838.1 ns ( crystal rate = 1.19318 Mhz ) */ unsigned long ReadMicroTimer(void) { _asm { mov bx, ticks ; ticks on entry mov al, 0x06 out 0x43, al in al, 0x40 mov ah, al in al, 0x40 xchg al, ah not ax inc ax mov dx, ticks cmp bx, dx je TIMERDONE
; latch tick count ; get lsb ; ; ; ;
get msb correct order convert from downcount to upcount
; has tick count incremented ?
; tick count has incremented cmp ax, 0x8000 ; past half way ? jb TIMERDONE ; yes-> ok ( nb downcount ! ) mov dx, bx ; no -> use latter value
}
}
TIMERDONE: ; return value: ; high word in DX ; low word in AX ; NOTE: COMPILER DEPENDENT !
/* interrupt handler. counts clock ticks ( 55 ms ticks ) */ void _interrupt _far inthndlr(void) { ticks++ ; /* increment cound of ticks */ if (ticks > 19) { ticks=0; intswitch ++; if (intswitch > 2) intswitch = 0; } (*oldvect)(); /* chain to other timer handlers */ } Figure 2.17 A sample timer interrupt application - part 3 of 3
2.29
2.30
70935 – Real-Time Systems
2.5.5
Types of Multi-tasking Systems
Having the ability to change tasks in a controlled manner raises the question: ”What is the best way in which tasks can be swapped in a real-time system?” That is - how does the programmer or the kernel decide which task is to run next? No definitive answer to that question has yet be to be found for an arbitrarily complex system, but several types of scheduling systems have been developed for various applications. The simplest is the round-robin system which simply divides the available CPU time into short intervals, of the order of 10ms, and allocates consecutive time slices to each task in turn. Each task runs until it is complete or until its allocated time slice is expired. At that time the task is suspended and its context saved for later retrieval. The context for the next task is loaded and it runs for up to one full time slice. All tasks in this system are assumed to be of the same importance and no one has priority over any other. While using equal priorities works well for systems with low time loading or few tasks, most systems require the ability to assign priorities to tasks to ensure response times can be guaranteed. In such systems we need the ability for higher priority tasks to interrupt or preempt lower priority tasks. Such a system is called a preemptive priority system. Priorities may be assigned at the design or programming phases based on the importance of the task, or may be assigned dynamically by a section of the kernel called the scheduler. Preemptive priority systems have the disadvantage that higher priority tasks can tend to hog system resources such as CPU time, single user input/output devices etc. This effect can be minimised by careful assignment of priorities or dynamic assignment of priorities. In systems which have a number of fixed rate interrupts it has been shown that the best performance is achieved when higher priorities are assigned to the interrupts with higher execution rates. Such systems are called rate-monotonic systems.
2.5.6
Foreground / Background Systems
Foreground / Background Systems are a combination of polled and interrupt driven systems where a polled loop is used to run useful background processing, and interrupts are used for foreground processing to service critical events or operate a multitasking kernel. Foreground / Background Systems are often used for embedded systems.
Background processes are typically used for non-critical functions including:
incrementing a counter to measure the system’s time loading incrementing task counters which get reset in the task to show the task is running self testing printing
Module 2 – Real-Time Software Design
2.31
The advantages of Foreground / Background Systems include:
the ability to achieve good response times improved reliability through the use of interrupt triggered events and scheduling of tasks.
The disadvantages of Foreground / Background Systems include:
interrupt handlers must be written for each device they are not very suitable for a system requiring a variable number of tasks.
2.6
Module Summary
This module has presented the concept of the software life cycle, outlining the six phases through which a real-time software design progresses. The concept of software engineering was discussed in terms of adopting an engineering methodology for the design and implementation of real-time systems to ensure a quality end product. Several techniques for creating system specifications and design documentation were presented for use as design tools for creation of real-time systems. The concept of a real-time kernel was introduced along with examples on how to implement several types of basic kernel.
2.7
Self Assessment Questions
1. Briefly outline the six phases of the Software Life Cycle by listing the major objectives of each phase. 2. Draft a Psuedo-code description for the procedure to change a flat tyre on a car. 3. Draft a mathematical specification for the computational section of a system to monitor the position (x; y ) of a robot moving on a flat surface at velocity v t and at an angle of t . Where v and can change at intervals of 1 second.
()
()
4. Draft a Flowchart for the procedure described above for changing a flat tyre. 5. Draft a Dataflow Diagram for a system to measure and control the temperature of a furnace according to a temperature set point which is entered by the user via a keypad. 6. Draft a Structure Chart for a system which might be used to operate a library catalog system.
2.32
70935 – Real-Time Systems
10101
7. Draft a State Diagram for a system to detect the sequence in a data stream (single bit input) and output a logic corresponding to the last bit and otherwise.
1
0
8. Draft a Statechart that represents the task(s) of driving a car. 9. Briefly outline the key features of the following real-time kernels: polled systems, state driven systems, and interrupt driven systems.
Module
3
THE C AND C++ PROGRAMMING LANGUAGES
Module 3 – The C and C++ Programming Languages
3.1
3.1
Module Overview
The C language is the basis of most “low-level” computer engineering tasks and is also commonly used for writing higher-level applications. The topics covered in the section on C are:
Compiling, linking and running a C program. Variable types and variable scope. Reading and writing data files. Large build management using the makefile.
The section on C++ covers:
Compiling, linking and running a C++ program. Objects, classes and object-oriented programming. Functions in C++, including calling C-language functions. C++ variable types and variable scope. Reading and writing data files in C++.
Later modules will cover more advanced features such as linking C and assembly code and dynamic memory allocation.
3.2
Introduction
C is sometimes referred to as a “low-level” language. The term “low-level” refers to the components commonly found in operating systems, device drivers and embedded systems. Most operating systems are written wholly or substantially in C. This module will give a brief overview of the C language by way of a set of examples. For completeness, an overview of the most important features of the C++ language is also given. It is emphasized, however, that the treatment is definitely not introductory and the student is expected to have a grasp of the principles of computer programming. The Cygnus Gnu C/C++ compiler was used for all of the examples presented here. However, the examples given in this module are sufficiently general to be able to be compiled with virtually any C++ compiler without change. a
3.2
70935 – Real-Time Systems
3.3
The C Language
C is a programming language which is very widely used in Engineering applications. These range from simple numerical and text processing tasks through to application programs such as text editors and databases, all the way to operating systems themselves. If carefully written, C code is highly portable across different operating systems and hardware platforms. C is one of the oldest programming languages, and arguably the most widely used. The original specification is called “K&R C” after it’s originators, Kernighan and Ritchie. The standard now is termed ANSI C (American National Standards Institute). Note that the C++ language is a “derivative” of C, in that C++ compilers are able to compile C programs (but the converse is not true – you cannot compile a C++ program with a plain C compiler). This document concentrates on the C language, although the second part contains an introduction to and overview of the main concepts of C++ . Note that C++ programs normally have a file extension of “.cpp”, whereas standard C programs have a “.c” extension.
3.3.1
Starting
C code produces a stand-alone, or executable program that may be run independently of the compilation process. This is unlike MATLAB (for example), which requires the MATLAB interpreter to be available on the user’s computer system in order to run. Note however that some compiler environments require more than the plain executable file to be distributed, and require certain library files (typically .lib, .dll or .so). In that case, the linker or development environment will normally have one or more options in order to produce a “stand-alone” executable file. It must be understood that C has no intrinsic input or output (I/O) functions of it’s own. The language includes constructs for variable declaration, numerical calculation, looping and the like, together with the ability to extend the basic functionality via library function calls. It might seem strange to have no inherent I/O, but remember that the notion of I/O is quite different in a DOS program, a Windows program or (for example) an embedded control computer for a car’s engine. Of course, some form of I/O is required, and thus a set of standard “library” functions like printf() for screen printing and scanf() for keyboard input are provided with each compiler. Further examples of code libraries include windowing code and network access functions.
3.3.2
Compiling C Programs
The so-called Integrated Development Environment (IDE) compilers are menu-based, and as such have on-screen help facilities. To compile programs, the menu choice
Module 3 – The C and C++ Programming Languages
myapp.c extern
other.c
void someFunc(void); void
int
f
3.3
main()
f
someFunc()
// function code someFunc();
g
// other code
g
Figure 3.1 External code modules.
is generally termed “build” or “make”. Note that simply “compile” will not produce an executable program, as compilation is only the first step. The source files normally have a “.c” extension. The compiler takes each .c file and produces an object file, which normally has an extension“.o” (under Unix and the Gnu DOS/Windows compilers) or “.obj” in other DOS compilers. The object files are linked together, using the “linker” program, which is normally invoked automatically after compilation to produce an executable file. In DOS, this normally has a “.exe” extension, while in Unix the extension is not significant. The fact that a Unix file is executable is seen by typing ls -l filename and examining the “x” flag. Other aspects, such as dynamic linking and the use of makefiles, are discussed at the end of this module. The Gnu C compiler is invoked by the command gcc. The simplest usage is as follows:
gcc myprog.c -o myprog.exe This runs the compiler and linker combined (the gcc program) on the source file myprog.c to produce the output file (as denoted by the -o option) myprog.exe1. Note that the output of the compiler is only an onject file, whereas the output of the linker is a full executable file. This simple invocation will work if the various components required by the compiler and linker are in the default directories. Breaking up the code into more than one source file is the normal practice for anything but the simplest of programs. If there is more than one source file, some references to code in other files (modules) will be required, as depicted in Figure 3.1. In this case, the compiler processes each code module separately into an object file, and the linker resolves the references to external code or data. The compiler must be told to expect certain variables or data to remain unresolved. This is done via the 1
The .exe extension should not be used on Unix systems.
3.4
70935 – Real-Time Systems
Source files
Object Files
Compile
Link
myapp.c
myapp.o
Executable
other.c
other.o
myapp.exe
more.c
more.o
Figure 3.2 Compiling and linking a C program.
extern declaration. For the example shown in Figure 3.1, myapp.c would require the following declaration at the start of the file:
extern void
someFunc(long);
which essentially states that the code for function someFunc() is in another module; it expects a “long” arguments and returns a “void” data type. There is no limit on the number of separate code modules – as many as are required for clarity. Normally code modules contain groups of related functions. The process is depicted graphically in Figure 3.2. The compile-link command required now is
gcc myapp.c other.c more.c -o myapp.exe Of course only one output file must be specified. It is common to have constants and/or data structures shared amongst code modules. Such constants may be, for example, the expected maximum number of students in a class. Data structures, which will be detailed further in a later section, are composite data types which group together related data (for example, a student name and grades). These constants and data structures are likely to be required by several code modules. It would be a maintenance nightmare if each separate file maintained separate definitions, as one change in a constant would require a change in each one of the source files. Of course, this could introduce hard-to-trace errors. So, “include files” are used for this purpose, which are shared between code modules. These files have a “.h” extension. They are included into the source code module by a statement of the form
#include
Module 3 – The C and C++ Programming Languages
Source files constant.h
Object Files
Compile
Include
3.5
Link
myapp.c
myapp.o
Executable
other.c
other.o
myapp.exe
more.c
more.o libwin.a Library files
Figure 3.3 Compiling and linking a C program with include and library files. or
#include "constant.h" The latter specifies the file as being in the same directory as the source file. The former specifies a “standard location” as will be seen shortly. Common code modules are also provided for various purposes. For example, a windowing system will have a common set of primitives to draw a window, resize a window and so forth. These are called libraries or archives and are effectively “pre-compiled”. That is to say, the user does not have access to the source code – only the object code. This is illustrated in Figure 3.3. In order to specify the library, the -l switch is used. Standard libraries reside in a directory called lib. On Unix systems, the standard math library (providing functions such as sin and sqrt) reside in the file libm.a. The library switch is such that the linkage specification -lx searches for a standard library of the form libx.a. The path to the library may be specified with the -L switch. Similarly, standard include files reside in a directory called include. The path to the include files may be specified with the -I switch. A default path is also defined – normally include under the compiler root directory. Putting this all together, the following DOS batch file compiles the source file specified on the command line into the corresponding executable file:
@rem compile using Gnu C, in a DOS box under Windows gcc -I incdir %1.c -o %1.exe -L libdir -lm Here, %1.c -I -o -L -lm
specifies the first command-line argument with .c appended specifies the path to the include (,h) files specifies that an output file follows (here the program name with .exe appended) specifies the path to the library files (unnecessary here) specifies loading of the math library functions such as sin() from libm.a
3.6
70935 – Real-Time Systems
It would be used on the command-line as follows:
gc myprog where gc.bat is the name of the batch file as outlined. A similar script file for Unix may also be used (using correct path separators and command-line switches). Unix shells use $1 for command-line argument 1. Unix does not use the extension to signify the executable nature of the script – simply create the file gc using a text editor and then change the mode using
chmod +x gc Note that the libraries are only searched on demand. The code for library functions is only added to the executable image if needed. This reduces the size of the final executable.
Module 3 – The C and C++ Programming Languages
3.7
activity 3.1
Compilation using “include” Files Enter the following text and name it ctest.c
#include "constant.h" int {
}
main(void) short
x;
x = SOME_CONSTANT;
Enter the following text and name it constant.h
#define SOME_CONSTANT
45
Compile ctest.c using
gcc ctest.c The default output is called a.exe on DOS systems, or a.out on Unix systems. Normally you would explicitly name the executable, so use
gcc ctest.c -o ctest.exe (Omit the .exe on Unix systems) Then execute the program by typing ctest at the command prompt. Now remove the #include line in the source C file. Compile and note the error messages. Restore the line. Now change the #include line to read
#include Try to compile and note the error messages. Now compile using
gcc ctest.c -I. -o ctest.exe Where -I sets the search path for include files specified with < > . The “.” after L specifies the current directory. It should compile correctly.
3.8
70935 – Real-Time Systems
activity 3.2
Function Prototypes and Library Linkage Enter the following text and name it ctest.c
#include int main() { double x; }
x = sin(3.14);
Compile ctest.c using
gcc ctest.c -o ctest.exe Now remove the #include line in the source C file. Compile and note the error messages about the sin() function. This is because the function prototype in the include file math.h has been omitted. Look at this file – normally in a directory called include below the compiler installation path on Windows or /usr/include on Unix. Note the function prototype for the sin() function shows the data types of the arguments expected and returned. Restore the #include line. Now compile using
gcc ctest.c -o ctest.exe -nostdlib Note the error messages. Because the standard library is not included due to the -nostdlib switch the library code for the sin() function will not be defined.
Module 3 – The C and C++ Programming Languages
3.9
activity 3.3
Compile-Only Test the “compile-only” option. Remove any object files:
del *.o (or rm *.o in Unix) Compile ctest.c using
gcc ctest.c -o ctest.exe Now look at the object file:
dir ctest.* (or ls -la ctest.* in Unix) In order to link this object file with the standard library enter
gcc ctest.o -o ctest.exe This will produce the executable.
activity 3.4
Libraries Compile the ctest.c program with the -lm option to link in the math library libm.a. Now look at the size of the resulting executable file and the size of the math library. Clearly the entire contents of the math library have not been included in the executable!
Lastly, it is worth noting that it is possible to view the assembly-language instructions corresponding to the C source code. A complete discussion of this topic is somewhat beyond the scope of this introductory tutorial, however the following exercise shows what is possible.
3.10
70935 – Real-Time Systems
activity 3.5
Assembly Output Enter the following text and name it ctest.c
int {
}
main(void) short
y, x;
x = 3; y = x + 1;
Compile ctest.c using
gcc ctest.c -S The -S option (note capitalization) forces the C to assembly stage only — it does not produce an executable. The output file will be the same as the source file with a .s extension (assembly). View the file ctest.s with a text editor. Note the processor assembly instructions such as movw and incl.
3.3.3
Basic Structure of a C Program
A rudimentary C program is shown in Figure 3.6. The “main” function is the first point of entry after the program begins. It is mandatory to have a function called main() In a Windows program rather than a command-shell one the entry point must be called WinMain(). The arguments to main() are void, meaning “nothing”; similarly, the return value of main() (on the left-hand side) is int. This does not have to be the case, but for a simple example it will suffice. Comments begin with “/*” and end with “*/”. If the compiler is C++ aware (the majority of C compilers), then a single-line comment may also be entered by starting the line with “//” As with any programming language, C allows the decomposition of problems into smaller sub-problems via functions. In other languages, functions are variously called “subroutines” or “procedures”. Functions may have any number of arguments passed in (enclosed by the brackets ()), but only return one value (on the left-hand side). This is illustrated in Figure 3.4. This is a “functiomn prototype”, or what the function “looks
Module 3 – The C and C++ Programming Languages
3.11
function name return value (out) arguments (in)
z
}|
{
short someFunc( short arg1, char *arg2, double arg3 )
Figure 3.4 C function arguments and return value.
) increasing memory address ) ! allocated but unused ! n
a
first location
m
e
0
x
x
x
x
last location = null
Figure 3.5 In C, strings (arrays of characters) are always null-terminated.
like” to the compiler, in terms of arguments passed and return value. This is because the compiler will encounter the call to the function PrintMessage() before the actual function itself. Thus, it is a form of consistency checking.
The lines beginning with #include are directives to the C preprocessor; in this case, it means literally include the files stdio.h and stdlib.h . These are called “header files”, and contain (amongst other things) function prototypes for the functions which will be used. Here, the printf() function is prototyped in stdio.h, and atoi() is prototyped in stdlib.h. The compiler’s help screens or manual will inform you as to which include files are required for each library function. The include files are located in a special dedicated directory, normally called include under the compiler’s installation directory.
In Figure 3.6, gets() gets the character string from the user. This is to be interpreted as a number, hence the function atoi (ASCII to integer) is used to convert the string (in C, an array of characters or “chars”) into an integer (in this program, a “short” integer of 16 bits size). In C, character strings are stored in “null-terminated” form as depicted in Figure 3.5
3.12
70935 – Real-Time Systems
/* basic.c - this is a basic C program */ #include #include // function prototype void PrintMessage( short NumTimes ); // main entry point void main() { char UserBuf[100]; short NumTimesToPrint; printf("How many times do you want it printed? "); gets( UserBuf ); NumTimesToPrint = atoi( UserBuf ); }
PrintMessage( NumTimesToPrint );
void PrintMessage( short NumTimes ) { short NumMessages;
}
for( NumMessages = 0; NumMessages < NumTimes; NumMessages++ ) { printf("This is message number %hd \n", NumMessages); }
Figure 3.6 The C program basic.c
Module 3 – The C and C++ Programming Languages
3.13
activity 3.6
Uninitialized Variables In the program basic.c remove via commenting-out, the three lines which prompt the user and subsequently sets the variable NumTimesToPrint. Re-compile and run the program. Are the results predictable?
activity 3.7
Compiler Warnings Enter the following program, called ctest.c
int {
main() double x; // other code would follow here
}
Now compile using
gcc ctest.c -o ctest.exe -Wunused This should issue a warning about “unused variable x”. This is an example of compiler warnings which may be available.
3.3.4
Data Types
C has moderately strong typecasting – you must declare all variables before use, and take care to assign appropriate quantities to variables. The main data types used are:
short long char double
a short integer, 16 bits a long integer, 32 bits a character, 8 bits a double-precision floating point variable
3.14
70935 – Real-Time Systems
The data type int may be used, however it can cause problems because it is defined to be the “native” size on the machine on which it was compiled. This may be either 16 or 32 bits, and is thus ambiguous. The data type float also exists for single-precision floating point values, however the extra precision of double is preferred because of the prevalence of floating-point coprocessors in modern CPU’s (double generally takes no longer to calculate with). A string is simply an array of characters: char MyName[20]. The array must be terminated by a character value of 0 (the “null character” value). C will allow certain invalid assignments to be made, and issue a warning. For example, if pi was declared as a short, and we had the statement pi = 3.14, then a warning such as “loss of precision” may be issued. The program would still run, however (pi would be set to 3). In many systems-level programming tasks, this rounding-off behaviour may be desirable, but generally is the cause of many bugs. This issue is termed data typing, or usually just typing. Type rules catch the passing of incorrect arguments to a function, which may compile correctly but not execute correctly. An example is the passing of a double where an int was expected. Languages are said to be “strongly typed” if the data types are strongly enforced. “Weakly typed” languages do not enforce correct argument typing. Assembler is the “weakest” in this regard, as the discipline of enforcing correct data storage sizes is entirely up to the programmer. Strongly-typed languages, though desirable, often make required operations impossible. C is somewhere in between; for example, the following is incorrect but on most compilers will not issue a warning by default:
int x; double d; d = 3.14159; x = d;
whereas the following, using a “type cast”, is acceptable:
int x; double d; d = 3.14159; x = (int)d;
Presumably the effort of placing the typecast (int) means that the programmer was sufficiently aware of the implications (in this case, truncating down 3.14159 to 3 when stored as an integer).
Module 3 – The C and C++ Programming Languages
3.15
activity 3.8
Data Size Errors Enter the following program:
#include int main() { short shortVar; long longVar; // something greater than the // maximum `short' allowable longVar = 75000;
}
shortVar = (short)longVar; printf("longVar=%ld shortVar=%hd \n", longVar, shortVar );
What is wrong here? Can you explain the output?
3.3.5
Variable Scope
C has “automatic” variables, which are declared after the opening brace {, remaining until the matching closing brace }. These are local to the function in which they are declared. Passed-in arguments appear between the brackets () of functions and cannot be changed by a function. Global variables (accessible by all functions) are declared outside any function scope. Figure 3.7 illustrates these concepts, with the function TestFunc() shown in Figure 3.8. The output of scope.c is shown in Figure 3.9. Note that local values are not altered within the scope of the calling function, and that RetVar is initially unassigned and has a random value. The pointer data type is discussed in the following section. The use of pointer variables will be discussed in the next section.
3.16
70935 – Real-Time Systems
/* scope.c * Simple illustration of variable scoping in the C language * * John Leis */ #include /* a global variable * If we wish to use this variable from other modules (C files) * we put the same declaration with the keyword "extern" in front. */ short
GlobalVar;
/* function declaration */ short TestFunc( short InVar1, short InVar2, short *PtrVar); /* the "main" entry point */ void main() { short Var1, Var2, RetVar; short *PointerToVar; Var1 = 4; Var2 = 5; GlobalVar = 6; printf("Before function call, RetVar = %hd, Var1 = %hd, Var2 = %hd\n", RetVar, Var1, Var2); RetVar = TestFunc( Var1, Var2, &Var1 ); printf("After function call, RetVar = %hd, Var1 = %hd, Var2 = %hd\n", RetVar, Var1, Var2); printf("GlobalVar = %hd\n", GlobalVar); printf("\n ---------------- \n\n");
}
printf("Before function call, RetVar = %hd, Var1 = %hd, Var2 = %hd\n", RetVar, Var1, Var2); PointerToVar = &Var1; RetVar = TestFunc( Var1, Var2, PointerToVar ); printf("After function call, RetVar = %hd, Var1 = %hd, Var2 = %hd\n", RetVar, Var1, Var2); printf("Contents of pointer variable = %hd\n", *PointerToVar);
Figure 3.7 The “main” section of the program scope.c
Module 3 – The C and C++ Programming Languages
/* a test function * The function return value is the sum of the passed-in arguments * (InVar1 + InVar2) * The contents of the pointer variable PtrVar are replaced * with the product (InVar1 * InVar2) * The global variable GlobalVar is changed to (InVar1 - InVar2) * Note that the function *attempts* to change InVar1 and InVar2, * but that they are not changed (pass-by-value). */ short TestFunc( short InVar1, short InVar2, short *PtrVar) { short SumResult, ProductResult; SumResult = InVar1 + InVar2; ProductResult = InVar1 * InVar2; *PtrVar = ProductResult; GlobalVar = InVar1 - InVar2; /* this will *not* change the values in the calling function */ InVar1 = 45; InVar2 = 54; return SumResult; }
Figure 3.8 The function TestFunc() from scope.c
Before function call, RetVar = 888, Var1 = 4, Var2 = 5 After function call, RetVar = 9, Var1 = 20, Var2 = 5 GlobalVar = -1 ---------------Before function call, RetVar = 9, Var1 = 20, Var2 = 5 After function call, RetVar = 25, Var1 = 100, Var2 = 5 Contents of pointer variable = 100
Figure 3.9 The output of the program scope.c
3.17
3.18
70935 – Real-Time Systems
3.3.6
Pointers
In the preceding example, the pointer data type was used. Variables in any program are stored in memory, and the pointer is just another variable that happens to hold the memory address of another variable. Although not strictly required for elementary programming tasks, the pointer is quite powerful and gives extreme flexibility in many situations. An code example of pointer variables and their use was given in the previous example (scope.c, Figure 3.7). Two symbols, “*” and “&”, are used in connection with pointers. Read them as “*” “contents of” “&” “address of” So the code fragment as follows:
short short
Var1; *PointerToVar1;
Var1 = 4; PointerToVar1 = &Var1; *PointerToVar1 = 5; may be read as Declare Var1 to be a short integer. The contents of PointerToVar1 is a short integer. Set Var1 to 4 as a direct assignment. PointerToVar1 is set to the address of Var1. Set the contents of PointerToVar1 to be 5. That is, set it indirectly. This is shown in Figure 3.10. Note that the value contained in PointerToVar1 is not assigned directly by the programmer, but depends on where the compiler, linker and run-time loader allocate the memory addresses. This taking-the-address-of is sometimes called “dereferencing” or more commonly “indirection”. The question may be asked as to why this added complexity is necessary. The answer is that such memory addressing facilitates rapid moving through arrays of characters (strings) or other data types and is often necessary for low-level operations. Sometimes it is very useful to have a more advanced construct, that of double indirection. This is where a “pointer-to-a-pointer” is required, as shown in Figure 3.11. Note how the pointer-to-pointer declaration syntax is consistent: simply declare two “contents-of”:
Module 3 – The C and C++ Programming Languages
3.19
16 bits
0 0 0 4
short Var1
short *PointerToVar memory
5 8 A B 7 6 9 8
Figure 3.10 Pointer dereferencing in the example program.
short **ppVar; Again, this added complexity gives much greater flexibility in accessing memory. Memoryefficient dynamic data structures such as doubly-linked lists use pointer dereferencing.
3.3.7
Command-Line Arguments
In DOS or Unix command-line (shell) programs, arguments may be sent to the program via the command-line itself. Windows also allows the specification of parameters when an application is started. For example, a command such as
copy somefile.txt other.bak requires the arguments somefile.txt and other.bak to be used in the copy program. The program cmdargs.c, shown in Figure 3.12, simply prints out all of its commandline arguments. Note in the sample output that the program name itself is the “zeroth” argument.
3.3.8
Loading & Saving Data
It is often necessary to load in some data files, or save calculated data for later reference or plotting.
3.20
70935 – Real-Time Systems
short *pVar b
short Var
short **ppVar b
Figure 3.11 Pointer-to-pointer dereferencing.
/* cmdargs.c * Command-line arguments * * example: d:\c\gnuc> cmdargs one two three There are 4 command-line arguments Argument number 0 is d:/c/gnuc/cmdargs.exe Argument number 1 is one Argument number 2 is two Argument number 3 is three * John Leis */ #include int main( int argc, char *argv[] ) { int argNum;
}
printf("There are %d command-line arguments\n", argc); for( argNum = 0; argNum < argc; argNum++) { printf("Argument number %d is %s\n", argNum, argv[argNum] ); }
Figure 3.12 Processing of command-line arguments.
Module 3 – The C and C++ Programming Languages
0.001 0.193 0.585 0.350 0.823 0.174 0.711 0.304 0.091 0.147
3.21
0.564 0.809 0.480 0.896 0.747 0.859 0.514 0.015 0.364 0.166
Figure 3.13 A text (ASCII) data file An important distinction to be made is between binary and text (or “ASCII”) formatted files. Text files contain plain, readable text which can be viewed with any text editor. For example, temp.txt might contain the data as shown in Figure 3.13. Text files may be created from a C program using the code fragment shown in Figure 3.14. Note the use of fscanf() to read formatted data. The format specifier %lf specifies a double-precision variable is being used (in this case, %5.3lf limits the output to a field of width 5 with 3 decimal places). We can then continue on to read back the text file using the code fragment shown in Figure 3.15. In the preceding example, the read and write functions are contained in the main program for ease of illustration. Of course, it is good practice to separate them into functions. In addition to text files as discussed above, you may encounter binary data files. These are not viewable using text editors – they consist of “raw” 8 or 16-bit quantities (usually), which are machine-readable and not human-readable. The exact representation depends on the CPU being used – for example, a Pentium CPU has a different representation for 16-bit integers to SUN Sparc CPU’s. The integer representations may be converted, but the situation for floating-point numbers is much more problematic. For this reason, the Institution of Electrical and Electronic Engineers (IEEE) format is often used. Binary files may be written by using the "wb" (write, binary) mode when calling fopen(), and using fread() to read the raw byte stream. Instead of formatted data handled by fprintf(), we use fwrite() to write a certain number of bytes (using the sizeof() operator). Figures 3.16 and 3.17 show the code necessary to write and read binary files. Of course, the binary data files themselves will not be directly printable — try this by loading temp.bin into a text editor such as DOS’ edit. Note the format specifier "rb" (read-only, in binary mode) as opposed to the text files discussed previously, which were opened using "r" mode (text is the default). On Unix systems, there is no need to explicitly specify b as part of the mode string.
3.22
70935 – Real-Time Systems
/* textfile.c * Illustrates reading and writing a text (ASCII) file * containing numerical data samples. */ #include #include #include #include
int main() { FILE double short char char
*fp; x, y; SampNum, NumSamples, NumSamplesRead; LineBuf[100]; *FileName = "temp.txt";
// Create the data text file. // Format is two numbers on each line - space to separate fp = fopen( FileName, "w" ); if( ! fp ) { printf("cannot open output file\n"); exit(1); } NumSamples = 10; // number of samples to write SampNum = 1; do { x = rand()/(double)RAND_MAX; y = rand()/(double)RAND_MAX; printf("%hd %5.3lf %5.3lf\n", SampNum, x, y); fprintf(fp, "%5.3lf %5.3lf\n", x, y); SampNum += 1; } while ( SampNum n ~x x | y x & y x ^ y
3.4
meaning shift x left n bits shift x right n bits complement x logical or x and y logical and x and y exclusive-or x and y
example
z x z z z z
= x >= 1 = ~x = z | y &= 0x00ff ^= 1
Advanced Topics
This section gives an overview of several topics which should be considered in various C development situations – portability across different hardware, some hardware issues themselves, dynamic memory allocation and compilation management for larger projects. Some of these are examined in greater detail later in the unit.
3.4.1
Portability
Generally, C code is highly portable between different platforms and operating systems, provided care is taken with library functions. The exceptions are file handling (as discussed above) and data sizes. The latter is why the int data type is not recommended. File handling in binary mode, if coded carefully, can be platform-independent and hence portable. More subtle structure alignment problems can occur when porting. If a structure size must have an exact member alignment, the #pragma pack(1) directive may be used.
Module 3 – The C and C++ Programming Languages
3.4.2
3.25
Makefiles
Typing the compilation command(s) on the command line can become tedious and tiring. A batch file with the appropriate commands can go a long way to simplifying this. However, for large projects, a better solution is required. As mentioned previously, it is good practice to break a project into code modules in separate files. A large project may run to dozens of files. Creating a single change, even just adding a comment (which of course does not change the executable code), requires compiling and linking all over again. For more than relatively simple projects, a makefile is recommended. In a large multisource project, if one file is edited then it is the only one which needs to be re-compiled – not all of the source files need be recompiled – followed by linking of the object files. The make utility solves this problem, and automates the building of large software projects. make does this by checking and comparing the dates on the source files and the corresponding object files. If the object file is newer, then it must have been compiled after the last modification to the source. If the source is newer than the corresponding object file, then that source file must be re-compiled. If any sources are re-compiled, the link stage must be performed again. Figure 3.19 shows an elementary makefile. The most important concepts are: Target is normally the executable program to be generated. More than one target may exist, but only one is built at a time. The target may be a dummy one in order to invoke some other operating-system command. Dependencies specify what files need to be re-built to create a specific target. Suffix Rules are the rules for building the targets, for example an object file from a C source, an object file from an assembly-code source, and an executable from object files. The first section contains some comments regarding the project. Following this the dependencies (object files) are listed – here they’re called DEPS and contain only one object (basic.o), but normally there would be many. The executable target is named TARGET. Following are the suffix rules — here, the only rule specified is to create an object file from a C source (compile only). Of course, other language compilers could be invoked or a rule added for creating an object from an assembly language source (assembly only). The executable target basic.exe specifies a dependency on all the object files (although here only one), followed by the appropriate command to run. This is specified on the following line and must be indented one tab stop. In this case, it is effectively just a link phase. Note well: the rule to be executed after the dependency must begin with a tab character, not spaces. Otherwise the error “missing separator” will result. So the second line of
# suffix rules .c.o: $(CC) -c $(CCOPTS) $(INCDIR) $*.c -o $*.o
3.26
70935 – Real-Time Systems
must start with a tab, as must the second line of
# make targets basic.exe: $(DEPS) $(CC) -o $(TARGET) $(DEPS) $(LDOPTS)
Other targets may be specified to enhance project maintenance. Here, the targets noexe and clean specify an action but no dependencies.
activity 3.9
Make Exercises Run the make utility on the sample makefile:
make Note the compile and link phases. Run it again:
make Note that the target is now up to date. Now delete the executable:
make noexe Then build the target again:
make Note that only the link phase is invoked. If there were more than once source C file, only the file(s) which have been changed are re-compiled. Now delete the object files:
make clean Then build again:
make Note that now both compile and link are performed.
Module 3 – The C and C++ Programming Languages
3.4.3
3.27
Public-Domain C Resources
A number of public-domain resources for C programming exist, in the form of C libraries for special functions (matrix calculations, graphics & animation, signal processing, to name a few). For more advanced work, the Frequency Asked Questions (FAQ) may be consulted on newsgroup comp.lang.c. Users of Gnu C should consult the FAQ which is available with the distribution.
3.28
70935 – Real-Time Systems
// binfile.c // Illustrates reading and writing a binary file // containing 16-bit integer data samples. #include #include #include #include
int main() { FILE *fp; short Sample; short SampNum, NumSamples, NumSamplesRead; double NumCycles=2, pi = 4.0*atan(1.0); char *FileName = "temp.bin"; // Create the binary file. // 16 bits (2 bytes) per sample. fp = fopen( FileName, "wb"); if( ! fp ) { printf("Cannot open output file `%s'\n", FileName); exit(1); } printf("Opened file `%s'. Data size = %hd bytes\n", FileName, sizeof(short) ); // write 10 samples to the file NumSamples = 10; // number of samples to write SampNum = 1; do { /* write samples of a sine wave to the file. * Note scaling by 1000 (maximum is approximately 32000), * because we are dealing with 16-bit signed integer quantities. */ Sample = (short)(1000.0 * sin(NumCycles*2.0*pi*(double)SampNum/(double)NumSamples)); fwrite( &Sample, sizeof(short), 1, fp); printf("%hd %hd \n", SampNum, Sample); SampNum += 1; } while( SampNum > 8) ); fflush(stdout);
}
} exit(0);
Figure 6.7 Using fork() for duplication then overlaying using exec().
Module 6 – Multitasking 6.11
child process - fork() returned 0, my pid via getpid() is 1001 About to execl()... Parent process ID 1000, child process is 1001 Child process here with PID 1001 Child: arg 0 = 'child' Child: arg 1 = 'some arg' Child sleeping...Child exiting. Parent: child PID 1001 gave exit status 5 Figure 6.8 Output of fork()+exec() test.
tightly integrated into NT, and that each process has a “main thread”. The output of this example is shown in Figure 6.10 – note that the same child code is used.
activity 6.3
Processes under Windows NT. Compile and run the process.c code. What happens if child.exe is not present? Change the child process name (variable char *processName) to another program, such as notepad.exe. If using notepad.exe it will be necessary to copy it from \winnt\notepad.exe
Note that graphical or GUI (graphical user interface) applications under Windows have the function WinMain() as their main entry point rather than main(), with different arguments.
6.4
Threads
A process, under either Unix or NT, is a separate stand-alone program with its own separate local and global variables, open files and so forth. Although child processes are given command-line arguments from the parent and can inherit a copy of the open file handles of the parent, the child has it’s own execution context. That is, it’s own variables, stack, and so forth. The child process has a main() or WinMain() at which execution begins. When main() reaches the end, or the child calls exit(), the process terminates. Threads are a different proposition. Threads are essentially a special function within the main parent process. However, when that function is invoked as a thread, it is
6.12
70935 – Real-Time Systems
// process.c - Windows NT processes (tasks). // Creates 4 child processes. // John Leis #include #include #include int main() { STARTUPINFO PROCESS_INFORMATION BOOL char int DWORD LPVOID
si; pi; fCreated; *processName = "child.exe"; nProcesses = 4, processNum; errorCode; lpMsgBuf;
memset(&si, 0, sizeof(STARTUPINFO) ); si.cb = sizeof(STARTUPINFO);
}
for( processNum = 1; processNum pserver Pipename = \\.\pipe\mypipe about to connect to named pipe... connect pipe OK 6 bytes read: hello * Example output (client) C:\usr\c\NT>pclient Pipename = \\.\pipe\mypipe 6 bytes written Figure 7.9 Windows NT pipe outputs. pipe using CreateFile() with the appropriate flags for read-only opening of an existing file (actually a pipe). A message is then written using WriteFile(). An example output is shown in Figure 7.9. When executing this example, the output is more easily understood if the client pclient and server pserver are invoked from separate console windows. In addition, the server must be executed first in order to create the pipe. In a practical situation, this means that the server would have to create the client in a parent/child situation, rather than having them invoked separately as is done here.
activity 7.1
Windows NT Pipes. Compile the code examples pserver and pclient. Create two separate command (DOS) windows. Invoke the client in one window – it should fail, as the code assumes the pipe has already been created and simply tries to open the existing pipe file. Now run the server in one window first, and then the client in the other window.
Module 7 – Interprocess Communication
/* pclient.c Windows NT named pipe - client * John Leis */ #include #include #include #define NBUF #define PIPENAME
100 "\\\\.\\pipe\\mypipe"
int main() { HANDLE hPipe; char *msg = "hello"; DWORD written, nBytes; LPSTR pBuf; BOOL fWrite; printf("Pipename = %s\n", PIPENAME); hPipe = CreateFile( TEXT(PIPENAME), // \\machine\pipe\name GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if( hPipe == INVALID_HANDLE_VALUE ) { printf("pipe open error\n"); exit(1); } pBuf = msg; nBytes = strlen(msg)+1; fWrite = WriteFile( hPipe, pBuf, nBytes, &written, NULL); if( fWrite == FALSE) { printf("client:write error\n"); exit(1); } printf("%ld bytes written\n", written); }
exit(0); Figure 7.10 A message pipe client under Windows NT.
7.11
7.12
70935 – Real-Time Systems
7.5
Shared Memory
The previous section introduced pipes, which are a queued, one-way mechanism for exchange of data. The pipe is inherently “serial” or sequential. As a result, communication is potentially slower, due to the queueing of the pipe buffers in the operating system kernel. A more fundamental problem is that the data must be accessed in the order in which it was sent — any urgent or high-priority messages must wait their turn. In some applications, another solution is needed. Normally each process has its own memory space, and cannot (deliberately or accidentally) read from or write to the address space of any other process. However, direct access to memory is the fastest possible type of interprocess communication, and it allows random access (rather than sequential as in pipes). As shown in Figure 7.11, the shared memory segment does not necessarily appear in the same address space of each process. process 1
process 2
shared segment
Figure 7.11 Illustrating process memory sharing or “memory mapping”.
7.5.1
Shared Memory under Unix
Different Unix variants implement different API methods for accessing shared memory. The POSIX file-mapping method is illustrated here, as it is more portable and quite similar to the Windows NT method shown in the next section. Figure 7.12 shows a complete shared-memory program. The abstraction uses a file, which is mapped into the address space of the process. The name of the file (here “mapfile”) is the common key for all processes using the shared memory segment. Examining the code, the file is first created using open() with the create and read-write flags. The mapping is performed using mmap(), which takes the file descriptor and returns a pointer to the shared memory block. This pointer may then be used as a conventional memory pointer. In the example, the first byte of the shared memory is incremented in a loop every one second.
Module 7 – Interprocess Communication
/* mmap.c - illustrates mmap() system call * This program creates a shared memory segment via a file * and writes some data into it. If two processes access * the shared memory, the output becomes unpredictable. * Platform: SunOS or cygwin/NT Compiler: gcc * John Leis */ #include #include #include #include #include #include
int main() { int fd, i, len = 10; off_t off = 0; char *pMem, c; // initialize device file fd = open("mapfile", (O_CREAT | O_RDWR), 0666 ); if( fd < 0 ) { perror("open(fmap)"); exit(1); } c = 'x'; write( fd, &c, 1); // map the file to memory pMem = (char *)mmap( (caddr_t)0, len, (PROT_READ | PROT_WRITE), MAP_SHARED, fd, off);
}
printf("pMem is %p\n", pMem); c = 'a'; for(i=0; i < 10; i++) { *pMem = c; sleep(1); printf("%d: c=%c *pMem = %c\n", getpid(), c, *pMem); c++ ; } munmap(pMem, len); close(fd); exit(0); Figure 7.12 Shared memory via mmap() under Unix.
7.13
7.14
70935 – Real-Time Systems
phanes (leis) [16] pMem is ff380000 18105: c=a *pMem = 18105: c=b *pMem = 18105: c=c *pMem = 18105: c=d *pMem = 18105: c=e *pMem = 18105: c=f *pMem = 18105: c=g *pMem = 18105: c=h *pMem = 18105: c=i *pMem =
mmap a b c d e f a b c
...(above & below run simultaneously)... phanes (leis) [38] pMem is ff380000 18106: c=a *pMem = 18106: c=b *pMem = 18106: c=c *pMem = 18106: c=d *pMem = 18106: c=e *pMem = 18106: c=f *pMem = 18106: c=g *pMem = 18106: c=h *pMem = 18106: c=i *pMem =
mmap h i j d e f g h i
Figure 7.13 Output of Unix shared memory example.
Starting two of these processes (preferably in two separate command windows for a saner output) results in two processes attempting to access the same area of shared memory. Figure 7.13 shows the situation when one is started and several seconds later the second started. If only one process were running, the character ‘c’ which was written would correspond to the character returned from the shared memory, *pMem.
The first read of the second process results in the character ‘h’ being returned, which was placed there by the first process. The second process then writes an ‘a’, which is read by the first process.
Evidently, the benefits of faster and random access to shared memory have introduced another problem which was not present with pipes: that of synchronization. Some method of guaranteeing exclusive access to a shared memory area is necessary so that operations may be performed atomically. This topic is discussed in the next module.
Module 7 – Interprocess Communication
7.15
activity 7.2
Unix shared memory. Explain the output of Figure 7.13 in terms of the source code. When did the scheduler interrupt one process and start another?
7.5.2
Shared Memory under Windows NT
Windows NT uses a similar analogy for accessing shared memory. The memory segment is “mapped” to a filename, much as the pipe message queues were. Figure 7.14 shows the creation of the file using CreateFile() This is effectively a standard disk file initially (here called “mapfile”). Next (Figure 7.15), the file is mapped into another file handle using CreateFileMapping(). This prepares the file for memory mapping, and uses a different filename (here “mapmemory”). Finally, the file contents are mapped to a memory address range within the process using MapViewOfFile(). This returns a pointer *pMapMem to the start of the shared memory block. In a manner similar to the Unix example, Figure 7.16 shows the result when two processes attempt to access the same shared memory block.
activity 7.3
Windows NT shared memory. Explain the output of Figure 7.16 in terms of the source code. When did the scheduler interrupt one process and start another?
7.16
70935 – Real-Time Systems
/* mmap.c - Windows NT shared memory via file mapping. * Run two of these processes in separate windows. Start * one a few seconds after the other. * Normally semaphores or some other access mechanism would * be used to control access to shared objects. * John Leis */ #include #include #include #define MAP_FILENAME #define MAP_MAPNAME
"mapfile" "mapmemory"
#define BYTES_TO_MAP
1
int main() { HANDLE hFile, hMap; char *pMapMem, c; SECURITY_ATTRIBUTES sa; SECURITY_DESCRIPTOR *psd; int n; // null acl - unlimited access psd = (SECURITY_DESCRIPTOR *)LocalAlloc( LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH); InitializeSecurityDescriptor(psd, SECURITY_DESCRIPTOR_REVISION); SetSecurityDescriptorDacl(psd, TRUE, NULL, FALSE); sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = psd; sa.bInheritHandle = TRUE; hFile = CreateFile(MAP_FILENAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL); if( hFile == NULL ) { printf("CreateFile() failed\n"); exit(1) ; } Figure 7.14 Shared memory under Windows NT (part 1 of 2).
Module 7 – Interprocess Communication
hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, BYTES_TO_MAP, MAP_MAPNAME); if( hMap == NULL ) { printf("CreateFileMapping() failed\n"); exit(1); } pMapMem = (char *)MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, BYTES_TO_MAP); if( pMapMem == NULL ) { printf("MapViewOfFile() failed\n"); exit(1); } printf("ptr %p\n", pMapMem); c = 'a'; for( n = 0; n < 10; n++) { *pMapMem = c; Sleep(1000L); printf("%d: c=%c *pMapMem = %c\n", getpid(), c, *pMapMem); }
c++ ;
CloseHandle(hMap); CloseHandle(hFile); }
exit(0); Figure 7.15 Shared memory under Windows NT (part 2 of 2).
7.17
7.18
70935 – Real-Time Systems
C:\usr\c\NT\console>mmap ptr 0x14030000 1000: c=a *pMapMem = a 1000: c=b *pMapMem = b 1000: c=c *pMapMem = c 1000: c=d *pMapMem = a 1000: c=e *pMapMem = b 1000: c=f *pMapMem = c 1000: c=g *pMapMem = d 1000: c=h *pMapMem = e 1000: c=i *pMapMem = f 1000: c=j *pMapMem = g Process 2: C:\usr\c\NT\console>mmap ptr 0x14030000 1001: c=a *pMapMem = e 1001: c=b *pMapMem = f 1001: c=c *pMapMem = g 1001: c=d *pMapMem = h 1001: c=e *pMapMem = i 1001: c=f *pMapMem = j 1001: c=g *pMapMem = g 1001: c=h *pMapMem = h 1001: c=i *pMapMem = i 1001: c=j *pMapMem = j Figure 7.16 Output of shared memory test for Windows NT.
Module 7 – Interprocess Communication
7.6
7.19
Windows Interprocess Communications
The Windows mechanism of inter-task communication is based on asynchronous event notification. The basic concepts will be discussed in this section, although a complete detailed discussion of the workings of the Windows graphical API is beyond the scope of this treatment (students are referred to the references for details for further study of the workings of Windows). The Unix-based XWindows system works in a similar way. Again, space precludes a complete detailed discussion of this topic. Figure 7.17 shows the screen view of a very basic Windows program which will be examined.
Figure 7.17 Snapshot of the Windows NT application nores. Figure 7.18 shows the basic structure of the initial portion of a Windows program. All Windows programs include the header file windows.h. The main entry point is not main() but WinMain(). The main routine is relatively simple, and does not perform any interactive processing. Instead, messages are send on an event queue to be handled by the window-processing callback function, which is called by the window manager. The main routine simply: 1. Registers the window class and creates the window using CreateWindowEx(). This does not make the window visible. Note the registration of the callback function, here WndProc(). 2. Displays the main window using ShowWindow() and UpdateWindow(). The latter function sends a message to the window message queue. 3. Enters an infinite GetMessage() loop, which calls DispatchMessage(). Figure 7.19 shows the first portion of the callback function, WndProc(). This function is called via the window manager when an event is to be processed for the application. The parameters to this function are: 1. hwnd, a handle (identifier) for the window. 2. message, an identifier of the type of message dispatched from the message queue.
7.20
70935 – Real-Time Systems
// nores.c - windows example without resource files. #include #include #include #include "nores.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); char szAppName[] = "NoResApp"; HINSTANCE hInstSave; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nWinMode) { HWND hwnd; MSG msg; WNDCLASSEX wndclass; wndclass.cbSize wndclass.style wndclass.lpfnWndProc wndclass.cbClsExtra wndclass.cbWndExtra wndclass.hInstance wndclass.hIcon wndclass.hIconSm wndclass.hCursor wndclass.hbrBackground wndclass.lpszMenuName wndclass.lpszClassName
= = = = = = = = = = = =
sizeof(WNDCLASSEX); CS_HREDRAW | CS_VREDRAW; WndProc; 0; 0; hInstance; LoadIcon(NULL, IDI_APPLICATION); LoadIcon(NULL, IDI_WINLOGO); LoadCursor(NULL, IDC_ARROW); (HBRUSH)GetStockObject(LTGRAY_BRUSH); ""; szAppName;
if( !RegisterClassEx(&wndclass) ) return 0; hwnd = CreateWindowEx(WS_EX_CLIENTEDGE, szAppName, "C Coded Windows", WS_OVERLAPPEDWINDOW, // normal CW_USEDEFAULT, CW_USEDEFAULT, // x, y 400, 150, // width, height may be CW_USEDEFAULT HWND_DESKTOP, NULL, hInstance, NULL); hInstSave = hInstance; ShowWindow(hwnd, nWinMode); UpdateWindow(hwnd);
}
// main message loop while( GetMessage(&msg, NULL, 0, 0) ) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; Figure 7.18 A basic Windows program: Part 1 of 4 – WinMain().
Module 7 – Interprocess Communication
7.21
3. wParam, a word parameter containing message-specific data. 4. lParam, a long parameter containing message-specific data. The body of this procedure consists of a switch statement, invoking the appropriate code sections according to the message. The predefined WM_COMMAND message is used to indicate a command message from a child control; here, these are simply the button controls within the window. Figure 7.20 shows the portion of the switch statement handling the WM_CREATE message. This message is sent to the callback procedure when the window is created. As can be seen from the figure, the various graphical objects on the window are created here. In the example, these are 3 buttons and an edit box. Figure 7.21 shows the portion of the switch statement handling the WM_SIZE, WM_PAINT and WM_DESTROY messages. The WM_SIZE message is sent to the callback procedure when the window is created. This typically involves re-drawing any graphical objects which may change according to the size of the overall window. The WM_PAINT message is sent to the callback procedure when the window needs repainting (re-drawing), because its status has changed (possibly from an icon to a normal window) or it is no longer obscured by other windows. The WM_DESTROY message is sent when the window is closing down. Finally, any messages not handled are passed to the default windows procedure, DefWindowProc().
7.22
70935 – Real-Time Systems
#define MAX_EDIT
20
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { short cxClient, cyClient; HDC hdc; PAINTSTRUCT ps; int dialogResp, len; TEXTMETRIC tm; char *prompt; static int nbPress = 0; static int cxChar, cyChar; static HWND hWndEdit; static char editBuf[MAX_EDIT+1]; switch( message ) { case WM_COMMAND: // menu & commands switch(LOWORD(wParam)) { case IDB_BUTTON_1: // set the text in the edit box nbPress++; wsprintf(editBuf, "count=%d", nbPress); SendMessage(hWndEdit, WM_SETTEXT, (WPARAM)0, (LPARAM)editBuf); break; case IDB_BUTTON_2: // get the text from the edit box len = (int)SendMessage(hWndEdit, EM_GETLINE, (WPARAM)0, (LPARAM)editBuf); editBuf[len] = '\0'; // null-terminate MessageBox(hwnd, editBuf, "Retrieve Text",MB_OK); break; case IDB_EXIT: dialogResp = MessageBox(hwnd, "Exit the program?", "Exit", MB_YESNO); if( dialogResp == IDYES ) PostMessage(hwnd, WM_DESTROY, (WPARAM)0, (LPARAM)0); break; default: break;
} return 0;
// WM_COMMAND handled
Figure 7.19 WndProc() and the WM COMMAND message.
Module 7 – Interprocess Communication
7.23
case WM_CREATE: hdc = GetDC(hwnd); SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT)); GetTextMetrics(hdc, &tm); cxChar = tm.tmAveCharWidth; cyChar = tm.tmHeight + tm.tmExternalLeading; CreateWindowEx(BS_PUSHBUTTON, "button", // window class name "button 1", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 50, 20, 100, 20, hwnd, (HMENU)IDB_BUTTON_1, hInstSave, (LPVOID)NULL); CreateWindowEx(BS_PUSHBUTTON, "button", // window class name "button 2", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 50, 80, 100, 20, hwnd, (HMENU)IDB_BUTTON_2, hInstSave, (LPVOID)NULL); CreateWindowEx(BS_PUSHBUTTON, "button", // window class name "exit", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 220, 20, 100, 20, hwnd, (HMENU)IDB_EXIT, hInstSave, (LPVOID)NULL); // create a one-line edit box // (placed later in WM_SIZE event) hWndEdit = CreateWindowEx(ES_LEFT, "edit", NULL, WS_VISIBLE | WS_CHILD | ES_LEFT | WS_BORDER | ES_AUTOHSCROLL | WS_TABSTOP | DS_3DLOOK, 0, 0, 0, 0, hwnd, (HMENU)IDE_EDIT, hInstSave, (LPVOID)NULL); // default text in the edit box strcpy( editBuf, "default"); ReleaseDC(hwnd, hdc); return 0; Figure 7.20 Processing the WndProc() WM CREATE message.
7.24
70935 – Real-Time Systems
case WM_SIZE: cxClient = LOWORD(lParam); cyClient = HIWORD(lParam); // draw the edit window MoveWindow(hWndEdit, 240, 80, 100, 20, TRUE); // set the default text in the edit window SendMessage(hWndEdit, WM_SETTEXT, (WPARAM)0, (LPARAM)editBuf); //MessageBox(hwnd, "size", "TestApp", MB_OK); return 0; case WM_PAINT: hdc = BeginPaint(hwnd, &ps) ; // set text prompt //SetBkColor(hdc, GetSysColor(COLOR_WINDOW) ); SetBkMode(hdc, TRANSPARENT); SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT)); prompt = "Enter:"; TextOut(hdc, 190, 82, prompt, lstrlen(prompt)); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: //MessageBox(hwnd, "destroy", "TestApp", MB_OK); PostQuitMessage(0); return 0; default: return DefWindowProc(hwnd, message, wParam, lParam);
}
} return 0;
Figure 7.21 Remaining WndProc() messages.
Module 7 – Interprocess Communication
7.25
activity 7.4
Windows Applications Compile and run the supplied nores.c code, using
gcc -mwindows -mno-cygwin -e_mainCRTStartup %1.c -o %1.exe Check that it runs and the buttons operate as expected. Now add the following: 1. Just before the return statement in the WM_SIZE message, the call
MessageBox(hwnd, "size", "TestApp", MB_OK);
2. Just before the return statement in the WM_PAINT message, the call
MessageBox(hwnd, "paint", "TestApp", MB_OK); 3. Just before the PostQuitMessage() statement in the WM_DESTROY message, the call
MessageBox(hwnd, "destroy", "TestApp", MB_OK); Re-compile and test what happens when the window is created, destroyed, iconified, maximized, and obscured by another window then made visible.
7.26
70935 – Real-Time Systems
7.7
Module Summary
Methods of task communication have been discussed, including
Message queues and pipes. Shared memory and file memory mapping.
The use of queued message buffers or shared memory areas depends on the particular problem at hand. Understanding the differences between message queues and shared memory is quite important.
Further Reading 70935 Real-Time Systems Further Referencesa
a
http://www.usq.edu.au/users/leis/units/70935/935link.html
Module
8
PROCESS SYNCHRONIZATION AND TIMING
Module 8 – Process Synchronization and Timing 8.1
8.1
Module Overview
The previous two modules have introduced the notion of multitasking, using processes and threads, and the means by which co-operating processes may communicate. The other side of the coin in this regard is how to synchronize processes when necessary. If the communication is asychronous via message queues or sockets, the normal flow of control proceeds in the recipient task. However, the situation of an expected message not arriving in time must also be catered for. If the process communication is via shared memory, access to the memory segment must be controlled in some way, otherwise the memory may become corrupted (as was demonstrated in the threads example). This is a particular case of the more general case of access to shared resources – typically disk files and memory. If the communication is to flag some extraneous event, there is no choice but to have a non-sequential method of handling the response. Because there are many problems, a number of solutions have developed. Those covered here are:
Events, signals & timers. File locking & semaphores.
In any given operating system, some or all of these may be supported to varying degrees.
8.2
Process Notification
This section examines some methods of signalling processes in Windows NT and Unix.
8.2.1
Unix Signals
Unix systems support signals, which are asynchronous notification of some event outside the process. They are terms “asynchronous” because they are not handled in the normal flow of execution, but are more akin to a hardware interrupt. A signal handler is a function which is invoked on receipt of a particular signal. Many signal types are defined, such as SIGKILL and SIGTERM when the process is to terminate, and SIGALRM when a timer alarm expires. These are defined in the include file signal.h. To register a signal handler, the signal(SIGNAME, SigHandler) function must be called. The function SigHandler() is invoked when the named signal is received. Because this could potentially occur at any time, the application itself and the signal handler must be aware of this. The signal function takes no arguments and returns no arguments (that is, a void data type) because it is not explicitly invoked (called) by another function. Note that the signal trigger is a “one-off”, and it is necessary to reset the signal action in the signal handler routine.
8.2
70935 – Real-Time Systems
Figure 8.1 shows the setting up of signal handling for a timer and for an “interrupt” (control-c). The timer is set using the setitimer(). The signal handlers are registered following this. The while(1) loop is simply to keep the process from exiting while the signals are trapped. Such a loop is termed a “busy wait” or a “spin loop”, and should be avoided in practice because it is simply wasting processor time. In reality, the sleep() or wait() functions are preferable. The signal handlers are shown in Figure 8.2. As noted previously, they both take no arguments and return nothing. A short run of the program is shown in Figure 8.3.
8.2.2
Windows NT Events
As seen in the previous module, Windows uses a callback function for synchronous event notification. A system timer defined by IDTIMER_MESSAGE may be defined to have an expiry time (in milliseconds) using the SetTimer() function. For example:
SetTimer(hwnd, IDTIMER_MESSAGE, TIMEOUT_MESSAGE*1000, NULL); requests Windows to send to the WndProc() the message at the appropriate time intervals. The timer is destroyed using KillTimer(hwnd, IDTIMER_MESSAGE). The switch statement in the main callback procedure may be used to handle the timer event by checking which timeout the event belongs to:
case WM_TIMER: if( wParam == (WPARAM)IDTIMER_MESSAGE ) { messageCount++ ; // other operations } return 1; Another mechanism is to define a callback function exclusively for the timeout. This is done using
SetTimer(hwnd, IDTIMER_FUNCTION, TIMEOUT_FUNCTION*1000, TimerFunc); The callback function is then called at the timeout intervals:
VOID CALLBACK TimerFunc(HWND hwnd, UINT msg, UINT timerID, DWORD SysTime) { procCount++ ; // other processing }
Module 8 – Process Synchronization and Timing 8.3
#include #include #include #include
// signal-handling functions void SigIntHandler(); void SigHupHandler(); void SigTermHandler(); void SigAlrmHandler(); int main() { struct itimerval Timeout ; Timeout.it_interval.tv_sec = 0; Timeout.it_interval.tv_usec = 500000; Timeout.it_value.tv_sec = 3; Timeout.it_value.tv_usec = 0; if( setitimer(ITIMER_REAL, &Timeout, NULL) < 0) { perror("setitimer()"); exit(1); } // SIGALRM = 14 signal(SIGALRM, SigAlrmHandler); // SIGINT = 2 signal(SIGINT, SigIntHandler); // SIGHUP = 1 signal(SIGHUP, SigHupHandler); // SIGTERM = 15 // send via kill -15 pid or kill -TERM pid signal(SIGTERM, SigTermHandler); printf("Signals have been set up...\n");
}
while(1) { getchar(); printf("waiting...\n"); } printf("Exiting.\n"); exit(0); Figure 8.1 Unix signals – initializing (part 1 of 2).
8.4
70935 – Real-Time Systems
// SIGHUP = 1 void SigHupHandler() { printf("HANGUP SIGNAL\n");
}
// reset the signal handler signal(SIGHUP, SigHupHandler);
// SIGINT = 2 void SigIntHandler() { printf("INTERRUPT SIGNAL\n");
}
// reset the signal handler signal(SIGINT, SigIntHandler);
// SIGTERM = 15 void SigTermHandler() { printf("TERMINATE SIGNAL\n"); // reset the signal handler //signal(SIGTERM, SigTermHandler);
}
printf("Exiting now.\n"); exit(1);
// SIGALRM = 14 void SigAlrmHandler() { printf("ALARM TIMER SIGNAL\n");
}
// reset the signal handler signal(SIGALRM, SigAlrmHandler); Figure 8.2 Unix signal-handling functions (part 2 of 2).
Module 8 – Process Synchronization and Timing 8.5
* Example output: ALARM TIMER SIGNAL waiting... ALARM TIMER SIGNAL waiting... INTERRUPT SIGNAL waiting... ALARM TIMER SIGNAL waiting... ALARM TIMER SIGNAL waiting... TERMINATE SIGNAL Exiting now. Figure 8.3
8.3
Output of the Unix signals test, for pid 213. kill -TERM 213 was entered at another console.
kill -INT 213 and
Atomic Resource Locking
In many instances, access to resources must be exclusive or atomic. For example, if two processes run concurrently on a system, and one writes a particular file and the other reads the same file, each read or write must run to completion without interference from the other. Attempting to read a partially written file, for example, gives incorrect results. Another easily-understood situation where this problem may occur is in accessing money in a bank. Suppose two people share a joint bank account. Each is independently making a withdrawal of funds from the account. A na¨ıve algorithm for the bank’s processing would be:
Given CurrentBalance, RequestedFunds if RequestedFunds >= CurrentBalance Allow withdrawal CurrentBalance = CurrentBalance - RequestedFunds else Disallow withdrawal, reason = "insufficient funds" endif Suppose one withdrawal request is part-way completed. The check for sufficient funds has been completed and the withdrawal has been allowed. The balance is about to be decremented. Now suppose the other process runs, but for whatever reason (faster CPU, less loading on that CPU, etc) it runs the entire algorithm whilst the first is paused. The check for sufficient funds succeeds, as the balance has not yet been decremented by the first process. Now the second process allows the withdrawal and decrements the balance. If, say, the original balance was $100, the first requested $70, and the second
8.6
70935 – Real-Time Systems
requested $60, then they could both succeed, with the net result that the balance is less than the available funds (-$30). If the bank does not allow overdrawn accounts (negative balances), the above represents a processing failure. This situation may arise in many real-world situations — for example, an airline database checking for available seats and then booking the seats, or a computer disk being backed up whilst users were writing files to the disk. From the above discussion, it is clear that the entire operation – testing availability and the actual transaction (bank balance deduction) – must be atomic. In more general terms, in order to acquire exclusive access to a shared resource, a process must: 1. Wait for the shared resource to become available. 2. Signify (flag) that it is using the shared resource. 3. Use the shared resource. 4. Release the resource and the locking flag. The first two must be performed as an atomic operation, a concept which has been met before. If, between checking if the resource is free and actually locking the resource, the process gets interrupted, another process may run and capture the resource lock. Thus there is the potential that two processes have captured the resource lock, thus both accessing the shared resource simultaneously – the very situation that was to be avoided. Another scenario, that of infinite deadlock, is possible. In this case, both processes are waiting for a resource which the other has but cannot release. The process will then wait indefinitely. One other issue which ought to be noted here is the wait for the shared resource. If this is a “busy wait”, then processor time is needlessly spent polling a resource lock.
8.3.1
Lock Files
One method of handling the resource-locking problem is to use semaphores, discussed in the next section. Another method is to use a lock file. Simply put, this is a file whose contents are unimportant, but the mere fact of its existence signals that exclusive access to a resource (possibly another file or a database record, for example) has been granted to a particular process. Figure 8.4 shows a process which locks a resource before attempting to use it. One possible method of handling lock files is shown in the support functions of Figure 8.5. The getLock() function enters what appears to be an infinite loop. Inside the loop, it attempts to create the lock file. If it can be created, the code returns as the existence of the lock file signifies to other processes that the resource is in use. If creation of the lock file fails, it will be because the file already exists (the open() function is called with “create, exclusive access” flags). Note that this only works because the unique
Module 8 – Process Synchronization and Timing 8.7
combination of O_CREAT and O_EXCL flags guarantee in Unix an atomic test-and-create operation. The attempt to acquire the lock is designed to fail “gracefully”. If the number of times around the loop exceeds WAIT_LOCKTIME then the function gives up. In addition, the loop is not a “busy wait” due to the sleep(1) call. This allows intervals of 1 second between polling of the lock file. Such mechanisms are required in robust systems. For example, if a process acquired the lock and the process died or was killed for some reason, the lock file would remain but belong to nobody. Processes will wait indefinitely for the lock file to be released. Ideally this program should incorporate a signal, so that interrupt signals (SIGINT, SIGHUP, SIGTERM) are caught and the lockfile removed. This will prevent problems if the application is killed with a lock held (the lock file still exists). On Unix systems, lock files are usually created in a common directory such as /var/lock/. This can then be checked and orhpaned lock files removed on startup. Figure 8.6 shows the example running as two separate processes. The first starts and acquires the lock. The second waits and checks the lock file at 1 second intervals, until finally the first process releases the lock by removing the lockfile.
8.4
Semaphores
Another method which is supported in many systems is the semaphore. These are similar in concept to the thread locks and atomic increments met in the module on multitasking, in the context of threads. The so-called “Dijkstra PV” operations on semaphores are defined as: P decrease (not available, capture) V increase (available, release) Thus a semaphore is a “free flag” for a resource, indicating that the resource is free and able to be used. A P operation decrements the count and flags the semaphore as locked. A V operation increments the count and thus releases the flag. These operations must be done using a kernel function.
8.4.1
Unix Semaphores
Figures 8.7 and 8.8 show a Unix client program which uses semaphores. The semaphore is first created (Figure 8.9). After all processes have terminated, the semaphore may be deleted from the system. Obviously this must be done by the parent process. Figure 8.10 shows the encapsulation of the semop() system call to acquire and release the semaphore. After the semaphore is created, its status may be examined using the ipcs -s command as shown in Figure 8.11.
8.8
70935 – Real-Time Systems
/* lockfile.c - creating a lockfile for exclusive access amongst * several processes. * John Leis */ #include #include #include #include
// open(), close() // for unlink()
#define WAIT_LOCKTIME 10 int getLock(char *lockfile); void releaseLock(char *lockfile, int lockfd); int main() { int lockfd; char *lockfile = "file.lk";
}
lockfd = getLock(lockfile); if( lockfd != -1) { printf("use resources...\n"); fflush(stdout); sleep(10); releaseLock(lockfile, lockfd); } else { printf("failed to obtain lock\n"); fflush(stdout); } exit(0); Figure 8.4 Using lock files — main calling code.
Module 8 – Process Synchronization and Timing 8.9
// returns -1 on fail, otherwise the lockfile descriptor int getLock(char *lockfile) { int tryCount = 0; int lockfd; do {
// check if the number of iterations has been exceeded if(++tryCount > WAIT_LOCKTIME) { printf("getLock(): timeout on obtaining lock\n"); return -1; }
// Try to open the lockfile. // O_CREAT | O_EXCL fails (returns -1) if the // file already exists. // The test is atomic (see man page) lockfd = open(lockfile, O_CREAT | O_EXCL, 0666); if(lockfd == -1) { // lockfd == -1, file creation error printf("getLock(): lockfile already exists\n"); sleep(1); } } while( lockfd == -1 );
}
printf("getLock(): ok\n"); return lockfd;
// release the lock // Ideally this should be invoked on a signal as well // to clean up the lockfile void releaseLock(char *lockfile, int lockfd) { close(lockfd); unlink(lockfile); } Figure 8.5 Lock file support functions.
8.10
70935 – Real-Time Systems
* Example output: Process 1 phanes (leis) [4] lockfile getLock(): lockfile created use resources... phanes (leis) [5] Process 2 (run slightly later) phanes (leis) [57] lockfile getLock(): lockfile already exists getLock(): lockfile already exists getLock(): lockfile already exists getLock(): lockfile already exists getLock(): lockfile already exists getLock(): lockfile already exists getLock(): lockfile created use resources... phanes (leis) [58] Figure 8.6 Output of lock file test.
8.4.2
Windows NT Semaphores
Semaphores are created under Windows NT using the system call CreateSemaphore(). Figure 8.12 shows the creation of the semaphore, which is then captured using WaitForSingleObject() as shown in Figure 8.13. Note the possible return codes from this call — the semaphore was acquired within the specified timeout, the timer expired before the semaphore was available, or the wait was abandoned altogether.
Any number of client processes may then attach to the system semaphore and wait on it. Figure 8.14 shows that a handle to the semaphore is requested using OpenSemaphore() in much the same way as a file is opened. Next, WaitForSingleObject() is used to atomically protect any accesses to shared resources. In the example of Figure 8.15 this is simply a Sleep() call, but of course in practice some more realistic operation would be performed. The actual operation must be done as quickly a possible, as the longer a process holds a semaphore the longer other processes may be delayed in waiting for the semaphore to become available.
Figure 8.16 shows the actual output of the semaphore client/server combination. However, the timing of the output is important but not conveyed in the printed listing.
Module 8 – Process Synchronization and Timing
/* * * * *
semop.c - semaphore operations under Unix Platform: SunOS Compiler: gcc Run several of these processes in separate terminal windows and verify the result.Use ipcs -s * to view semaphores. Use ipcrm -s * remove the semaphore, where is the ID from ipcs -s * John Leis */
#include #include #include #include
// required definition - see man page for semctl union semun { int val; struct semid_ds *buf; ushort_t *array; } arg ; /* system-wide key for our semaphore * This will be the key shown in "ipcs -s" */ #define SEM_KEY 0x7645 /* easier access to semaphores when only a single * semaphore is required (Unix allows an array of semaphores * as well as many access primitives ) */ int CreateSem(); // create a semaphore - return ID void GetSem(int SemID); // set a semaphore - blocking void ReleaseSem(int SemID);// release the semaphore void DeleteSem(int SemID); // delete semaphore from the system Figure 8.7 Semaphores under Unix — part 1 of 2.
8.11
8.12
70935 – Real-Time Systems
int main() { int SemID, TestNum , MyPID; printf("Warning: if the process is stopped before completion, "); printf("use \n ipcs -s\n"); printf("to check for the semaphore.\n"); printf("Remove the semaphore using \n"); printf("ipcrm -s \n"); SemID = CreateSem(); MyPID = getpid(); // spin the test a few times... for( TestNum = 0; TestNum < 10; TestNum++) { // get the semaphore. will block in GetSem() // until we have the semaphore GetSem(SemID); printf("Process %d has the semaphore\n", MyPID); fflush(stdout); // Do some critical operation that requires the // semaphore. This is usually access to a shared resource // This section should always be as short as possible! sleep(1); printf("Process %d releasing the semaphore\n", MyPID); // flush the output queue - same reason as before fflush(stdout); // release access to the semaphore ReleaseSem(SemID);
}
// used to simulate the rest of the process. // necessary so that other processes can access the // semaphore (and hence shared resource) sleep(2);
// Delete the semaphore from the system. // This is normally done only by the 'master' // process who created the semaphore. DeleteSem(SemID); }
exit(0); Figure 8.8 Semaphores under Unix – part 2 of 2.
Module 8 – Process Synchronization and Timing
/* CreateSem() - create a semaphore. * Returns the ID of a semaphore */ int CreateSem() { int SemID; union semun SemCtlArg; if( (SemID = semget((key_t)SEM_KEY, 1, IPC_CREAT | 0666)) < 0 ) { perror("semget()"); exit(1); } /* set the semaphore to 1 initially. */ SemCtlArg.val = 1; if( semctl( SemID, 0, SETVAL, &SemCtlArg) < 0) { perror("semctl()"); exit(1); } }
return SemID ;
/* DeleteSem() - delete a semaphore resource from the system */ void DeleteSem(int SemID) { union semun SemCtlArg;
}
if( semctl( SemID, 0, IPC_RMID, &SemCtlArg) < 0) { perror("semctl()"); exit(1); } Figure 8.9 Semaphore functions for Unix – part 1 of 2.
8.13
8.14
70935 – Real-Time Systems
/* GetSem() - get exclusive access to a semaphore * for this process. This function will sleep until * the semaphore is available. */ void GetSem(int SemID) { struct sembuf SemOpBuf; /* set the semaphore operation field to -1 * ie. try and decrement the semaphore to zero. * If it's already zero, sleep until someone * else releases it. */ SemOpBuf.sem_num = 0; SemOpBuf.sem_op = -1; SemOpBuf.sem_flg = SEM_UNDO ;
}
if( semop( SemID, &SemOpBuf, 1) < 0) { perror("semop()"); exit(1); }
/* ReleaseSem() - release the semaphore for someone else. * Note the use of SEM_UNDO in case this process does * a premature exit() before proper release of the semaphore. */ void ReleaseSem(int SemID) { struct sembuf SemOpBuf; SemOpBuf.sem_num = 0; SemOpBuf.sem_op = 1; SemOpBuf.sem_flg = SEM_UNDO ;
}
if( semop( SemID, &SemOpBuf, 1) < 0) { perror("semop()"); exit(1); } Figure 8.10 Semaphore functions for Unix — part 2 of 2.
Module 8 – Process Synchronization and Timing
* Run several of these processes in separate terminal windows * and verify the result.Use ipcs -s * to view semaphores. Use ipcrm -s * remove the semaphore, where is the ID from ipcs -s * Example output: phanes (leis) [11] semop Process 17516 has the semaphore Process 17516 releasing the semaphore Process 17516 has the semaphore Process 17516 releasing the semaphore Example ipcs -s output: phanes (leis) [39] ipcs -s IPC status from Sunday T ID KEY MODE Semaphores: s 0 0x187cf --ra-ra-ras 327681 0x7645 --ra-ra-raphanes (leis) [40] ... phanes (leis) [41] ipcrm -s 393217 phanes (leis) [42] ipcs -s IPC status from Sunday T ID KEY MODE Semaphores: s 0 0x187cf --ra-ra-raFigure 8.11 Output of Unix semaphore test.
February 13 15:06:32 OWNER GROUP root leis
root staff
February 13 15:08:54 OWNER GROUP root
root
8.15
8.16
70935 – Real-Time Systems
/* semserver.c * Windows NT semaphores * John Leis */ #include #include #include #define SEM_NAME
"mysem"
int main() { HANDLE hSemaphore; LONG semIncr = 1L, semPrev; DWORD waitStatus; // no security attributes, // initial count 1 (released), max count 1 hSemaphore = CreateSemaphore( NULL, 1L, 1L, SEM_NAME); if( hSemaphore == NULL ) { printf("CreateSemaphore() failed\n"); exit(1); } Figure 8.12 Windows semaphore server — semaphore setup (part 1 of 2).
activity 8.1
Windows NT Semaphores. Compile the code examples semserver and semclient. Create two separate command (DOS) windows. Invoke the client in one window – it should fail, as the code assumes the semaphore has already been created and simply tries to open the existing semaphore. Now run the server in one window first, and then the client in the other window.
Module 8 – Process Synchronization and Timing
printf("waiting for semaphore....\n"); fflush(stdout); // decrements count // timeout is in milliseconds, or INFINITE waitStatus = WaitForSingleObject(hSemaphore, INFINITE); printf("semaphore wait done, status = "); fflush(stdout); switch( waitStatus ) { case WAIT_ABANDONED: printf("abandoned\n"); break; case WAIT_OBJECT_0: printf("available within timeout\n"); break; case WAIT_TIMEOUT: printf("timeout\n"); break; case WAIT_FAILED: printf("failed\n"); break; } printf("sleeping for 10 seconds..."); fflush(stdout); Sleep(10000L); printf("awake\n"); fflush(stdout); printf("releasing semaphore\n"); fflush(stdout); ReleaseSemaphore(hSemaphore, semIncr, &semPrev); // wait before closing printf("sleep..."); fflush(stdout); Sleep(10000L); printf("awake\n"); fflush(stdout); CloseHandle(hSemaphore); }
return 0; Figure 8.13 Windows semaphore server — capturing the semaphore.
8.17
8.18
70935 – Real-Time Systems
/* semclient.c * Windows NT semaphores * John Leis */ #include #include #include #define SEM_NAME
"mysem"
int main() { HANDLE hSemaphore; LONG semIncr = 1L, semPrev; DWORD waitStatus; // enable NT event waits hSemaphore = OpenSemaphore(SEMAPHORE_MODIFY_STATE | SYNCHRONIZE, TRUE, SEM_NAME); if( hSemaphore == NULL ) { printf("OpenSemaphore() failed\n"); exit(1); } Figure 8.14 Windows semaphore client — semaphore setup (part 1 of 2).
Module 8 – Process Synchronization and Timing
printf("waiting for semaphore....\n"); fflush(stdout); // decrements count // timeout is in milliseconds, or INFINITE waitStatus = WaitForSingleObject(hSemaphore, INFINITE); printf("semaphore wait done, status = "); fflush(stdout); switch( waitStatus ) { case WAIT_ABANDONED: printf("abandoned\n"); break; case WAIT_OBJECT_0: printf("available within timeout\n"); break; case WAIT_TIMEOUT: printf("timeout\n"); break; case WAIT_FAILED: printf("failed\n"); break; } printf("sleeping for 2 seconds..."); fflush(stdout); Sleep(2000L); printf("awake\n"); fflush(stdout); printf("releasing semaphore\n"); fflush(stdout); ReleaseSemaphore(hSemaphore, semIncr, &semPrev); CloseHandle(hSemaphore); }
return 0; Figure 8.15 Windows semaphore client — capturing the semaphore.
8.19
8.20
70935 – Real-Time Systems
C:\usr\c\NT>semclient waiting for semaphore.... semaphore wait done, status = available within timeout sleeping for 2 seconds...awake releasing semaphore C:\usr\c\NT>semserver waiting for semaphore.... semaphore wait done, status = available within timeout sleeping for 10 seconds...awake releasing semaphore sleep...awake Figure 8.16 Output from Windows semaphore client/server.
Module 8 – Process Synchronization and Timing
8.5
8.21
Module Summary
Synchronization is the cause of many problems in real-time systems, so an understanding of some methods of synchronization is required. The important concepts from this module are:
Events, signals & timers. File locking & semaphores.
The conceptual difference between each of these, and where they would be used, should be fully understood.
Further Reading 70935 Real-Time Systems Further Referencesa
a
http://www.usq.edu.au/users/leis/units/70935/935link.html
Module
9
VIDEO GRAPHICS AND ANIMATION
Module 9 – Video Graphics and Animation 9.1
9.1
Module Overview
Controlling graphics and audio output devices represent one of the most difficult realtime design challenges. So-called “hard real-time” systems such as this pose particular challenges to the software engineer. Not only must the principles of the underlying hardware be understood, they must be mastered subject to the performance constraints of the processing system on which they are implemented. For this reason, a case study of a simple graphics animation represents a very instructive example of a real-time system. The topics examined in this module are
Video display basics. Creating a real-time animation under Windows.
Although video displays are considered here, similar principles are applicable to realtime audio output. These ideas extend to the implementation of networking protcols and real-time control systems.
9.2
Video Subsystem
The basic scanned video device, such as a television display or conventional computer monitor, is depicted in Figure 9.1. An electron beam scans from left to right, top to bottom, at a very rapid rate. This rate must be fast enough so as to prevent flicker, or at least make it nearly imperceptible. The illusion of motion is brought about by the projection of several successive pictures onto the screen in quick succession. If the images are redrawn too slowly, the “motion” of objects becomes visible. This is at the hardware level, and the parameters are set by the design of the monitor (and by implication, the video graphics display hardware). When constructing synthetic animation, the problem is compounded by the need to erase the moving object(s) whilst keeping the background constant. The latter problem is of fundamental interest in what follows. The video screen image is effectively a matrix of successive picture elements, called pixels. The pixels are rendered in a particular color according to the values stored in video memory (RAM). Each pixel corresponds to one or more bits in the video memory. Because memory is “linear” — meaning that successive memory locations form a onedimensional list of bytes stored — the video memory must be mapped onto the twodimensional screen. (As an aside, the projection of three-dimensional images onto a two-dimensional viewing plane is done using mathematical viewing transformations from 3 dimensions to two, normally in software, sometimes using special-purpose video processors). The mapping of one-dimensional memory into two-dimensional viewing space is depicted in Figure 9.2. For a given width w and height h in pixels, the desired Cartesian co-ordinate of a pixel x; y is defined as an offset or displacement from the start of the video memory as d yw x. This may be derived easily from inspection of the
( ) = +
9.2
70935 – Real-Time Systems
(0 0)
figure. The convention is that the origin, or point ; , is in the upper-left corner of the screen, but this need not always be the case. Assuming one byte is required per pixel, a screen resolution of 1024 (width) by 768 (height) requires almost 1 MByte of video RAM. video screen
visible horizontal retrace vertical retrace
Figure 9.1 Scanning of the beam on a video screen. One or more bits in each video memory location define, directly or indirectly, the color of the displayed pixel. At the most basic level, a single bit could be used to represent black or white. Representing color becomes more complicated. The three primary colors – red, green, and blue (RGB) – may be combined in the appropriate amounts to produce any desired color. Thus one common scheme is to reserve three bytes to use for each pixel, with each byte representing the RGB triplet. Each byte is used as an intensity for the red, green and blue electron guns of the display. A DAC or digital to analog converter performs the conversion from a binary value into a relative voltage strength. Thus the relative value of each byte, 0 to 255, represents the relative strength of each color. For example, red 0, green 100% and blue 100% may be mixed to represent cyan; red 100%, green 100% with no blue gives a strong yellow. Equal red, green and blue gives a shade of gray ranging from black to white. Using a direct representation gives the best possible color rendition, at the expense of the amount of memory required. Using three bytes per pixel, a screen resolution of 1024 (width) by 768 (height) requires over 2MBytes of video RAM. An alternative is an indirect representation using a palette as shown in Figure 9.3. Each pixel then (usually) corresponds to one byte. The byte is used as an offset into the palette. The palette is used as a color lookup table. Each entry is comprised of three values, for red, green and blue. The relative strength of the RGB triplet defines the color represented by that palette entry. The advantage of this scheme is that substantially
Module 9 – Video Graphics and Animation 9.3
offset = y w
x
y
b
b
b
b
b
b
+x
b
b
b
h
w
Figure 9.2 Mapping of linear screen memory into two dimensions.
less memory is required for the video display. The obvious disadvantage is that fewer colors are available. A more subtle problem is that of color re-use. In a windowing system, if one window is very graphics-intensive and uses up most of the color palette entries, other applications suffer. High-speed graphics cards often use an interleaving scheme as shown in Figure 9.4. This is because the data access time of RAM, defined as the time from when the address is presented to when the data is available, must be quite small for high-resolution, fast-refresh video displays. Video RAM having a fast access time is generally more expensive. To use lower-speed memory, interleaving fetches the byte for pixel n from bank 1, the byte for pixel n from bank 2, and so forth. Using, say, four memory banks in an interleaved fashion means that RAM with approximately one-quarter of the access speed is required, which reduces system cost.
+1
9.4
70935 – Real-Time Systems
Screen
b
Palette
b
b
b
blue green red
Figure 9.3 Using a pixel value to lookup the color palette.
successive pixels b
b
b
b
memory banks
byte
Figure 9.4 Video RAM interleaving.
Module 9 – Video Graphics and Animation 9.5
9.3
Animation Effects
In order to effect the illusion of motion and animation, an image on a screen or in a window must be erased and then redrawn at a slightly offset position many times per second. This erase/redraw cycle must be accomplished in less than about 20ms in order to avoid flicker. This redraw may be assisted by special-purpose hardware. In effect, two video memories are required: one which is currently being displayed, whilst the other is being erased and re-drawn. After the nominated display hold-time has elapsed, the roles of the two buffers are reversed: one is used for output whilst the other is being reloaded. Such a technique is termed double-buffering, and is also used in audio playback to avoid audible “clicks” when a sound playback buffer is reloaded. As Figure 9.5 indicates, this may be controlled for full-screen animation through a special control register in hardware. If the animation is to be done within one window on a Windows system, the double-buffering technique is still applicable, although of course it is not done using special hardware — naturally the hardware cannot reflect the position of all screen windows (only the entire screen). However, special-purpose high-speed memory copy operations, used with appropriate synchronization, allows the effect of animation in application windows.
hidden
control register b
visible
Figure 9.5 Video screen page swapping.
9.3.1
Direct Repaint Approach
Figure 9.6 shows a snapshot of the winanim1.c application which will now be discussed. The box and oval are moving slowly in opposite directions, and “bounce” when they hit the window edges and reverse direction. The objective is to achieve smooth animation without flickering or distortion. The basic idea is from from [21], although the following represents a standard approach to the problem. In the first approach, after each timer interval expires:
9.6
70935 – Real-Time Systems
Figure 9.6 Windows animation example.
1. The window is erased by setting it to the background color. 2. The new position of the objects is calculated. This is normally a small offset depending on the current direction of motion. However, if the object appears to hit the window’s edge, the direction is reversed. 3. The window is re-drawn with the objects in their new position. This “cartoon-sequence” of animation provides the desired result, provided the processor is fast enough to handle the timer-based redraws and allow sufficient time for other processing. However, the performance is not entirely satisfactory. The display is plagued by a horizontal flickering which is quite noticeable, even on a 400MHz CPU. Before considering how to solve this problem, the internal workings of the application will be dissected.
Module 9 – Video Graphics and Animation 9.7
activity 9.1
Windows animation – direct repaint approach. Compile and run the winanim1.c code using
gcc -mwindows -e_mainCRTStartup winanim1.c -o winanim1.exe where the flags are: -m emulation -e start address -lgdi32 may be required for some systems -mno-cygwin is for stand-alone applications Verify that, as stated in the text, the animation is not very satisfactory and contains significant “flickering” in the display window.
The window event-handling is not unlike that discussed in the previous module. The windows callback function processes events, which are passed as an integer identifier from the operating-system event queue. It is defined as
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) where message is the event identifier. Certain event messages have additional parameters associated with them, such as the identity of the timer which expired in the case of timeout messages. These are passed in the wParam and lParam variables. Because the code is event-driven (meaning only activated when an event occurs, as necessary), the state of the application window must be saved. In this case, the current position of the objects is saved using
static static static static static static
int int int int int int
xPosRect = 0; xPosBall = 50; xStepRect = 2; xStepBall = 1; BallWidth = 60; RectWidth = 50;
// // // // // //
must must must must must must
be be be be be be
static static static static static static
These are declared as static variables, meaning that they retain their value from one invocation to the next. This is in contrast to automatic variables, the space for which is allocated each time the function is called. Internally, static variables are stored in the static data area, whereas automatic variables are stored on the stack. If the above were not declared as static, their value would be different each time WndProc() is invoked. For this reason, Windows applications tend to use many global and/or static variables.
9.8
70935 – Real-Time Systems
case WM_TIMER: InvalidateRect(hwnd, NULL, TRUE); return 0; Figure 9.7 Windows animation, version 1 – handling the WM TIMER message. The timer event is used to handle the window update. Figure 9.7 shows the WM TIMER event, which simply calls InvalidateRect(). This has the effect of queueing a paint message for the entire window. Upon receipt of a WM PAINT message, the window is re-drawn by filling the rectangle with a background color, followed by re-drawing of the objects in their new positions. Figure 9.8 shows the WM PAINT event, which implements the erase-redraw strategy. The object’s positions are then calculated in readiness for the next iteration.
9.3.2
Memory-Copy Approach
The erase-redraw strategy as outlined in the previous section, although correct, produces a less-than-satisfactory animation. A better strategy is to use double buffering. In essence, this means that the updates to the window are done using a buffer which represents the window’s contents. The buffer itself is not visible whilst it is being updated. Then, the contents of the window are copied as a bitmap (pixel map) to the visible window. Thus two buffers are required: the hidden buffer and the visible screen window itself. This is termed double-buffering. In order to allocate the buffer, the size must be known. This changes when the window is resized, and it captured when a WM SIZE event is sent to the callback procedure. The necessary code is shown in Figure 9.9. GetClientRect() captures the new size of the window and creates a memory bitmap using CreateCompatibleBitmap(). The redraw is implemented as follows. Figure 9.9 shows the WM TIMER! event, which now draws to a memory bitmap rather than to the screen directly. The device context of the visible window is obtained, using GetDC(hwnd). A bitmap, which represents the contents of the window, is selected using CreateCompatibleDC() and SelectObject(). The drawing commands such as Rectangle() use the handle to the memory buffer, hcdMem. Finally, the bitmap buffer is copied in its entirety to the visible window using BitBlt(). This function is optimized for high-speed copying of an entire bitmap, and thus to eliminate the flicker which was present in the previous example. To summarize, the difference between the two methods is as follows. In the first, the timer is used to trigger a repaint of the window. The drawing is done directly to the visible window. In the second approach, the timer is used to draw to a hidden bitmap, which is copied to the visible window. The size event is used to capture the required size of the hidden bitmap buffer. This double-buffering approach may be used in realtime systems to guarantee satisfactory performance where direct hardware I/O (typically video and audio) is required.
Module 9 – Video Graphics and Animation 9.9
case WM_PAINT: hdc = BeginPaint(hwnd, &ps) ; SetMapMode(hdc, MM_TEXT); GetClientRect(hwnd, &ClientRect);
// pixel mode
// filled rectangle for background hbrushb = CreateSolidBrush(RGB(200, 200, 200)); SelectObject(hdc, hbrushb); Rectangle(hdc, ClientRect.left, ClientRect.top, ClientRect.right, ClientRect.bottom); hbrush = CreateSolidBrush(RGB(0, 200, 0)); SelectObject(hdc, hbrush); hpen = CreatePen(PS_SOLID, 1, RGB(200, 0, 0)); SelectObject(hdc, hpen); MoveToEx(hdc, 0, 0, &oldPoint); LineTo(hdc, ClientRect.right, ClientRect.bottom); // ball bouncing Ellipse(hdc, xPosBall, 150, xPosBall+BallWidth, 180); if( xPosBall+BallWidth > ClientRect.right ) xStepBall = -xStepBall; // hit right-hand side if( xPosBall < ClientRect.left ) xStepBall = -xStepBall; // hit left-hand side xPosBall += xStepBall; // update position // box bouncing Rectangle(hdc, xPosRect, 50, xPosRect+RectWidth, 100); if( xPosRect+RectWidth > ClientRect.right ) xStepRect = -xStepRect; // hit right-hand side if( xPosRect < ClientRect.left ) xStepRect = -xStepRect; // hit left-hand side xPosRect += xStepRect; // update position DeleteObject(hpen); DeleteObject(hbrush); DeleteObject(hbrushb); EndPaint(hwnd, &ps) ; return 0; Figure 9.8 Windows animation, version 1 – handling the WM PAINT message.
9.10
70935 – Real-Time Systems
case WM_SIZE: hdc = GetDC(hwnd); cxClient = LOWORD(lParam); cyClient = HIWORD(lParam); GetClientRect(hwnd, &ClientRect); hdcMem = CreateCompatibleDC(hdc); // if bitmap exists (old size), delete it if( hBitMap ) DeleteObject(hBitMap); hBitMap = CreateCompatibleBitmap(hdc, ClientRect.right, ClientRect.bottom); ReleaseDC(hwnd, hdc); return 0; Figure 9.9 Windows animation, version 2 – handling the WM SIZE message.
activity 9.2
Windows animation – memory copy. Compile and run the winanim2.c code using
gcc -mwindows -e_mainCRTStartup winanim2.c -o winanim2.exe Verify that, as stated in the text, the animation is far superior and without “flickering” in the display window.
Module 9 – Video Graphics and Animation
9.11
case WM_TIMER: if( ! hBitMap ) break; hdc = GetDC(hwnd); SetMapMode(hdc, MM_TEXT); // pixel mode GetClientRect(hwnd, &ClientRect); hdcMem = CreateCompatibleDC(hdc); SelectObject(hdcMem, hBitMap); // filled rectangle hbrushb = CreateSolidBrush(RGB(200, 200, 200)); SelectObject(hdcMem, hbrushb); Rectangle(hdcMem, ClientRect.left, ClientRect.top, ClientRect.right, ClientRect.bottom); hbrush = CreateSolidBrush(RGB(0, 200, 0)); SelectObject(hdcMem, hbrush); // line drawing hpen = CreatePen(PS_SOLID, 1, RGB(200, 0, 0)); SelectObject(hdcMem, hpen); MoveToEx(hdcMem, 0, 0, &oldPoint); LineTo(hdcMem, ClientRect.right, ClientRect.bottom); DeleteObject(hpen); // ball bouncing Ellipse(hdcMem, xPosBall, 150, xPosBall+BallWidth, 180); if( xPosBall+BallWidth > ClientRect.right ) xStepBall = -xStepBall; // hit right-hand side if( xPosBall < ClientRect.left ) xStepBall = -xStepBall; // hit left-hand side xPosBall += xStepBall; // update position // box bouncing Rectangle(hdcMem, xPosRect, 50, xPosRect+RectWidth, 100); if( xPosRect+RectWidth > ClientRect.right ) xStepRect = -xStepRect; // hit right-hand side if( xPosRect < ClientRect.left ) xStepRect = -xStepRect; // hit left-hand side xPosRect += xStepRect; // update position // copy from memory bitmap to window bitmap BitBlt(hdc, ClientRect.left, ClientRect.top, ClientRect.right - ClientRect.left, ClientRect.bottom - ClientRect.top, hdcMem, ClientRect.left, ClientRect.top, SRCCOPY); DeleteObject(hbrush); DeleteObject(hbrushb); ReleaseDC(hwnd, hdc); DeleteDC(hdcMem); return 0; Figure 9.10 Windows animation, version 2 – handling the WM TIMER message.
9.12
70935 – Real-Time Systems
9.4
Module Summary
This module has brought together a number of concepts from previous modules. The basic operation of a video-display was outlined, followed by a design and implementation study of real-time animation. The most important new principle examined in this module is that of double-buffering. The need for double-buffering should be well understood.
Further Reading 70935 Real-Time Systems Further Referencesa
a
http://www.usq.edu.au/users/leis/units/70935/935link.html
References [1] Phillip A. Laplante, Real-Time Systems Design and Analysis – An Engineer’s Hanbbook, IEEE Press, 1993. [2] Phillip A. Laplante, “Basic real-time concepts”, in Real-Time Systems Design and Analysis – An Engineer’s Hanbbook, chapter 1. IEEE Press, 1993. [3] Nancy G. Leveson and Clark S. Turner, “An Investigation of the Therac-25 Accidents”, IEEE Computer, vol. 26, no. 7, pp. 18–41, July 1993. [4] Stuart Ritchie, “Systems Programming in Java”, IEEE Micro, vol. 17, no. 3, pp. 30–35, May/June 1997. [5] Thomand J. Penello, “Compiler Challenges with RISCs”, IEEE Micro, vol. 10, no. 1, pp. 37–43, Feb. 1990. [6] Ramesh Subramaniam and Kiran Kundargi, “Programming the Pentium Processor”, Dr Dobb’s Journal, pp. 34–42, June 1993. [7] A. Holub, Compiler Design in C, Prentice-Hall, 1990. [8] Donald Lewine, POSIX Programmer’s Guide: Writing Portable UNIX Programs, O’Reilly & Associates, Inc., 1991. [9] Bill O. Gallmeister, POSIX. 4: Programming for the Real World, O’Reilly & Associates, Inc., 1995. [10] Stephen G. Kochan and Patrick H. Wood, “The unix system interface”, in Topics in C Programming, chapter 5. Hayden Books, 1987. [11] Donald E. Knuth, The Art of Computer Programming, Addison-Wesley, 1973. [12] Niklaus Wirth, Algorithms + data structures=programs, Prentice-Hall, 1976. [13] Robert Sedgewick, Algorithms, Addison-Wesley, 1988. [14] Robert Sedgewick, Algorithms in C, Addison-Wesley, 1990. [15] Robert Sedgewick, Algorithms in C++, Addison-Wesley, 1992. [16] Jeffrey H. Kingston, Algorithms and data structures : design, correctness, analysis, Addison-Wesley, 1990. [17] Mickey Williams, “Threads”, in Programming Windows NT4, chapter 22. SAMS Publishing, 1996.
[18] Herbert Schildt, “Thread-based multitasking”, in Windows NT4 Programming, chapter 15. Osbourne McGraw-Hill, 1997. [19] Borland/Inprise, Win32s API Help, Inprise Corporation, 1999. [20] Mickey Williams, “Pipes”, in Programming Windows NT4, chapter 23. SAMS Publishing, 1996. [21] Charles Petzold, Programming Windows 3.1, Microsoft Press, 1993.