Inside OS/2
This is the text from the book.
INSIDE OS/2
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
INSIDE OS/2
Gordon Letwin
Chief Architect, Systems Software, Microsoft(R)
Foreword by Bill Gates
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
PUBLISHED BY
Microsoft Press
A Division of Microsoft Corporation
16011 NE 36th Way, Box 97017, Redmond, Washington 98073-9717
Copyright (C) 1988 by Microsoft Press All rights reserved. No part of the contents of this book may be reproduced or transmitted in any form or by any means without the written permission of the publisher.
Library of Congress Cataloging in Publication Data Letwin, Gordon. Inside OS/2.
Includes index. 1. MS OS/2 (Computer operating system) I. Title. II. Title: Inside OS/Two. QA76.76.063L48 1988 005.4'46 87-31579 ISBN 1-55615-117-9
Printed and bound in the United States of America
1 2 3 4 5 6 7 8 9 MLML 8 9 0 9 8
Distributed to the book trade in the United States by Harper & Row.
Distributed to the book trade in Canada by General Publishing Company, Ltd.
Distributed to the book trade outside the United States and Canada by Penguin Books Ltd.
Penguin Books Ltd., Harmondsworth, Middlesex, England Penguin Books Australia Ltd., Ringwood, Victoria, Australia Penguin Books N.Z. Ltd., 182-190 Wairau Road, Auckland 10, New Zealand
British Cataloging in publication Data available
Editor: Patricia Pratt
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Dedication
To R.P.W.
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Contents
Foreword by Bill Gates
Introduction
Part I: The Project
Chapter 1. History of the Project
1.1 MS-DOS version 1.0 1.2 MS-DOS version 2.0 1.3 MS-DOS version 3.0 1.4 MS-DOS version 4.0
Chapter 2. Goals and Compatibility Issues
2.1 Goals 2.1.1 Graphical User Interface 2.1.2 Multitasking 2.1.3 Memory Management 2.1.4 Protection 2.1.5 Encapsulation 2.1.6 Interprocess Communication (IPC) 2.1.7 Direct Device Access 2.2 Compatibility Issues 2.2.1 Real Mode vs Protect Mode 2.2.2 Running Applications in Real (Compatibility) Mode 2.2.2.1 Memory Utilization 2.2.2.2 File Locking 2.2.2.3 Network Piggybacking 2.2.3 Popular Function Compatibility 2.2.4 Downward Compatibility 2.2.4.1 Family API 2.2.4.2 Network Server-Client Compatibility
Chapter 3. The OS/2 Religion
3.1 Maximum Flexibility 3.2 Stable Environment 3.2.1 Memory Protection 3.2.2 Side-Effects Protection 3.3 Localization of Errors 3.4 Software Tools Approach
Part II: The Architecture
Chapter 4. Multitasking
4.1 Subtask Model 4.1.1 Standard File Handles 4.1.2 Anonymous Pipes 4.1.3 Details, Details 4.2 PIDs and Command Subtrees 4.3 DosExecPgm 4.4 DosCWait 4.5 Control of Child Tasks and Command Subtrees 4.5.1 DosKillProcess 4.5.2 DosSetPriority
Chapter 5. Threads and Scheduler/Priorities
5.1 Threads 5.1.1 Thread Stacks 5.1.2 Thread Uses 5.1.2.1 Foreground and Background Work 5.1.2.2 Asynchronous Processing 5.1.2.3 Speed Execution 5.1.2.4 Organizing Programs 5.1.3 Interlocking 5.1.3.1 Local Variables 5.1.3.2 RAM Semaphores 5.1.3.3 DosSuspendThread 5.1.3.4 DosEnterCritSec/DosExitCritSec 5.1.4 Thread 1 5.1.5 Thread Death 5.1.6 Performance Characteristics 5.2 Scheduler/Priorities 5.2.1 General Priority Category 5.2.1.1 Background Subcategory 5.2.1.2 Foreground and Interactive Subcategories 5.2.1.3 Throughput Balancing 5.2.2 Time-Critical Priority Category 5.2.3 Force Background Priority Category 5.2.4 Setting Process/Thread Priorities
Chapter 6. The User Interface
6.1 VIO User Interface 6.2 The Presentation Manager User Interface 6.3 Presentation Manager and VIO Compatibility
Chapter 7. Dynamic Linking
7.1 Static Linking 7.2 Loadtime Dynamic Linking 7.3 Runtime Dynamic Linking 7.4 Dynlinks, Processes, and Threads 7.5 Data 7.5.1 Instance Data 7.5.2 Global Data 7.6 Dynamic Link Packages As Subroutines 7.7 Subsystems 7.7.1 Special Subsystem Support 7.8 Dynamic Links As Interfaces to Other Processes 7.9 Dynamic Links As Interfaces to the Kernel 7.10 The Architectural Role of Dynamic Links 7.11 Implementation Details 7.11.1 Dynlink Data Security 7.11.2 Dynlink Life, Death, and Sharing 7.11.3 Dynlink Side Effects 7.12 Dynlink Names
Chapter 8. File System Name Space
8.1 Filenames 8.2 Network Access 8.3 Name Generation and Compatibility 8.4 Permissions 8.5 Other Objects in the File System Name Space
Chapter 9. Memory Management
9.1 Protection Model 9.2 Memory Management API 9.2.1 Shared Memory 9.2.2 Huge Memory 9.2.3 Executing from Data Segments 9.2.4 Memory Suballocation 9.3 Segment Swapping 9.3.1 Swapping Miscellany 9.4 Status and Information
Chapter 10. Environment Strings
Chapter 11. Interprocess Communication (IPC)
11.1 Shared Memory 11.2 Semaphores 11.2.1 Semaphore Recovery 11.2.2 Semaphore Scheduling 11.3 Named Pipes 11.4 Queues 11.5 Dynamic Data Exchange (DDE) 11.6 Signals 11.7 Combining IPC Forms
Chapter 12. Signals
Chapter 13. The Presentation Manager and VIO
13.1 Choosing Between PM and VIO 13.2 Background I/O 13.3 Graphics Under VIO
Chapter 14. Interactive Programs
14.1 I/O Architecture 14.2 Ctrl-C and Ctrl-Break Handling
Chapter 15. The File System
15.1 The OS/2 File System 15.2 Media Volume Management 15.3 I/O Efficiency
Chapter 16. Device Monitors, Data Integrity, and Timer Services
16.1 Device Monitors 16.2 Data Integrity 16.2.1 Semaphores 16.2.2 DosBufReset 16.2.3 Writethroughs 16.3 Timer Services
Chapter 17. Device Drivers and Hard Errors
17.1 Device Drivers 17.1.1 Device Drivers and OS/2 Communication 17.1.2 Device Driver Programming Model 17.1.3 Device Management 17.1.4 Dual Mode 17.2 Hard Errors 17.2.1 The Hard Error Daemon 17.2.2 Application Hard Error Handling
Chapter 18. I/O Privilege Mechanism and Debugging/Ptrace
18.1 I/O Privilege Mechanism 18.2 Debugging/Ptrace
Chapter 19. The 3x Box
Chapter 20. Family API
Part III: The Future
Chapter 21. The Future
21.1 File System 21.2 The 80386 21.2.1 Large Segments 21.2.2 Multiple Real Mode Boxes 21.2.3 Full Protection Capability 21.2.4 Other Features 21.3 The Next Ten Years
Glossary
Index
Acknowledgments
Although a book can have a single author, a work such as OS/2 necessarily
owes its existence to the efforts of a great many people. The architecture
described herein was hammered out by a joint Microsoft/ IBM design team:
Ann, Anthony, Carolyn, Ed, Gordon, Jerry, Mark, Mike, Ray, and Ross. This
team accomplished a great deal of work in a short period of time.
The bulk of the credit, and my thanks, go to the engineers who
designed and implemented the code and made it work. The size of the teams involved throughout the project prevents me from listing all the names here. It's hard for someone who has not been involved in a software project of this scope to imagine the problems, pressure, chaos, and "reality shifts" that arise in a never-ending stream. These people deserve great credit for their skill and determination in making OS/2 come to pass.
Thanks go to the OS/2 development staffers who found time, in the heat
of the furnace, to review and critique this book: Ian Birrell, Ross Cook, Rick Dewitt, Dave Gilman, Vic Heller, Mike McLaughlin, Jeff Parsons, Ray Pedrizetti, Robert Reichel, Rajen Shah, Anthony Short, Ben Slivka, Pete Stewart, Indira Subramanian, Bryan Willman, and Mark Zbikowski.
I'd like to give special thanks to Mark Zbikowski and Aaron Reynolds,
the "gurus of DOS." Without their successes there would never have been an opportunity for a product such as OS/2.
And finally I'd like to thank Bill Gates for creating and captaining
one hell of a company, thereby making all this possible.
Foreword
OS/2 is destined to be a very important piece of software. During the
next 10 years, millions of programmers and users will utilize this system. From time to time they will come across a feature or a limitation and wonder why it's there. The best way for them to understand the overall philosophy of the system will be to read this book. Gordon Letwin is Microsoft's architect for OS/2. In his very clear and sometimes humorous way, Gordon has laid out in this book why he included what he did and why he didn't include other things.
The very first generation of microcomputers were 8-bit machines, such
as the Commodore Pet, the TRS-80, the Apple II, and the CPM 80 based machines. Built into almost all of them was Microsoft's BASIC Interpreter. I met Gordon Letwin when I went to visit Heath's personal computer group (now part of Zenith). Gordon had written his own BASIC as well as an operating system for the Heath system, and he wasn't too happy that his management was considering buying someone else's. In a group of about 15 people, he bluntly pointed out the limitations of my BASIC versus his. After Heath licensed my BASIC, I convinced Gordon that Microsoft was the place to be if you wanted your great software to be popular, and so he became one of Microsoft's first 10 programmers. His first project was to single-handedly write a compiler for Microsoft BASIC. He put a sign on his door that read
Do not disturb, feed, poke, tease...the animal
and in 5 months wrote a superb compiler that is still the basis for all our BASIC compilers. Unlike the code that a lot of superstar programmers write, Gordon's source code is a model of readability and includes precise explanations of algorithms and why they were chosen.
When the Intel 80286 came along, with its protected mode completely
separate from its compatible real mode, we had no idea how we were going to get at its new capabilities. In fact, we had given up until Gordon came up with the patented idea described in this book that has been referred to as "turning the car off and on at 60 MPH." When we first explained the idea to Intel and many of its customers, they were sure it wouldn't work. Even Gordon wasn't positive it would work until he wrote some test programs that proved it did.
Gordon's role as an operating systems architect is to overview our
designs and approaches and make sure they are as simple and as elegant as possible. Part of this job includes reviewing people's code. Most programmers enjoy having Gordon look over their code and point out how it could be improved and simplified. A lot of programs end up about half as big after Gordon has explained a better way to write them. Gordon doesn't mince words, however, so in at least one case a particularly sensitive programmer burst into tears after reading his commentary. Gordon isn't content to just look over other people's code. When a particular project looks very difficult, he dives in. Currently, Gordon has decided to personally write most of our new file system, which will be dramatically faster than our present one. On a recent "vacation" he wrote more than 50 pages of source code.
This is Gordon's debut as a book author, and like any good designer he
has already imagined what bad reviews might say. I think this book is both fun and important. I hope you enjoy it as much as I have.
Bill Gates
Introduction
Technological breakthroughs develop in patterns that are distinct from
patterns of incremental advancements. An incremental advancement--an
improvement to an existing item--is straightforward and unsurprising. An
improvement is created; people see the improvement, know what it will do
for them, and start using it.
A major advance without closely related antecedents--a technological
breakthrough--follows a different pattern. The field of communication is a good example. Early in this century, a large infrastructure existed to facilitate interpersonal communication. Mail was delivered twice a day, and a variety of efficient services relayed messages. A businessman dictated a message to his secretary, who gave it to a messenger service. The service carried the message to its nearby destination, where a secretary delivered it to the recipient.
Into this environment came a technological breakthrough--the
telephone. The invention of the telephone was a breakthrough, not an incremental advance, because it provided an entirely new way to communicate. It wasn't an improvement over an existing method. That it was a breakthrough development impeded its acceptance. Most business people considered it a newfangled toy, of little practical use. "What good does it do me? By the time I dictate the message, and my secretary writes it down and gives it to the mailroom, and they phone the addressee's mailroom, and the message is copied--perhaps incorrectly--and delivered to the addressee's secretary, it would have been as fast to have it delivered by messenger! All my correspondents are close by, and, besides, with messengers I don't have to pay someone to sit by the telephone all day in case a message comes in."
This is a classic example of the earliest stages of breakthrough
technology--potential users evaluate it by trying to fit it into present work patterns. Our example businessman has not yet realized that he needn't write the message down anymore and that it needn't be copied down at the destination. He also doesn't realize that the reason his recipients are close by is that they have to be for decent messenger delivery. The telephone relaxed this requirement, allowing more efficient locations near factories and raw materials or where office space was cheaper. But it was necessary for the telephone to be accepted before these advantages could be realized.
Another impedance to the acceptance of a breakthrough technology is
that the necessary new infrastructure is not in place. A telephone did little good if your intended correspondent didn't have one. The nature of telephones required a standard; until that standard was set, your correspondent might own a phone, but it could be connected to a network unreachable by you. Furthermore, because the technology was in its infancy, the facilities were crude.
These obstacles were not insurmountable. The communications
requirements of some people were so critical that they were willing to invent new procedures and to put up with the problems of the early stages. Some people, because of their daring or ambition, used the new system to augment their existing system. And finally, because the new technology was so powerful, some used it to enhance the existing technology. For example, a messenger service might establish several offices with telephone linkage between them and use the telephones to speed delivery of short messages by phoning them to the office nearest the destination, where they were copied down and delivered normally. Using the telephone in this fashion was wasteful, but where demand for the old service was high enough, any improvement, however "wasteful," was welcome.
After it has a foot in the door, a breakthrough technology is
unstoppable. After a time, standards are established, the bugs are worked out, and, most important, the tool changes its users. Once the telephone became available, business and personal practices developed in new patterns, patterns that were not considered before because they were not possible. Messenger services used to be fast enough, but only because, before the telephone, the messenger service was the fastest technology available. The telephone changed the life-style of its users.
This change in the structure of human activity explains why an
intelligent person could say, "Telephones are silly gadgets," and a few years later say, "Telephones are indispensable." This change in the tool user--caused by the tool itself--also makes predicting the ultimate effect of the new technology difficult. Extrapolating from existing trends is wildly inaccurate because the new tool destroys many practices and creates wholly unforeseen ones. It's great fun to read early, seemingly silly predictions of life in the future and to laugh at the predictors, but the predictors were frequently intelligent and educated. Their only mistake was in treating the new development as an incremental advance rather than as a breakthrough technology. They saw how the new development would improve their current practices, but they couldn't see how it would replace those practices.
Digital computers are an obvious breakthrough technology, and they've
shared the classic three-stage pattern: "exotic toys," "limited use," and "indispensable." Mainframe computers have gone the full route, in the milieu of business and scientific computing. IBM's initial estimate of the computer market was a few dozen machines. But, as the technology and the support infrastructure grew, and as people's ways of working adapted to computers, the use of computers grew--from the census bureau, to life insurance companies, to payroll systems, and finally to wholly new functions such as MIS (Management Information Sciences) systems and airline reservation networks.
Microcomputers are in the process of a similar development. The
"exotic toy" stage has already given way to the "limited use" stage. We're just starting to develop standards and infrastructure and are only a few years from the "indispensable" stage. In anticipation of this stage, Microsoft undertook the design and the development of OS/2.
Although studying the mainframe computer revolution helps in trying to
predict the path of the microcomputer revolution, microcomputers are more than just "cheap mainframes." The microcomputer revolution will follow the tradition of breakthroughs, creating new needs and new uses that cannot be anticipated solely by studying what happened with mainframe systems.
This book was written because of the breakthrough nature of the
microcomputer and the impact of the coming second industrial revolution. The designers of OS/2 tried to anticipate, to the greatest extent possible, the demands that would be placed on the system when the tool--the personal computer--and the tool user reached their new equilibrium. A knowledge of MS-DOS and a thorough reading of the OS/2 reference manuals will not, in themselves, clarify the key issues of the programming environment that OS/2 was written to support. This is true not only because of the complexity of the product but because many design elements were chosen to provide services that from a prebreakthrough perspective--don't seem needed and solve problems that haven't yet arisen.
Other books provide reference information and detailed how-to
instructions for writing OS/2 programs. This book describes the underlying architectural models that make up OS/2 and discusses how those models are expected to meet the foreseen and unforeseen requirements of the oncoming office automation revolution. It focuses on the general issues, problems, and solutions that all OS/2 programs encounter regardless of the programming and interface models that a programmer may employ.
As is often the case in a technical discussion, everything in OS/2 is
interconnected in some fashion to everything else. A discussion on the shinbone naturally leads to a discussion of the thighbone and so on. The author and the editor of this book have tried hard to group the material into a logical progression without redundancy, but the very nature of the material makes complete success at this impossible. It's often desirable, in fact, to repeat material, perhaps from a different viewpoint or with a different emphasis. For these reasons, the index references every mention of an item or a topic, however peripheral. Having too many references (including a few worthless ones) is far better than having too few references. When you're looking for information about a particular subject, I recommend that you first consult the contents page to locate the major discussion and then peruse the index to pick up references that may appear in unexpected places.
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Part I The Project
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
1 History of the Project
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Microsoft was founded to realize a vision of a microcomputer on every desktop--a vision of the second industrial revolution. The first industrial revolution mechanized physical work. Before the eighteenth century, nearly all objects were created and constructed by human hands, one at a time. With few exceptions, such as animal-powered plowing and cartage, all power was human muscle power. The second industrial revolution will mechanize routine mental work. Today, on the verge of the revolution, people are still doing "thought work," one piece at a time.
Certain tasks--those massive in scope and capable of being rigidly
described, such as payroll calculations--have been automated, but the majority of "thought work" is still done by people, not by computers. We have the computer equivalent of the plow horse, but we don't have the computer equivalent of the electric drill or the washing machine.
Of course, computers cannot replace original thought and creativity
(at least, not in the near future) any more than machines have replaced design and creativity in the physical realm. But the bulk of the work in a white-collar office involves routine manipulation of information. The second industrial revolution will relieve us of the "grunt work"--routine data manipulation, analysis, and decisions--freeing us to deal only with those situations that require human judgment.
Most people do not recognize the inevitability of the second
industrial revolution. They can't see how a computer could do 75 percent of their work because their work was structured in the absence of computers. But, true to the pattern for technological breakthroughs, the tremendous utility of the microcomputer will transform its users and the way they do their work.
For example, a great deal of work is hard to computerize because the
input information arrives on paper and it would take too long to type it all in. Ten years ago, computer proponents envisioned the "paperless office" as a solution for this problem: All material would be generated by computer and then transferred electronically or via disk to other computers. Offices are certainly becoming more paperless, and the arrival of powerful networking systems will accelerate this, but paper continues to be a very useful medium. As a result, in recent years growth has occurred in another direction--incorporating paper as a computer input and output device. Powerful laser printers, desktop publishing systems, and optical scanners and optical character recognition will make it more practical to input from and output to paper.
Although the founders of Microsoft fully appreciate the impact of the
second industrial revolution, nobody can predict in detail how the revolution will unfold. Instead, Microsoft bases its day-to-day decisions on dual sets of goals: short-term goals, which are well known, and a long- term goal--our vision of the automated office. Each decision has to meet our short-term goals, and it must be consonant with our long-term vision, a vision that becomes more precise as the revolution progresses.
When 16-bit microprocessors were first announced, Microsoft knew that
the "iron" was now sufficiently powerful to begin to realize this vision. But a powerful computer environment requires both strong iron and a sophisticated operating system. The iron was becoming available, but the operating system that had been standard for 8-bit microprocessors was inadequate. This is when and why Microsoft entered the operating system business: We knew that we needed a powerful operating system to realize our vision and that the only way to guarantee its existence and suitability was to write it ourselves.
1.1 MS-DOS version 1.0
MS-DOS got its start when IBM asked Microsoft to develop a disk operating system for a new product that IBM was developing, the IBM Personal Computer (PC). Microsoft's only operating system product at that time was XENIX, a licensed version of AT&T's UNIX operating system. XENIX/UNIX requires a processor with memory management and protection facilities. Because the 8086/8088 processors had neither and because XENIX/UNIX memory requirements--modest by minicomputer standards of the day--were nonetheless large by microcomputer standards, a different operating system had to be developed.
CP/M-80, developed by Digital Research, Incorporated (DRI), had been
the standard 8-bit operating system, and the majority of existing microcomputer software had been written to run on CP/M-80. For this reason, Microsoft decided to make MS-DOS version 1.0 as compatible as possible with CP/M-80. The 8088 processor would not run the existing CP/M-80 programs, which were written for the 8080 processor, but because 8080 programs could be easily and semiautomatically converted to run on the 8088, Microsoft felt that minimizing adaptation hassles by minimizing operating system incompatibility would hasten the acceptance of MS-DOS on the IBM PC.
A major software product requires a great deal of development time,
and IBM was in a hurry to introduce its PC. Microsoft, therefore, looked around for a software product to buy that could be built onto to create MS- DOS version 1.0. Such a product was found at Seattle Computer Products. Tim Paterson, an engineer there, had produced a CP/M-80 "clone," called SCP- DOS, that ran on the 8088 processor. Microsoft purchased full rights to this product and to its source code and used the product as a starting point in the development of MS-DOS version 1.0.
MS-DOS version 1.0 was released in August 1981. Available only for the
IBM PC, it consisted of 4000 lines of assembly-language source code and ran in 8 KB of memory. MS-DOS version 1.1 was released in 1982 and worked with double-sided 320 KB floppy disks.
Microsoft's goal was that MS-DOS version 1.0 be highly CP/M
compatible, and it was. Ironically, it was considerably more compatible than DRI's own 8088 product, CP/M-86. As we shall see later, this CP/M compatibility, necessary at the time, eventually came to cause Microsoft engineers a great deal of difficulty.
1.2 MS-DOS version 2.0
In early 1982, IBM disclosed to Microsoft that it was developing a hard disk-based personal computer, the IBM XT. Microsoft began work on MS-DOS version 2.0 to provide support for the new disk hardware. Changes were necessary because MS-DOS, in keeping with its CP/M-80 compatible heritage, had been designed for a floppy disk environment. A disk could contain only one directory, and that directory could contain a maximum of 64 files. This decision was reasonable when first made because floppy disks held only about 180 KB of data.
For the hard disk, however, the 64-file limit was much too small, and
using a single directory to manage perhaps hundreds of files was clumsy. Therefore, the MS-DOS version 2.0 developers--Mark Zbikowski, Aaron Reynolds, Chris Peters, and Nancy Panners--added a hierarchical file system. In a hierarchical file system a directory can contain other directories and files. In turn, those directories can con- tain a mixture of files and directories and so on. A hierarchically designed system starts with the main, or "root," directory, which itself can contain (as seen in Figure 1-1) a tree-structured collection of files and directories.
directory WORK ADMIN ÚÄÄÄÄÄÄÄÄÄÄ BUDGET ³ CALENDAR ³ LUNCH.DOC ³ PAYROLL ÄÄÄÄÄÄÄÄÄ¿ ³ PHONE.LST ³ ³ SCHED.DOC ³ ³ ³ directory BUDGET directory PAYROLL MONTH ADDRESSES QUARTER MONTHLY YEAR NAMES 1986 ÄÄÄÄÄÄÄÄ¿ RETIRED ÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄ 1985 ³ VACATION ³ ³ ³ WEEKLY ³ ³ ³ ³ directory 1985 directory 1986 directory RETIRED ³ ³ ³
Figure 1-1. A directory tree hierarchy. Within the WORK directory are five files (ADMIN, CALENDAR, LUNCH.DOC, PHONE.LST, SCHED.DOC) and two subdirectories (BUDGET, PAYROLL). Each subdirectory has its own subdirectories.
1.3 MS-DOS version 3.0
MS-DOS version 3.0 was introduced in August 1984, when IBM announced the IBM PC/AT. The AT contains an 80286 processor, but, when running DOS, it uses the 8086 emulation mode built into the chip and runs as a "fast 8086." The chip's extended addressing range and its protected mode architecture sit unused. 1. Products such as Microsoft XENIX/UNIX run on the PC/AT and compatibles, using the processor's protected mode. This is possible because XENIX/UNIX and similar systems had no preexisting real mode applications that needed to be supported. 1
MS-DOS version 3.1 was released in November 1984 and contained
networking support. In January 1986, MS-DOS version 3.2--a minor revision-- was released. This version supported 3-1/2-inch floppy disks and contained the formatting function for a device in the device driver. In 1987, MS-DOS version 3.3 followed; the primary enhancement of this release was support for the IBM PS/2 and compatible hardware.
1.4 MS-DOS version 4.0
Microsoft started work on a multitasking version of MS-DOS in January 1983. At the time, it was internally called MS-DOS version 3.0. When a new version of the single-tasking MS-DOS was shipped under the name MS-DOS version 3.0, the multitasking version was renamed, internally, to MS-DOS version 4.0. A version of this product--a multitasking, real-mode only MS- DOS--was shipped as MS-DOS version 4.0. Because MS-DOS version 4.0 runs only in real mode, it can run on 8088 and 8086 machines as well as on 80286 machines. The limitations of the real mode environment make MS-DOS version 4.0 a specialized product. Although MS-DOS version 4.0 supports full preemptive multitasking, system memory is limited to the 640 KB available in real mode, with no swapping. 2. It is not feasible to support general purpose swapping without memory management hardware that is unavailable in 8086 real mode. 2 This means that all processes have to fit into the single 640 KB memory area. Only one MS-DOS version 3.x compatible real mode application can be run; the other processes must be special MS- DOS version 4.0 processes that understand their environment and cooperate with the operating system to coexist peacefully with the single MS-DOS version 3.x real mode application.
Because of these restrictions, MS-DOS version 4.0 was not intended for
general release, but as a platform for specific OEMs to support extended PC architectures. For example, a powerful telephone management system could be built into a PC by using special MS-DOS version 4.0 background processes to control the telephone equipment. The resulting machine could then be marketed as a "compatible MS-DOS 3 PC with a built-in superphone."
Although MS-DOS version 4.0 was released as a special OEM product, the
project--now called MS-DOS version 5.0--continued. The goal was to take advantage of the protected mode of the 80286 to provide full general purpose multitasking without the limitations--as seen in MS-DOS version 4.0--of a real-mode only environment. Soon, Microsoft and IBM signed a Joint Development Agreement that provided for the design and development of MS-DOS version 5.0 (now called CP/DOS). The agreement is complex, but it basically provides for joint development and then subsequent joint ownership, with both companies holding full rights to the resulting product.
As the project neared completion, the marketing staffs looked at
CP/DOS, nee DOS 5, nee DOS 4, nee DOS 3, and decided that it needed...you guessed it...a name change. As a result, the remainder of this book will discuss the design and function of an operating system called OS/2.
2 Goals and Compatibility Issues
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
OS/2 is similar to traditional multitasking operating systems in many ways: It provides multitasking, scheduling, disk management, memory management, and so on. But it is also different in many ways, because a personal computer is very different from a multiuser minicomputer. The designers of OS/2 worked from two lists: a set of goals and a set of compatibility issues. This chapter describes those goals and compatibility issues and provides the context for a later discussion of the design itself.
2.1 Goals
The primary goal of OS/2 is to be the ideal office automation operating system. The designers worked toward this goal by defining the following intermediate and, seemingly, contradictory goals:
þ To provide device-independent graphics drivers without introducing any significant overhead.
þ To allow applications direct access to high-bandwidth peripherals but maintain the ability to virtualize or apportion the usage of those peripherals.
þ To provide multitasking without reducing the performance and response available from a single-tasking system.
þ To provide a fully customized environment for each program and its descendants yet also provide a standard environment that is unaffected by other programs in the system.
þ To provide a protected environment to ensure system stability yet one that will not constrain applications from the capabilities they have under nonprotected systems.
2.1.1 Graphical User Interface
By far the fastest and easiest way people receive information is through
the eye. We are inherently visual creatures. Our eyes receive information
rapidly; they can "seek" to the desired information and "zoom" their
attention in and out with small, rapid movements of the eye muscles. A
large part of the human brain is dedicated to processing visual
information. People abstract data and meaning from visual material--from
text to graphics to motion pictures--hundreds of times faster than from any
other material.
As a result, if an office automation system is to provide quantities
of information quickly and in a form in which it can be easily absorbed, a powerful graphics capability is essential. Such capabilities were rare in earlier minicomputer operating systems because of the huge memory and compute power costs of high-resolution displays. Today's microcomputers have the memory to contain the display information, they have the CPU power to create and manipulate that information, and they have no better use for those capabilities than to support powerful, easy-to-use graphical applications.
Graphics can take many forms--pictures, tables, drawings, charts--
perhaps incorporating color and even animation. All are powerful adjuncts to the presentation of alphanumeric text. Graphical applications don't necessarily employ charts and pictures. A WYSIWYG (What You See Is What You Get) typesetting program may display only text, but if that text is drawn in graphics mode, the screen can show any font, in any type size, with proportional spacing, kerning, and so on.
The screen graphics components of OS/2 need to be device independent;
that is, an application must display the proper graphical "picture" without relying on the specific characteristics of any particular graphical display interface board. Each year the state of the art in displays gets better; it would be extremely shortsighted to tie applications to a particular display board, for no matter how good it is, within a couple of years it will be obsolete.
The idea is to encapsulate device-specific code by requiring that each
device come with a software package called a device driver. The application program issues commands for a generic device, and the device driver then translates those commands to fit the characteristics of the actual device. The result is that the manufacturer of a new graphics display board needs to write an appropriate device driver and supply it with the board. The application program doesn't need to know anything about the device, and the device driver doesn't need to know anything about the application, other than the specification of the common interface they share. This common interface describes a virtual display device; the general technique of hiding a complicated actual situation behind a simple, standard interface is called "virtualization."
Figure 2-1 shows the traditional operating system device driver
architecture. Applications don't directly call device drivers because device drivers need to execute in the processor's privilege mode to manipulate their device; the calling application must run in normal mode. In the language of the 80286/80386 family of processors, privilege mode is called ring 0, and normal mode is called ring 3. The operating system usually acts as a middleman: It receives the request, validates it, deals with issues that arise when there is only one device but multiple applications are using it, and then passes the request to the device driver. The device driver's response or return of data takes the reverse path, winding its way through the operating system and back to the application program.
Application Kernel Device driver (ring 3) (ring 0) (ring 0)
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Request ³ packet: ³ ÚÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄ¿ Device ³ Device ³ ³ ³ ³ descriptor ÄÅÄ´ driver ³
Call deviceio (arg 1...argn) ³ function ³ #1 ÀÄÄÄÄÄÄÄÄÄÄÙ
³ ³ arg1 ³ ú ³ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ�³ ù ÃÄ¿ ú Ring ³ ³ argn ³ ³ ú ³ transition ³ ³ ³ ú ÚÄÄÄÄÄÄÄÄÄÄ¿ ³ ÀÄÄÄÄÄÄÄÄÄÄÙ À� Device ³ ³ Device ³ descriptor ÄÄÄ´ driver ³ ³ #N ³ ÀÄÄÄÄÄÄÄÄÄÄÙ
Figure 2-1. Traditional device driver architecture. When an application wants to do device I/0, it calls the operating system, which builds a device request packet, determines the target device, and delivers the packet. The device driver's response follows the opposite route through the kernel back to the application.
This approach solves the device virtualization problem, but at a cost
in performance. The interface between the application and the device driver is narrow; that is, the form messages can take is usually restricted. Commonly, the application program is expected to build a request block that contains all the information and data that the device driver needs to service the request; the actual call to the operating system is simply "pass this request block to the device driver." Setting up this block takes time, and breaking it down in the device driver again takes time. More time is spent on the reply; the device driver builds, the operating system copies, and the application breaks down. Further time is spent calling down through the internal layers of the operating system, examining and copying the request block, routing to the proper device driver, and so forth. Finally, the transition between rings (privilege and normal mode) is also time-consuming, and two such transitions occur--to privilege mode and back again.
Such a cost in performance was acceptable in nongraphics-based systems
because, typically, completely updating a screen required only 1920 (or fewer) bytes of data. Today's graphics devices can require 256,000 bytes or more per screen update, and future devices will be even more demanding. Furthermore, applications may expect to update these high-resolution screens several times a second. 1. It's not so much the amount of data that slows the traditional device driver model, but the number of requests and replies. Disk devices work well through the traditional model because disk requests tend to be large (perhaps 40,000 bytes). Display devices tend to be written piecemeal--a character, a word, or a line at a time. It is the high rate of these individual calls that slows the device driver model, not the number of bytes written to the screen. 1
OS/2 needed powerful, device-independent graphical display support
that had a wide, efficient user interface--one that did not involve ring transitions, the operating system, or other unnecessary overhead. As we'll see later, OS/2 meets this requirement by means of a mechanism called dynamic linking.
2.1.2 Multitasking
To be really useful, a personal computer must be able to do more than one
chore at a time--an ability called multitasking. We humans multitask all
the time. For example, you may be involved in three projects at work, be
halfway through a novel, and be taking Spanish lessons. You pick up each
task in turn, work on it for a while, and then put it down and work on
something else. This is called serial multitasking. Humans can also do some
tasks simultaneously, such as driving a car and talking. This is called
parallel multitasking.
In a serial multitasking computer environment, a user can switch
activities at will, working for a while at each. For example, a user can leave a word-processing program without terminating it, consult a spreadsheet, and then return to the waiting word-processing program. Or, if someone telephones and requests an appointment, the user can switch from a spreadsheet to a scheduling program, consult the calendar, and then return to the spreadsheet.
The obvious value of multitasking makes it another key requirement for
OS/2: Many programs or applications can run at the same time. But multitasking is useful for more than just switching between applications: Parallel multitasking allows an application to do work by itself--perhaps print a large file or recalculate a large spreadsheet--while the user works with another application. Because OS/2 supports full multitasking, it can execute programs in addition to the application(s) the user is running, providing advanced services such as network mail without interrupting or interfering with the user's work. 2. Present-day machines contain only one CPU, so at any instant only one program can be executing. At this microscopic level, OS/2 is a serial multitasking system. It is not considered serial multitasking, however, because it performs preemptive scheduling. At any time, OS/2 can remove the CPU from the currently running program and assign it to another program. Because these rescheduling events may occur many times a second at totally unpredictable places within the running programs, it is accurate to view the system as if each program truly runs simultaneously with other programs. 2
2.1.3 Memory Management
Multitasking is fairly easy to achieve. All that's necessary is a source of
periodic hardware interrupts, such as a clock circuit, to enable the
operating system to effect a "context switch," or to reschedule. To be
useful, however, a multitasking system needs an effective memory management
system. For example, a user wants to run two applications on a system. Each
starts at as low a memory location as possible to maximize the amount of
memory it can use. Unfortunately, if the system supports multitasking and
the user tries to run both applications simultaneously, each attempts to
use the same memory cells, and the applications destroy each other.
A memory management system solves this problem by using special
hardware facilities built into 80286/80386 processors (for example, IBM PC/AT machines and compatibles and 80386-based machines). 3. Earlier 8086/8088 processors used in PCs, PC/XTs, and similar machines lack this hardware. This is why earlier versions of MS-DOS didn't support multitasking and why OS/2 won't run on such machines. 3 The memory management system uses the hardware to virtualize the memory of the machine so that each program appears to have all memory to itself.
Memory management is more than keeping programs out of each other's
way. The system must track the owner or user(s) of each piece of memory so that the memory space can be reclaimed when it is no longer needed, even if the owner of the memory neglects to explicitly release it. Some operating systems avoid this work by assuming that no application will ever fail to return its memory when done or by examining the contents of memory and ascertaining from those contents whether the memory is still being used. (This is called "garbage collection.") Neither alternative was acceptable for OS/2. Because OS/2 will run a variety of programs written by many vendors, identifying free memory by inspection is impossible, and assuming perfection from the applications themselves is unwise. Tracking the ownership and usage of memory objects can be complex, as we shall see in our discussion on dynamic link libraries.
Finally, the memory management system must manage memory overcommit.
The multitasking capability of OS/2 allows many applications to be run simultaneously; thus, RAM must hold all these programs and their data. Although RAM becomes cheaper every year, buying enough to hold all of one's applications at one time is still prohibitive. Furthermore, although RAM prices continue to drop, the memory requirements of applications will continue to rise. Consequently, OS/2 must contain an effective mechanism to allocate more memory to the running programs than in fact physically exists. This is called memory overcommit.
OS/2 accomplishes this magic with the classic technique of swapping.
OS/2 periodically examines each segment of memory to see if it has been used recently. When a request is made for RAM and none is available, the least recently used segment of memory (the piece that has been unused for the longest time) is written to a disk file, and the RAM it occupied is made available. Later, if a program attempts to use the swapped-out memory, a "memory not present" fault occurs. OS/2 intercepts the fault and reloads the memory information from the disk into memory, swapping out some other piece of memory, if necessary, to make room. This whole process is invisible to the application that uses the swapped memory area; the only impact is a small delay while the needed memory is read back from the disk.
The fundamental concepts of memory overcommit and swapping are simple,
but a good implementation is not. OS/2 must choose the right piece of memory to swap out, and it must swap it out efficiently. Not only must care be taken that the swap file doesn't grow too big and consume all the free disk space but also that deadlocks don't occur. For example, if all the disk swap space is filled, it may be impossible to swap into RAM a piece of memory because no free RAM is available, and OS/2 can't free up RAM because no swap space exists to write it out to. Naturally, the greater the load on the system, the slower the system will be, but the speed degradation must be gradual and acceptable, and the system must never deadlock.
The issues involved in memory management and the memory management
facilities that OS/2 provides are considerably more complex than this overview. We'll return to the subject of memory management in detail in Chapter 9.
2.1.4 Protection
I mentioned earlier that OS/2 cannot trust applications to behave
correctly. I was talking about memory management, but this concern
generalizes into the next key requirement: OS/2 must protect applications
from the proper or improper actions of other applications that may be
running on the system.
Because OS/2 will run applications and programs from a variety of
vendors, every user's machine will execute a different set of applications, running in different ways on different data. No software vendor can fully test a product in all possible environments. This makes it critical that an error on the part of one program does not crash the system or some other program or, worse, corrupt data and not bring down the system. Even if no data is damaged, system crashes are unacceptable. Few users have the background or equipment even to diagnose which application caused the problem.
Furthermore, malice, as well as accident, is a concern. Microsoft's
vision of the automated office cannot be realized without a system that is secure from deliberate attack. No corporation will be willing to base its operations on a computer network when any person in that company--with the help of some "cracker" programs bought from the back of a computer magazine--can see and change personnel or payroll files, billing notices, or strategic planning memos.
Today, personal computers are being used as a kind of super-
sophisticated desk calculator. As such, data is secured by traditional means--physical locks on office doors, computers, or file cabinets that store disks. Users don't see a need for a protected environment because their machine is physically protected. This lack of interest in protection is another example of the development of a breakthrough technology. Protection is not needed because the machine is secure and operates on data brought to it by traditional office channels. In the future, however, networked personal computers will become universal and will act both as the processors and as the source (via the network) of the data. Thus, in this role, protection is a key requirement and is indeed a prerequisite for personal computers to assume that central role.
2.1.5 Encapsulation
When a program runs in a single-tasking system such as MS-DOS version 3.x,
its environment is always constant--consisting of the machine and MS-DOS.
The program can expect to get the same treatment from the system and to
provide exactly the same interaction with the user each time it runs. In a
multitasking environment, however, many programs can be running. Each
program can be using files and devices in different ways; each program can
be using the mouse, each program can have the screen display in a different
mode, and so on. OS/2 must encapsulate, or isolate, each program so that it
"sees" a uniform environment each time it runs, even though the computer
environment itself may be different each time.
2.1.6 Interprocess Communication (IPC)
In a single-tasking environment such as MS-DOS version 3.x, each program
stands alone. If it needs a particular service not provided by the
operating system, it must provide that service itself. For example, every
application that needs a sort facility must contain its own.
Likewise, if a spreadsheet needs to access values from a database, it
must contain the code to do so. This extra code complicates the spreadsheet program, and it ties the program to a particular database product or format. A user might be unable to switch to a better product because the spreadsheet is unable to understand the new database's file formats.
A direct result of such a stand-alone environment is the creation of
very large and complex "combo" packages such as Lotus Symphony. Because every function that the user may want must be contained within one program, vendors supply packages that attempt to contain everything.
In practice, such chimeric programs tend to be large and cumbersome,
and their individual functional components (spreadsheets, word processors, and databases, for example) are generally more difficult to use and less sophisticated than individual applications that specialize in a single function.
The stand-alone environment forces the creation of larger and more
complex programs, each of which typically understands only its own file formats and works poorly, if at all, with data produced by other programs. This vision of personal computer software growing monstrous until collapsing from its own weight brings about another OS/2 requirement: Applications must be able to communicate, easily and efficiently, with other applications.
More specifically, an application must be able to find (or name) the
application that provides the information or service that the client needs, and it must be able to establish efficient communication with the provider program without requiring that either application have specific knowledge of the internal workings of the other. Thus, a spreadsheet program must be able to communicate with a database program and access the values it needs. The spreadsheet program is therefore not tied to any particular database program but can work with any database system that recognizes OS/2 IPC requests.
Applications running under OS/2 not only retain their full power as
individual applications but also benefit from cross-application communication. Furthermore, the total system can be enhanced by upgrading an application that provides services to others. When a new, faster, or more fully featured database package is installed, not only is the user's database application improved but the database functions of the spreadsheet program are improved as well.
The OS/2 philosophy is that no program should reinvent the wheel.
Programs should be written to offer their services to other programs and to take advantage of the offered services of other programs. The result is a maximally effective and efficient system.
2.1.7 Direct Device Access
Earlier, we discussed the need for a high-performance graphical interface
and the limitations of the traditional device driver architecture. OS/2
contains a built-in solution for the screen graphical interface, but what
about other, specialized devices that may require a higher bandwidth
interface than device drivers provide? The one sure prediction about the
future of a technological breakthrough is that you can't fully predict it.
For this reason, the final key requirement for OS/2 is that it contain an
"escape hatch" in anticipation of devices that have performance needs too
great for a device driver model.
OS/2 provides this expandability by allowing applications direct
access to hardware devices--both the I/O ports and any device memory. This must be done, of course, in such a way that only devices which are intended to be used in this fashion can be so accessed. Applications are prevented from using this access technique on devices that are being managed by the operating system or by a device driver. This facility gives applications the ability to take advantage of special nonstandard hardware such as OCRs (Optical Character Readers), digitizer tablets, Fax equipment, special purpose graphics cards, and the like.
2.2 Compatibility Issues
But OS/2 has to do more than meet the goals we've discussed: It must be compatible with 8086/8088 and 80286 architecture, and it must be compatible with MS-DOS. By far the easiest solution would have been to create a new multitasking operating system that would not be compatible with MS-DOS, but such a system is unacceptable. Potential users may be excited about the new system, but they won't buy it until applications are available. Application writers may likewise be excited, but they won't adapt their products for it until the system has sold enough copies to gain significant market share. This "catch 22" means that the only people who will buy the new operating system are the developers' mothers, and they probably get it at a discount anyway.
2.2.1 Real Mode vs Protect Mode
The first real mode compatibility issue relates to the design of the 80286
microprocessor--the "brain" of an MS-DOS computer. This chip has two
incompatible modes--real (compatibility) mode and protect mode. Real mode
is designed to run programs in exactly the same manner as they run on the
8086/8088 processor. In other words, when the 80286 is in real mode, it
"looks" to the operating system and programs exactly like a fast
8088.
But the designers of the 80286 wanted it to be more than a fast 8088.
They wanted to add such features as memory management, memory protection, and the ring protection mechanism, which allows the operating system to protect one application from another. They weren't able to do this while remaining fully compatible with the earlier 8088 chip, so they added a second mode to the 80286--protect mode. When the processor is running in protect mode, it provides these important new features, but it will not run most programs written for the 8086/8088.
In effect, an 80286 is two separate microprocessors in one package. It
can act like a very fast 8088--compatible, but with no new capabilities--or it can act like an 80286--incompatible, but providing new features. Unfortunately, the designers of the chip didn't appreciate the importance of compatibility in the MS-DOS marketplace, and they designed the 80286 so that it can run in either mode but can't switch back and forth at will. 4. The 80286 initializes itself in real mode. There is a command to switch from real mode to protect mode, but there is no command to switch back. 4 In other words, an 80286 was designed to run only old 8086/8088 programs, or it can run only new 80286 style programs, but never both at the same time.
In summary, OS/2 was required to do something that the 80286 was not
designed for--execute both 8086/8088 style (real mode) and 80286 style (protect) mode programs at the same time. The existence of this book should lead you to believe that this problem was solved, and indeed it was.
2.2.2 Running Applications in Real (Compatibility) Mode
Solving the real mode vs protect mode problem, however, presented other
problems. In general, the problems came about because the real mode
programs were written for MS-DOS versions 2.x or 3.x, both of which are
single-tasking environments.
Although MS-DOS is normally spoken of as an operating system, it could
just as accurately be called a "system executive." Because it runs in an unprotected environment, applications are free to edit interrupt vectors, manipulate peripherals, and in general take over from MS-DOS wherever they wish. This flexibility is one reason for the success of MS-DOS; if MS-DOS doesn't offer the service your program needs, you can always help yourself. Developers were free to explore new possibilities, often with great success. Most applications view MS-DOS as a program loader and as a set of file system subroutines, interfacing directly with the hardware for all their other needs, such as intercepting interrupt vectors, editing disk controller parameter tables, and so on.
It may seem that if a popular application "pokes" the operating system
and otherwise engages in unsavory practices that the authors or users of the application will suffer because a future release, such as OS/2, may not run the application correctly. To the contrary, the market dynamics state that the application has now set a standard, and it's the operating system developers who suffer because they must support that standard. Usually, that "standard" operating system interface is not even known; a great deal of experimentation is necessary to discover exactly which undocumented side effects, system internals, and timing relationships the application is dependent on.
Offering an MS-DOS-compatible Applications Program Interface (API)
provides what we call level 1 compatibility. Allowing applications to continue to manipulate system hardware provides level 2 compatibility. Level 3 issues deal with providing an execution environment that supports the hidden assumptions that programs written for a single-tasking environment may make. Three are discussed below by way of illustration.
2.2.2.1 Memory Utilization
The existing real mode applications that OS/2 must support were written for
an environment in which no other programs are running. As a result,
programs typically consume all available memory in the system in the belief
that, since no other program is around to use any leftover memory, they
might as well use it all. If a program doesn't ask for all available memory
at first, it may ask for the remainder at some later time. Such a
subsequent request could never be refused under MS-DOS versions 2.x and
3.x, and applications were written to depend on this. Therefore, such a
request must be satisfied under OS/2 to maintain full compatibility.
Even the manner of a memory request depends on single-tasking
assumptions. Programs typically ask for all memory in two steps. First, they ask for the maximum amount of memory that an 8088 can provide--1 MB. The application's programmer knew that the request would be refused because 1 MB is greater than the 640 KB maximum supported by MS-DOS; but when MS- DOS refuses the request, it tells the application exactly how much memory is available. Programs then ask for that amount of memory. The programmer knew that MS-DOS would not refuse the second memory request for insufficient memory because when MS-DOS responded to the first request it told the application exactly how much memory was available. Consequently, programmers rarely included a check for an "insufficient memory" error from the second call.
This shortcut introduces problems in the OS/2 multitasking
environment. When OS/2 responded to the first too-large request, it would return the amount of memory available at that exact moment. Other programs are simultaneously executing; by the time our real mode program makes its second request, some more memory may have been given out, and the second request may also be too large. It won't do any good for OS/2 to respond with an error code, however, because the real mode application does not check for one (it was written in the belief that it is impossible to get such a code on the second call). The upshot is that even if OS/2 refused the second call the real mode application would assume that it had been given the memory, would use it, and in the process would destroy the other program(s) that were the true owners of that memory.
Obviously, OS/2 must resolve this and similar issues to support the
existing base of real mode applications.
2.2.2.2 File Locking
Because multitasking systems run more than one program at the same time,
two programs may try to write or to modify the same file at the same time.
Or one may try to read a file while another is changing that file's
contents. Multitasking systems usually solve this problem by means of a
file-locking mechanism, which allows one program to temporarily prevent
other programs from reading and/or writing a particular file.
An application may find that a file it is accessing has been locked by
some other application in the system. In such a situation, OS/2 normally returns a "file locked" error code, and the application typically gives up or waits and retries the operation later. OS/2 cannot return a "file locked" error to an old-style real mode application, though, because when the application was written (for MS-DOS versions 2.x or 3.x) no such error code existed because no such error was possible. Few real mode applications even bother to check their read and write operations for error codes, and those that do wouldn't "understand" the error code and wouldn't handle it correctly.
OS/2 cannot compromise the integrity of the file-locking mechanism by
allowing the real mode application to ignore locks, but it cannot report that the file is locked to the application either. OS/2 must determine the proper course of action and then take that action on behalf of the real mode application.
2.2.2.3 Network Piggybacking
Running under MS-DOS version 3.1, an application can use an existing
network virtual circuit to communicate with an application running on the
server machine to which the virtual circuit is connected. This is called
"piggybacking" the virtual circuit because the applications on each end are
borrowing a circuit that the network redirector established for other
purposes. The two sets of programs can use a single circuit for two
different purposes without confusion under MS-DOS version 3.1 because of
its single-tasking nature. The redirector only uses the circuit when the
application calls MS-DOS to perform a network function. Because the CPU is
inside MS-DOS, it can't be executing the application software that sends
private messages, which leaves the circuit free for use by the
redirector.
Conversely, if the application is sending its own private messages--
piggybacking--then it can't be executing MS-DOS, and therefore the redirector code (which is built into MS-DOS) can't be using the virtual circuit.
This is no longer the case in OS/2. OS/2 is a multitasking system, and
one application can use the redirector at the same time that the real mode application is piggybacking the circuit. OS/2 must somehow interlock access to network virtual circuits so that multiple users of a network virtual circuit do not conflict.
2.2.3 Popular Function Compatibility
We've discussed some issues of binary compatibility, providing applications
the internal software interfaces they had in MS-DOS. This is because it is
vitally important that existing applications run correctly, unchanged,
under the new operating system.
5. An extremely high degree of compatibility is required for
virtually any application to run because a typical application
uses a great many documented and undocumented interfaces and
features of the earlier system. If any one of those interfaces
is not supplied, the application will not run correctly.
Consequently, we cannot provide 90 percent compatibility and
expect to run 90 percent of existing applications; 99.9 percent
compatibility is required for such a degree of success.
5 OS/2 also needs to provide functional
compatibility; it has to allow the creation of protect mode applications
that provide the functions that users grew to know and love in real mode
applications.
This can be difficult because many popular applications (for example,
"terminate and stay resident loadable helper" routines such as SideKick) were written for a single-tasking, unprotected environment without regard to the ease with which their function could be provided in a protected environment. For example, a popular application may implement some of its features by patching (that is, editing) MS-DOS itself. This cannot be allowed in OS/2 (the reason is discussed in Chapter 4), so OS/2 must provide alternative mechanisms for protect mode applications to provide services that users have grown to expect.
2.2.4 Downward Compatibility
So far, our discussion on compatibility has focused exclusively on upward
compatibility--old programs must run in the new system but not vice versa.
Downward compatibility--running new programs under MS-DOS--is also
important. Developers are reluctant to write OS/2-only applications until
OS/2 has achieved major penetration of the market, yet this very
unavailability of software slows such penetration. If it's possible to
write applications that take advantage of OS/2's protect mode yet also run
unchanged under MS-DOS version 3.x, ISVs (Independent Software Vendors) can
write their products for OS/2 without locking themselves out of the
existing MS-DOS market.
2.2.4.1 Family API
To provide downward compatibility for applications, OS/2 designers
integrated a Family
6.Family refers to the MS-DOS/OS/2 family of
operating systems.
6 Applications Program Interface (Family API) into the
OS/2 project. The Family API provides a standard execution environment
under MS-DOS version 3.x and OS/2. Using the Family API, a programmer can
create an application that uses a subset of OS/2 functions (but a superset
of MS-DOS version 3.x functions) and that runs in a binary compatible
fashion under MS-DOS version 3.x and OS/2. In effect, some OS/2 functions
can be retrofitted into an MS-DOS version 3.x environment by means of the
Family API.
2.2.4.2 Network Server-Client Compatibility
Another important form of upward and downward compatibility is the network
system. You can expect any OS/2 system to be on a network, communicating
not only with MS-DOS 3.x systems but, one day, with a new version of OS/2
as well. The network interface must be simultaneously upwardly and
downwardly compatible with all past and future versions of networking MS-
DOS.
3 The OS/2 Religion
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Religion, in the context of software design, is a body of beliefs about design rights and design wrongs. A particular design is praised or criticized on the basis of fact--it is small or large, fast or slow--and also on the basis of religion--it is good or bad, depending on how well it obeys the religious precepts. Purpose and consistency underlie the design religion as a whole; its influence is felt in every individual judgment.
The purpose of software design religion is to specify precepts that
designers can follow when selecting an approach from among the many possibilities before them. A project of the size and scope of OS/2 needed a carefully thought out religion because OS/2 will dramatically affect this and future generations of operating systems. It needed a strong religion for another reason: to ensure consistency among wide-ranging features implemented by a large team of programmers. Such consistency is very important; if one programmer optimizes design to do A well, at the expense of doing B less well, and another programmer--in the absence of religious guidance--does the opposite, the end result is a product that does neither A nor B well.
This chapter discusses the major architectural dogmas of the OS/2
religion: maximum flexibility, a stable environment, localization of errors, and the software tools approach.
3.1 Maximum Flexibility
The introduction to this book discusses the process of technological breakthroughs. I have pointed out that one of the easiest predictions about breakthroughs is that fully predicting their course is impossible. For example, the 8088 microprocessor is designed to address 1 MB of memory, but the IBM PC and compatible machines are designed so that addressable memory is limited to 640 KB. When this decision was made, 640 KB was ten times the memory that the then state-of-the-art 8080 machines could use; the initial PCs were going to ship with 16 KB in them, and it seemed to all concerned that 640 KB was overly generous. Yet it took only a few years before 640 KB became the typical memory complement of a machine, and within another year that amount of memory was viewed as pitifully small.
OS/2's design religion addresses the uncertain future by decreeing
that--to the extent compatible with other elements in the design religion-- OS/2 shall be as flexible as possible. The tenet of flexibility is that each component of OS/2 should be designed as if massive changes will occur in that area in a future release. In other words, the current component should be designed in a way that does not restrict new features and in a way that can be easily supported by a new version of OS/2, one that might differ dramatically in internal design.
Several general principles result from a design goal of flexibility.
All are intended to facilitate change, which is inevitable in the general yet unpredictable in the specific:
1. All OS/2 features should be sufficiently elemental (simple) that they can be easily supported in any future system, including systems fundamentally different in design from OS/2. Either the features themselves are this simple, or the features are built using base features that are this simple. The adjective simple doesn't particularly refer to externals--a small number of functions and options--but to internals. The internal operating system infrastructure necessary to provide a function should be either very simple or so fundamental to the nature of operating systems that it is inevitable in future releases. By way of analogy, as time travelers we may be able to guess very little about the twenty-first century, but we do know that people will still need to eat. The cuisine of the twenty-first century may be unguessable, but certainly future kitchens will contain facilities to cut and heat food. If we bring food that needs only those two operations, we'll find that even if there's nothing to our liking on the twenty-first-century standard menu the kitchen can still meet our needs. We've seen how important upward compatibility is for computer operating systems, so we can rest assured that the future MS-DOS "kitchen" will be happy to make the necessary effort to support old programs. All we have to do today is to ensure that such support is possible. Producing a compatible line of operating system releases means more than looking backward; it also means looking forward.
2. All system interfaces need to support expansion in a future release. For example, if a call queries the status of a disk file, then in addition to passing the operating system a pointer to a structure to fill in with the information, the application must also pass in the length of that structure. Although the current release of the operating system returns N bytes of information, a future release may support new kinds of disk files and may return M bytes of information. Because the application tells the operating system, via the buffer length parameter, which version of the information structure that the application understands (the old short version or the new longer version), the operating system can support both old programs and new programs simultaneously. In general, all system interfaces should be designed to support the current feature set without restraining the addition of features to the interfaces in future releases. Extra room should be left in count and flag arguments for future expansion, and all passed and returned structures need either to be self-sizing or to include a size argument. One more interface deserves special mention--the file system interface. Expanding the capabilities of the file system, such as allowing filenames longer than eight characters, is difficult because many old applications don't know how to process filenames that are longer than eight characters or they regard the longer names as illegal and reject them. OS/2 solves this and similar problems by specifying that all filenames supplied to or returned from the operating system be zero-terminated strings (ASCIIZ strings) of arbitrary length. Programmers are specifically cautioned against parsing or otherwise "understanding" filenames. Programs should consider file system pathnames as "magic cookies" to be passed to and from the operating system, but not to be parsed by the program. The details of this interface and other expandable interfaces are discussed in later chapters.
3. OS/2 needs to support the addition of functions at any time. The implementation details of these functions need to be hidden from the client applications so that those details can be changed at any time. Indeed, OS/2 should disguise even the source of a feature. Some APIs are serviced by kernel code, others are serviced by subroutine libraries, and still others may be serviced by other processes running in the system. Because a client application can't tell the difference, the system designers are free to change the implementation of an API as necessary. For example, an OS/2 kernel API might be considerably changed in a future release. The old API can continue to be supported by the creation of a subroutine library routine. This routine would take the old form of the API, convert it to the new form, call the OS/2 kernel, and then backconvert the result. Such a technique allows future versions of OS/2 to support new features while continuing to provide the old features to existing programs. These techniques are discussed in detail in Chapter 7, Dynamic Linking.
4. Finally, to provide maximum flexibility, the operating system should be extensible and expandable in a piecemeal fashion out in the field. In other words, a user should be able to add functions to the system--for example, a database engine--or to upgrade or replace system components--such as a new graphics display driver-- without a new release from Microsoft. A microcomputer design that allows third-party hardware additions and upgrades in the field is called an open system. The IBM PC line is a classic example of an open system. A design that contains no provisions for such enhancements is called a closed system. The earliest version of the Apple Macintosh is an example. At first glance, MS-DOS appears to be a closed software system because it contains no provisions for expansion. In practice, its unprotected environment makes MS- DOS the king of the open software systems because every application is free to patch the system and access the hardware as it sees fit. Keeping a software system open is as important as keeping a hardware system open. Because OS/2 is a protected operating system, explicit features, such as dynamic linking, are provided to allow system expansion by Microsoft, other software vendors, and users themselves. The topic of open systems is discussed more fully in Chapter 7.
3.2 A Stable Environment
An office automation operating system has to provide its users--the application programs and the human operator--with a stable environment. Every application should work the same way each time it's run; and each time an application is given the same data, it should produce the same result. The normal operation of one application should not affect any other application. Even a program error (bug) should not affect other programs in the system. Finally, if a program has bugs, the operating system should detect those bugs whenever possible and report them to the user. These certainly are obvious goals, but the nature of present-day computers makes them surprisingly difficult to achieve.
3.2.1 Memory Protection
Modern computers are based on the Von Neumann design--named after John Von
Neumann, the pioneering Hungarian-born American mathematician and computer
scientist. A Von Neumann computer consists of only two parts: a memory unit
and a processing unit. The memory unit contains both the data to be
operated on and the instructions (or program) that command the processing
unit. The processing unit reads instructions from memory; these
instructions may tell it to issue further reads to memory to retrieve data,
to operate on data retrieved earlier, or to store data back into
memory.
A Von Neumann computer does not distinguish between instructions and
data; both are stored in binary code in the computer's memory. Individual programs are responsible for keeping track of which memory locations hold instructions and which hold data, and each program uses the memory in a different way. Because the computer does not distinguish between instructions and data, a program may operate on its own instructions exactly as it operates on data. A program can read, modify, and write computer instructions at will. 1. This is a simplification. OS/2 and the 80286 CPU contain features that do distinguish somewhat between instructions and data and that limit the ability of programs to modify their own instructions. See 9.1 Protection Model for more information. 1
This is exactly what OS/2 does when it is commanded to run a program:
It reads the program into memory by treating it as data, and then it causes the data in those locations to be executed. It is even possible for a program to dynamically "reprogram" itself by manipulating its own instructions.
Computer programs are extremely complex, and errors in their logic can
cause the program to unintentionally modify data or instructions in memory. For example, a carelessly written program might contain a command buffer 80 bytes in size because it expects no commands longer than 80 bytes. If a user types a longer command, perhaps in error, and the program does not contain a special check for this circumstance, the program will overwrite the memory beyond the 80-byte command buffer, destroying the data or instructions placed there. 2. This is a simplified example. Rarely would a present-day, well-tested application contain such a naive error, but errors of this type--albeit in a much more complex form--exist in nearly all software. 2
In a single-tasking environment such as MS-DOS, only one application
runs at a time. An error such as our example could damage memory belonging to MS-DOS, the application, or memory that is not in use. In practice (due to memory layout conventions) MS-DOS is rarely damaged. An aberrant program typically damages itself or modifies memory not in use. In any case, the error goes undetected, the program produces an incorrect result, or the system crashes. In the last two cases, the user loses work, but it is clear which application is in error--the one executing at the time of the crash. (For completeness, I'll point out that it is possible for an aberrant application to damage MS-DOS subtly enough so that the application itself completes correctly, but the next time an application runs, it fails. This is rare, and the new application generally fails immediately upon startup; so after a few such episodes with different applications, the user generally identifies the true culprit.)
As we have seen, errors in programs are relatively well contained in a
single-tasking system. MS-DOS cannot, unfortunately, correct the error, nor can it very often detect the error (these tasks can be shown to be mathematically impossible, in the general case). But at least the errors are contained within the aberrant application; and should errors in data or logic become apparent, the user can identify the erring application. When we execute a second program in memory alongside the first, the situation becomes more complicated.
The first difficulty arises because the commonest error for a program
to make is to use memory that MS-DOS has not allocated to it. In a single- tasking environment these memory locations are typically unused, but in a multitasking environment the damaged location(s) probably belong to some other program. That program will then either give incorrect results, damage still other memory locations, crash, or some combination of these. In summary, a memory addressing error is more dangerous because there is more in memory to damage and that damage will have a more severe effect.
The second difficulty arises, not from explicit programming errors,
but from conflicts in the normal operation of two or more co-resident programs that are in some fashion incompatible. A simple example is called "hooking the keyboard vector" (see Figure 3-1).
BEFORE
Vector table Keyboard device ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ driver routine Device interrupt ³ ³ ÚÄÄÄÄÄÄÄÄ� x x x x: ÄÄÄÄÄÄ
ÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ÄÄÄÄÄÄ
ÀÄÄÄÄÄ� ³ x x x x ÃÄÄÄÄÄÄÄÄÙ ÄÄÄÄÄÄ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ ÄÄÄÄÄÄ ³ ³ ÄÄÄÄÄÄ ³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ
AFTER Application Keyboard device
Vector table edits table driver ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Ú� x x x x: ÄÄÄÄÄÄ Device interrupt ³ ³ ³ ³ ÄÄÄÄÄÄ
ÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ �ÄÄÙ ÀÄÄÄÄÄÄÄÄÄ¿ ÄÄÄÄÄÄ
ÀÄÄÄÄÄ� ³ y y y y ÃÄ¿ Application ³ ÄÄÄÄÄÄ ÃÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ routine ³ ÄÄÄÄÄÄ ³ ³ ÀÄ� y y y y: ÄÄÄÄÄÄ ³ ³ ³ ÄÄÄÄÄÄ ³ ³ ³ ÄÄÄÄÄÄ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ÄÄÄÄÄÄ ³ ÄÄÄÄÄÄ ³ jmp x x x x ÄÙ
Figure 3-1. Hooking the keyboard vector.
In this case, an application modifies certain MS-DOS memory locations
so that when a key is pressed the application code, instead of the MS-DOS code, is notified by the hardware. Applications do this because it allows them to examine certain keyboard events, such as pressing the shift key without pressing any other key, that MS-DOS does not pass on to applications which ask MS-DOS to read the keyboard for them. It works fine for one application to "hook" the keyboard vector; although hooking the keyboard vector modifies system memory locations that don't belong to the application, the application generally gets away with it successfully. In a multitasking environment, however, a second application may want to do the same trick, and the system probably won't function correctly. The result is that a stable environment requires memory protection. An application must not be allowed to modify, accidentally or deliberately, memory that isn't assigned to that application.
3.2.2 Side-Effects Protection
A stable environment requires more than memory protection; it also requires
that the system be designed so that the execution of one application
doesn't cause side effects for any other application. Side effects can be
catastrophic or they can be unremarkable, but in all cases they violate the
tenet of a stable environment.
For example, consider the practice of hooking the keyboard interrupt
vector. If one application uses this technique to intercept keystrokes, it will intercept all keystrokes, even those intended for some other application. The side effects in this case are catastrophic--the hooking application sees keystrokes that aren't intended for it, and the other applications don't get any keystrokes at all.
Side effects can plague programs even when they are using official
system features if those features are not carefully designed. For example, a mainframe operating system called TOPS-10 contains a program that supports command files similar to MS-DOS .BAT files, and it also contains a program that provides delayed offline execution of commands. Unfortunately, both programs use the same TOPS-10 facility to do their work. If you include a .BAT file in a delayed command list, the two programs will conflict, and the .BAT file will not execute.
OS/2 deals with side effects by virtualizing to the greatest extent
possible each application's operating environment. This means that OS/2 tries to make each application "see" a standard environment that is unaffected by changes in another application's environment. The effect is like that of a building of identical apartments. When each tenant moves in, he or she gets a standard environment, a duplicate of all the apartments. Each tenant can customize his or her environment, but doing so doesn't affect the other tenants or their environments.
Following are some examples of application environment issues that
OS/2 virtualizes.
þ Working Directories. Each application has a working (or current) directory for each disk drive. Under MS-DOS version 3.x, if a child process changes the working directory for drive C and then exits, the working directory for drive C remains changed when the parent process regains control. OS/2 eliminates this side effect by maintaining a separate list of working directories for each process in the system. Thus, when an application changes its working directories, the working directories of other applications in the system remain unchanged.
þ Memory Utilization. The simple act of memory consumption produces side effects. If one process consumes all available RAM, none is left for the others. The OS/2 memory management system uses memory overcommit (swapping) so that the memory needs of each application can be met.
þ Priority. OS/2 uses a priority-based scheduler to assign the CPU to the processes that need it. Applications can adjust their priority and that of their child processes as they see fit. However, the very priority of a task causes side effects. Consider a process that tells OS/2 that it must run at a higher priority than any other task in the system. If a second process makes the same request, a conflict occurs: Both processes cannot be the highest priority in the system. In general, the priority that a process wants for itself depends on the priorities of the other processes in the system. The OS/2 scheduler contains a sophisticated absolute/relative mechanism to deal with these conflicts.
þ File Utilization. As discussed earlier, one application may modify the files that another application is using, causing an unintended side effect. The OS/2 file-locking mechanism prevents unintended modifications, and the OS/2 record-locking mechanism coordinates intentional parallel updates to a single file.
þ Environment Strings. OS/2 retains the MS-DOS concept of environment strings: Each process has its own set. A child process inherits a copy of the parent's environment strings, but changing the strings in this copy will not affect the original strings in the parent's environment.
þ Keyboard Mode. OS/2 applications can place the keyboard in one of two modes--cooked or raw. These modes tell OS/2 whether the application wants to handle, for example, the backspace character (raw mode) or whether it wants OS/2 to handle the backspace character for it (cooked mode). The effect of these calls on subsequent keyboard read operations would cause side effects for other applications reading from the keyboard, so OS/2 maintains a record of the cooked/raw status of each application and silently switches the mode of the keyboard when an application issues a keyboard read request.
3.3 Localization of Errors
A key element in creating a stable environment is localizing errors. Humans always make errors, and human creations such as computer programs always contain errors. Before the development of computers, routine human errors were usually limited in scope. Unfortunately, as the saying goes, a computer can make a mistake in 60 seconds that it would take a whole office force a year to make. Although OS/2 can do little to prevent such errors, it needs to do its best to localize the errors.
Localizing errors consists of two activities: minimizing as much as
possible the impact of the error on other applications in the system, and maximizing the opportunity for the user to understand which of the many programs running in the computer caused the error. These two activities are interrelated in that the more successful the operating system is in restricting the damage to the domain of a single program, the easier it is for the user to know which program is at fault.
The most important aspect of error localization has already been
discussed at length--memory management and protection. Other error localization principles include the following:
þ No program can crash or hang the system. A fundamental element of the OS/2 design religion is that no application program can, accidentally or even deliberately, crash or hang the system. If a failing application could crash the system, obviously the system did not localize the error! Furthermore, the user would be unable to identify the responsible application because the entire system would be dead.
þ No program can make inoperable any screen group other than its own. As we'll see in later chapters of this book, sometimes design goals, design religions, or both conflict. For example, the precept of no side effects conflicts with the requirement of supporting keyboard macro expander applications. The sole purpose of such an application is to cause a side effect--specifically to translate certain keystroke sequences into other sequences. OS/2 resolves this conflict by allowing applications to examine and modify the flow of data to and from devices (see Chapter 16) but in a controlled fashion. Thus, an aberrant keyboard macro application that starts to "eat" all keys, passing none through to the application, can make its current screen group unusable, but it can't affect the user's ability to change screen groups. Note that keyboard monitors can intercept and consume any character or character sequence except for the keystrokes that OS/2 uses to switch screen groups (Ctrl-Esc and Alt-Esc). This is to prevent aberrant keyboard monitor applications from accidentally locking the user into his or her screen group by consuming and discarding the keyboard sequences that are used to switch from screen groups.
þ Applications cannot intercept general protection (GP) fault errors. A GP fault occurs when a program accesses invalid memory locations or accesses valid locations in an invalid way (such as writing into read-only memory areas). OS/2 always terminates the operation and displays a message for the user. A GP fault is evidence that the program's logic is incorrect, and therefore it cannot be expected to fix itself or trusted to notify the user of its ill health. The OS/2 design does allow almost any other error on the part of an application to be detected and handled by that application. For example, "Illegal filename" is an error caused by user input, not by the application. The application can deal with this error as it sees fit, perhaps correcting and retrying the operation. An error such as "Floppy disk drive not ready" is normally handled by OS/2 but can be handled by the application. This is useful for applications that are designed to operate unattended; they need to handle errors themselves rather than waiting for action to be taken by a nonexistent user.
3.4 Software Tools Approach
In Chapter 2 we discussed IPC and the desirability of having separate functions contained in separate programs. We discussed the flexibility of such an approach over the "one man band" approach of an all-in-one application. We also touched on the value of being able to upgrade the functionality of the system incrementally by replacing individual programs. All these issues are software tools issues.
Software tools refers to a design philosophy which says that
individual programs and applications should be like tools: Each should do one job and do it very well. A person who wants to turn screws and also drive nails should get a screwdriver and a hammer rather than a single tool that does neither job as well.
The tools approach is used routinely in nonsoftware environments and
is taken for granted. For example, inside a standard PC the hardware and electronics are isolated into functional components that communicate via interfaces. The power supply is in a box by itself; its interface is the line cord and some power connectors. The disk drives are separate from the rest of the electronics; they interface via more connectors. Each component is the equivalent of a software application: It does one job and does it well. When the disk drive needs power, it doesn't build in a power supply; it uses the standard interface to the power supply module--the power "specialist" in the system.
Occasionally, the software tools approach is criticized for being
inefficient. People may argue that space is wasted and time is lost by packaging key functions separately; if they are combined, the argument goes, nothing is wasted. This argument is correct in that some RAM and CPU time is spent on interface issues, but it ignores the gains involved in "sending out the work" to a specialist rather than doing it oneself. One could argue, for example, that if I built an all-in-one PC system I'd save money because I wouldn't have to buy connectors to plug everything together. I might also save a little by not having to buy buffer chips to drive signals over those connectors. But in doing so, I'd lose the advantage of being able to buy my power supply from a very high-volume and high-efficiency supplier--someone who can make a better, cheaper supply, even with the cost of connectors, than my computer company can.
Finally, the user gains from the modular approach. If you need more
disk capability, you can buy one and plug it in. You are not limited to one disk maker but rather can choose the one that's right for your needs-- expensive and powerful or cheap and modest. You can buy third-party hardware, such as plug-in cards, that the manufacturer of your computer doesn't make. All in all, the modest cost of a few connectors and driver chips is paid back manyfold, both in direct system costs (due to the efficiency of specialization) and in the additional capability and flexibility of the machine.
As I said earlier, the software tools approach is the software
equivalent of an open system. It's an important part of the OS/2 religion: Although the system doesn't require a modular tools approach from applications programs, it should do everything in its power to facilitate such systems, and it should itself be constructed in that fashion.
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Part II The Architecture
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
4 Multitasking
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
I have discussed the goals and compatibility issues that OS/2 is intended to meet, and I have described the design religion that was established for OS/2. The following chapters discuss individual design elements in some detail, emphasizing not only how the elements work and are used but the role they play in the system as a whole.
In a multitasking operating system, two or more programs can execute
at the same time. Some benefits of such a feature are obvious: You (the user) can switch between several application programs without saving work and exiting one program to start another. When the telephone rings, for example, you can switch from the word processor application you are using to write a memo and go to the application that is managing your appointment calendar or to the spreadsheet application that contains the figures that are necessary to answer your caller's query.
This type of multitasking is similar to what people do when they're
not working with a computer. You may leave a report half read on your desk to address a more pressing need, such as answering the phone. Later, perhaps after other tasks intervene, you return to the report. You don't terminate a project and return your reference materials to the bookshelf, the files, and the library to answer the telephone; you merely switch your attention for a while and later pick up where you left off.
This kind of multitasking is called serial multitasking because
actions are performed one at a time. Although you probably haven't thought of it this way, you've spent much of your life serially multitasking. Every day when you leave for work, you suspend your home life and resume your work life. That evening, you reverse the process. You serially multitask a hobby--each time picking it up where you left off last time and then leaving off again. Reading the comics in a daily newspaper is a prodigious feat of human serial multitasking--you switch from one to another of perhaps 20 strips, remembering for each what has gone on before and then waiting until tomorrow for the next installment. Although serial multitasking is very useful, it is not nearly as useful as full multitasking--the kind of multitasking built into OS/2.
Full multitasking on the computer involves doing more than one thing--
running more than one application--at the same time. Humans do a little of this, but not too much. People commonly talk while they drive cars, eat while watching television, and walk while chewing gum. None of these activities requires one's full concentration though. Humans generally can't fully multitask activities that require a significant amount of concentration because they have only one brain.
For that matter, a personal computer has only one "brain"--one CPU.
1. Although multiple-CPU computers are well known, personal computers with multiple CPUs are uncommon. In any case, this discussion applies, with the obvious extensions, to multiple-CPU systems. 1 But OS/2 can switch this CPU from one activity to another very rapidly-- dozens or even hundreds of times a second. All executing programs seem to be running at the same time, at least on the human scale of time. For example, if five programs are running and each in turn gets 0.01 second of CPU time (that is, 10 milliseconds), in 1 second each program receives 20 time slices. To most observers, human or other computer software, all five programs appear to be running simultaneously but each at one-fifth its maximum speed. We'll return to the topic of time slicing later; for now, it's easiest--and, as we shall see, best--to pretend that all executing programs run simultaneously.
The full multitasking capabilities of OS/2 allow the personal computer
to act as more than a mere engine to run applications; the personal computer can now be a system of services. The user can interact with a spreadsheet program, for example, while a mail application is receiving network messages that the user can read later. At the same time, other programs may be downloading data from a mainframe computer or spooling output to a printer or a plotter. The user may have explicitly initiated some of these activities; a program may have initiated others. Regardless, they all execute simultaneously, and they all do their work without requiring the user's attention or intervention.
Full multitasking is useful to programs themselves. Earlier, we
discussed the advantages of a tools approach--writing programs so that they can offer their services to other programs. The numerous advantages of this technique are possible only because of full multitasking. For example, if a program is to be able to invoke another program to sort a data file, the sort program must execute at the same time as its client program. It wouldn't be very useful if the client program had to terminate in order for the sort program to run.
Finally, full multitasking is useful within a program itself. A thread
is an OS/2 mechanism that allows more than one path of execution through a particular application. (Threads are discussed in detail later; for now it will suffice to imagine that several CPUs can be made to execute the same program simultaneously.) This allows individual applications to perform more than one task at a time. For example, if the user tells a spreadsheet program to recalculate a large budget analysis, the program can use one thread to do the calculating and another to prompt for, read, and obey the user's next command. In effect, multiple operations overlap during execution and thereby increase the program's responsiveness to the user.
OS/2 uses a time-sliced, priority-based preemptive scheduler to
provide full multitasking. In other words, the OS/2 scheduler preempts-- takes away--the CPU from one application at any time the scheduler desires and assigns the CPU another application. Programs don't surrender the CPU when they feel like it; OS/2 preempts it. Each program in the system (more precisely, each thread in the system) has its own priority. When a thread of a higher priority than the one currently running wants to run, the scheduler preempts the running thread in favor of the higher priority one. If two or more runnable threads have the same highest priority, OS/2 runs each in turn for a fraction of a second--a time slice.
The OS/2 scheduler does not periodically look around to see if the
highest priority thread is running. Such an approach wastes CPU time and slows response time because a higher priority thread must wait to run until the next scheduler scan. Instead, other parts of the system call the scheduler when they think that a thread other than the one running should be executed.
4.1 Subtask Model
The terms task and process are used interchangeably to describe the direct result of executing a binary (.EXE) file. A process is the unit of ownership under OS/2, and processes own resources such as memory, open files, connections to dynlink libraries, and semaphores. Casual users would call a process a "program"; and, in fact, under MS-DOS all programs and applications consist of a single process. OS/2 uses the terms task or process because a single application program under OS/2 may consist of more than one process. This section describes how this is done.
First, some more terminology. When a process creates, or execs,
another process, the creator process is called the parent process, and the created process is called the child process. The parent of the parent is the child's grandparent and so on. As with people, each process in the system has or had a parent. 2. Obviously, during boot-up OS/2 creates an initial parentless process by "magic," but this is ancient history by the time any application may run, so the anomaly may be safely ignored. 2 Although we use genealogical terms to describe task relationships, a child task, or process, is more like an agent or employee of the parent task. Employees are hired to do work for an employer. The employer provides a workplace and access to the information employees need to do their jobs. The same is generally true for a child task. When a child task is created, it inherits (or receives a copy of) a great deal of the parent task's environment. For example, it inherits, or takes on, the parent's base scheduling priority and its screen group. The term inherit is a little inappropriate because the parent task has not died. It is alive and well, going about its business.
The most important items a child task inherits are its parent's open
file handles. OS/2 uses a handle mechanism to perform file I/O, as do MS- DOS versions 2.0 and later. When a file is opened, OS/2 returns a handle-- an integer value--to the process. When a program wants to read from or write to a file, it gives OS/2 the file handle. Handles are not identical among processes. For example, the file referred to by handle 6 of one process bears no relationship to the file referred to by another process's handle 6, unless one of those processes is a child of the other. When a parent process creates a child process, the child process, by default, inherits each of the parent's open file handles. For example, a parent process has the file \WORK\TEMPFILE open on handle 5; when the child process starts up, handle 5 is open and references the \WORK\TEMPFILE file.
This undoubtedly seems brain damaged if you are unfamiliar with this
model. Why is it done in this crazy way? What use does the child process have for these open files? What's to keep the child from mucking up the parent's files? All this becomes clearer when the other piece of the puzzle is in place--the standard file handles.
4.1.1 Standard File Handles
Many OS/2 functions use 16-bit integer values called handles for their
interfaces. A handle is an object that programmers call a "magic cookie"--
an arbitrary value that OS/2 provides the application so that the
application can pass the value back to OS/2 on subsequent calls. Its
purpose is to simplify the OS/2 interface and speed up the particular
service. For example, when a program creates a system semaphore, it is
returned a semaphore handle--a magic cookie--that it uses for subsequent
request and release operations. Referring to the semaphore via a 16-bit
value is much faster than passing around a long filename. Furthermore, the
magic in magic cookie is that the meaning of the 16-bit handle value is
indecipherable to the application. OS/2 created the value, and it has
meaning only to OS/2; the application need only retain the value and
regurgitate it when appropriate. An application can never make any
assumptions about the values of a magic cookie.
File handles are an exceptional form of handle because they are not
magic cookies. The handle value, in the right circumstances, is meaningful to the application and to the system as a whole. Specifically, three handle values have special meaning: handle value 0, called STDIN (for standard input); handle value 1, called STDOUT (standard output); and handle value 2, called STDERR (standard error). A simple program--let's call it NUMADD-- will help to explain the use of these three handles. NUMADD will read two lines of ASCII text (each containing a decimal number), convert the numbers to binary, add them, and then convert the results to an ASCII string and write out the result. Note that we're confining our attention to a simple non-screen-oriented program that might be used as a tool, either directly by a programmer or by another program (see Figure 4-1 and Listing 4-1).
STDIN ÚÄÄÄÄÄÄÄÄÄÄ¿
123 ÄÄÄ¿ ³ ³ STDOUT 14 ÀÄÄÄ�³ NUMADD ÃÄÄÄ¿
³ ³ ÀÄÄÄÄ� 137 ÀÄÄÄÄÄÄÄÄÄÄÙ
Figure 4-1. Program NUMADD operation--interactive.
- include <stdio.h> /* defines stdin and stdout */
main() {
int value1, value2, sum;
fscanf (stdin, "%d", &value1); fscanf (stdin, "%d", & value2); sum = value1 + value2; fprintf (stdout, "%d\n", sum);
}
Listing 4-1. Program NUMADD.
By convention, all OS/2 programs read input from STDIN and write
output to STDOUT. Any error messages are written to STDERR. The program itself does not open these handles; it inherits them from the parent process. The parent may have opened them itself or inherited them from its own parent. As you can see, NUMADD would not contain DosOpen calls; instead, it would start immediately issuing fscanf calls on handle 0 (STDIN), which in turn issues DosRead calls, and, when ready, directly issue fprintf calls to handle 1 (STDOUT), which in turn issues DosWrites.
Figure 4-2 and Listing 4-2 show a hypothetical application, NUMARITH.
NUMARITH reads three text lines. The first line contains an operation character, such as a plus (+) or a minus (-); the second and third lines contain the values to be operated upon. The author of this program doesn't want to reinvent the wheel; so when the program NUMARITH encounters a + operation, it executes NUMADD to do the work. As shown, the parent process NUMARITH has its STDIN connected to the keyboard and its STDOUT connected to the screen device drivers. 3. CMD.EXE inherited these handles from its own parent. This process is discussed later. 3 When NUMADD executes, it reads input from the keyboard via STDIN. After the user types the two numbers, NUMADD displays the result on the screen via STDOUT. NUMARITH has invoked NUMADD to do some work for it, and NUMADD has silently and seamlessly acted as a part of NUMARITH. The employee metaphor fits well here. NUMADD acted as an employee of NUMARITH, making use of NUMARITH's I/O streams, and as a result the contribution of the NUMADD employee to the NUMARITH company is seamless.
keyboard screen
³ STDIN ÚÄÄÄÄÄÄÄÄÄÄ¿ STDOUT ³ ÀÄÄÄÄÄÄÄ¿ ³ ³ ÚÄÄÄÄÄÄÄÄÙ ÃÄÄÄÄÄ�³ NUMARITH ÃÄÄ�ÄÄ´ � ³ ³ � ³ ÀÄÄÄÄÄÂÄÄÄÄÙ ³ ³ ³ DosExec ³ inherits ³ ÚÄÄÄÄÄ�ÄÄÄÄ¿ ³ inherits ³ ³ ³ ³ ÀÄÄÄÄÄ�³ NUMADD ÃÄÄ�ÄÄÙ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÙ
Figure 4-2. Program NUMARITH operation--interactive.
- include <stdio.h>
/** Numarith - Perform ASCII Arithmetic
- Numarith reads line triplets:
- operation
- value1
- value2
- performs the specified operation (+, -, *, /) on
- the two values and prints the result on stdout.
- /
main() {
char operation;
fscanf (stdin, "%c", &operation);
switch (operation) {
case '+': execl ("numadd", 0); break;
case '-': execl ("numsub", 0); break;
case '*': execl ("nummul", 0); break;
case '/': execl ("numdiv", 0); break; }
}
Listing 4-2. Program NUMARITH.
Figure 4-3 shows a similar situation. In this case, however,
NUMARITH's STDIN and STDOUT handles are open on two files, which we'll call DATAIN and DATAOUT. Once again, NUMADD does its work seamlessly. The input numbers are read from the command file on STDIN, and the output is properly intermingled in the log file on STDOUT. The key here is that this NUMADD is exactly the same program that ran in Listing 4-2; NUMADD contains no special code to deal with this changed situation. In both examples, NUMADD simply reads from STDIN and writes to STDOUT; NUMADD neither knows nor cares where those handles point. Exactly the same is true for the parent. NUMARITH doesn't know and doesn't care that it's working from files instead of from the screen; it simply uses the STDIN and STDOUT handles that it inherited from its parent.
File data in File data out
___ ___ / \ / \ ³\ ___ /³ ³\ ___ /³ ³ ³ ³ ³ ³ ÃÄÄ¿ ÚÄ�³ ³ ³ ³ ³ STDIN ÚÄÄÄÄÄÄÄÄÄÄ¿ STDOUT ³ ³ ³ \ ___ / ÀÄÄÄÄ¿ ³ ³ ÚÄÄÄÄÄÙ \ ___ / ÃÄÄÄÄÄ�³ NUMARITH ÃÄÄ�ÄÄ´ � ³ ³ � ³ ÀÄÄÄÄÄÂÄÄÄÄÙ ³ ³ ³ DosExec ³ inherits ³ ÚÄÄÄÄÄ�ÄÄÄÄ¿ ³ inherits ³ ³ ³ ³ ÀÄÄÄÄÄ�³ NUMADD ÃÄÄ�ÄÄÙ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÙ
Figure 4-3. Program NUMARITH operation--from files.
This is the single most important concept in the relationship and
inheritance structure between processes. The reason a process inherits so much from its parent is so that the parent can set up the tool's environment--make it read from the parent's STDIN or from a file or from an anonymous pipe (see 4.1.2 Anonymous Pipes). This gives the parent the flexibility to use a tool program as it wishes, and it frees the tool's author from the need to be "all things to all people."
Of equal importance, the inheritance architecture provides
nesting encapsulation of child processes. NUMARITH's parent process doesn't know and doesn't need to know how NUMARITH does its job. NUMARITH can do the additions itself, or it can invoke NUMADD as a child, but the architecture encapsulates the details of NUMARITH's operation so that NUMADD's involvement is hidden from NUMARITH's parent. Likewise, the decision of NUMARITH's parent to work from a file or from a device or from a pipe is encapsulated (that is, hidden) from NUMARITH and from any child processes that NUMARITH may execute to help with its work. Obviously, this architecture can be extended arbitrarily: NUMADD can itself execute a child process to help NUMADD with its work, and this would silently and invisibly work. Neither NUMARITH nor its parent would know or need to know anything about how NUMADD was doing its work. Other versions can replace any of these applications at any time. The new versions can invoke more or fewer child processes or be changed in any other way, and their client (that is, parent) processes are unaffected. The architecture of OS/2 is tool-based; as long as the function of a tool remains constant (or is supersetted), its implementation is irrelevant and can be changed arbitrarily.
The STDIN, STDOUT, and STDERR architecture applies to all programs,
even those that only use VIO, KBD, or the presentation manager and that never issue operations on these handles. See Chapter 14, Interactive Programs.
4.1.2 Anonymous Pipes
NUMADD and NUMARITH are pretty silly little programs; a moment's
consideration will show how the inheritance architecture applies to more
realistic programs. An example is the TREE program that runs when the TREE
command is given to CMD.EXE. TREE inherits the STDIN and STDOUT handles,
but it does not use STDIN; it merely writes output to STDOUT. As a result,
when the user types TREE at a CMD.EXE prompt, the output appears on the
screen. When TREE appears in a batch file, the output appears in the LOG
file or on the screen, depending on where STDOUT is pointing.
This is all very useful, but what if an application wants to further
process the output of the child program rather than having the child's output intermingled with the application's output? OS/2 does this with anonymous pipes. The adjective anonymous distinguishes these pipes from a related facility, named pipes, which are not implemented in OS/2 version 1.0.
An anonymous pipe is a data storage buffer that OS/2 maintains. When a
process opens an anonymous pipe, it receives two file handles--one for writing and one for reading. Data can be written to the write handle via the DosWrite call and then read back via the read handle and the DosRead call. An anonymous pipe is similar to a file in that it is written and read via file handle operations, but an anonymous pipe and a file are significantly different. Pipe data is stored only in RAM buffers, not on a disk, and is accessed only in FIFO (First In First Out) fashion. The DosChgFilePtr operation is illegal on pipe handles.
An anonymous pipe is of little value to a single process, since it
acts as a simple FIFO (First In First Out) storage buffer of limited size and since the data has to be copied to and from OS/2's pipe buffers when DosWrites and DosReads are done. What makes an anonymous pipe valuable is that child processes inherit file handles. A parent process can create an anonymous pipe and then create a child process, and the child process inherits the anonymous pipe handles. The child process can then write to the pipe's write handle, and the parent process can read the data via the pipe's read handle. Once we add the DosDupHandle function, which allows handles to be renumbered, and the standard file handles (STDIN, STDOUT, and STDERR), we have the makings of a powerful capability.
Let's go back to our NUMARITH and NUMADD programs. Suppose NUMARITH
wants to use NUMADD's services but that NUMARITH wants to process NUMADD's results itself rather than having them appear in NUMARITH's output. Furthermore, assume that NUMARITH doesn't want NUMADD to read its arguments from NUMARITH's input file; NUMARITH wants to supply NUMADD's arguments itself. NUMARITH can do this by following these steps:
1. Create two anonymous pipes.
2. Preserve the item pointed to by the current STDIN and STDOUT handles (the item can be a file, a device, or a pipe) by using DosDupHandle to provide a duplicate handle. The handle numbers of the duplicates may be any number as long as it is not the number of STDIN, STDOUT, or STDERR. We know that this is the case because DosDupHandle assigns a handle number that is not in use, and the standard handle numbers are always in use.
3. Close STDIN and STDOUT via DosClose. Whatever object is "on the other end" of the handle is undisturbed because the application still has the object open on another handle.
4. Use DosDupHandle to make the STDIN handle a duplicate of one of the pipe's input handles, and use DosDupHandle to make the STDOUT handle a duplicate of the other pipe's output handle.
5. Create the child process via DosExecPgm.
6. Close the STDIN and STDOUT handles that point to the pipes, and use DosDupHandle and DosClose to effectively rename the objects originally described by STDIN and STDOUT back to those handles.
The result of this operation is shown in Figure 4-4. NUMADD's STDIN
and STDOUT handles are pointing to two anonymous pipes, and the parent process is holding the other end of those pipes. The parent process used DosDupHandle and DosClose to effectively "rename" the STDIN and STDOUT handles temporarily so that the child process can inherit the pipe handles rather than its parent's STDIN and STDOUT. At this point the parent, NUMARITH, can write input values into the pipe connected to NUMADD's STDIN and read NUMADD's output from the pipe connected to NUMADD's STDOUT.
STDIN ÚÄÄÄÄÄÄÄÄÄÄ¿ STDOUT ÄÄÄÄÄÄÄÄÄ¿ ³ ³ ÚÄÄÄÄÄÄÄÄÄ ÀÄÄÄÄÄ�³ NUMARITH ÃÄÄ�ÄÄÙ ÚÄÄÄÄÄÄ´ ³�ÄÄÄÄÄÄÄ¿ ³ ÀÄÄÄÄÄÂÄÄÄÄÙ ³
anonymous ³ ³ DosExecPgm ³ anonymous
pipe ³ ÚÄÄÄÄÄ�ÄÄÄÄ¿ ³ pipe ³ ³ ³ ³ ÀÄÄÄÄÄ�³ NUMADD ÃÄÄÄÄÄÄÄÄÙ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÙ
Figure 4-4. Invoking NUMADD via an anonymous pipe.
If you compare Figure 4-4 with Listing 4-2, Figure 4-2, and Figure
4-3, you see another key feature of this architecture: NUMADD has no special code or design to allow it to communicate directly with NUMARITH. NUMADD functions correctly whether working from the keyboard and screen, from data files, or as a tool for another program. The architecture is fully recursive: If NUMADD invokes a child process to help it with its work, everything still functions correctly. Whatever mechanism NUMADD uses to interact with its child/tool program is invisible to NUMARITH. Likewise, if another program uses NUMARITH as a tool, that program is not affected by whatever mechanism NUMARITH uses to do its work.
This example contains one more important point. Earlier I said that a
process uses DosRead and DosWrite to do I/O over a pipe, yet our NUMADD program uses fscanf and fprintf, two C language library routines. fscanf and fprintf themselves call DosRead and DosWrite, and because a pipe handle is indistinguishable from a file handle or a device handle for these operations, not only does NUMADD work unchanged with pipes, but the library subroutines that it calls work as well. This is another example of the principle of encapsulation as expressed in OS/2, 4. And in UNIX, from which this aspect of the architecture was adapted. 4 in which the differences among pipes, files, and devices are hidden behind, or encapsulated in, a standardized handle interface.
4.1.3 Details, Details
While presenting the "big picture" of the OS/2 tasking and tool
architecture, I omitted various important details. This section discusses
them, in no particular order.
STDERR (handle value 2) is an output handle on which error messages
are written. STDERR is necessary because STDOUT is a program's normal output stream. For example, suppose a user types:
DIR filename >logfile
where the file filename does not exist. If STDERR did not exist as a separate entity, no error message would appear, and logfile would apparently be created. Later, when the user attempted to examine the contents of the file, he or she would see the following message:
FILE NOT FOUND
For this reason, STDERR generally points to the display screen, and applications rarely redirect it.
The special meanings of STDIN, STDOUT, and STDERR are not hard coded
into the OS/2 kernel; they are system conventions. All programs should follow these conventions at all times to preserve the flexibility and utility of OS/2's tool-based architecture. Even programs that don't do handle I/O on the STD handles must still follow the architectural conventions (see Chapter 14, Interactive Programs). However, the OS/2 kernel code takes no special action nor does it contain any special cases in support of this convention. Various OS/2 system utilities, such as CMD.EXE and the presentation manager, do contain code in support of this convention.
I mentioned that a child process inherits all its parent's file
handles unless the parent has explicitly marked a handle "no inherit." The use of STDIN, STDOUT, and STDERR in an inherited environment has been discussed, but what of the other handles?
Although a child process inherits all of its parent's file handles, it
is usually interested only in STDIN, STDOUT, and STDERR. What happens to the other handles? Generally, nothing. Only handle values 0 (STDIN), 1 (STDOUT), and 2 (STDERR) have explicit meaning. All other file handles are merely magic cookies that OS/2 returns for use in subsequent I/O calls. OS/2 doesn't guarantee any particular range or sequence of handle values, and applications should never use or rely on explicit handle values other than the STD ones.
Thus, for example, if a parent process has a file open on handle N and
the child process inherits that handle, little happens. The child process won't get the value N back as a result of a DosOpen because the handle is already in use. The child will never issue operations to handle N because it didn't open any such handle and knows nothing of its existence. Two side effects can result from inheriting "garbage" handles. One is that the object to which the handle points cannot be closed until both the parent and the child close their handles. Because the child knows nothing of the handle, it won't close it. Therefore, a handle close issued by a parent won't be effective until the child and all of that child's descendant processes (which in turn inherited the handle) have terminated. If another application needs the file or device, it is unavailable because a child process is unwittingly holding it open.
The other side effect is that each garbage handle consumes an entry in
the child's handle space. Although you can easily increase the default maximum of 20 open handles, a child process that intends to open only 10 files wouldn't request such an increase. If a parent process allows the child to inherit 12 open files, the child will run out of available open file handles. Writing programs that always raise their file handle limit is not good practice because the garbage handles are extra overhead and the files-held-open problem remains. Instead, parent programs should minimize the number of garbage handles they allow child processes to inherit.
Each time a program opens a file, it should do so with the DosOpen
request with the "don't inherit" bit set if the file is of no specific interest to any child programs. If the bit is not set at open time, it can be set later via DosSetFHandState. The bit is per-handle, not per-file; so if a process has two handles open on the same file, it can allow one but not the other to be inherited. Don't omit this step simply because you don't plan to run any child processes; unbeknownst to you, dynlink library routines may run child programs on your behalf. Likewise, in dynlink programs the "no inherit" bit should be set when file opens are issued because the client program may create child processes.
Finally, do not follow the standard UNIX practice of blindly closing
file handles 3 through 20 during program initialization. Dynlink subsystems are called to initialize themselves for a new client before control is passed to the application itself. If subsystems have opened files during that initialization, a blanket close operation will close those files and cause the dynlink package to fail. All programs use dynlink subsystems, whether they realize it or not, because both the OS/2 interface package and the presentation manager are such subsystems. Accidentally closing a subsystem's file handles can cause bizarre and inexplicable problems. For example, when the VIO subsystem is initialized, it opens a handle to the screen device. A program that doesn't call VIO may believe that closing this handle is safe, but it's not. If a handle write is done to STDOUT when STDOUT points to the screen device, OS/2 calls VIO on behalf of the application--with potentially disastrous effect.
I discussed how a child process, inheriting its parent's STDIN and
STDOUT, extracts its input from the parent's input stream and intermingles its output in the parent's output stream. What keeps the parent process from rereading the input consumed by the child, and what keeps the parent from overwriting the output data written by the child? The answer is in the distinction between duplicated or inherited handles to a file and two handles to the same file that are the result of two separate opens.
Each time a file is opened, OS/2 allocates a handle to that process
and makes an entry in the process's handle table. This entry then points to the System File Table (SFT) inside OS/2. The SFT contains the seek pointer to the file--the spot in the file that is currently being read from or written to. When a handle is inherited or duplicated, the new handle points to the same SFT entry as the original. Thus, for example, the child's STDIN handle shares the same seek pointer as the parent's STDIN handle. When our example child program NUMADD read two lines from STDIN, it advanced the seek pointer of its own STDIN and that of its parent's STDIN (and perhaps that of its grandparent's STDIN and so forth). Likewise, when the child writes to STDOUT, the seek pointer advances on STDOUT so that subsequent writing by the parent appends to the child's output rather than overwriting it.
This mechanism has two important ramifications. First, in a situation
such as our NUMARITH and NUMADD example, the parent process must refrain from I/O to the STD handles until the child process has completed so that the input or output data doesn't intermingle. Second, the processes must be careful in the way they buffer data to and from the STD handles.
Most programs that read data from STDIN do so until they encounter an
EOF (End Of File). These programs can buffer STDIN as they wish. A program such as NUMARITH, in which child processes read some but not all of its STDIN data, cannot use buffering because the read-ahead data in the buffer might be the data that the child process was to read. NUMARITH can't "put the data back" by backing up the STDIN seek pointer because STDIN might be pointing to a device (such as the keyboard) or to a pipe that cannot be seeked. Likewise, because NUMADD was designed to read only two lines of input, it also must read STDIN a character at a time to be sure it doesn't "overshoot" its two lines.
Programs must also be careful about buffering STDOUT. In general, they
can buffer STDOUT as they wish, but they must be sure to flush out any buffered data before they execute any child processes that might write to STDOUT.
Finally, what if a parent process doesn't want a child process to
inherit STDIN, STDOUT, or STDERR? The parent process should not mark those handles "no inherit" because then those handles will not be open when the child process starts. The OS/2 kernel has no built-in recognition of the STD file handles; so if the child process does a DosOpen and handle 1 is unopened (because the process's parent set "no inherit" on handle 1), OS/2 might open the new file on handle 1. As a result, output that the child process intends for STDOUT appears in the other file that unluckily was assigned handle number 1.
If for some reason a child process should not inherit a STD handle,
the parent should use the DosDupHandle/rename technique to cause that handle to point to the NULL device. You do this by opening a handle on the NULL device and then moving that handle to 0, 1, or 2 with DosDupHandle. This technique guarantees that the child's STD handles will all be open.
The subject of STDIN, STDOUT, and handle inheritance comes up again in
Chapter 14, Interactive Programs.
4.2 PIDs and Command Subtrees
The PID (process identification) is a unique code that OS/2 assigns to each process when it is created. The number is a magic cookie. Its value has no significance to any process; it's simply a name for a process. The PID may be any value except 0. A single PID value is never assigned to two processes at the same time. PID values are reused but not "rapidly." You can safely remember a child's PID and then later attempt to affect that child by using the PID in an OS/2 call. Even if the child process has died unexpectedly, approximately 65,000 processes would have to be created before the PID value might be reassigned; even a very active system takes at least a day to create, execute, and terminate that many processes.
I've discussed at some length the utility of an architecture in which
child programs can create children and those children can create grandchildren and so on. I've also emphasized that the parent need not know the architecture of a child process--whether the child process creates children and grandchildren of its own. The parent need not know and indeed should not know because such information may make the parent dependent on a particular implementation of a child or grandchild program, an implementation that might change. Given that a parent process starting up a child process can't tell if that child creates its own descendants, how can the parent process ask the system if the work that the child was to do has been completed? The system could tell the parent whether the child process is still alive, but this is insufficient. The child process may have farmed out all its work to one or more grandchildren and then terminated itself before the actual work was started. Furthermore, the parent process may want to change the priority of the process(es) that it has created or even terminate them because an error was detected or because the user typed Ctrl-C.
All these needs are met with a concept called the command subtree.
When a child process is created, its parent is told the child's PID. The PID is the name of the single child process, and when taken as a command subtree ID, this PID is the name of the entire tree of descendant processes of which the child is the root. In other words, when used as a command subtree ID, the PID refers not only to the child process but also to any of its children and to any children that they may have and so on. A single command subtree can conceivably contain dozens of processes (see Figure 4-5 and Figure 4-6).
ÚÄÄÄÄÄÄÄÄÄÄÄ¿ ³ PARENT ³ ÀÄÄÄÄÄÂÄÄÄÄÄÙ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ³ A ³ ÀÄÄÂÄÄÄÄÄÂÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄ¿
ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ³ B ³ ³ C ³ ÀÄÄÄÄÄÂÄÄÄÄÄÙ ÀÄÄÂÄÄÄÄÄÂÄÄÙ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄ¿ ³ D ³ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÀÄÄÄÄÄÂÄÄÄÄÄÙ ³ F ³ ³ G ³ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÂÄÄÄÄÄÂÄÄÙ ³ E ³ ÚÄÄÄÄÄÙ ÀÄÄÄÄÄ¿ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÚÄÄÄÄÄÁÄÄÄÄÄ¿
³ H ³ ³ I ³ ÀÄÄÄÄÄÂÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ³ J ³ ÀÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 4-5. Command subtree. A is the root of (one of) the parent's command subtrees. B and C are the root of two of A's subtrees and so on.
ÚÄÄÄÄÄÄÄÄÄÄÄ¿ ³ PARENT ³ ÀÄÄÄÄÄÂÄÄÄÄÄÙ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ³°°°°°A°°°°°³ ÀÄÄÂÄÄÄÄÄÂÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄ¿
ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ³ B ³ ³ C ³ ÀÄÄÄÄÄÂÄÄÄÄÄÙ ÀÄÄÂÄÄÄÄÄÂÄÄÙ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄ¿ ³ D ³ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÀÄÄÄÄÄÂÄÄÄÄÄÙ ³ F ³ ³ G ³ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÂÄÄÄÄÄÂÄÄÙ ³°°°°°E°°°°°³ ÚÄÄÄÄÄÙ ÀÄÄÄÄÄ¿ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ÚÄÄÄÄÄÁÄÄÄÄÄ¿
³°°°°°H°°°°°³ ³ I ³ ÀÄÄÄÄÄÂÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÁÄÄÄÄÄ¿ ³ J ³ ÀÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 4-6. Command subtree. The shaded processes have died, but the subtrees remain. PARENT can still use subtree A to affect all remaining subprocesses. Likewise, an operation by C on subtree G will affect process J.
Some OS/2 functions, such as DosCWait and DosKillProcess, can take
either PID or command subtree values, depending on the subfunction requested. When the PID form is used, only the named process is affected. When the command subtree form is used, the named process and all its descendants are affected. This is true even if the child process no longer exists or if the family tree of processes contains holes as a result of process terminations.
No statute of limitations applies to the use of the command subtree
form. That is, even if child process X died a long time ago, OS/2 still allows references to the command subtree X. Consequently, OS/2 places one simple restriction on the use of command subtrees so that it isn't forced to keep around a complete process history forever: Only the direct parent of process X can reference the command subtree X. In other words, X's grandparent process can't learn X's PID from its parent and then issue command subtree forms of commands; only X's direct parent can. This puts an upper limit on the amount and duration of command subtree information that OS/2 must retain; when a process terminates, information pertaining to its command subtrees can be discarded. The command subtree concept is recursive. OS/2 discards information about the terminated process's own command subtrees, but if any of its descendant processes still exist, the command subtree information pertaining to their child processes is retained. And those surviving descendants are still part of the command subtree belonging to the terminated process's parent process. 5. Assuming that the parent process itself still exists. In any case, all processes are part of a nested set of command subtrees belonging to all its surviving ancestor processes. 5
4.3 DosExecPgm
To execute a child process, you use the DosExecPgm call. The form of the call is shown in Listing 4-3.
extern unsigned far pascal DOSEXECPGM (
char far *OBJNAMEBUF, unsigned OBJNAMEBUFL, unsigned EXECTYPE, char far *ARGSTRING, char far *ENVSTRING, struct ResultCodes far *CODEPID, char far *PGMNAME);
Listing 4-3. DosExecPgm call.
The obj namebuf arguments provide an area where OS/2 can return a
character string if the DosExecPgm function fails. In MS-DOS version 2.0 and later, the EXEC function was quite simple: It loaded a file into memory. Little could go wrong: file not found, file bad format, insufficient memory, to name some possibilities. A simple error code sufficed to diagnose problems. OS/2's dynamic linking facility is much more complicated. The earliest prototype versions of OS/2 were missing these obj namebuf arguments, and engineers were quickly frustrated by the error code "dynlink library load failed." "Which library was it? The application references seven of them! But all seven of those libraries are alive and well. Perhaps it was a library that one of those libraries referenced. But which libraries do they reference? Gee, I dunno..." For this reason, the object name arguments were added. The buffer is a place where OS/2 returns the name of the missing or defective library or other load object, and the length argument tells OS/2 the maximum size of the buffer area. Strings that will not fit in the area are truncated.
The exectype word describes the form of the DosExecPgm. The values are
as follows.
0: Execute the child program synchronously. The thread issuing the DosExecPgm will not execute further until the child process has finished executing. The thread returns from DosExecPgm when the child process itself terminates, not when the command subtree has terminated. This form of DosExecPgm is provided for ease in converting MS-DOS version 3.x programs to OS/2. It is considered obsolete and should generally be avoided. Its use may interfere with proper Ctrl-C and Ctrl-Break handling (see Chapter 14, Interactive Programs).
1: Execute the program asynchronously. The child process begins executing as soon as the scheduler allows; the calling thread returns from the DosExecPgm call immediately. You cannot assume that the child process has received CPU time before the parent thread returns from the DosExecPgm call; neither can you assume that the child process has not received such CPU service. This form instructs OS/2 not to bother remembering the child's termination code for a future DosCWait call. It is used when the parent process doesn't care what the result code of the child may be or when or if it completes, and it frees the parent from the necessity of issuing DosCWait calls. Programs executing other programs as tools would rarely use this form. This form might be used by a system utility, for example, whose job is to fire off certain programs once an hour but not to take any action or notice should any of those programs fail. Note that, unlike the detach form described below, the created process is still recorded as a child of the executing parent. The parent can issue a DosCWait call to determine whether the child subtree is still executing, although naturally there is no return code when the child process does terminate.
2: This form is similar to form 1 in that it executes the child process asynchronously, but it instructs OS/2 to retain the child process's exit code for future examination by DosCWait. Thus, a program can determine the success or failure of a child process. The parent process should issue an appropriate DosCWait "pretty soon" because OS/2 version 1.0 maintains about 2600 bytes of data structures for a dead process whose parent is expected to DosCWait but hasn't yet done so. To have one of these structures lying around for a few minutes is no great problem, but programs need to issue DosCWaits in a timely fashion to keep from clogging the system with the carcasses of processes that finished hours ago. OS/2 takes care of all the possible timing considerations, so it's OK to issue the DosCWait before or after the child process has completed. Although a parent process can influence the relative assignment of CPU time between the child and parent processes by setting its own and its child's relative priorities, there is no reliable way of determining which process will run when. Write your program in such a way that it doesn't matter if the child completes before or after the DosCWait, or use some form of IPC to synchronize the execution of parent and child processes. See 4.4 DosCWait for more details.
3: This form is used by the system debugger, CodeView. The system architecture does not allow one process to latch onto another arbitrary process and start examining and perhaps modifying the target process's execution. Such a facility would result in a massive side effect and is contrary to the tenet of encapsulation in the design religion. Furthermore, such a facility would prevent OS/2 from ever providing an environment secure against malicious programs. OS/2 solves this problem with the DosPtrace function, which peeks, pokes, and controls a child process. This function is allowed to affect only processes that were executed with this special mode value of 3.
4: This form executes the child process asynchronously but also detaches it from the process issuing the DosExecPgm call. A detached process does not inherit the parent process's screen group; instead, it executes in a special invalid screen group. Any attempt to do screen, keyboard, or mouse I/O from within this screen group returns an error code. The system does not consider a detached process a child of the parent process; the new process has no connection with the creating process. In other words, it's parentless. This form of DosExecPgm is used to create daemon programs that execute without direct interaction with the user.
The EnvString argument points to a list of ASCII text strings that
contain environment values. OS/2 supports a separate environment block for each process. A process typically inherits its parent's environment strings. In this case, the EnvPointer argument should be NULL, which tells OS/2 to supply the child process with a copy of the parent's environment strings (see Listing 4-4).
PATH=C:\DOS;C:\EDITORS;C:\TOOLS;C:\XTOOLS;C:\BIN;C:\UBNET
INCLUDE=\include
TERM=h19
INIT=c:\tmp
HOME=c:\tmp
USER=c:\tmp
TEMP=c:\tmp
TERM=ibmpc
LIB=c:\lib
PROMPT=($p)
Listing 4-4. A typical environment string set.
The environment strings are normally used to customize a particular
execution environment. For example, suppose a user creates two screen groups, each running a copy of CMD.EXE. Each CMD.EXE is a direct child of the presentation manager, which manages and creates screen groups, and each is also the ancestor of all processes that will run in its particular screen group. If the user is running utility programs that use the environment string "TEMP=<dirname>" to specify a directory to hold temporary files, he or she may want to specify different TEMP= values for each copy of CMD.EXE. As a result, the utilities that use TEMP= will access the proper directory, depending on which screen group they are run in, because they will have inherited the proper TEMP= environment string from their CMD.EXE ancestor. See Chapter 10, Environment Strings, for a complete discussion.
Because of the inheritable nature of environment strings, parent
processes that edit the environment list should remove or edit only those strings with which they are involved; any unrecognized strings should be preserved.
4.4 DosCWait
When a process executes a child process, it usually wants to know when that child process has completed and whether the process succeeded or failed. DosCWait, the OS/2 companion function to DosExecPgm, returns such information. Before we discuss DosCWait in detail, two observations are in order. First, although each DosExecPgm call starts only a single process, it's possible--and not uncommon--for that child process to create its own children and perhaps they their own and so on. A program should not assume that its child process won't create subchildren; instead, programs should use the command-subtree forms of DosCWait. One return code from the direct child process (that is, the root of the command subtree) is sufficient because if that direct child process invokes other processes to do work for it the direct child is responsible for monitoring their success via DosCWait. In other words, if a child process farms out some of its work to a grandchild process and that grandchild process terminates in error, then the child process should also terminate with an error return.
Second, although we discuss the process's child, in fact processes can
have multiple child processes and therefore multiple command subtrees at any time. The parent process may have interconnected the child processes via anonymous pipes, or they may be independent of one another. Issuing separate DosCWaits for each process or subtree is unnecessary. The form of the DosCWait call is shown in Listing 4-5.
extern unsigned far pascal DOSCWAIT (
unsigned ACTIONCODE, unsigned WAITOPTION, struct ResultCodes far *RESULTWORD, unsigned far *PIDRETURN, unsigned PID);
Listing 4-5. DosCWait function.
Three of the arguments affect the scope of the command: ActionCode,
WaitOption, and PID. It's easiest to show how these interact by arranging their possible values into tables.
DosCWait forms: Command Subtrees
These forms of DosCWait operate on the entire command subtree, which may, of course, consist of only one child process. We recommend these forms because they will continue to work correctly if the child process is changed to use more or fewer child processes of its own.
ActionCode WaitOption ProcessId Action
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
1 0 n Wait until the command subtree has completed and then return the direct child's termination code.
1 1 n If the command subtree has completed, return the direct child's termination code. Otherwise, return the ERROR_CHILD_NOT_COMPLETE error code.
DosCWait forms: Individual Processes
These forms of DosCWait are used to monitor individual child processes. The processes must be direct children; grandchild or unrelated processes cannot be DosCWaited. Use these forms only when the child process is part of the same application or software package as the parent process; the programmer needs to be certain that she or he can safely ignore the possibility that grandchild processes might still be running after the direct child has terminated.
1. It is in itself not an error to collect a child process's termination code via DosCWait while that child still has living descendant processes. However, such a case generally means that the child's work, whatever that was, is not yet complete. 1
ActionCode WaitOption ProcessId Action
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
0 0 0 DosCWait returns as soon as a direct child process terminates. If a child process had already terminated at the time of this call, it will return immediately.
0 0 N DosCWait returns as soon as the direct child process N terminates. If it had already terminated at the time of the call, DosCWait returns immediately.
0 1 0 DosCWait checks for a terminated direct child process. If one is found, its status is returned. If none is found, an error code is returned.
0 1 N DosCWait checks the status of the direct child process N. If it is terminated, its status is returned. If it is still running, an error code is returned.
DosCWait forms: Not Recommended
ActionCode WaitOption ProcessId Action
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
1 0 0 DosCWait waits until a direct child has terminated and then waits until all of that child's descendants have terminated. It then returns the direct child's exit code. This form does not wait until the first command subtree has terminated; it selects a command subtree based on the first direct child that terminates, and then it waits as long as necessary for the remainder of that command subtree, even if other command subtrees meanwhile complete.
1 1 0 This form returns ERROR_CHILD_NOT_COMPLETE if any process in any of the caller's subtrees is still executing. If all subtrees have terminated, this form returns with a direct child's exit code. If no direct child processes have unwaited exit codes, the code ERROR_WAIT_NO_CHILDREN is returned.
4.5 Control of Child Tasks and Command Subtrees
A parent process has only limited control over its child processes because the system is designed to minimize the side effects, or cross talk, between processes. Specifically, a parent process can affect its command subtrees in two ways: It can change their CPU priority, and it can terminate (kill) them. Once again, the command subtree is the recommended form for both commands because that form is insensitive to the operational details of the child process.
4.5.1 DosKillProcess
A parent process may initiate a child process or command subtree and then
decide to terminate that activity before the process completes normally.
Often this comes about because of a direct user command or because the user
typed Ctrl-Break. See Chapter 14, Interactive Programs, for special
techniques concerning Ctrl-Break and Ctrl-C.
DosKillProcess flags each process in the command subtree (or the
direct child process if that form is used) for termination. A process flagged for termination normally terminates as soon as all its threads leave the system (that is, as soon as all its threads return from all system calls). The system aborts calls that might block for more than a second or two, such as those that read a keyboard character, so that the process can terminate quickly. A process can intercept SIGKILL to delay termination longer, even indefinitely. Delaying termination inordinately via SetSignalHandler/SIGKILL is very bad practice and is considered a bug rather than a feature.
4.5.2 DosSetPrty
A child process inherits its parent's process priority when the DosExecPgm
call is issued. After the DosExecPgm call, the parent can still change the
process priority of the command subtree or of only the direct child
process. The command subtree form is recommended; if the child process's
work deserves priority N, then any child processes that it executes to help
in that work should also run at priority N.
5 Threads and Scheduler/Priorities
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
5.1 Threads
Computers consist of a CPU (central processing unit) and RAM (random access memory). A computer program consists of a sequence of instructions that are placed, for the most part, one after the other in RAM. The CPU reads each instruction in sequence and executes it. The passage of the CPU through the instruction sequence is called a thread of execution. All versions of MS- DOS executed programs, so they necessarily supported a thread of execution. OS/2 is unique, however, in that it supports multiple threads of execution within a single process. In other words, a program can execute in two or more spots in its code at the same time.
Obviously, a multitasking system needs to support multiple threads in
a systemwide sense. Each process necessarily must have a thread; so if there are ten processes in the system, there must be ten threads. Such an existence of multiple threads in the system is invisible to the programmer because each program executes with only one thread. OS/2 is different from this because it allows an individual program to execute with multiple threads if it desires.
Because threads are elements of processes and because the process is
the unit of resource ownership, all threads that belong to the same process share that process's resources. Thus, if one thread opens a file on file handle X, all threads in that process can issue DosReads or DosWrites to that handle. If one thread allocates a memory segment, all threads in that process can access that memory segment. Threads are analogous to the employees of a company. A company may consist of a single employee, or it may consist of two or more employees that divide the work among them. Each employee has access to the company's resources--its office space and equipment. The employees themselves, however, must coordinate their work so that they cooperate and don't conflict. As far as the outside world is concerned, each employee speaks for the company. Employees can terminate and/or more can be hired without affecting how the company is seen from outside. The only requirement is that the company have at least one employee. When the last employee (thread) dies, the company (process) also dies.
Although the process is the unit of resource ownership, each thread
does "own" a small amount of private information. Specifically, each thread has its own copy of the CPU's register contents. This is an obvious requirement if each thread is to be able to execute different instruction sequences. Furthermore, each thread has its own copy of the floating point registers. OS/2 creates the process's first thread when the program begins execution. Any additional threads are created by means of the DosCreateThread call. Any thread can create another thread. All threads in a process are considered siblings; there are no parent-child relationships. The initial thread, thread 1, has some special characteristics and is discussed below.
5.1.1 Thread Stacks
Each thread has its own stack area, pointed to by that thread's SS and SP
values. Thread 1 is the process's primal thread. OS/2 allocates it stack
area in response to specifications in the .EXE file. If additional threads
are created via the DosCreateThread call, the caller specifies a stack area
for the new thread. Because the memory in which each thread's stack resides
is owned by the process, any thread can modify this memory; the programmer
must make sure that this does not happen. The size of the segment in which
the stack resides is explicitly specified; the size of a thread's stack is
not. The programmer can place a thread's stack in its own segment or in a
segment with other data values, including other thread stacks. In any case,
the programmer must ensure sufficient room for each thread's needs. Each
thread's stack must have at least 2 KB free in addition to the thread's
other needs at all times. This extra space is set aside for the needs of
dynamic link routines, some of which consume considerable stack space. All
threads must maintain this stack space reserve even if they are not used to
call dynamic link routines. Because of a bug in many 80286 processors,
stack segments must be preallocated to their full size. You cannot overrun
a stack segment and then assume that OS/2 will grow the segment;
overrunning a stack segment will cause a stack fault, and the process will
be terminated.
5.1.2 Thread Uses
Threads have a great number of uses. This section describes four of them.
These examples are intended to be inspirations to the programmer; there are
many other uses for threads.
5.1.2.1 Foreground and Background Work
Threads provide a form of multitasking within a single program; therefore,
one of their most obvious uses is to provide simultaneous foreground and
background
1. Here I mean foreground and background in the sense of directly
interacting with the user, not as in foreground and background
screen groups.
1 processing for a program. For example, a spreadsheet program
might use one thread to display menus and to read user input. A second
thread could execute user commands, update the spreadsheet, and so on.
This arrangement generally increases the perceived speed of the
program by allowing the program to prompt for another command before the previous command is complete. For example, if the user changes a cell in a spreadsheet and then calls for recalculation, the "execute" thread can recalculate while the "command" thread allows the user to move the cursor, select new menus, and so forth. The spreadsheet should use RAM semaphores to protect its structures so that one thread can't change a structure while it is being manipulated by another thread. As far as the user can tell, he or she is able to overlap commands without restriction. In actuality, the previous command is usually complete before the user can finish entering the new command. Occasionally, however, the new command is delayed until the first has completed execution. This happens, for example, when the user of a spreadsheet deletes a row right after saving the spreadsheet to disk. Of course, the performance in this worst case situation is no worse than a standard single-thread design is for all cases.
5.1.2.2 Asynchronous Processing
Another common use of threads is to provide asynchronous elements in a
program's design. For example, as a protection against power failure, you
can design an editor so that it writes its RAM buffer to disk once every
minute. Threads make it unnecessary to scatter time checks throughout the
program or to sit in polling loops for input so that a time event isn't
missed while blocked on a read call. You can create a thread whose sole job
is periodic backup. The thread can call DosSleep to sleep for 60 seconds,
write the buffer, and then go back to sleep for another 60 seconds.
The asynchronous event doesn't have to be time related. For example,
in a program that communicates over an asynchronous serial port, you can dedicate a thread to wait for the modem carrier to come on or to wait for a protocol time out. The main thread can continue to interact with the user.
Programs that provide services to other programs via IPC can use
threads to simultaneously respond to multiple requests. For example, one thread can watch the incoming work queue while one or more additional threads perform the work.
5.1.2.3 Speed Execution
You can use threads to speed the execution of single processes by
overlapping I/O and computation. A single-threaded process can perform
computations or call OS/2 for disk reads and writes, but not both at the
same time. A multithreaded process, on the other hand, can compute one
batch of data while reading the next batch from a device.
Eventually, PCs containing multiple 80386 processors will become
available. An application that uses multiple threads may execute faster by using more than one CPU simultaneously.
5.1.2.4 Organizing Programs
Finally, you can use threads to organize and simplify the structure of a
program. For example, in a program for a turnkey security/alarm system, you
can assign a separate thread for each activity. One thread can watch the
status of the intrusion switches; a second can send commands to control the
lights; a third can run the telephone dialer; and a fourth can interface
with each control panel.
This structure simplifies software design. The programmer needn't
worry that an intrusion switch is triggering unnoticed while the CPU is executing the code that waits on the second key of a two-key command. Likewise, the programmer doesn't have to worry about talking to two command consoles at the same time; because each has its own thread and local (stack) variables, multiple consoles can be used simultaneously without conflict.
Of course, you can write such a program without multiple threads; a
rat's nest of event flags and polling loops would do the job. Much better would be a family of co-routines. But best, and simplest of all, is a multithreaded design.
5.1.3 Interlocking
The good news about threads is that they share a process's data, files, and
resources. The bad news is that they share a process's data, files, and
resources--and that sometimes these items must be protected against
simultaneous update by multiple threads. As we discussed earlier, most OS/2
machines have a single CPU; the "random" preemption of the scheduler,
switching the CPU among threads, gives the illusion of the simultaneous
execution of threads. Because the scheduler is deterministic and priority
based, scheduling a process's threads is certainly not random; but good
programming practice requires that it be considered so. Each time a program
runs, external events will perturb the scheduling of the threads. Perhaps
some other, higher priority task needs the CPU for a while. Perhaps the
disk arm is in a different position, and a disk read by one thread takes a
little longer this time than it did the last. You cannot even assume that
only the highest priority runnable thread is executing because a multiple-
CPU system may execute the N highest priority threads.
The only safe assumption is that all threads are executing
simultaneously and that--in the absence of explicit interlocking or semaphore calls--each thread is always doing the "worst possible thing" in terms of simultaneously updating static data or structures. Writing to looser standards and then testing the program "to see if it's OK" is unacceptable. The very nature of such race conditions, as they are called, makes them extremely difficult to find during testing. Murphy's law says that such problems are rare during testing and become a plague only after the program is released.
5.1.3.1 Local Variables
The best way to avoid a collision of threads over static data is to write
your program to minimize static data. Because each thread has its own
stack, each thread has its own stack frame in which to store local
variables. For example, if one thread opens and reads a file and no other
thread ever manipulates that file, the memory location where that file's
handle is stored should be in the thread's stack frame, not in static
memory. Likewise, buffers and work areas that are private to a thread
should be on that thread's stack frame. Stack variables that are local to
the current procedure are easily referenced in high-level languages and in
assembly language. Data items that are referenced by multiple procedures
can still be located on the stack. Pascal programs can address such items
directly via the data scope mechanism. C and assembly language programs
will need to pass pointers to the items into the procedures that use them.
5.1.3.2 RAM Semaphores
Although using local variables on stack frames greatly reduces problems
among threads, there will always be at least a few cases in which more than
one thread needs to access a static data item or a static resource such as
a file handle. In this situation, write the code that manipulates the
static item as a critical section (a body of code that manipulates a data
resource in a nonreentrant way) and then use RAM semaphores to reserve each
critical section before it is executed. This procedure guarantees that only
one thread at a time is in a critical section. See 16.2 Data Integrity for
a more detailed discussion.
5.1.3.3 DosSuspendThread
In some situations it may be possible to enumerate all threads that might
enter a critical section. In these cases, a process's thread can use
DosSuspendThread to suspend the execution of the other thread(s) that might
enter the critical section. The DosSuspendThread call can only be used to
suspend threads that belong to the process making the call; it cannot be
used to suspend a thread that belongs to another process. Multiple threads
can be suspended by making multiple DosSuspendThread calls, one per thread.
If a just-suspended thread is in the middle of a system call, work on that
system call may or may not proceed. In either case, there will be no
further execution of application (ring 3) code by a suspended thread.
It is usually better to protect critical sections with a RAM semaphore
than to use DosSuspendThread. Using a semaphore to protect a critical section is analogous to using a traffic light to protect an intersection (an automotive "critical section" because conflicting uses must be prevented). Using DosSuspendThread, on the other hand, is analogous to your stopping the other cars each time you go through an intersection; you're interfering with the operation of the other cars just in case they might be driving through the same intersection as you, presumably an infrequent situation. Furthermore, you need a way to ensure that another vehicle isn't already in the middle of the intersection when you stop it. Getting back to software, you need to ensure that the thread you're suspending isn't already executing the critical section at the time that you suspend it. We recommend that you avoid DosSuspendThread when possible because of its adverse effects on process performance and because of the difficulty in guaranteeing that all the necessary threads have been suspended, especially when a program undergoes future maintenance and modification.
A DosResumeThread call restores the normal operation of a suspended
thread.
5.1.3.4 DosEnterCritSec/DosExitCritSec
DosSuspendThread suspends the execution of a single thread within a
process. DosEnterCritSec suspends all threads in a process except the one
making the DosEnterCritSec call. Except for the scope of their operation,
DosEnterCritSec and DosExitCritSec are similar to DosSuspendThead and
DosResumeThread, and the same caveats and observations apply.
DosExitCritSec will not undo a DosSuspendThread that was already in
effect. It releases only those threads that were suspended by DosEnterCritSec.
5.1.4 Thread 1
Each thread in a process has an associated thread ID. A thread's ID is a
magic cookie. Its value has no intrinsic meaning to the application; it
has meaning only as a name for a thread in an operating system call. The
one exception to this is the process's first thread, whose thread ID is
always 1.
Thread 1 is special: It is the thread that is interrupted when a
process receives a signal. See Chapter 12, Signals, for further details.
5.1.5 Thread Death
A thread can die in two ways. First, it can terminate itself with the
DosExit call. Second, when any thread in a process calls DosExit with the
"exit entire process" argument, all threads belonging to that process are
terminated "as soon as possible." If they were executing application code
at the time DosExit was called, they terminate immediately. If they were in
the middle of a system call, they terminate "very quickly." If the system
call executes quickly enough, its function may complete (although the CPU
will not return from the system call itself); but if the system call
involves delays of more than 1 second, it will terminate without
completing. Whether a thread's last system call completes is usually moot,
but in a few cases, such as writes to some types of devices, it may be
noticed that the last write was only partially completed.
When a process wants to terminate, it should use the "terminate entire
process" form of DosExit rather than the "terminate this thread" form. Unbeknownst to the calling process, some dynlink packages, including some OS/2 system calls, may create threads. These threads are called captive threads because only the original calling thread returns from the dynlink call; the created thread remains "captive" inside the dynlink package. If a program attempts to terminate by causing all its known threads to use the DosExit "terminate this thread" form, the termination may not be successful because of such captive threads.
Of course, if the last remaining thread of a process calls DosExit
"terminate this thread," OS/2 terminates the process.
5.1.6 Performance Characteristics
Threads are intended to be fast and cheap. In OS/2 version 1.0, each
additional thread that is created consumes about 1200 bytes of memory
inside the OS/2 kernel for its kernel mode stack. This is in addition to
the 2048 bytes of user mode stack space that we recommend you provide from
the process's data area. Terminating a thread does not release the kernel
stack memory, but subsequently creating another thread reuses this memory.
In other words, the system memory that a process's threads consume is the
maximum number of threads simultaneously alive times 1200 bytes. This
figure is exclusive of each thread's stack, which is provided by the
process from its own memory.
The time needed to create a new thread depends on the process's
previous thread behavior. Creating a thread that will reuse the internal memory area created for a previous thread that has terminated takes approximately 3 milliseconds. 2. All timings in this book refer to a 6 mHz IBM AT with one wait-state memory. This represents a worst case performance level. 2 A request to create a new thread that extends the process's "thread count high-water mark" requires an internal memory allocation operation. This operation may trigger a memory compaction or even a segment swapout, so its time cannot be accurately predicted.
It takes about 1 millisecond for the system to begin running an
unblocked thread. In other words, if a lower-priority thread releases a RAM semaphore that is being waited on by a higher-priority thread, approximately 1 millisecond passes between the lower-priority thread's call to release the semaphore and the return of the higher-priority thread from its DosSemRequest call.
Threads are a key feature of OS/2; they will receive strong support in
future versions of OS/2 and will play an increasingly important architectural role. You can, therefore, expect thread costs and performance to be the same or to improve in future releases.
5.2 Scheduler/Priorities
A typical running OS/2 system contains a lot of threads. Frequently, several threads are ready to execute at any one time. The OS/2 scheduler decides which thread to run next and how long to run it before assigning the CPU to another thread. OS/2's scheduler is a priority-based scheduler; it assigns each thread a priority and then uses that priority to decide which thread to run. The OS/2 scheduler is also a preemptive scheduler. If a higher-priority thread is ready to execute, OS/2 does not wait for the lower-priority thread to finish with the CPU before reassigning the CPU; the lower-priority thread is preempted--the CPU is summarily yanked away. Naturally, the state of the preempted thread is recorded so that its execution can resume later without ill effect.
The scheduler's dispatch algorithm is very straightforward: It
executes the highest-priority runnable thread for as long as the thread wants the CPU. When that thread gives up the CPU--perhaps by waiting for an I/O operation--that thread is no longer runnable, and the scheduler executes the thread with the highest priority that is runnable. If a blocked thread becomes runnable and it has a higher priority than the thread currently running, the CPU is immediately preempted and assigned to the higher-priority thread. In summary, the CPU is always running the highest-priority runnable thread.
The scheduler's dispatcher is simplicity itself: It's blindly priority
based. Although the usual focus for OS/2 activities is the process--a process lives, dies, opens files, and so on--the scheduler components of OS/2 know little about processes. Because the thread is the dispatchable entity, the scheduler is primarily thread oriented. If you're not used to thinking in terms of threads, you can mentally substitute the word process for the word thread in the following discussion. In practice, all of a process's threads typically share the same priority, so it's not too inaccurate to view the system as being made up of processes that compete for CPU resources.
In OS/2 threads are classified and run in three categories: general
priority, time-critical priority, and low priority. These categories are further divided into subcategories. Figure 5-1 shows the relationship of the three priority categories and their subcategories.
High
ÚÄÄÄÄÄÄÄÄ¿ ³ ³ /ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ / ³ Foreground ³ ³ Force ³ / ³ screen ³ ³ run ³ / ³ group ³ ³ ³ / ³ ³ ³ ³ / ³ interactive ³ ÃÄÄÄÄÄÄÄÄ´/ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ³ Foreground ³ ³ ³ ³ screen ³ ³ Normal ³ ³ group ³ ³ ³ ³ ³ ³ ³ ³ noninteractive ³ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ÃÄÄÄÄÄÄÄÄ´\ ³ ³ ³ ³ \ ³ Background ³ ³ ³ \ ³ screen ³ ³ Idle ³ \ ³ group ³ ³ time ³ \ ³ ³ ³ ³ \ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ÀÄÄÄÄÄÄÄÄÙ
Low
Figure 5-1. Priority categories.
5.2.1 General Priority Category
The majority of threads in the system run in the general priority category
and belong to one of three subcategories: background, foreground, or
interactive. To a limited extent, OS/2 dynamically modifies the priorities
of threads in the general priority category.
5.2.1.1 Background Subcategory
The purpose of the OS/2 priority design is to optimize response rather than
throughput. In other words, the system is not concerned about ensuring that
all runnable threads get at least some CPU time, and the system is not
primarily concerned about trying to keep the disks busy when the highest-
priority thread is compute bound. Instead, OS/2 is concerned about keeping
less important work from delaying or slowing more important work. This is
the reason for the background subcategory. The word background has been
used in many different ways to describe how tasks are performed in many
operating systems; we use the word to indicate processes that are
associated with a screen group not currently being displayed.
For example, a user is working with a word-processing program but then
switches from that program to a spreadsheet program. The word-processing program becomes background, and the spreadsheet program is promoted from background to foreground. When the user selects different screen groups, threads change from foreground to background or background to foreground. Background threads have the lowest priority in the general priority category. Background applications get the CPU (and, through it, the disks) only when all foreground threads are idle. As soon as a foreground thread is runnable, the CPU is preempted from the background thread. Background threads can use leftover machine time, but they can never compete with foreground threads.
5.2.1.2 Foreground and Interactive Subcategories
All processes associated with the currently active screen group are made
members of the foreground subcategory. The process that is currently
interacting with the keyboard is promoted to the interactive subcategory.
This ensures that the user will get the fastest possible response to a
command. When the interactive process's threads release the CPU (via
blocking on some OS/2 call), the noninteractive foreground threads get the
next crack at it because those threads are usually doing work on behalf of
the interactive process or work that is in some way related. If no
foreground thread needs the CPU, background threads may run.
Although the scheduler concerns itself with threads rather than
processes, it's processes that switch between categories--foreground, background, and interactive. When a process changes category--for example, when a process shows itself to be in the interactive subcategory by doing keyboard I/O--the priorities of all its threads are adjusted appropriately.
Because background threads are the "low men on the totem pole" that is
composed of quite a few threads, it may seem that they'll never get to run. This isn't the case, though, over a long enough period of time. Yes, a background thread can be totally starved for CPU time during a 5-second interval, but it would be very rare if it received no service during a 1- minute interval. Interactive application commands that take more than a few seconds of CPU time are rare. Commands involving disk transfer may take longer, but the CPU is available for lower-priority threads while the interactive process is waiting for disk operations. Finally, a user rarely keeps an interactive application fully busy; the normal "type, look, and think" cycle has lots of spare time in it for background threads to run.
But how does this apply to the presentation manager? The presentation
manager runs many independent interactive tasks within the same screen group, so are they all foreground threads? How does OS/2 know which is the interactive process? The answer is that the presentation manager advises the scheduler. When the presentation manager screen group is displayed, all threads within that screen group are placed in the foreground category. When the user selects a particular window to receive keyboard or mouse events, the presentation manager tells the scheduler that the process using that window is now the interactive process. As a result, the system's interactive performance is preserved in the presentation manager's screen group.
5.2.1.3 Throughput Balancing
We mentioned that some operating systems try to optimize system throughput
by trying to run CPU-bound and I/O-bound applications at the same time. The
theory is that the I/O-bound application ties up the disk but needs little
CPU time, so the disk work can be gotten out of the way while the CPU is
running the CPU-bound task. If the disk thread has the higher priority, the
tasks run in tandem. Each time the disk operation is completed, the I/O-
bound thread regains the CPU and issues another disk operation. Leftover
CPU time goes to the CPU-bound task that, in this case, has a lower
priority.
This won't work, however, if the CPU-bound thread has a higher
priority than the I/O-bound thread. The CPU-bound thread will tend to hold the CPU, and the I/O-bound thread won't get even the small amount of CPU time that it needs to issue another I/O request. Traditionally, schedulers have been designed to deal with this problem by boosting the priority of I/O-bound tasks and lowering the priority of CPU-bound tasks so that, eventually, the I/O-bound thread gets enough service to make its I/O requests.
The OS/2 scheduler incorporates this design to a limited extent. Each
time a thread issues a system call that blocks, the scheduler looks at the period between the time the CPU was assigned to the thread and the time the thread blocked itself with a system call. 3. We use "blocking" rather than "requesting an I/O operation" as a test of I/O boundedness because nearly all blocking operations wait for I/O. If a thread's data were all in the buffer cache, the thread could issue many I/O requests and still be compute bound. In other words, when we speak of I/O-bound threads, we really mean device bound--not I/O request bound. 3 If that period of time is short, the thread is considered I/O bound, and its priority receives a small increment. If a thread is truly I/O bound, it soon receives several such increments and, thus, a modest priority promotion. On the other hand, if the thread held the CPU for a longer period of time, it is considered CPU bound, and its priority receives a small decrement.
The I/O boundedness priority adjustment is small. No background
thread, no matter how I/O bound, can have its priority raised to the point where it has a higher priority than any foreground thread, no matter how CPU bound. This throughput enhancing optimization applies only to "peer" threads--threads with similar priorities. For example, the threads of a single process generally have the same base priority, so this adjustment helps optimize the throughput of that process.
5.2.2 Time-Critical Priority Category
Foreground threads, particularly interactive foreground threads, receive
CPU service whenever they want it. Noninteractive foreground threads and,
particularly, background threads may not receive any CPU time for periods
of arbitrary length. This approach improves system response, but it's not
always a good thing. For example, you may be running a network or a
telecommunications application that drops its connection if it can't
respond to incoming packets in a timely fashion. Also, you may want to make
an exception to the principle of "response, not throughput" when it comes
to printers. Most printers are much slower than their users would like, and
most printer spooler programs require little in the way of CPU time; so the
OS/2 print spooler (the program that prints queued output on the printer)
would like to run at a high priority to keep the printer busy.
Time-critical applications are so called because the ability to run in
a timely fashion is critical to their well-being. Time-critical applications may or may not be interactive, and they may be in the foreground or in a background screen group, but this should not affect their high priority. The OS/2 scheduler contains a time-critical priority category to deal with time-critical applications. A thread running in this priority category has a higher priority than any non-time-critical thread in the system, including interactive threads. Unlike priorities in the general category, a time-critical priority is never adjusted; once given a time-critical priority, a thread's priority remains fixed until a system call changes it.
Naturally, time-critical threads should consume only modest amounts of
CPU time. If an application has a time-critical thread that consumes considerable CPU time--say, more than 20 percent--the foreground interactive application will be noticeably slowed or even momentarily stopped. System usability is severely affected when the interactive application can't get service. The screen output stutters and stumbles, characters are dropped when commands are typed, and, in general, the computer becomes unusable.
Not all threads in a process have to be of the same priority. An
application may need time-critical response for only some of its work; the other work can run at a normal priority. For example, in a telecommunications program a "receive incoming data" thread might run at a time-critical priority but queue messages in memory for processing by a normal-priority thread. If the time-critical thread finds that the normal- priority thread has fallen behind, it can send a "wait for me" message to the sending program.
We strongly recommend that processes that use monitors run the monitor
thread, and only the monitor thread, at a time-critical priority. This prevents delayed device response because of delays in processing the monitor data stream. See 16.1 Device Monitors for more information.
5.2.3 Low Priority Category
If you picture the general priority category as a range of priorities, with
the force run priority category as a higher range, there is a third range,
called the low priority category, that is lower in priority than the
general priority category. As a result, threads in this category get CPU
service only when no other thread in the other categories needs it. This
category is a mirror image of the time-critical priority category in that
the system call that sets the thread fixes the priority; OS/2 never changes
a low priority.
I don't expect the low priority category to be particularly popular.
It's in the system primarily because it falls out for free, as a mirror image of the time-critical category. Turnkey systems may want to run some housekeeping processes at this priority. Some users enjoy computing PI, doing cryptographic analysis, or displaying fractal images; these recreations are good candidates for soaking up leftover CPU time. On a more practical level, you could run a program that counts seconds of CPU time and yields a histogram of CPU utilization during the course of a day.
5.2.4 Setting Process/Thread Priorities
We've discussed at some length the effect of the various priorities, but we
haven't discussed how to set these priorities. Because inheritance is an
important OS/2 concept, how does a parent's priority affect that of the
child? Finally, although we said that priority is a thread issue rather
than a process one, we kept bringing up processes anyway. How does all this
work?
Currently, whenever a thread is created, it inherits the priority of
its creator thread. In the case of DosCreateThread, the thread making the call is the creator thread. In the case of thread 1, the thread in the parent process that is making the DosExecPgm call is the creator thread. When a process makes a DosSetPrty call to change the priority of one of its own threads, the new priority always takes effect. When a process uses DosSetPrty to change the priority of another process, only the threads in that other process which have not had their priorities explicitly set from within their own process are changed. This prevents a parent process from inadvertently lowering the priority of, say, a time-critical thread by changing the base priority of a child process.
In a future release, we expect to improve this algorithm so that each
process has a base priority. A new thread will inherit its creator's base priority. A process's thread priorities that are in the general priority category will all be relative to the process's base priority so that a change in the base priority will raise or lower the priority of all the process's general threads while retaining their relative priority relationships. Threads in the time-critical and low priority categories will continue to be unaffected by their process's base priority.
6 The User Interface
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
OS/2 contains several important subsystems: the file system, the memory management subsystem, the multitasking subsystem, and the user interface subsystem--the presentation manager. MS-DOS does not define or support a user interface subsystem; each application must provide its own. MS-DOS utilities use a primitive line-oriented interface, essentially unchanged from the interface provided by systems designed to interface with TTYs.
OS/2 is intended to be a graphics-oriented operating system, and as
such it needs to provide a standard graphical user interface (GUI) subsystem--for several reasons. First, because such systems are complex to create, to expect that each application provide its own is unreasonable. Second, a major benefit of a graphical user interface is that applications can be intermingled. For example, their output windows can share the screen, and the user can transfer data between applications using visual metaphors. If each application had its own GUI package, such sharing would be impossible. Third, a graphical user interface is supposed to make the machine easier to use, but this will be so only if the user can learn one interface that will work with all applications.
6.1 VIO User Interface
The full graphical user interface subsystem will not ship with OS/2 version 1.0, so the initial release will contain the character-oriented VIO/KBD/MOU subsystem (see Chapter 13, The Presentation Manager and VIO, for more details). Although VIO doesn't provide any graphical services, it does allow applications to sidestep VIO and construct their own. The VIO screen group interface is straightforward. When the machine is booted up, the screen displays the screen group list. The user can select an existing screen group or create a new one. From within a screen group, the user can type a magic key sequence to return the screen to the screen group list. Another magic key sequence allows the user to toggle through all existing screen groups. One screen group is identified in the screen group list as the real mode screen group.
6.2 The Presentation Manager User Interface
The OS/2 presentation manager is a powerful and flexible graphical user interface. It supports such features as windowing, drop-down and pop-up menus, and scroll bars. It works best with a graphical pointing device such as a mouse, but it can be controlled exclusively from the keyboard.
The presentation manager employs screen windows to allow multiple
applications to use the screen and keyboard simultaneously. Each application uses one or more windows to display its information; the user can size and position each window, overlapping some and perhaps shrinking others to icons. Mouse and keyboard commands change the input focus between windows; this allows the presentation manager to route keystrokes and mouse events to the proper application.
Because of its windowing capability, the presentation manager doesn't
need to use the underlying OS/2 screen group mechanism to allow the user to switch between running applications. The user starts an application by pointing to its name on a menu display; for most applications the presentation manager creates a new window and assigns it to the new process. Some applications may decline to use the presentation manager's graphical user interface and prefer to take direct control of the display. When such an application is initiated, the presentation manager creates a private screen group for it and switches to that screen group. The user can switch away by entering a special key sequence that brings up a menu which allows the user to select any running program. If the selected program is using the standard presentation manager GUI, the screen is switched to the screen group shared by those programs. Otherwise, the screen is switched to the private screen group that the specified application is using.
To summarize, only the real mode application and applications that
take direct control of the display hardware need to run in their own screen groups. The presentation manager runs all other processes in a single screen group and uses its windowing facilities to share the screen among them. The user can switch between applications via a special menu; if both the previous and the new application are using the standard interface, the user can switch the focus directly without going though the menu.
6.3 Presentation Manager and VIO Compatibility
In OS/2 version 1.1 and in all subsequent releases, the presentation manager will replace and superset the VIO interface. Applications that use the character mode VIO interface will continue to work properly as windowable presentation manager applications, as will applications that use the STDIN and STDOUT file handles for interactive I/O. Applications that use the VIO interface to obtain direct access to the graphical display hardware will also be supported; as described above, the presentation manager will run such applications in their own screen group.
7 Dynamic Linking
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
A central component of OS/2 is dynamic linking. Dynamic links play several critical architectural roles. Before we can discuss them at such an abstract level, however, we need to understand the nuts and bolts of their workings.
7.1 Static Linking
A good preliminary to the study of dynamic links (called dynlinks, for short) is a review of their relative, static links. Every programmer who has gone beyond interpreter-based languages such as BASIC is familiar with static links. You code a subroutine or a procedure call to a routine that is not present in that compiland (or source file), which we'll call Foo. The missing routine is declared external so that the assembler or compiler doesn't flag it as an undefined symbol. At linktime, you present the linker with the .OBJ file that you created from your compiland, and you also provide a .OBJ file 1. Or a .LIB library file that contains the .OBJ file as a part of it. 1 that contains the missing routine Foo. The linker combines the compilands into a final executable image--the .EXE file--that contains the routine Foo as well as the routines that call it. During the combination process, the linker adjusts the calls to Foo, which had been undefined external references, to point to the place in the .EXE file where the linker relocated the Foo routine. This process is diagramed in Figure 7-1.
.OBJ .LIB
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ Foo ÄÄÄÄÄÄ ³ ³ Call Foo ³ ³ ÄÄÄÄÄÄ ³ ³ ³ ³ ÄÄÄÄÄÄ ³ ÀÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÙ .EXE file
³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÀÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ + ³ ³ Call �ÄÄÄÄÄÄÅÄ¿ ÚÄ�ÄÄÄÄÄÄÄÄÄÄÄ�Ä¿ ³ ³ ³ ³ ³ ³ ³ ³ ³ Linker ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ�³ ³ ³ ³ ³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ ³ ÄÄÄÄÄÄ �ÄÄÄÄÄÄÅÄÙ ³ ÄÄÄÄÄÄ ³ ³ ÄÄÄÄÄÄ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 7-1. Static linking.
In other words, with static linking you can write a program in pieces.
You can compile one piece at a time by having it refer to the other pieces as externals. A program called a linker or a link editor combines these pieces into one final .EXE image, fixing up the external references (that is, references between one piece and another) that those pieces contain.
Writing and compiling your program piecemeal is useful, but the
primary advantage of static linking is that you can use it to reference a standard set of subroutines--a subroutine library--without compiling or even possessing the source code for those subroutines. Nearly all high- level language packages come with one or more standard runtime libraries that contain various useful subroutines that the compiler can call implicitly and that the programmer can call explicitly. Source for these runtime libraries is rarely provided; the language supplier provides only the .OBJ object files, typically in library format.
To summarize, in traditional static linking the target code (that is,
the external subroutine) must be present at linktime and is built into the final .EXE module. This makes the .EXE file larger, naturally, but more important, the target code can't be changed or upgraded without relinking to the main program's .OBJ files. Because the personal computer field is built on commercial software whose authors don't release source or .OBJ files, this relinking is out of the question for the typical end user. Finally, the target code can't be shared among several (different) applications that use the same library routines. This is true for two reasons. First, the target code was relocated differently by the linker for each client; so although the code remains logically the same for each application, the address components of the binary instructions are different in each .EXE file. Second, the operating system has no way of knowing that these applications are using the same library, and it has no way of knowing where that library is in each .EXE file. Therefore, it can't avoid having duplicate copies of the library in memory.
7.2 Loadtime Dynamic Linking
The mechanical process of loadtime dynamic linking is the same as that of static linking. The programmer makes an external reference to a subroutine and at linktime specifies a library file (or a .OBJ file) that defines the reference. The linker produces a .EXE file that OS/2 then loads and executes. Behind the scenes, however, things are very much different.
Step 1 is the same for both kinds of linking. The external reference
is compiled or assembled, resulting in a .OBJ file that contains an external reference fixup record. The assembler or compiler doesn't know about dynamic links; the .OBJ file that an assembler or a compiler produces may be used for static links, dynamic links, or, more frequently, a combination of both (some externals become dynamic links, others become static links).
In static linking, the linker finds the actual externally referenced
subroutine in the library file. In dynamic linking, the linker finds a special record that defines a module name string and an entry point name string. For example, in our hypothetical routine Foo, the library file contains only these two name strings, not the code for Foo itself. (The entry point name string doesn't have to be the name by which programs called the routine.) The resultant .EXE file doesn't contain the code for Foo; it contains a special dynamic link record that specifies these module and entry point names for Foo. This is illustrated in Figure 7-2.
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿
³ .OBJ ³ Extern ³ .LIB ³ ³ .EXE ³
³ ÃÄÄÄÄÄÄÄÄ�³ Foo: ³ ³ ³
³ Call Foo ³ ³ Module dlpack ³ ³ ÚÄÄÄÄ¿ ³
³ ³ ³ entry Foo ³ ³ Call ³????ÃÄÄÄÅÄÄ¿
ÀÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÙ ³ ÀÄÄÄÄÙ ³ ³
³ ³ ÚÄÄ�ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ³ ³ ³ Reference to ³�ÄÙ ÀÄÄÄÄÄÄÄÄÄ� + �ÄÄÄÄÄÄÄÄÄÙ ³ ³ dlpack: Foo ³ ÚÄÄÄÄÄÄÄÄÄ¿ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ Link ³ ³ ÀÄÄÄÄÂÄÄÄÄÙ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 7-2. Dynamic linking.
When this .EXE file is run, OS/2 loads the code in the .EXE file into
memory and discovers the dynamic link record(s). For each dynamic link module that is named, OS/2 locates the code in the system's dynamic link library directory and loads it into memory (unless the module is already in use; see below). The system then links the external references in the application to the addresses of the called entry points. This process is diagramed in Figure 7-3.
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿
³ .EXE ³ ³ .DLL ³
³ ³ ³ dlpack: Foo ³
³ Call ÄÄÄÄÄÅÄÄ¿ ³ ÄÄÄÄÄÄ ³
ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ÄÄÄÄÄÄ ³
³ dlpack: Foo ³�ÄÙ Disk ³ ÄÄÄÄÄÄ ³
ÀÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÙ
³ ³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³
Ä Ä Ä Ä Å Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Å Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä Ä
³ ³
ÚÄÄÄÄÄÄÄ�ÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄ�ÄÄÄÄÄÄÄ¿ ³ ³ RAM ³ ³ ³ ³ Fixup ³ ÄÄÄÄÄÄ ³ ³ ³ by OS/2 ³ ÄÄÄÄÄÄ ³ ³ Call ÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ�³ ÄÄÄÄÄÄ ³ ³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 7-3. Loadtime dynlink fixups.
To summarize, instead of linking in the target code at linktime, the linker
places a module name and an entry point name into the .EXE file. When the
program is loaded (that is, executed), OS/2 locates the target code, loads
it, and does the necessary linking. Although all we're doing is postponing
the linkage until loadtime, this technique has several important
ramifications. First, the target code is not in the .EXE file but in a
separate dynamic link library (.DLL) file. Thus, the .EXE file is smaller
because it contains only the name of the target code, not the code itself.
You can change or upgrade the target code at any time simply by replacing
this .DLL file. The next time a referencing application is loaded,
2. With some restrictions. See 7.11.2 Dynlink Life, Death, and Sharing.
2 it is
linked to the new version of the target code. Finally, having the target
code in a .DLL file paves the way for automatic code sharing. OS/2 can
easily understand that two applications are using the same dynlink code
because it loaded and linked that code, and it can use this knowledge to
share the pure segments of that dynlink package rather than loading
duplicate copies.
A final advantage of dynamic linking is that it's totally invisible to
the user, and it can even be invisible to the programmer. You need to understand dynamic linking to create a dynamic link module, but you can use one without even knowing that it's not an ordinary static link. The one disadvantage of dynamic linking is that programs sometimes take longer to load into memory than do those linked with static linking. The good news about dynamic linking is that the target code(s) are separate from the main .EXE file; this is also the bad news. Because the target code(s) are separate from the main .EXE file, a few more disk operations may be necessary to load them.
The actual performance ramifications depend on the kind of dynlink
module that is referenced and whether this .EXE file is the first to reference the module. This is discussed in more detail in 7.11 Implementation Details.
Although this discussion has concentrated on processes calling dynlink
routines, dynlink routines can in fact be called by other dynlink routines. When OS/2 loads a dynlink routine in response to a process's request, it examines that routine to see if it has any dynlink references of its own. Any such referenced dynlink routines are also loaded and so on until no unsatisfied dynlink references remain.
7.3 Runtime Dynamic Linking
The dynamic linking that we have been describing is called load-time dynamic linking because it occurs when the .EXE file is loaded. All dynamic link names need not appear in the .EXE file at loadtime; a process can link itself to a dynlink package at runtime as well. Runtime dynamic linking works exactly like loadtime dynamic linking except that the process creates the dynlink module and entry point names at runtime and then passes them to OS/2 so that OS/2 can locate and load the specified dynlink code.
Runtime linking takes place in four steps.
1. The process issues a DosLoadModule call to tell OS/2 to locate and load the dynlink code into memory.
2. The DosGetProcAddr call is used to obtain the addresses of the routines that the process wants to call.
3. The process calls the dynlink library entry points by means of an indirect call through the address returned by DosGetProcAddr.
4. When the process has no more use for the dynlink code, it can call DosFreeModule to release the dynlink code. After this call, the process will still have the addresses returned by DosGetProcAddr, but they will be illegal addresses; referencing them will cause a GP fault.
Runtime dynamic links are useful when a program knows that it will
want to call some dynlink routines but doesn't know which ones. For example, a charting program may support four plotters, and it may want to use dynlink plotter driver packages. It doesn't make sense for the application to contain loadtime dynamic links to all four plotters because only one will be used and the others will take up memory and swap space. Instead, the charting program can wait until it learns which plotter is installed and then use the runtime dynlink facility to load the appropriate package. The application need not even call DosLoadModule when it initializes; it can wait until the user issues a plot command before it calls DosLoadModule, thereby reducing memory demands on the system.
The application need not even be able to enumerate all the modules or
entry points that may be called. The application can learn the names of the dynlink modules from another process or by looking in a configuration file. This allows the user of our charting program, for example, to install additional plotter drivers that didn't even exist at the time that the application was written. Of course, in this example the calling sequences of the dynlink plotter driver must be standardized, or the programmer must devise a way for the application to figure out the proper way to call these newly found routines.
Naturally, a process is not limited to one runtime dynlink module;
multiple calls to DosLoadModule can be used to link to several dynlink modules simultaneously. Regardless of the number of modules in use, DosFreeModule should be used if the dynlink module will no longer be used and the process intends to continue executing. Issuing DosFreeModules is unnecessary if the process is about to terminate; OS/2 releases all dynlink modules at process termination time.
7.4 Dynlinks, Processes, and Threads
Simply put, OS/2 views dynlinks as a fancy subroutine package. Dynlinks aren't processes, and they don't own any resources. A dynlink executes only because a thread belonging to a client process called the dynlink code. The dynlink code is executing as the client thread and process because, in the eyes of the system, the dynlink is merely a subroutine that process has called. Before the client process can call a dynlink package, OS/2 ensures that the dynlink's segments are in the address space of the client. No ring transition or context switching overhead occurs when a client calls a dynlink routine; the far call to a dynlink entry point is just that--an ordinary far call to a subroutine in the process's address space.
One side effect is that dynlink calls are very fast; little CPU time
is spent getting to the dynlink package. Another side effect is no separation between a client's segments and a dynlink package's segments 3. Subsystem dynlink packages may be sensitive to this. For detailed information, see 7.11.1 Dynlink Data Security. 3 because segments belong to processes and only one process is running both the client and the dynlink code. The same goes for file handles, semaphores, and so on.
7.5 Data
The careful reader will have noticed something missing in this discussion of dynamic linking: We've said nothing about how to handle a dynlink routine's data. Subroutines linked with static links have no problem with having their own static data; when the linker binds the external code with the main code, it sees how much static data the external code needs and allocates the necessary space in the proper data segment(s). References that the external code makes to its data are then fixed up to point to the proper location. Because the linker is combining all the .OBJ files into a .EXE file, it can easily divide the static data segment(s) among the various compilands.
This technique doesn't work for dynamic link routines because their
code and therefore their data requirements aren't present at linktime. It's possible to extend the special dynlink .OBJ file to describe the amount of static data that the dynlink package will need, but it won't work. 4. And even if it did work, it would be a poor design because it would restrict our ability to upgrade the dynlink code in the field. 4 Because the main code in each application uses different amounts of static data, the data area reserved for the dynlink package would end up at a different offset in each .EXE file that was built. When these .EXE files were executed, the one set of shared dynlink code segments would need to reference the data that resides at different addresses for each different client. Relocating the static references in all dynlink code modules at each occurrence of a context switch is clearly out of the question.
An alternative to letting dynamic link routines have their own static
data is to require that their callers allocate the necessary data areas and pass pointers to them upon every call. We easily rejected this scheme: It's cumbersome; call statements must be written differently if they're for a dynlink routine; and, finally, this hack wouldn't support subsystems, which are discussed below.
Instead, OS/2 takes advantage of the segmented architecture of the
80286. Each dynamic link routine can use one or more data segments to hold its static data. Each client process has a separate set of these segments. Because these segments hold only the dynlink routine's data and none of the calling process's data, the offsets of the data items within that segment will be the same no matter which client process is calling the dynlink code. All we need do to solve our static data addressability problem is ensure that the segment selectors of the dynlink routine's static data segments are the same for each client process.
OS/2 ensures that the dynlink library's segment selectors are the same
for each client process by means of a technique called the disjoint LDT space. I won't attempt a general introduction to the segmented architecture of the 80286, but a brief summary is in order. Each process in 80286 protect mode can have a maximum of 16,383 segments. These segments are described in two tables: the LDT (Local Descriptor Table) and the GDT (Global Descriptor Table). An application can't read from or write to these tables. OS/2 manages them, and the 80286 microprocessor uses their contents when a process loads selectors into its segment registers.
In practice, the GDT is not used for application segments, which
leaves the LDT 8192 segments--or, more precisely, 8192 segment selectors, which OS/2 can set up to point to memory segments. The 80286 does not support efficient position-independent code, so 80286 programs contain within them, as part of the instruction stream, the particular segment selector needed to access a particular memory location, as well as an offset within that segment. This applies to both code and data references.
When OS/2 loads a program into memory, the .EXE file describes the
number, type, and size of the program's segments. OS/2 creates these segments and allocates a selector for each from the 8192 possible LDT selectors. There isn't any conflict with other processes in the system, at this point, because each process has its own LDT and its own private set of 8192 LDT selectors. After OS/2 chooses a selector for each segment, both code and data, it uses a table of addresses provided in the .EXE file to relocate each segment reference in the program, changing the place holder value put there by the linker into the proper segment selector value. OS/2 never combines or splits segments, so it never has to relocate the offset part of addresses, only the segment parts. Address offsets are more common than segment references. Because the segment references are relatively few, this relocation process is not very time-consuming.
If OS/2 discovers that the process that it's loading references a
dynlink routine--say, our old friend Foo--the situation is more complex. For example, suppose that the process isn't the first caller of Foo; Foo is already in memory and already relocated to some particular LDT slots in the LDT of the earlier client of Foo. OS/2 has to fill in those same slots in the new process's LDT with pointers to Foo; it can't assign different LDT slots because Foo's code and data have already been relocated to the earlier process's slots. If the new process is already using Foo's slot numbers for something else, then we are in trouble. This is a problem with all of Foo's segments, both data segments and code segments.
This is where the disjoint LDT space comes in. OS/2 reserves many of
each process's LDT slots 5. In version 1.0, more than half the LDT slots are reserved for this disjoint area. 5 for the disjoint space. The same slot numbers are reserved in every process's LDT. When OS/2 allocates an LDT selector for a memory segment that may be shared between processes, it allocates an entry from the disjoint LDT space. After a selector is allocated, that same slot in all other LDTs in the system is reserved. The slot either remains empty (that is, invalid) or points to this shared segment; it can have no other use. This guarantees that a process that has been running for hours and that has created dozens of segments can still call DosLoadModule to get access to a dynlink routine; OS/2 will find that the proper slots in this process's LDT are ready and waiting. The disjoint LDT space is used for all shared memory objects, not just dynlink routines. Shared memory data segments are also allocated from the disjoint LDT space. A process's code segments are not allocated in the disjoint LDT space, yet they can still be shared. 6. The sharing of pure segments between multiple copies of the same program is established when the duplicate copies are loaded. OS/2 will use the same selector to do segment mapping as it did when it loaded the first copy, so these segments can be shared even though their selectors are not in the disjoint space. 6 Figure 7-4 illustrates the disjoint LDT concept. Bullets in the shaded selectors denote reserved but invalid disjoint selectors. These are reserved in case that process later requests access to the shared memory segments that were assigned those disjoint slots. Only process A is using the dynlink package DLX, so its assigned disjoint LDT slots are reserved for it in Process B's LDT as well as in the LDT of all other processes in the system. Both processes are using the dynlink package DLY.
Process A Process B
Segment table Segment table
(LDT) for A (LDT) for B ÚÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄ¿ Process ÚÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄ¿ ³ ÃÄÄÄ´ ³ A's ³ ³ ÚÄ´ ³ Process ÃÄÄÄÄÄÄÄ´ ³ ³ segments ÃÄÄÄÄÄÄÄ´ ³ ³ ³ B's ³°°°°°°°³ ÀÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄ¿ ³°°°°°°°³ ³ ÀÄÄÄÄÄÄÄÄÙ segments ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ÃÄÄÄÄÄÄÄ´ ³ ÚÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ ³ ³ ÃÄÙ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ÚÄÄÄÄÄÄÄÄ¿ ÀÄÄÄÄÄÄÄÄÙ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄÄ´ ³ ³°°°°°°°³ ³Ú´ ³ ³°°°°°°°³ ³ ÀÄÄÄÄÄÄÄÄÙ ÃÄÄÄÄÄÄÄ´ ³³³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÙ³ÀÄÄÄÄÄÄÄÄÙ ³ ÃÄÄÄÄÙ ÃÄÄÄÄÄÄÄ´ ³ ÃÄÄÄÄÄÄÄ´ ³°°°°°°°³ ³ ³°°°°°°°³ ÃÄÄÄÄÄÄÄ´ ³ ÃÄÄÄÄÄÄÄ´ ³ ÃÄÄÙ ³ ³ ÃÄÄÄÄÄÄÄ´ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄ¿ ³°°°�°°°³ ³°°°°°°°ÃÄÄÄ´ ³ Dynlink ÃÄÄÄÄÄÄÄ´ ÃÄÄÄÄÄÄÄ´ ³ ³ DLZ's ³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÙ segments ÃÄÄÄÄÄÄÄ´ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄ¿ ³°°°�°°°³ ³°°°°°°°ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ÃÄÄÄÄÄÄÄ´ ÃÄÄÄÄÄÄÄ´ ³ ³ ³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÙ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄ¿ Dynlink ÃÄÄÄÄÄÄÄ´ ³°°°°°°°ÃÄÄÄ´ ³ DLX's ³°°°�°°°³ ÃÄÄÄÄÄÄÄ´ ³ ³ segments ÃÄÄÄÄÄÄÄ´ ³ ³ ÀÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄ¿ ³ ³ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄ´ ³ ÃÄÄÄÄÄÄÄ´ ³°°°°°°°ÃÄÄÄÄÄÄÄÙ ³ ³ ³°°°�°°°³ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄ¿ ÀÄÄÄÄÄÄÄÄÙ ÃÄÄÄÄÄÄÄ´ ³ ³ ÚÄ´ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ³°°°°°°°ÃÄÙ ÀÄÄÄÄÄÄÄÄÙ ³°°°�°°°³ ÃÄÄÄÄÄÄÄ´ ÃÄÄÄÄÄÄÄ´ ³ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ÃÄÄÄÄÄÄÄ´ ³°°°°°°°³ ³°°°°°°°³ ÃÄÄÄÄÄÄÄ´ ÃÄÄÄÄÄÄÄ´ ³ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄ¿ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄ¿ ³°°°°°°°ÃÄÄÄ´ ³ Dynlink ³°°°°°°°ÃÄÄÄ´ ³ Dynlink ÃÄÄÄÄÄÄÄ´ ³ ³ DLY's ÃÄÄÄÄÄÄÄ´ ³ ³ DLY's ³ ³ ÀÄÄÄÄÄÄÄÄÙ segments ³ ³ ÀÄÄÄÄÄÄÄÄÙ segments ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄ¿ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄ¿ ³°°°°°°°ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³°°°°°°°ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÙ ³ ³ ÀÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÙ
Figure 7-4. The disjoint LDT space.
7.5.1 Instance Data
OS/2 supports two types of data segments for dynlink routines--instance
and global. Instance data segments hold data specific to each instance of
the dynlink routine. In other words, a dynlink routine has a separate set
of instance data segments for each process using it. The dynlink code has
no difficulty addressing its data; the code can reference the data segment
selectors as immediate values. The linker and OS/2's loader conspire so
that the proper selector value is in place when the code executes.
The use of instance data segments is nearly invisible both to the
client process and to the dynlink code. The client process simply calls the dynlink routine, totally unaffected by the presence or absence of the routine's instance data segment(s). A dynlink routine can even return addresses of items in its data segments to the client process. The client cannot distinguish between a dynlink routine and a statically linked one. Likewise, the code that makes up the dynlink routine doesn't need to do anything special to use its instance data segments. The dynlink code was assembled or compiled with its static data in one or more segments; the code itself references those segments normally. The linker and OS/2 handle all details of allocating the disjoint LDT selectors, loading the segments, fixing up the references, and so on.
A dynlink routine that uses only instance data segments (or no data
segments at all) can be written as a single client package, as would be a statically linked subroutine. Although such a dynlink routine may have multiple clients, the presence of multiple clients is invisible to the routine itself. Each client has a separate copy of the instance data segment(s). When a new client is created, OS/2 loads virgin copies of the instance data segments from the .DLL file. The fact that OS/2 is sharing the pure code segments of the routine has no effect on the operation of the routine itself.
7.5.2 Global Data
The second form of data segment available to a dynlink routine is a global
data segment. A global data segment, as the name implies, is not duplicated
for each client process. There is only one copy of each dynlink module's
global data segment(s); each client process is given shared access to that
segment. The segment is loaded only once--when the dynlink package is first
brought into memory to be linked with its first client process. Global data
segments allow a dynlink routine to be explicitly aware of its multiple
clients because changes to a global segment made by calls from one client
process are visible to the dynlink code when called from another client
process. Global data segments are provided to support subsystems, which are
discussed later. Figure 7-5 illustrates a dynlink routine with both
instance and global data segments.
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿
³ ³
³ Code Segment(s) ³
³ ³
ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿
³ ³ ÚÁÄÄÄÄÄÄÄÄÄÄÄÄÄ¿³
³ Global ³ ÚÁÄÄÄÄÄÄÄÄÄÄÄÄÄ¿³³
³ data ³ ³ ³³³
³ segment(s) ³ ³ Instance ³³³
³ ³ ³ data ³³³
ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ segment(s) ³ÃÙ
³ ÃÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 7-5. Dynlink segments.
7.6 Dynamic Link Packages As Subroutines
Dynamic link subroutines (or packages) generally fall into two categories-- subroutines and subsystems. As we discussed earlier, a dynamic link subroutine is written and executes in much the same way as a statically linked subroutine. The only difference is in the preparation of the dynamic link library file, which contains the actual subroutines, and in the preparation of the special .OBJ file, to which client programs can link. During execution, both the dynlink routines and the client routines can use their own static data freely, and they can pass pointers to their data areas back and forth to each other. The only difference between static linking and dynamic linking, in this model, is that the dynlink routine cannot reference any external symbols that the client code defines, nor can the client externally reference any dynlink package symbols other than the module entry points. Figure 7-6 illustrates a dynamic link routine being used as a subroutine. The execution environment is nearly identical to that of a traditional statically linked subroutine; the client and the subroutine each reference their own static data areas, all of which are contained in the process's address space. Note that a dynlink package can reference the application's data and the application can reference the dynlink package's data, but only if the application or the dynlink package passes a pointer to its data to the other.
Process address space
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ far calls far calls ³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ far ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ÃÄÄ�³ ³calls³ Dynlink ÃÄÄ�³ Dynlink ³ ³ ³ ³ APP code ³ ³ APP code ÃÄÄÄÄ�³ code ³ ³ code ³ ³ ³ ³ segment #1 ³�ÄÄ´ segment #n ³ ³ segment #1 ³�ÄÄ´ segment #n ³ ³ ³ ÀÄÄÂÄÄÄÄÄÄÄÂÄÙ ÀÄÂÄÄÄÄÄÄÄÂÄÄÙ ÀÄÄÂÄÄÄÄÄÄÄÂÄÙ ÀÄÂÄÄÄÄÄÄÄÂÄÄÙ ³ ³ ³ ÚÄÅÄÄÄÄÄÄÄÙ data ³ ³ ÚÄÅÄÄÄÄÄÄÄÙ data ³ ³ ³ ³ ³ ³ references³ ³ ³ ³ references³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ÚÄÄ�ÄÄÄÄÄ�ÄÄÄ¿ ÚÄÄÄ�ÄÄÄÄÄ�ÄÄ¿ ÚÄÄ�ÄÄÄÄÄ�ÄÄÄ¿ ÚÄÄÄ�ÄÄÄÄÄ�ÄÄ¿ ³ ³ ³ ³ ³ ³ ³ Dynlink ³ ³ Dynlink ³ ³ ³ ³ APP data ³ ³ APP data ³ ³ data ³ ³ data ³ ³ ³ ³ segment #1 ³ ³ segment #n ³ ³ segment #1 ³ ³ segment #n ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 7-6. Dynamic link routines as subroutines.
7.7 Subsystems
The term dynlink subsystems refers to the design and intended function of a particular style of dynlink package and is somewhat artificial. Although OS/2 provides special features to help support subsystems, OS/2 does not actually classify dynlink modules as subroutines or subsystems; subsystem is merely a descriptive term.
The term subsystem refers to a dynlink module that provides a set of
services built around a resource. 7. In the most general sense of the word. I don't mean a "presentation manager resource object." 7 For example, OS/2's VIO dynlink entry points are considered a dynlink subsystem because they provide a set of services to manage the display screen. A subsystem usually has to manage a limited resource for an effectively unlimited number of clients; VIO does this, managing a single physical display controller and a small number of screen groups for an indefinite number of clients.
Because subsystems generally manage a limited resource, they have one
or more global data segments that they use to keep information about the state of the resource they're controlling; they also have buffers, flags, semaphores, and so on. Per-client work areas are generally kept in instance data segments; it's best to reserve the global data segment(s) for global information. Figure 7-7 illustrates a dynamic link routine being used as a subsystem. A dynlink subsystem differs from a dynlink being used as a subroutine only by the addition of a static data segment.
Process address space
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ far calls far calls ³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ far ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ÃÄÄ�³ ³calls³ Dynlink ÃÄÄ�³ Dynlink ³ ³ ³ ³ APP code ³ ³ APP code ÃÄÄÄÄ�³ code ³ ³ code ³ ³ ³ ³ segment #1 ³�ÄÄ´ segment #n ³ ³ segment #1 ³�ÄÄ´ segment #n ³ ³ ³ ÀÄÄÂÄÄÄÄÄÄÄÂÄÙ ÀÄÂÄÄÄÄÄÄÄÂÄÄÙ ÀÄÄÂÄÄÄÄÄÄÄÂÄÙ ÀÄÂÄÄÄÄÄÄÄÂÄÄÙ ³ ³ ³ ÚÄÅÄÄÄÄÄÄÄÙ data ³ ³ ÚÄÅÄÄÄÄÄÄÄÙ data ³ ³ ³ ³ ³ ³ references³ ³ ³ ³ references³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ÚÍÍ�ÍÍÍÍÍ�ÍÍÍ» ÚÍÍÍ�ÍÍÍÍÍ�ÍÍ» ÚÄÄ�ÄÄÄÄÄ�ÄÄÄ¿ ÖÄÄÄÁÄÄÄÄÄÁÄÄ¿ ³ ³ ³ º ³ º ³ global ³ º instance ³ ³ ³ ³ APP data º ³ APP data º ³ data ³ º data ³ ³ ³ ³ segment #1 º ³ segment #n º ³ segment ³ º segment ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄĽ ÀÄÄÄÄÄÄÄÄÄÄÄĽ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ÈÍÍÍÍÍÍÍÍÍÍÍÍÙ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 7-7. Dynamic link routines as subsystems.
7.7.1 Special Subsystem Support
Two OS/2 features are particularly valuable to subsystems: global data
segments (which we've already discussed) and special client initialization
and termination support. Clearly, if a subsystem is going to manage a
resource, keeping track of its clients in a global data segment, it needs
to know when new clients arrive and when old clients terminate. The simple
dynlink subroutine model doesn't provide this information in a reliable
fashion. A subsystem undoubtedly has initialize and terminate entry points,
but client programs may terminate without having called a subsystem's
terminate entry point. Such a failure may be an error on the part of the
client, but the system architecture decrees that errors should be
localized; it's not acceptable for a bug in a client process to be able to
hang up a subsystem and thus all its clients as well.
The two forms of subsystem initialization are global and instance. A
subsystem can specify either service but not both. If global initialization is specified, the initialization entry point is called only once per activation of the subsystem. When the subsystem dynlink package is first referenced, OS/2 allocates the subsystem's global data segment(s), taking their initial values from the .DLL file. OS/2 then calls the subsystem's global initialization entry point so that the module can do its one-time initialization. The thread that is used to call the initialization entry point belongs to that first client process, 8. The client process doesn't explicitly call a dynlink package's initialization entry points. OS/2 uses its godlike powers to borrow a thread for the purpose. The mechanism is invisible to the client program. It goes without saying, we hope, that it would be extremely rude to the client process, not to say damaging, were the dynlink package to refuse to return that initialization thread or if it were to damage it in some way, such as lowering its priority or calling DosExit with it! 8 so the first client's instance data segments are also set up and may be used by the global initialization process. This means that although the dynlink subsystem is free to open files, read and write their contents, and close them again, it may not open a handle to a file, store the handle number in a global data segment, and expect to use that handle in the future.
Remember, subsystems don't own resources; processes own resources.
When a dynlink package opens a file, that file is open only for that one client process. That handle has meaning only when that particular client is calling the subsystem code. If a dynlink package were to store process A's handle number in a global data segment and then attempt to do a read from that handle when running as process B, at best the read would fail with "invalid handle"; at worst some unrelated file of B's would be molested. And, of course, when client process A eventually terminates, the handle becomes invalid for all clients.
The second form of initialization is instance initialization. The
instance initialization entry point is called in the same way as the global initialization entry point except that it is called for every new client when that client first attaches to the dynlink package. Any instance data segments that exist will already be allocated and will have been given their initial values from the .DLL file. The initialization entry point for a loadtime dynlink is called before the client's code begins executing. The initialization entry point for a runtime dynlink is called when the client calls the DosLoadModule function. A dynlink package may not specify both global and instance initialization; if it desires both, it should specify instance initialization and use a counter in one of its global data segments to detect the first instance initialization.
Even more important than initialization control is termination
control. In its global data area, a subsystem may have records, buffers, or semaphores on behalf of a client process. It may have queued-up requests from that client that it needs to purge when the client terminates. The dynlink package need not release instance data segments; because these belong to the client process, they are destroyed when the client terminates. The global data segments themselves are released if this is the dynlink module's last client, so the module may want to take this last chance to update a log file, release a system semaphore, and so on.
Because a dynlink routine runs as the calling client process, it could
use DosSetSigHandler to intercept the termination signal. This should never be done, however, because the termination signal is not activated for all causes of process termination. For example, if the process calls DosExit, the termination signal is not sent. Furthermore, there can be only one handler per signal type per process. Because client processes don't and shouldn't know what goes on inside a dynlink routine, the client process and a dynlink routine may conflict in the use of the signal. Such a conflict may also occur between two dynlink packages.
Using DosExitList service prevents such a collision. DosExitList
allows a process to specify one or more subroutine addresses that will be called when the process terminates. Addresses can be added to and removed from the list. DosExitList is ideally suited for termination control. There can be many such addresses, and the addresses are called under all termination conditions. Both the client process and the subsystem dynlinks that it calls can have their own termination routine or routines. DosExitList is discussed in more detail in 16.2 Data Integrity.
7.8 Dynamic Links As Interfaces to Other Processes
Earlier, I mentioned that dynlink subsystems have difficulty dealing with resources--other than global memory--because resource ownership and access are on a per-process basis. Life as a dynlink subsystem can be schizophrenic. Which files are open, which semaphores are owned and so on depends on which client is running your code at the moment. Global memory is different; it's the one resource that all clients own jointly. The memory remains as long as the client count doesn't go to zero.
One way to deal with resource issues is for a dynlink package to act
as a front end for a server process. During module initialization, the dynlink module can check a system semaphore to see whether the server process is already running and, if not, start it up. It needs to do this with the "detach" form of DosExecPgm so that the server process doesn't appear to the system as a child of the subsystem's first client. Such a mistake could mean that the client's parent thinks that the command subtree it founded by running the client never terminates because the server process appears to be part of the command subtree (see Figure 7-8).
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Grandparent ³ ³ Grandparent ³ ÀÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÙ ³ ³
ÚÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄ¿ ³°°°°°°°°°°°³°°°°°°°°°°°³ ³°°°°°°°°°°°³°°°°°°°°°°°³ Ú �³ Daemon ³ ³°°ÚÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄ¿°°³ ³°°ÚÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄ¿°°³ | ÀÄÄÄÄÄÄÄÄÄÄÙ ³°°³ Process ³°°³ ³°°³ Process ³Ä Å Ä Ù ³°°ÀÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÙ°°³ ³°°ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ°°³ ³°°°°°°°°°°°³°°°°°°°°°°°³ ³°°° Command subtree °°°³ ³°°ÚÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄ¿°°³ ³°°°°°°°°°°°°°°°°°°°°°°°³ ³°°³ Daemon ³°°³ ³°°°°°°°°°°°°°°°°°°°°°°°³ ³°°ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ°°³ ³°°°°°°°°°°°°°°°°°°°°°°°³ ³°°° Command subtree °°°³ ³°°°°°°°°°°°°°°°°°°°°°°°³ ³°°°°°°°°°°°°°°°°°°°°°°°³ ³°°°°°°°°°°°°°°°°°°°°°°°³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 7-8. Dynlink daemon initiation.
When the server process is running, the dynlink subsystem can forward
some or all requests to it by one of the many IPC facilities. For example, a database subsystem might want to use a dedicated server process to hold open the database file and do reads and writes to it. It might keep buffers and ISAM directories in a shared memory segment to which the dynlink subsystem requests access for each of its clients; then requests that can be satisfied by data from these buffers won't require the IPC to the server process.
The only function of some dynlink packages is to act as a procedural
interface to another process. For example, a spreadsheet program might provide an interface through which other applications can retrieve data values from a spreadsheet. The best way to do this is for the spreadsheet package to contain a dynamic link library that provides clients a procedural interface to the spreadsheet process. The library routine itself will invoke a noninteractive copy (perhaps a special subset .EXE) of the spreadsheet to recover the information, passing it back to the client via IPC. Alternatively, the retrieval code that understands the spreadsheet data formats could be in the dynlink package itself because that package ships with the spreadsheet and will be upgraded when the spreadsheet is. In this case, the spreadsheet itself could use the package instead of duplicating the functionality in its own .EXE file. In any case, the implementation details are hidden from the client process; the client process simply makes a procedure call that returns the desired data.
Viewed from the highest level, this arrangement is simple: A client
process uses IPC to get service from a server process via a subroutine library. From the programmer's point of view, though, the entire mechanism is encapsulated in the dynlink subsystem's interface. A future upgrade to the dynlink package may use an improved server process and different forms of IPC to talk to it but retain full binary compatibility with the existing client base. Figure 7-9 illustrates a dynlink package being used as an interface to a daemon process. The figure shows the dynlink package interfacing with the daemon process by means of a shared memory segment and some other form of IPC, perhaps a named pipe.
client process | daemon process |
ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ Far call ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ | ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ APP ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ�³ Dynlink ³�ÄÄÄÄÄÄ|ÄÄÄÄÄÄÄÄ´ Daemon ³ ³ code ³ ³ code ³ IPC ³ code ³ ³ segment(s) ³ ³ segment(s) ÃÄÄÄÄÄÄÄ|ÄÄÄÄÄÄÄ�³ segment(s) ³ ÀÄÄÄÄÄÂÄÄÄÄÄÄÙ ÀÄÄÄÄÄÂÄÄÄÂÄÄÙ | ÀÄÄÂÄÄÄÂÄÄÄÄÄÙ
³ ³ ³ | ³ ³ ³ ³ ÀÄÄÄÄÄÄÄ¿ | ÚÄÄÄÄÄÄÄÙ ³ ³ ³ ³ | ³ ³
ÚÄÄÄÄÄ�ÄÄÄÄÄÄ¿ ÚÄÄÄÄÄ�ÄÄÄÄÄÄ¿ ÚÄÄ�ÄÄ|ÄÄÄ�ÄÄ¿ ÚÄÄÄÄÄÄ�ÄÄÄÄÄ¿ ³ APP ³ ³ Dynlink ³ ³ | ³ ³ Daemon ³ ³ data ³ ³ data ³ ³ Shared ³ ³ data ³ ³ segment(s) ³ ³ segment(s) ³ ³ memory ³ ³ segment(s) ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄ|ÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ
|
Figure 7-9. Dynamic link routines as daemon interfaces.
7.9 Dynamic Links As Interfaces to the Kernel
We've seen how dynlink libraries can serve as simple subroutine libraries, how they can serve as subsystems, and how they can serve as interfaces to other processes. OS/2 has one more trick up its sleeve: Dynlink libraries can also serve as interfaces to OS/2 itself.
Some OS/2 calls are actually implemented as simple library routines.
For example, DosErrClass is implemented in OS/2 version 1.0 as a simple library routine. It takes an error code and locates, in a table, an explanatory text string, an error classification, and a recommended action. Services such as these were traditionally part of the kernel of operating systems, not because they needed to use privileged instructions, but because their error tables needed to be changed each time an upgrade to the operating system was released. If the service has been provided as a statically linked subroutine, older applications running on newer releases would receive new error codes that would not be in the library code's tables.
Although OS/2 implements DosErrClass as a library routine, it's a
dynlink library routine, and the .DLL file is bundled with the operating system itself. Any later release of the system will contain an upgraded version of the DosErrClass routine, one that knows about new error codes. Consequently, the dynlink facility provides OS/2 with a great deal of flexibility in packaging its functionality.
Some functions, such as "open file" or "allocate memory," can't be
implemented as ordinary subroutines. They need access to key internal data structures, and these structures are of course protected so that they can't be changed by unprivileged code. To get these services, the processor must make a system call, entering the kernel code in a very controlled fashion and there running with sufficient privilege to do its work. This privilege transition is via a call gate--a feature of the 80286/80386 hardware. A program calls a call gate exactly as it performs an ordinary far call; special flags in the GDT and LDT tell the processor that this is a call gate rather than a regular call.
In OS/2, system calls are indistinguishable from ordinary dynlink
calls. All OS/2 system calls are defined in a dynlink module called DosCalls. When OS/2 fixes up dynlink references to this module, it consults a special table, built into OS/2, of resident functions. If the function is not listed in this table, then an ordinary dynlink is set up. If the function is in the table, OS/2 sets up a call gate call in place of the ordinary dynlink call. The transparency between library and call gate functions explains why passing an invalid address to an OS/2 system call causes the calling process to GP fault. Because the OS/2 kernel code controls and manages the GP fault mechanism, OS/2 calls that are call gates could easily return an error code if an invalid address causes a GP fault. If this were done, however, the behavior of OS/2 calls would differ depending on their implementation: Dynlink entry points would GP fault for invalid addresses; 9. The LAR and LSL instructions are not sufficient to prevent this because another thread in that process may free a segment after the LAR but before the reference. 9 call gate entries would return an error code. OS/2 prevents this dichotomy and preserves its freedom to, in future releases, move function between dynlink and call gate entries by providing a uniform reaction to invalid addresses. Because non-call-gate dynlink routines must generate GP faults, call gate routines produce them as well.
7.10 The Architectural Role of Dynamic Links
Dynamic links play three major roles in OS/2: They provide the system interface; they provide a high-bandwidth device interface; and they support open architecture nonkernel service packages.
The role of dynamic links as the system interface is clear. They
provide a uniform, high-efficiency interface to the system kernel as well as a variety of nonkernel services. The interface is directly compatible with high-level languages, and it takes advantage of special speed- enhancing features of the 80286 and 80386 microprocessors. 10. Specifically, automatic argument passing on calls to the ring 0 kernel code. 10 It provides a wide and convenient name space, and it allows the distribution of function between library code and kernel code. Finally, it provides an essentially unlimited expansion capability.
But dynamic links do much more than act as system calls. You'll recall
that in the opening chapters I expressed a need for a device interface that was as device independent as device drivers but without their attendant overhead. Dynamic links provide this interface because they allow applications to make a high-speed call to a subroutine package that can directly manipulate the device (see Chapter 18, I/O Privilege Mechanism and Debugging/Ptrace). The call itself is fast, and the package can specify an arbitrarily wide set of parameters. No privilege or ring transition is needed, and the dynlink package can directly access its client's data areas. Finally, the dynlink package can use subsystem support features to virtualize the device or to referee its use among multiple clients. Device independence is provided because a new version of the dynlink interface can be installed whenever new hardware is installed. VIO and the presentation manager are examples of this kind of dynlink use. Dynlink packages have an important drawback when they are being used as device driver replacements: They cannot receive hardware interrupts. Some devices, such as video displays, do not generate interrupts. Interrupt-driven devices, though, require a true device driver. That driver can contain all of the device interface function, or the work can be split between a device driver and a dynlink package that acts as a front end for that device driver. See Chapters 17 and 18 for further discussion of this.
Dynlink routines can also act as nonkernel service packages--as an
open system architecture for software. Most operating systems are like the early versions of the Apple Macintosh computer: They are closed systems; only their creators can add features to them. Because of OS/2's open system architecture, third parties and end users can add system services simply by plugging in dynlink modules, just as hardware cards plug into an open hardware system. The analogy extends further: Some hardware cards become so popular that their interface defines a standard. Examples are the Hayes modem and the Hercules Graphics Card. Third-party dynlink packages will, over time, establish similar standards. Vendors will offer, for example, improved database dynlink routines that are advertised as plug compatible with the standard database dynlink interface, but better, cheaper, and faster.
Dynlinks allow third parties to add interfaces to OS/2; they also
allow OS/2's developers to add future interfaces. The dynlink interface model allows additional functionality to be implemented as subroutines or processes or even to be distributed across a network environment.
7.11 Implementation Details
Although dynlink routines often act very much like traditional static subroutines, a programmer must be aware of some special considerations involved. This section discusses some issues that must be dealt with to produce a good dynlink package.
7.11.1 Dynlink Data Security
We have discussed how a dynlink package runs as a subroutine of the client
process and that the client process has access to the dynlink package's
instance and global data segments.
11. A client process has memory access (addressability) to all of
the package's global segments but only to those instance data
segments associated with that process.
11 This use of the dynlink interface is
efficient and thus advantageous, but it's also disadvantageous because
aberrant client processes can damage the dynlink package's global data
segments.
In most circumstances, accidental damage to a dynlink package's data
segments is rare. Unless the dynlink package returns pointers into its data segments to the client process, the client doesn't "know" the dynlink package's data segment selectors. The only way such a process could access the dynlink's segments would be to accidentally create a random selector value that matched one belonging to a dynlink package. Because the majority of selector values are illegal, a process would have to be very "lucky" to generate a valid dynlink package data selector before it generated an unused or code segment selector. 12.Because if a process generates and writes with a selector that is invalid or points to a code segment, the process will be terminated immediately with a GP fault. 12 Naturally, dynlink packages shouldn't use global data segments to hold sensitive data because a malicious application can figure out the proper selector values.
The measures a programmer takes to deal with the security issue depend
on the nature and sensitivity of the dynlink package. Dynlink packages that don't have global data segments are at no risk; an aberrant program can damage its instance data segments and thereby fail to run correctly, but that's the expected outcome of a program bug. A dynlink package with global data segments can minimize the risk by never giving its callers pointers into its (the dynlink package's) global data segment. If the amount of global data is small and merely detecting damage is sufficient, the global data segments could be checksummed.
Finally, if accidental damage would be grave, a dynlink package can
work in conjunction with a special dedicated process, as described above. The dedicated process can keep the sensitive data and provide it on a per- client basis to the dynlink package in response to an IPC request. Because the dedicated process is a separate process, its segments are fully protected from the client process as well as from all others.
7.11.2 Dynlink Life, Death, and Sharing
Throughout this discussion, I have referred to sharing pure segments. The
ability to share pure segments is an optimization that OS/2 makes for all
memory segments whether they are dynlink segments or an application's .EXE
file segments. A pure segment is one that is never modified during its
lifetime. All code segments (except for those created by DosCreateCSAlias)
are pure; read-only data segments are also pure. When OS/2 notices that
it's going to load two copies of the same pure segment, it performs a
behind-the-scenes optimization and gives the second client access to the
earlier copy of the segment instead of wasting memory with a duplicate
version.
For example, if two copies of a program are run, all code segments are
pure; at most, only one copy of each code segment will be in memory. OS/2 flags these segments as "internally shared" and doesn't release them until the last user has finished with the segment. This is not the same as "shared memory" as it is generally defined in OS/2. Because pure segments can only be read, never written, no process can tell that pure segments are being shared or be affected by that sharing. Although threads from two or more processes may execute the same shared code segment at the same time, this is not the same as a multithreaded process. Each copy of a program has its own data areas, its own stack, its own file handles, and so on. They are totally independent of one another even if OS/2 is quietly sharing their pure code segments among them. Unlike multiple threads within a single process, threads from different processes cannot affect one another; the programmer can safely ignore their possible existence in shared code segments.
Because the pure segments of a dynlink package are shared, the second
and subsequent clients of a dynlink package can load much more quickly (because these pure segments don't have to be loaded from the .DLL disk file). This doesn't mean that OS/2 doesn't have to "hit the disk" at all: Many dynlink packages use instance data segments, and OS/2 loads a fresh copy of the initial values for these segments from the .DLL file.
A dynlink package's second client is its second simultaneous client.
Under OS/2, only processes have a life of their own. Objects such as dynlink packages and shared memory segments exist only as possessions of processes. When the last client process of such an object dies or otherwise releases the object, OS/2 destroys it and frees up the memory. For example, when the first client (since bootup) of a dynlink package references it, OS/2 loads the package's code and data segments. Then OS/2 calls the package's initialization routine--if the package has one. OS/2 records in an internal data structure that this dynlink package has one client. If additional clients come along while the first is still using the dynlink package, OS/2 increments the package's user count appropriately. Each time a client disconnects or dies, the user count is decremented. As long as the user count remains nonzero, the package remains in existence, each client sharing the original global data segments. When the client count goes to zero, OS/2 discards the dynlink package's code and global data segments and in effect forgets all about the package. When another client comes along, OS/2 reloads the package and reloads its global data segment as if the earlier use had never occurred.
This mechanism affects a dynlink package only in the management of the
package's global data segment. The package's code segments are pure, so it doesn't matter if they are reloaded from the .DLL file. The instance data segments are always reinitialized for each new client, but the data in a package's global data segment remains in existence only as long as the package has at least one client process. When the last client releases the package, the global data segment is discarded. If this is a problem for a dynlink package, an associated "dummy" process (which the dynlink package could start during its loadtime initialization) can reference the dynlink package. As long as this process stays alive, the dynlink package and its global data segments stay alive. 13. If you use this technique, be sure to use the detached form of DosExec; see the warning in 7.8 Dynamic Links As Interfaces to Other Processes. 13
An alternative is for the dynlink package to keep track of the count
of its clients and save the contents of its global data segments to a disk file when the last client terminates, but this is tricky. Because a process may fail to call a dynlink package's "I'm finished" entry point (presumably part of the dynlink package's interface) before it terminates, the dynlink package must get control to write its segment via DosExitList. If the client process is connected to the dynlink package via DosLoadModule (that is, via runtime dynamic linking), it cannot disconnect from the package via DosFreeModule as long as a DosExitList address points into the dynlink package. An attempt to do so returns an error code. Typically, one would expect the application to ignore this error code; but because the dynlink package is still attached to the client process, it will receive DosExitList service when the client eventually terminates. It's important that dynlink packages which maintain client state information and therefore need DosExitList also offer an "I'm finished" function. When a client calls this function, the package should close it out and then remove its processing address from DosExitList so that DosFreeModule can take effect if the client wishes.
Note that OS/2's habit of sharing in-use dynlink libraries has
implications for the replacement of dynlink packages. Specifically, OS/2 holds the dynlink .DLL file open for as long as that library has any clients. To replace a dynlink library with an upgraded version, you must first ensure that all clients of the old package have been terminated.
While we're on the subject, I'll point out that dynlink segments, like
.EXE file segments, can be marked (by the linker) as "preload" or "load on demand." When a dynlink module or a .EXE file is loaded, OS/2 immediately loads all segments marked "preload" but usually 14. Segments that are loaded from removable media will be fully loaded, regardless of the "load on demand" bit. 14 does not load any segments marked "load on demand." These segments are loaded only when (and if) they are referenced. This mechanism speeds process and library loading and reduces swapping by leaving infrequently used segments out of memory until they are needed. Once a segment is loaded, its "preload" or "load on demand" status has no further bearing; the segment will be swapped or discarded without consideration for these bits.
Finally, special OS/2 code keeps track of dynamic link "circular
references." Because dynlink packages can call other dynlink packages, package A can call package B, and package B can call package A. Even if the client process C terminates, packages A and B might appear to be in use by each other, and they would both stay in memory. OS/2 keeps a graph of dynlink clients, both processes and other dynlink packages. When a process can no longer reach a dynlink package over this graph--in other words, when a package doesn't have a process for a client and when none of its client packages have processes for clients and so on--the dynlink package is released. Figure 7-10 illustrates a dynamic link circular reference. PA and PB are two processes, and LA through LG are dynlink library routines.
ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿
³°°°°°°°°°°°°³ ³°°°°°°°°°°°°³
³°°°°°PA°°°°°³ ³°°°°°PB°°°°°³
³°°°°°°°°°°°°³ ³°°°°°°°°°°°°³
ÀÄÄÄÄÄÂÄÄÄÄÄÄÙ ÀÄÂÄÄÄÄÄÄÄÄÂÄÙ
³ ³ ³
ÚÄÄÄÄÄ�ÄÄÄÄÄÄ¿ ³ ÚÄÄÄÄÄ�ÄÄÄÄÄÄ¿ ³ LA ³�ÄÄÄÄÄÄÄ¿ ³ ³ LF ³ ÀÄÄÂÄÄÄÄÄÄÂÄÄÙ ³ ³ ÀÄÄÄÄÄÂÄÄÄÄÄÄÙ
³ ÚÄÄÁÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ ³ LB ÃÄÙ ³ ÚÄÄÄÄÄ�ÄÄÄÄÄÄ¿ ³ ÀÄÂÄÄÄÄÄÄÄÄÄÄÙ ³ ³ LG ³ ³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÂÄÄÄÄÄÄÙ
ÚÄÄ�ÄÄÄÄÄ�ÄÄ�¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ LC ÃÄÄÄÄÄÄÄÄÄÄ�³ LD ³�ÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄ�ÄÄÄÄÄÄÙ ÀÄÄÄÄÄÂÄÄÄÄÄÄÙ
ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 7-10. Dynamic link circular references.
7.11.3 Dynlink Side Effects
A well-written dynlink library needs to adhere to the OS/2 religious tenet
of zero side effects. A dynlink library should export to the client process
only its functional interface and not accidentally export side effects that
may interfere with the consistent execution of the client.
Some possible side effects are obvious: A dynlink routine shouldn't
close any file handles that it didn't itself open. The same applies to other system resources that the client process may be accessing, and it applies in the inverse, as well: A dynlink routine that obtains resources for itself, in the guise of the client process, should do so in a way that doesn't affect the client code. For example, consuming many of the available file handles would be a side effect because the client would then unexpectedly be short of available file handles. A dynlink package with a healthy file handle appetite should be sure to call OS/2 to raise the maximum number of file handles so that the client process isn't constrained. Finally, the amount of available stack space is a resource that a dynlink package must not exhaust. A dynlink routine should try to minimize its stack needs, and an upgrade to an existing dynlink package must not consume much more stack space than did the earlier version, lest the upgrade cause existing clients to fail in the field.
Dynlink routines can also cause side effects by issuing some kinds of
system calls. Because a dynlink routine runs as a subroutine of the client process, it must be sure that calls that it makes to OS/2 on behalf of the client process don't affect the client application. For example, each signal event can have only one handler address; if a dynlink routine establishes a signal handler, then that signal handler preempts any handler set up by the client application. Likewise, if a dynlink routine changes the priority of the thread with which it was called, the dynlink routine must be sure to restore that priority before it returns to its caller. Several other system functions such as DosError and DosSetVerify also cause side effects that can affect the client process.
Enumerating all forms of side effects is not possible; it's up to the
programmer to take the care needed to ensure that a dynlink module is properly house-trained. A dynlink module should avoid the side effects mentioned as well as similar ones, and, most important, it should behave consistently so that if a client application passes its acceptance tests in the lab it won't mysteriously fail in the field. This applies doubly to upgrades for existing dynlink routines. Upgrades must be written so that if a client application works with the earlier release of the dynlink package it will work with the new release; obviously the author of the application will not have an opportunity to retest existing copies of the application against the new release of the dynlink module.
7.12 Dynlink Names
Each dynlink entry point has three names associated with it: an external name, a module name, and an entry point name. The name the client program calls as an external reference is the external name. The programmer works with this name, and its syntax and form must be compatible with the assembler or compiler being used. The name should be simple and explanatory yet unlikely to collide with another external name in the client code or in another library. A name such as READ or RESET is a poor choice because of the collision possibilities; a name such as XR23P11 is obviously hard to work with.
The linker replaces the external name with a module name and an entry
point name, which are embedded in the resultant .EXE file. OS/2 uses the module name to locate the dynlink .DLL file; the code for module modname is in file MODNAME.DLL. The entry point name specifies the entry point in the module; the entry point name need not be the same as the external name. For modules with a lot of entry points, the client .EXE file size can be minimized and the loading speed maximized by using entry ordinals in place of entry point names. See the OS/2 technical reference literature for details.
Runtime dynamic links are established by using the module name and the
entry point name; the external name is not used.
8 File System Name Space
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
File system name space is a fancy term for how names of objects are defined in OS/2. The words file system are a hint that OS/2 uses one naming scheme both for files and for everything else with a name in ASCII format--system semaphores, named shared memory, and so forth. First, we'll discuss the syntax of names and how to manipulate them; we'll wind up with a discussion of how and why we use one naming scheme for all named objects.
8.1 Filenames
Before we discuss OS/2 filenames, let's review the format of filenames under MS-DOS. In MS-DOS, filenames are required to fit the 8.3 format: a name field (which can contain a maximum of 8 characters) and an extension field (which can contain a maximum of 3 characters). 1. As an aside, these sizes date from a tradition established many years ago by Digital Equipment Corporation. Digital's very early computers used a technique called RAD50 to store 3 uppercase letters in one 16-bit word, so their file systems allowed a 6-character filename and a 3-character extension. CP/M later picked up this filename structure. CP/M didn't use RAD50, so, in a moment of generosity, it allowed 8-character filenames; but the 3-character extension was kept. 1 The period character (.) between the name and the extension is not part of the filename; it's a separator character. The filename can consist of uppercase characters only. If a user or an application creates a filename that contains lowercase characters or a mixture of uppercase and lowercase, MS-DOS converts the filename to all uppercase. If an application presents a filename whose name or extension field exceeds the allotted length, MS-DOS silently truncates the name to the 8.3 format before using it. MS-DOS establishes and enforces these rules and maintains the file system structure on the disks. The file system that MS-DOS version 3.x supports is called the FAT (File Allocation Table) file system. The following are typical MS-DOS and OS/2 filenames:
\FOOTBALL\SRC\KERNEL\SCHED.ASM
Football is a development project, so this name describes the source for the kernel scheduler for the football project.
\MEMOS\286\MODESWIT.DOC
is a memo discussing 80286 mode switching.
\\HAGAR\SCRATCH\GORDONL\FOR_MARK
is a file in my scratch directory on the network server HAGAR, placed there for use by Mark.
The OS/2 architecture views file systems quite differently. As
microcomputers become more powerful and are used in more and more ways, file system characteristics will be needed that might not be met by a built-in OS/2 file system. Exotic peripherals, such as WORM 2. Write Once, Read Many disks. These are generally laser disks of very high capacity, but once a track is written, it cannot be erased. These disks can appear to be erasable by writing new copies of files and directories each time a change is made, abandoning the old ones. 2 drives, definitely require special file systems to meet their special characteristics. For this reason, the file system is not built into OS/2 but is a closely allied component--an installable file system (IFS). An IFS is similar to a device driver; it's a body of code that OS/2 loads at boot time. The code talks to OS/2 via a standard interface and provides the software to manage a file system on a storage device, including the ability to create and maintain directories, to allocate disk space, and so on.
If you are familiar with OS/2 version 1.0, this information may be
surprising because you have seen no mention of an IFS in the reference manuals. That's because the implementation hasn't yet caught up with the architecture. We designed OS/2, from the beginning, to support installable file systems, one of which would of course be the familiar FAT file system. We designed the file system calls, such as DosOpen and DosClose, with this in mind. Although scheduling pressures forced us to ship OS/2 version 1.0 with only the FAT file system--still built in--a future release will include the full IFS package. Although at this writing the IFS release of OS/2 has not been announced, this information is included here so that you can understand the basis for the system name architecture. Also, this information will help you write programs that work well under the new releases of OS/2 that contain the IFS.
Because the IFS will interpret filenames and pathnames and because
installable file systems can vary considerably, OS/2 3. Excluding the IFS part. 3 doesn't contain much specific information about the format and meaning of filenames and pathnames. In general, the form and meaning of filenames and pathnames are private matters between the user and the IFS; both the application and OS/2 are simply go-betweens. Neither should attempt to parse or understand filenames and pathnames. Applications shouldn't parse names because some IFSs will support names in formats other than the 8.3 format. Applications shouldn't even assume a specific length for a filename or a pathname. All OS/2 filename and pathname interfaces, such as DosOpen, DosFindNext, and so on, are designed to take name strings of arbitrary length. Applications should use name buffers of at least 256 characters to ensure that a long name is not truncated.
8.2 Network Access
Two hundred and fifty-six characters may seem a bit extreme for the length of a filename, and perhaps it is. But OS/2 filenames are often pathnames, and pathnames can be quite lengthy. To provide transparent access to files on a LAN (local area network), OS/2 makes the network part of the file system name space. In other words, a file's pathname can specify a machine name as well as a directory path. An application can issue an open to a name string such as \WORK\BOOK.DAT or \\VOGON\TEMP\RECALC.ASM. The first name specifies the file BOOK.DAT in the directory WORK on the current drive of the local machine; the second name specifies the file RECALC.ASM in the directory TEMP on the machine VOGON. 4. Network naming is a bit more complex than this; the name TEMP on the machine VOGON actually refers to an offered network resource and might appear in any actual disk directory. 4 Future releases of the Microsoft LAN Manager will make further use of the file system name space, so filenames, especially program-generated filenames, can easily become very long.
8.3 Name Generation and Compatibility
Earlier, I said that applications should pass on filenames entered by the user, ignoring their form. This is, of course, a bit unrealistic. Programs often need to generate filenames--to hold scratch files, to hold derivative filenames (for example, FOO.OBJ derived from FOO.ASM), and so forth. How can an application generate or permute such filenames and yet ensure compatibility with all installable file systems? The answer is, of course: Use the least common denominator approach. In other words, you can safely assume that a new IFS must accept the FAT file system's names (the 8.3 format) because otherwise it would be incompatible with too many programs. So if an application sticks to the 8.3 rules when it creates names, it can be sure that it is compatible with future file systems. Unlike MS-DOS, OS/2 5. More properly, the FAT installable file system installed in OS/2. 5 will not truncate name or extension fields that are too long; instead, an error will be returned. The case of a filename will continue to be insignificant. Some operating systems, such as UNIX, are case sensitive; for example, in UNIX the names "foo" and "Foo" refer to different files. This works fine for a system used primarily by programmers, who know that a lowercase f is ASCII 66 (SUB16) and that an uppercase F is ASCII 46 (SUB 16). Nonprogrammers, on the other hand, tend to see f and F as the same character. Because most OS/2 users are nonprogrammers, OS/2 installable file systems will continue to be case insensitive.
I said that it was safe if program-generated names adhered to the 8.3
rule. Program-permuted names are likewise safe if they only substitute alphanumeric characters for other alphanumeric characters, for example, FOO.OBJ for FOO.ASM. Lengthening filenames is also safe (for example, changing FOO.C to FOO.OBJ) if your program checks for "invalid name" error codes for the new name and has some way to deal with that possibility. In any case, write your program so that it isn't confused by enhanced pathnames; in the above substitution cases, the algorithm should work from the end of the path string and ignore what comes before.
8.4 Permissions
Future releases of OS/2 will use the file system name space for more than locating a file; it will also contain the permissions for the file. A uniform mechanism will associate an access list with every entry in the file system name space. This list will prevent unauthorized access-- accidental or deliberate--to the named file.
8.5 Other Objects in the File System Name Space
As we've seen, the file system name space is a valuable device in several aspects. First, it allows the generation of a variety of names. You can group names together (by putting them in the same directory), and you can generate entire families of unique names (by creating a new subdirectory). Second, the name space can encompass all files and devices on the local machine as well as files and devices on remote machines. Finally, file system names will eventually support a flexible access and protection mechanism.
Thus, it comes as no surprise that when the designers of OS/2 needed a
naming mechanism to deal with nonfile objects, such as shared memory, system semaphores, and named pipes, we chose to use the file system name space. One small disadvantage to this decision is that a shared memory object cannot have a name identical to that of a system semaphore, a named pipe, or a disk file. This drawback is trivial, however, compared with the benefits of sharing the file system name space. And, of course, you can use separate subdirectory names for each type of object, thus preventing name collision.
Does this mean that system semaphores, shared memory, and pipes have
actual file system entries on a disk somewhere? Not yet. The FAT file system does not support special object names in its directories. Although changing it to do so would be easy, the file system would no longer be downward compatible with MS-DOS. (MS-DOS 3.x could not read such disks written under OS/2.) Because only the FAT file system is available with OS/2 version 1.0, that release keeps special RAM-resident pseudo directories to hold the special object names. These names must start with \SEM\, \SHAREMEM\, \QUEUES\, and \DEV\ to minimize the chance of name collision with a real file when they do become special pseudo files in a future release of OS/2.
Although all file system name space features--networking and (in the
future) permissions--apply to all file system name space objects from an architectural standpoint, not all permutations may be supported. Specifically, supporting named shared memory across the network is very costly 6. The entire shared memory segment must be transferred across the network each time any byte within it is changed. Some clever optimizations can reduce this cost, but none works well enough to be feasible. 6 and won't be implemented.
9 Memory Management
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
A primary function of any multitasking operating system is to allocate system resources to each process according to its need. The scheduler allocates CPU time among processes (actually, among threads); the memory manager allocates both physical memory and virtual memory.
9.1 Protection Model
Although MS-DOS provided a simple form of memory management, OS/2 provides memory protection. Under MS-DOS 3.x, for example, a program should ask the operating system to allocate a memory area before the program uses it. Under OS/2, a program must ask the operating system to allocate a memory area before the program uses it. As we discussed earlier, the 80286 microprocessor contains special memory protection hardware. Each memory reference that a program makes explicitly or implicitly references a segment selector. The segment selector, in turn, references an entry in the GDT or the LDT, depending on the form of the selector. Before any program, including OS/2 itself, can reference a memory location, that memory location must be described in an LDT or a GDT entry, and the selector for that entry must be loaded into one of the four segment registers.
This hardware design places some restrictions on how programs can use
addresses.
þ A program cannot address memory not set up for it in the LDT or GDT. The only way to address memory is via the LDT and GDT.
þ Each segment descriptor in the LDT and GDT contains the physical address and the length of that segment. A program cannot reference an offset into a segment beyond that segment's length.
þ A program can't put garbage (arbitrary values) into a segment register. Each time a segment register is loaded, the hardware examines the corresponding LDT and GDT to see if the entry is valid. If a program puts an arbitrary value--for example, the lower half of a floating point number--into a segment register, the arbitrary value will probably point to an invalid LDT or GDT entry, causing a GP fault.
þ A program can't execute instructions from within a data segment. Attempting to load a data segment selector into the CS register (usually via a far call or a far jump) causes a GP fault.
þ A program can't write into a code segment. Attempting to do so causes a GP fault.
þ A program can't perform segment arithmetic. Segment arithmetic refers to activities made possible by the addressing mechanism of the 8086 and 8088 microprocessors. Although they are described as having a segment architecture, they are actually linear address space machines that use offset registers--the so-called segment registers. An 8086 can address 1 MB of memory, which requires a 20- bit address. The processor creates this address by multiplying the 16-bit segment value by 16 and adding it to the 16-bit offset value. The result is an address between 0 and 1,048,575 (that is, 1 MB).
1. Actually, it's possible to produce addresses beyond 1 MB (2^20) by this method if a large enough segment and offset value are chosen. The 8086 ignores the carry into the nonexistent 21st address bit, effectively wrapping around such large addresses into the first 65 KB-16 bytes of physical memory. 1 The reason these are not true segments is that they don't
have any associated length and their names (that is, their selectors) aren't names at all but physical addresses divided by 16. These segment values are actually scaled offsets. An address that has a segment value of 100 and an offset value of 100 (shown as 100(SUB10):100(SUB10)), and the address (99(SUB10):116(SUB10)) both refer to the same memory location. Many real mode programs take advantage of this situation. Some programs that keep a great many pointers store them as 20-bit values, decomposing those values into the segment:offset form only when they need to de-reference the pointer. To ensure that certain objects have a specific offset value, other programs choose a matching segment value so that the resultant 20-bit address is correct. Neither technique works in OS/2 protect mode. Each segment selector describes its own segment, a segment with a length and an address that are independent of the numeric value of the segment selector. The memory described by segment N has nothing in common with the memory described by segment N+4 or by any other segment unless OS/2 explicitly sets it up that way.
The segmentation and protection hardware allows OS/2 to impose further
restrictions on processes.
þ Processes cannot edit or examine the contents of the LDT or the GDT. OS/2 simply declines to build an LDT or GDT selector that a process can use to access the contents of those tables. Certain LDT and GDT selectors describe the contents of those tables themselves, but OS/2 sets them up so that they can only be used by ring 0 (that is, privileged) code.
þ Processes cannot hook interrupt vectors. MS-DOS version 3.x programs commonly hook interrupt vectors by replacing the address of the interrupt handler with an address from their own code. Thus, these programs can monitor or intercept system calls made via INT 21h, BIOS calls also made via interrupts, and hardware interrupts such as the keyboard and the system clock. OS/2 programs cannot do this. OS/2 declines to set up a segment selector that processes can use to address the interrupt vector table.
þ Processes cannot call the ROM BIOS code because no selector addresses the ROM BIOS code. Even if such a selector were available, it would be of little use. The ROM BIOS is coded for real mode execution and performs segment arithmetic operations that are no longer legal. If OS/2 provided a ROM BIOS selector, calls to the ROM BIOS would usually generate GP faults.
þ Finally, processes cannot run in ring 0, that is, in privileged mode. Both OS/2 and the 80286 hardware are designed to prevent an application program from ever executing in ring 0. Code running in ring 0 can manipulate the LDT and GDT tables as well as other hardware protection features. If OS/2 allowed processes to run in ring 0, the system could never be stable or secure. OS/2 obtains its privileged (literally) state by being the first code loaded at boot time. The boot process takes place in ring 0 and grants ring 0 permission to OS/2 by transferring control to OS/2 while remaining in ring 0. OS/2 does not, naturally, extend this favor to the application programs it loads; it ensures that applications can only run in ring 3 user mode.
2. Applications can also run in ring 2 (see 18.1 I/O Privilege Mechanism). 2
9.2 Memory Management API
OS/2 provides an extensive memory management API. This book is not a reference manual, so I won't cover all the calls. Instead, I'll focus on areas that may not be completely self-explanatory.
9.2.1 Shared Memory
OS/2 supports two kinds of shared memory--named shared memory and giveaway
shared memory. In both, the memory object shared is a segment. Only an
entire segment can be shared; sharing part of a segment is not possible.
Named shared memory is volatile because neither the name of the named
shared memory nor the memory itself can exist on the FAT file system. When
the number of processes using a shared memory segment goes to zero, the
memory is released. Shared memory can't stay around in the absence of
client processes; it must be reinitialized via DosAllocShrSeg after a
period of nonuse.
Giveaway shared memory allows processes to share access to the same
segment. Giveaway shared memory segments don't have names because processes can't ask to have access to them; a current user of the segment has to give access to the segment to a new client process. The term giveaway is a bit of a misnomer because the giving process retains access to the memory--the access is "given" but not especially "away." Giveaway shared memory is not as convenient as named shared memory. The owner 3. One of the owners. Anyone with access to a giveaway shared segment can give it away itself. 3 has to know the PID of the recipient and then communicate the recipient's segment selector (returned by DosGiveSeg) to that recipient process via some form of IPC.
Despite its limitations, giveaway shared memory has important virtues.
It's a fast and efficient way for one process to transfer data to another; and because access is passed "hand to hand," the wrong process cannot accidentally or deliberately gain access to the segment. Most clients of giveaway shared memory don't retain access to the segment once they've passed it off; they typically call DosFreeSeg on their handle after they've called DosGiveSeg. For example, consider the design of a database dynlink subsystem that acts as a front end for a database serving process. As part of the dynlink initialization process, the package arranged for its client process to share a small named shared memory segment with the database process. It might be best to use a named pipe or named shared memory-- created by the database process--to establish initial communication and then use this interface only to set up a private piece of giveaway shared memory for all further transactions between the client process (via the dynlink subsystem) and the database process. Doing it this way, rather than having one named shared segment hold service requests from all clients, provides greater security. Because each client has its own separate shared memory communications area, an amok client can't damage the communications of other clients.
When a client process asks the database process to read it a record,
the database process must use a form of IPC to transfer the data to the client. Pipes are too slow for the volume of data that our example anticipates; shared memory is the best technique. If we were to use named shared memory, the database package would have to create a unique shared memory name for each record, allocate the memory, and then communicate the name to the client (actually, to the dynlink subsystem called by the client) so that it can request access. This process has some drawbacks:
þ A new unique shared memory name must be created for each request. We could reuse a single shared memory segment, but this would force the client to copy the data out of the segment before it could make another request--too costly a process for an application that must handle a high volume of data.
þ Creating named shared memory segments is generally slower than creating giveaway shared memory segments, especially if a large number of named shared memory objects exist, as would be the case in this scenario. The client spends more time when it then requests access to the segment. Creating named shared memory segments is plenty fast enough when it's done once in a while, but in a high- frequency application such as our example, it could become a bottleneck.
Instead, the database process can create a giveaway shared memory
segment, load the data into it, and then give it to the client process. The database process can easily learn the client's PID; the dynlink interface, which runs as the client process, can include it as part of the data request. Likewise, the database process can easily return the new client selector to the client. This process is fast and efficient and doesn't bog down the system by forcing it to deal with a great many name strings.
Note that you must specify, at the time of the DosAllocSeg, that the
segment might be "given away." Doing so allows OS/2 to allocate the selector in the disjoint space, as we discussed earlier.
9.2.2 Huge Memory
The design of the 80286 microprocessor specifies the maximum size of a
memory segment as 64 KB. For many programs, this number is far too small.
For example, the internal representation of a large spreadsheet commonly
takes up 256 KB or more. OS/2 can do nothing to set up a segment that is
truly larger than 64 KB, but the OS/2 facility called huge segments
provides a reasonable emulation of segments larger than 64 KB. The trick is
that a huge segment of, for example, 200 KB is not a single segment but a
group of four segments, three of which are 64 KB and a fourth of 8 KB. With
minimal programming burden, OS/2 allows an application to treat the group
of four segments as a single huge segment.
When a process calls DosAllocHuge to allocate a huge segment, OS/2
allocates several physical segments, the sum of whose size equals the size of the virtual huge segment. All component segments are 64 KB, except possibly the last one. Unlike an arbitrary collection of segment selectors, DosAllocHuge guarantees that the segment selectors it returns are spaced uniformly from each other. The selector of the N+1th component segment is that of the Nth segment plus i, where i is a power of two. The value of i is constant for any given execution of OS/2, but it may vary between releases of OS/2 or as a result of internal configuration during bootup. In other words, a program must learn the factor i every time it executes; it must not hard code the value. There are three ways to learn this value. First, a program can call DosGetHugeShift; second, it can read this value from the global infoseg; and third, it can reference this value as the undefined absolute externals DOSHUGESHIFT (log(SUB2)(i)) or DOSHUGEINCR (i). OS/2 will insert the proper value for these externals at loadtime. This last method is the most efficient and is recommended. Family API programs should call DosGetHugeShift. Figure 9-1 illustrates the layout of a 200 KB huge memory object. Selectors n + 4i and n + 5i are currently invalid but are reserved for future growth of the huge object.
ÚÄÄÄÄÄÄÄ¿ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ�³ ³
n+0i ³ ÃÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³
ÃÄÄÄÄÄÄÄ´ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³
n+1i ³ ÃÄÄÄÄÄÄÅÄÄÄÄÄÄÄ¿ ³
ÃÄÄÄÄÄÄÄ´ ³ ³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÀÄÄÄÄÄÄ�³ ³
n+2i ³ ÃÄÄÄÄÄÄÙ ³ ³ ³
ÃÄÄÄÄÄÄÄ´ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿
n+3i ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄ�³ ³
ÃÄÄÄÄÄÄÄ´ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ³
n+4i ³ � ³ ³
ÃÄÄÄÄÄÄÄ´ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ ³ ÃÄÄÄÄÄÄÄ´ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄ�³ ³
n+5i ³ � ³ ³ ³
ÃÄÄÄÄÄÄÄ´ ÀÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄ´ ³ ³ ÀÄÄÄÄÄÄÄÙ
Figure 9-1. Huge memory objects.
Once an application has the first segment selector of the huge segment
group, called the base segment, and the value log(SUB2)(i), computing the address of the Nth byte in the huge segment is easy. Take the high-order word of the value of N (that is, N/64 KB), shift it left by log(SUB2)(i) (that is, by the DosHugeShift value), and add the base segment selector returned by DosAllocHuge. The resultant value is the segment selector for the proper component segment; the low-order 16 bits of i are the offset into that segment. This computation is reasonably quick to perform since it involves only a shift and an addition.
Huge segments can be shrunk or grown via DosReallocHuge. If the huge
segment is to be grown, creating more component physical segments may be necessary. Because the address generation rules dictate which selector this new segment may have, growing the huge segment may not be possible if that selector has already been allocated for another purpose. DosAllocHuge takes a maximum growth parameter; it uses this value to reserve sufficient selectors to allow the huge segment to grow that big. Applications should not provide an unrealistically large number for this argument because doing so will waste LDT selectors.
The astute reader will notice that the segment arithmetic of the 8086
environment is not dead; in a sense, it's been resurrected by the huge segment mechanism. Applications written for the 8086 frequently use this technique to address memory regions greater than 64 KB, using a shift value of 12. In other words, if you add 2^12 to an 8086 segment register value, the segment register will point to an address 2^12*16, or 64 KB, further in physical memory. The offset value between the component segment values was always 4096 because of the way the 8086 generated addresses. Although the steps involved in computing the segment value are the same in protect mode, what's actually happening is considerably different. When you do this computation in protect mode, the segment selector value has no inherent relationship to the other selectors that make up the huge object. The trick only works because OS/2 has arranged for equally spaced-out selectors to exist and for each to point to an area of physical memory of the appropriate size. Figure 9-2 illustrates the similarities and differences between huge model addressing in real and protect modes. The application code sequence is identical: A segment selector is computed by adding N*i to the base selector. In real mode i is always 4096; in protect mode OS/2 provides i.
REAL MODE | PROTECT MODE | Physical | Memory | Segment ÚÄÄÄÄÄÄ¿ | Selector Physical ³ ³ | Table Memory ³ ³ | ÚÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄ¿
n+0i ÄÄÄÄÄ�³ ³± | ³ ³ ÚÄÄÄÄÄ�³ ³
³ ³±�64 KB | n+0i ÄÄÄÄÄ�³ ÃÄÄÅÄÄÄ¿ ÀÄÄÄÄÄÄÙ
n+1i ÄÄÄÄÄ�³ ³± | ³ ³ ³ ³
³ ³ | n+1i ÄÄÄÄÄ�³ ÃÄÄÅÄ¿ ³ ÚÄÄÄÄÄÄ¿
n+2i ÄÄÄÄÄ�³ ³ | ³ ³ ³ ³ ÀÄ�³ ³
³ ³ | n+2i ÄÄÄÄÄ�³ ÃÄÄÙ ³ ÀÄÄÄÄÄÄÙ
n+3i ÄÄÄÄÄ�³ ³ | ³ ³ ³ ÚÄÄÄÄÄÄ¿
³ ³ | n+3i ÄÄÄÄÄ�³ ÃÄÄÄÄÅÄÄÄ�³ ³ ³ ³ | ³ ³ ³ ÀÄÄÄÄÄÄÙ ³ ³ | ³ ³ ³ ÚÄÄÄÄÄÄ¿ ³ ³ | ÀÄÄÄÄÄÄÙ ÀÄÄÄ�³ ³ ³ ³ | i is set by OS/2 ÀÄÄÄÄÄÄÙ ³ ³ | ³ ³ | ³ ³ | ³ ³ | ÀÄÄÄÄÄÄÙ | i = 4096 |
Figure 9-2. Huge model addressing in real mode and in protect mode.
Although the similarity between 8086 segment arithmetic and OS/2 huge
segments is only apparent, it does make it easy to write a program as a dual mode application. By using the shift value of 12 in real mode and using the OS/2 supplied value in protect mode, the same code functions correctly in either mode.
9.2.3 Executing from Data Segments
We saw that OS/2 provides the huge segment mechanism to get around the
segment size restriction imposed by the hardware. OS/2 likewise circumvents
another hardware restriction--the inability to execute code from data
segments. Although the demand loading, the discarding, and the swapping of
code segments make one use of running code from data segments--code
overlays--obsolete, the capability is still needed. Some high-performance
programs--the presentation manager, for example--compile "on the fly"
special code to perform time-critical tasks, such as flipping bits in EGA
display memory. The optimal sequence may differ depending on several
factors, so a program may need to compile such code and execute it, gaining
a significant increase in efficiency over some other approach. OS/2
supports this need by means of the DosCreateCSAlias call.
When DosCreateCSAlias is called with a selector for a data segment, it
creates a totally different code segment selector (in the eyes of 80286 hardware) that by some strange coincidence points to exactly the same memory locations as does the data segment selector. As a result, code is not actually executing from a data segment but from a code segment. Because the code segment exactly overlaps that other data segment, the desired effect is achieved. The programmer need only be careful to use the data selector when writing the segment and to use the code selector when executing it.
9.2.4 Memory Suballocation
All memory objects discussed so far have been segments. OS/2 provides a
facility called memory suballocation that allocates pieces of memory from
within an application's segment. Pieces of memory can be suballocated from
within a segment, grown, shrunk, and released. OS/2 uses a classic heap
algorithm to do this. The DosSubAlloc call uses space made available from
earlier DosSubFrees when possible, growing the segment as necessary when
the free heap space is insufficient. We will call the pieces of memory
returned by DosSubAlloc heap objects.
The memory suballocation package works within the domain of a process.
The suballocation package doesn't allocate the memory from some "system pool" outside the process's address space, as does the segment allocator. The suballocation package doesn't even allocate segments; it manages only segments supplied by and owned by (or at least accessible to) the caller. This is a feature because memory protection is on a per-segment basis. If the suballocation package were to get its space from some system global segment, a process that overwrote its heap object could damage one belonging to another process. Figure 9-3 illustrates memory suballocation. It shows a segment j being suballocated. H is the suballocation header; the shaded areas are free space.
ÚÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Segment ³ ³ Segment ³ ³ Segment ³ ³ i ³ ³ j ³ ³ k ³ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÙ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \
ÚÄÄÂÄÄÂÄÄÄÄÄÂÄÄÄÂÂÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÂÄÂÄÄÄÄÄÄÄÂÄÄÄÂÄÄÄÄÂÄÄÄÂÄÄÄÄÄÄÄ¿ ³H ³ ³ ³°°°³³°°°°³ ³ ³°°°°°°°³ ³ ³ ³°°°°°°°³ ³ ³ ³ ³°°°³³°°°°³ ³ ³°°°°°°°³ ³ ³ ³°°°°°°°³ ÀÄÄÁÄÄÁÄÄÄÄÄÁÄÄÄÁÁÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÁÄÁÄÄÄÄÄÄÄÁÄÄÄÁÄÄÄÄÁÄÄÄÁÄÄÄÄÄÄÄÙ
Figure 9-3. Memory suballocation.
We said that the memory suballocator subdivides segments that are
accessible to the client process. This means that you can use it to subdivide space in a shared memory segment. Such a technique can be handy when two or more processes are using a shared memory segment for intercommunication, but there is risk because an error in one process can easily corrupt the heap objects of another.
Earlier, in the discussion of dynamic link subsystems, we described
facilities and techniques for writing a reliable subsystem. The OS/2 memory suballocation package is a good example of such a subsystem, so let's look at its workings more closely. The first try at the suballocation package produced a straightforward heap allocator, much like the one in a C or Pascal runtime library. It maintained a free chain of heap objects and allocated them at its client's request. If the closest-size free heap object was still bigger than the request, it was split into an allocated part and a free part. Freed heap objects were coalesced with any adjacent free objects. The suballocation package took a segment pointer and some other arguments and returned some values--an offset and the changed data in the segment itself where the heap headers were stored. If we stretch things a little and consider the changed state of the supplied data segment as a returned value, then the suballocation package at this stage is much like a function: It has no state of its own; it merely returns values computed only from the input arguments. This simple suballocation dynlink routine uses no global data segments and doesn't even need an instance data segment.
This simple implementation has an important drawback: More than one
process can't safely use it to manage a shared memory segment; likewise, multiple threads within one process can't use it. The heap free list is a critical section; if multiple threads call the suballocator on the same segment, the heap free list can become corrupted. This problem necessitated upgrading the suballocation package to use a semaphore to protect the critical section. If we didn't want to support suballocation of shared memory and were only worried about multiple threads within a task, we could use RAM semaphores located in the managed segment itself to protect the critical section. The semaphore might be left set if the process died unexpectedly, but the managed segment isn't shared. It's going to be destroyed in any case, so we don't care.
But, even in this simple situation of managing only privately owned
segments, we must concern ourselves with some special situations. One problem is signals: What if the suballocator is called with thread 1, and a signal (such as SIGINT, meaning that the user pressed Ctrl-C) comes in? Thread 1 is interrupted from the suballocation critical section to execute the signal handler. Often signal handlers return to the interrupted code, and all is well. But what if the signal handler does not return but jumps to the application's command loop? Or what if it does return, but before it does so calls the memory suballocator? In these two cases, we'd have a deadlock on the critical section. We can solve these problems by using the DosHoldSignal function. DosHoldSignal does for signals what the CLI instruction does for hardware interrupts: It holds them off for a short time. Actually, it holds them off forever unless the application releases them, but holding signals for more than a second or two is poor practice. If you precede the critical section's semaphore claim call with a signal hold and follow the critical section's semaphore release call with a signal release, you're protected from deadlocks caused by signal handling.
Note that unlike the CLI instruction, DosHoldSignal calls nest. OS/2
counts the number of DosHoldSignal "hold" calls made and holds signals off until an equal number of "release" calls are issued. This means that a routine can safely execute a hold/release pair without affecting the state of its calling code. If the caller had signals held at the time of the call, they will remain held. If signals were free at the time of the call, the callee's "release" call restores them to that state.
Whenever dynlink packages make any call that changes the state of the
process or the thread, they must be sure to restore that state before they return to their caller. Functions that nest, such as DosHoldSignal, accomplish this automatically. For other functions, the dynlink package should explicitly discover and remember the previous state so that it can be restored.
Our problems aren't over though. A second problem is brought about by
the DosExitList facility. If a client process's thread is in the suballocation package's critical section and the client terminates suddenly--it could be killed externally or have a GP fault--the process might not die immediately. If any DosExitList handlers are registered, they will be called. They might call the memory suballocator, and once again we face deadlock. We could solve this situation with the classic approach of making a bug into a feature: Document that the suballocator can't be called at exitlist time. This may make sense for some dynlink subsystems, but it's too restrictive for an important OS/2 facility. We've got to deal with this problem too.
The DosHoldSignal trick won't help us here. It would indeed prevent
external kills, but it would not prevent GP faults and the like. We could say, "A program that GP faults is very sick, so all bets are off." This position is valid, except that if the program or one of its dynlink subsystems uses DosExitList and the DosExitList handler tries to allocate or release a heap object, the process will hang and never terminate correctly. This is unacceptable because the user would be forced to reboot to get rid of the moribund application. The answer is to use a system semaphore rather than a RAM semaphore to protect the memory segment. System semaphores are a bit slower than RAM semaphores, but they have some extra features. One is that they can be made exclusive; only the thread that owns the semaphore can release it. Coupled with this is an "owner death" notification facility that allows a process's DosExitList handler an opportunity to determine that one of its threads has orphaned a semaphore (see 16.2 Data Integrity for details). Our suballocation package can now protect itself by using exclusive system semaphores to protect its critical section and by registering a DosExitList handler to release that semaphore. The exitlist code can discover if a thread in its process has orphaned the semaphore and, if so, can release it. Of course, releasing the semaphore won't help if the heap headers are in an inconsistent state. You can write the suballocation package so that the heap is never in an inconsistent state, or you can write it to keep track of the modified state so that the exitlist handler can repair the heap structure.
In this later case, be sure the DosExitList handler you establish to
clean up the heap is called first (see DosExitList documentation).
Finally, even if we decide that the client application won't be
allowed to issue suballocation requests during its own exitlist processing, we want the memory suballocator to support allocating a shared segment among many different processes. Because of this, the actual OS/2 suballocation package makes use of DosExitList so that the suballocation structure and semaphores can be cleaned up should a client thread terminate while in the suballocation critical section.
The suballocation dynlink package does more than illustrate subsystem
design; it also illustrates the value of a system architecture that uses dynlinks as a standard system interface, regardless of the type of code that provides the service. As you have seen, the memory suballocation package released with OS/2 version 1.0 doesn't reside in the kernel; it's effectively a subroutine package. OS/2 in an 80286 environment will undoubtedly preserve this approach in future releases, but a forthcoming 80386 version of OS/2 may not. The 80386 architecture supports paged virtual memory, so memory swapping (actually, paging) can take place on part of a segment. This future paging environment may precipitate some changes in the memory suballocator. Perhaps we'll want to rearrange the heap for better efficiency with paging, or perhaps the OS/2 kernel will want to become involved so that it can better anticipate paging demands. In any case, any future release of OS/2 has complete flexibility to upgrade the memory suballocation package in any externally compatible fashion, thanks to the standard interface provided by dynamic links.
9.3 Segment Swapping
One of the most important features of the 80286 memory management hardware is swapping support. Swapping is a technique by which some code or data segments in memory are written to a disk file, thus allowing the memory they were using to be reclaimed for another purpose. Later, the swapped-out code or data is reloaded into memory. This technique lets you run more programs than can simultaneously fit in memory; all you need is enough memory to hold the programs that are running at that particular moment. Quiescent programs can be swapped to disk to make room for active ones. Later, when the swapped programs become active, OS/2 reads them in and resumes them. If necessary OS/2 first makes memory available by swapping out another quiescent program.
Although I used the word program above, swapping is actually done on a
segment basis. Segments are swapped out individually and completely; the OS/2 swapping code doesn't pay attention to relationships between segments (they aren't swapped in groups), and the 80286 hardware does not allow only part of a segment to be swapped. I simplified the concept a bit in the above paragraph. You need not swap out an entire process; you can swap out some segments and leave others in memory. OS/2 can and commonly does run a process when some of its segments are swapped out. As long as a process does not try to use the swapped-out segments, it runs unhindered. If a process references a swapped-out segment, the 80286 hardware generates a special trap that OS/2 intercepts. The segment fault trap handler swaps in the missing segment, first swapping out some other if need be, and then the process resumes where it left off. Segment faulting is invisible to a process; the process executes normally, except that a segment load instruction takes on the order of 30 milliseconds instead of the usual 3 microseconds.
When memory is depleted and a segment must be swapped, OS/2 has to
choose one to swap out. Making the right choice is important; for example, consider a process that alternates references between segment A and segment B. If A is swapped out, a poorly designed system might choose B to swap out to make room for A. After a few instructions are executed, B has to be swapped in. If A is in turn swapped out to make room for B, the system would soon spend all its time swapping A and B to and from the disk. This is called thrashing, and thrashing can destroy system performance. In other words, the effect of swapping is to make some segment loads take 10,000 times longer than they would if the segment were in memory. Although the number 10,000 seems very large, the actual time of about 30 milliseconds is not, as long as we don't have to pay those 30 milliseconds very often.
A lot hinges on choosing segments to swap out that won't be referenced
in the near future. OS/2 uses the LRU (Least Recently Used) scheme to determine which segment it will swap out. The ideal choice is the segment-- among those currently in memory--that will be referenced last because this postpones the swap-in of that segment as long as possible. Unfortunately, it's mathematically provable that no operating system can predict the behavior of arbitrary processes. Instead, operating systems try to make an educated guess as to which segment in memory is least likely to be referenced in the immediate future. The LRU scheme is precisely that--a good guess. OS/2 figures that if a segment hasn't been used in a long time then it probably won't be used for a long time yet, so it swaps out the segment that was last used the longest time ago--in other words, the least recently used segment.
Of course, it's easy to construct an example where the LRU decision is
the wrong one or even the worst one. The classic example is a program that references, round robin, N segments when there is room in memory for only N-1. When you attempt to make room for segment I, the least recently used segment will be I+1, which in fact is the segment that will next be used. A discussion of reference locality and working set problems, as these are called, is beyond the scope of this book. Authors of programs that will make repetitious accesses to large bodies of data or code should study the available literature on virtual memory systems. Remember, on an 80286, OS/2 swaps only on a segment basis. A future 80386 release of OS/2 will swap, or page, on a 4 KB page basis.
The swapping algorithm is strictly LRU among all swap-eligible
segments in the system. Thread/process priority is not considered; system segments that are marked swappable get no special treatment. Some system segments are marked nonswappable, however. For example, swapping out the OS/2 code that performs swap-ins would be embarrassing. Likewise, the disk driver code for the swapping disk must not be swapped out. Some kernel and device driver code is called at interrupt time; this is never swapped because of the swap-in delay and because of potential interference between the swapped-out interrupt handling code and the interrupt handling code of the disk driver that will do the swap-in. Finally, some kernel code is called in real mode in response to requests from the 3x box. No real mode code can be swapped because the processor does not support segment faults when running in real mode.
The technique of running more programs then there is RAM to hold them
is called memory overcommit. OS/2 has to keep careful track of the degree of overcommit so that it doesn't find itself with too much of a good thing- -not enough free RAM, even with swapping, to swap in a swapped-out process. Such a situation is doubly painful: Not only can the user not access or save the data that he or she has spent the last four hours working on, but OS/2 can't even tell the program what's wrong because it can't get the program into memory to run it. To prevent this, OS/2 keeps track of its commitments and overcommitments in two ways. First, before it starts a process, OS/2 ensures that there is enough swap space to run it. Second, it ensures that there is always enough available RAM to execute a swapped-out process.
At first glance, knowing if RAM is sufficient to run a process seems
simple--either the process fits into memory or it doesn't. Life is a bit more complicated than that under OS/2 because the segments of a program or a dynlink library may be marked for demand loading. This means that they won't come in when the program starts executing but may be called in later. Obviously, once a program starts executing, it can make nearly unlimited demands for memory. When a program requests a memory allocation, however, OS/2 can return an error code if available memory is insufficient. The program can then deal with the problem: make do with less, refuse the user's command, and so forth.
OS/2 isn't concerned about a program's explicit memory requests
because they can always be refused; the implicit memory requests are the problem--faulting in a demand load segment, for example. Not only is there no interface to give the program an error code, 4. A demand load segment is faulted in via a "load segment register" instruction. These CPU instructions don't return error codes! 4 but the program may be unable to proceed without the segment. As a result, when a program is first loaded (via a DosExecPgm call), OS/2 sums the size of all its impure segments even if they are marked for "load on demand." The same computation is done for all the loadtime dynlink libraries it references and for all the libraries they reference and so on. This final number, plus the internal system per-process overhead, is the maximum implicit memory demand of the program. If that much free swap space is available, the program can start execution.
You have undoubtedly noticed that I said we could run the program if
there was enough swap space. But a program must be in RAM to execute, so why don't we care about the amount of available RAM space? We do care. Not about the actual amount of free RAM when we start a program, but about the amount of RAM that can be made free--by swapping--if needed. If some RAM contains a swappable segment, then we can swap it because we set aside enough swap space for the task. Pure segments, by the way, are not normally swapped. In lieu of a swap-out, OS/2 simply discards them. When it's time to swap them in, OS/2 reloads them from their original .EXE or .DLL files. 5. An exception to this is programs that were executed from removable media. OS/2 preloads all pure segments from such .EXE and .DLL files and swaps them as necessary. This prevents certain deadlock problems involving the hard error daemon and the volume management code. 5
Because not all segments of a process need to be in memory for the
process to execute, we don't have to ensure enough free RAM for the entire process, just enough so that we can simultaneously load six 64 KB segments- -the maximum amount of memory needed to run any process. The numbers 6 and 64 KB are derived from the design of the 80286. To execute even a single instruction of a process, all the segments selected by the four segment registers must be in memory. The other two necessary segments come from the worst case scenario of a program trying to execute a far return instruction from a ring 2 segment (see 18.1 I/O Privilege Mechanism). The four segments named in the registers must be present for the instruction to start, and the two new segments--CS and SS--that the far return instruction will reference must be present for the instruction to complete. That makes six; the 64 KB comes from the maximum size a segment can reach. As a result, as long as OS/2 can free up those six 64 KB memory regions, by swapping and discarding if necessary, any swapped-out program can execute.
Naturally, if that were the only available memory and it had to be
shared by all running processes, system response would be very poor. Normally, much more RAM space is available. The memory overcommit code is concerned only that all processes can run; it won't refuse to start a process because it might execute slowly. It could be that the applications that a particular user runs and their usage pattern are such that the user finds the performance acceptable and thus hasn't bought more memory. Or perhaps the slowness is a rare occurrence, and the user is willing to accept it just this once. In general, if the system thrashes-- spends too much time swapping--it's a soft failure: The user knows what's wrong, the user knows what to do to make it get better (run fewer programs or buy more memory), and the user can meanwhile continue to work.
Clearly, because all segments of the applications are swappable and
because we've ensured that the swap space is sufficient for all of them, initiating a new process doesn't consume any of our free or freeable RAM. It's the device drivers and their ability to allocate nonswappable segments that can drain the RAM pool. For this reason, OS/2 may refuse to load a device driver or to honor a device driver's memory allocation request if to do so would leave less than six 64 KB areas of RAM available.
9.3.1 Swapping Miscellany
The system swap space consists of a special file, called SWAPPER.DAT,
created at boot time. The location of the file is described in the
CONFIG.SYS file. OS/2 may not allocate the entire maximum size of the swap
file initially; instead, it may allocate a smaller size and grow the swap
file to its maximum size if needed. The swap file may grow, but in OS/2
version 1.0 it never shrinks.
The available swap space in the system is more than the maximum size
of the swap file; it also includes extra RAM. Clearly, a system with 8 MB of RAM and a 200 KB swap file should be able to run programs that consume more than 200 KB. After setting aside the memory consumed by nonswappable segments and our six 64 KB reserved areas, the remaining RAM is considered part of the swap file for memory overcommit accounting purposes.
We mentioned in passing that memory used in real mode can't be
swapped. This means that the entire 3x box memory area is nonswappable. In fact, the casual attitude of MS-DOS applications toward memory allocation forces OS/2 to keep a strict boundary between real mode and protect mode memory. Memory below the RMSIZE value specified in CONFIG.SYS belongs exclusively to the real mode program, minus that consumed by the device drivers and the parts of the OS/2 kernel that run in real mode.
Early in the development of OS/2, attempts were made to put protect
mode segments into any unused real mode memory, but we abandoned this approach. First, because the risk was great that the real mode program might overwrite part of the segment. Although this is technically a bug on the part of the real mode application, such bugs generally do not affect program execution in an MS-DOS environment because that memory is unused at the time. Thus, such bugs undoubtedly exist unnoticed in today's MS-DOS applications, waiting to wreak havoc in the OS/2 environment.
A second reason concerns existing real mode applications having been
written for a single-tasking environment. Such an application commonly asks for 1 MB of memory, a request that must be refused. The refusal, however, also specifies the amount of memory available at the time of the call. Real mode applications then turn around and ask for that amount, but they don't check to see if an "insufficient memory" error code was returned from the second call. After all, how could such a code be returned? The operating system has just said that the memory was available. This coding sequence can cause disaster in a multitasking environment where the memory might have been allocated elsewhere between the first and second call from the application. This is another reason OS/2 sets aside a fixed region of memory for the 3x box and never uses it for other purposes, even if it appears to be idle.
We mentioned that OS/2's primary concern is that programs be able to
execute at all; whether they execute well is the user's problem. This approach is acceptable because OS/2 is a single-user system. Multiuser systems need to deal with thrashing situations because the users that suffer from thrashing may not be the ones who created it and may be powerless to alleviate it. In a single-user environment, however, the user is responsible for the load that caused the thrashing, the user is the one who is suffering from it, and the user is the one who can fix the situation by buying more RAM or terminating a few applications. Nevertheless, applications with considerable memory needs should be written so as to minimize their impact on the system swapper.
Fundamentally, all swapping optimization techniques boil down to one
issue: locality of reference. This means keeping the memory locations that are referenced near one another in time and in space. If your program supports five functions, put the code of each function in a separate segment, with another segment holding common code. The user can then work with one function, and the other segments can be swapped. If each function had some code in each of five segments, all segments would have to be in memory at all times.
A large body of literature deals with these issues because of the
prevalence of virtual memory systems in the mainframe environment. Most of this work was done when RAM was very expensive. To precisely determine which segments or pages should be resident and which should be swapped was worth a great deal of effort. Memory was costly, and swapping devices were fast, so algorithms were designed to "crank the screws down tight" and free up as much memory as possible. After all, if they misjudged and swapped something that was needed soon, it could be brought back in quickly. The OS/2 environment is inverted: RAM is comparatively cheap, and the swapping disk, being the regular system hard disk, is comparatively slow. Consequently, OS/2's swapping strategy is to identify segments that are clearly idle and swap them (because cheap RAM doesn't mean free RAM) but not to judge things so closely that segments are frequently swapped when they should not be.
A key concept derived from this classic virtual memory work is that of
the working set. A thread's working set is the set of segments it will reference "soon"--in the next several seconds or few minutes. Programmers should analyze their code to determine its working sets; obviously the set of segments in the working set will vary with the work the application is doing. Code and data should be arranged between segments so that the size of each common working set consists of a minimum amount of memory. For example, if a program contains extensive code and data to deal with uncommon error situations, these items should reside in separate segments so that they aren't resident except when needed. You don't want to burden the system with too many segments; two functions that are frequently used together should occupy the same segment, but large unrelated bodies of code and data should have their own segments or be grouped with other items that are in their working set. Consider segment size when packing items into segments. Too many small segments increase system overhead; large segments decrease the efficiency of the swap mechanism. Splitting a segment in two doesn't make sense if all code in the segment belongs to the same working set, but it does make sense to split large bodies of unrelated code and data.
As we said before, an exhaustive discussion of these issues is beyond
the scope of this book. Programmers writing memory-intensive applications should study the literature and their programs to optimize their performance in an OS/2 environment. Minimizing an application's memory requirements is more than being a "good citizen"; the smaller a program's working set, the better it will run when the system load picks up.
9.4 Status and Information
OS/2 takes advantage of the 80286 LDT and GDT architecture in providing two special segments, called infosegs, that contain system information. OS/2 updates these segments when changes take place, so their information is always current. One infoseg is global, and the other is local. The global infoseg contains information about the system as a whole; the local infoseg contains process specific data. Naturally, the global infoseg is read only and is shared among all processes. Local infosegs are also read only, but each process has its own.
The global infoseg contains time and date information. The "seconds
elapsed since 1970" field is particularly useful for time-stamping events because calculating the interval between two times is easy. Simply subtract and then divide by the number of seconds in the unit of time in which you're interested. It's important that you remember that the date/time fields are 32-bit fields but the 80286 reads data 16 bits at a time. Thus, if an application reads the two time-stamp words at the same time as they are being updated, it may read a bad value--not a value off by 1, but a value that is off by 63335. The easiest way to deal with this is to read the value and then compare the just read value with the infoseg contents. If they are the same, your read value is correct. If they differ, continue reading and comparing until the read and infoseg values agree. The RAS 6. Reliability, Availability, and Serviceability. A buzzword that refers to components intended to aid field diagnosis of system malfunctions. 6 information is used for field system diagnosis and is not of general interest to programmers.
The local infoseg segment contains process and thread information. The
information is accurate for the currently executing thread. The subscreen group value is used by the presentation manager subsystem and is not of value to applications. For more information on global and local infosegs, see the OS/2 reference manual.
10 Environment Strings
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
A major requirement of OS/2 is the ability to support logical device and directory names. For example, a program needs to write a temporary scratch file to the user's fastest disk. Which disk is that? Is it drive C, the hard disk? Some machines don't have a hard disk. Is it drive B, the floppy drive? Some machines don't have a drive B. And even if a hard disk on drive C exists, maybe drive D also exists and has more free space. Or perhaps drive E is preferred because it's a RAM disk. Perhaps it's not, though, because the user wants the scratch file preserved when the machine is powered down. This program needs the ability to specify a logical directory--the scratch file directory--rather than a physical drive and directory such as A:\ or C:\TEMP. The user could then specify the physical location (drive and directory) that corresponds to the logical directory.
Another example is a spell-checker program that stores two
dictionaries on a disk. Presumably, the dictionary files were copied to a hard disk when the program was installed, but on which drive and directory? The checker's author could certainly hard code a directory such as C:\SPELLCHK\DICT1. But what if the user doesn't have a C drive, or what if drive C is full and the user wants to use drive D instead? How can this program offer the user the flexibility of putting the dictionary files where they best fit and yet still find them when it needs them?
The answer to these problems is a logical device and directory name
facility. Such a facility should have three characteristics:
þ It should allow the user to map the logical directories onto the actual (physical) devices and directories at will. It should be possible to change these mappings without changing the programs that use them.
þ The set of possible logical devices and directories should be very large and arbitrarily expandable. Some new program, such as our spelling checker, will always need a new logical directory.
þ The name set should be large and collision free. Many programs will want to use logical directory names. If all names must come from a small set of possibilities, such as X1, X2, X3, and so on, two applications, written independently, may each choose the same name for conflicting uses.
The original version of MS-DOS did not provide for logical devices and
directories. In those days a maximum PC configuration consisted of two floppy disks. Operating the machine entailed playing a lot of "disk jockey" as the user moved system, program, and data disks in and out of the drives. The user was the only one who could judge which drive should contain which floppy and its associated data, and data files moved from drive to drive dynamically. A logical device mechanism would have been of little use. Logical directories were not needed because MS-DOS version 1.0 didn't support directories. MS-DOS versions 2.x and 3.x propagated the "physical names only" architecture because of memory limitations and because of the catch-22 of new operating system features: Applications won't take advantage of the new feature because many machines are running older versions of MS-DOS without that new feature.
None of these reasons holds true for OS/2. All OS/2 protect mode
applications will be rewritten. OS/2 has access to plenty of memory. Finally, OS/2 needs a logical drive/directory mechanism: All OS/2 machines have hard disks or similar facilities, and all OS/2 machines will run a variety of sophisticated applications that need access to private files and work areas. As a result, the environment string mechanism in MS-DOS has been expanded to serve as the logical name in OS/2.
Because of the memory allocation techniques employed by MS-DOS
programs and because of the lack of segment motion and swapping in real mode, the MS-DOS environment list was very limited in size. The size of the environment segment was easily exceeded. OS/2 allows environment segments to be grown arbitrarily, at any time, subject only to the hardware's 64 KB length limitation. In keeping with the OS/2 architecture, each process has its own environment segment. By default, the child inherits a copy of the parent's segment, but the parent can substitute other environment values at DosExecPgm time.
Using the environment string facility to provide logical names is
straightforward. If a convention for the logical name that you need doesn't already exist, you must choose a meaningful name. Your installation instructions or software should document how to use the environment string; the application should display an error message or use an appropriate default if the logical names do not appear in the environment string. Because each process has its own environment segment that it inherited from its parent, batch files, startup scripts, and initiator programs that load applications can conveniently set up the necessary strings. This also allows several applications or multiple copies of the same application to define the same logical name differently.
The existing conventions are:
PATH= PATH defines a list of directories that CMD.EXE searches when it has been instructed to execute a program. The directories are searched from left to right and are separated by semicolons. For example,
PATH=C:\BIN;D:\TOOLS;.
means search C:\BIN first, D:\TOOLS second, and the current working directory third.
DPATH= DPATH defines a list of directories that programs may search to locate a data file. The directories are searched from left to right and are separated by semicolons. For example:
DPATH=C:\DBM;D:\TEMP;.
Applications use DPATH as a convenience to the user: A user can work from one directory and reference data files in another directory, named in the DPATH string, without specifying the full path names of the data files. Obviously, applications and users must use this technique with care. Searching too widely for a filename is extremely dangerous; the wrong file may be found because filenames themselves are often duplicated in different directories. To use the DPATH string, an application must first use DosScanEnv to locate the DPATH string, and then it must use DosSearchPath to locate the data file.
INCLUDE= The INCLUDE name defines the drive and directory where compiler and assembler standard include files are located.
INIT= The INIT name defines the drive and directory that contains initialization and configuration information for the application. For example, some applications define files that contain the user's preferred defaults. These files might be stored in this directory.
LIB= The LIB name defines the drive and directory where the standard language library modules are kept.
PROMPT= The PROMPT name defines the CMD.EXE prompt string. Special character sequences are defined so that the CMD.EXE prompt can contain the working directory, the date and time, and so on. See CMD.EXE documentation for details.
TEMP= The TEMP name defines the drive and directory for temporary files. This directory is on a device that is relatively fast and has sufficient room for scratch files. The TEMP directory should be considered volatile; its contents can be lost during a reboot operation.
The environment segment is a very flexible tool that you can use to
customize the environment of an application or a group of applications. For example, you can use environment strings to specify default options for applications. Users can use the same systemwide default or change that value for a particular screen group or activation of the application.
11 Interprocess Communication
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Interprocess Communication (IPC) is central to OS/2. As we discussed earlier, effective IPC is needed to support both the tool-based architecture and the dynlink interface for interprocess services. Because IPC is so important, OS/2 provides several forms to fulfill a variety of needs.
11.1 Shared Memory
Shared memory has already been discussed in some detail. To summarize, the two forms are named shared memory (access is requested by the client by name) and giveaway shared memory (a current owner gives access to another process). Shared memory is the most efficient form of IPC because no data copying or calls to the operating system kernel are involved once the shared memory has been set up. Shared memory does require more effort on the part of the client processes; a protocol must be established, semaphores and flags are usually needed, and exposure to amok programs and premature termination must be considered. Applications that expect to deal with a low volume of data may want to consider using named pipes.
11.2 Semaphores
A semaphore is a flag or a signal. In its basic form a semaphore has only two states--on and off or stop and go. A railroad semaphore, for example, is either red or green--stop or go. In computer software, a semaphore is a flag or a signal used by one thread of execution to flag or signal another. Often it's for purposes of mutual exclusion: "I'm in here, stay out." But sometimes it can be used to indicate other events: "Your data is ready."
OS/2 supports two kinds of semaphores, each of which can be used in
two different ways. The two kinds of semaphores--RAM semaphores and system semaphores--have a lot in common, and the same system API is used to manipulate both. A RAM semaphore, as its name implies, uses a 4-byte data structure kept in a RAM location that must be accessible to all threads that use it. The system API that manipulates RAM semaphores is located in a dynlink subsystem. This code claims semaphores with an atomic test-and-set operation, 1. An atomic operation is one that is indivisible and therefore cannot be interrupted in the middle. 1 so it need not enter the kernel (ring 0) to protect itself against preemption. As a result, the most common tasks--claiming a free semaphore and freeing a semaphore that has no waiters--are very fast, on the order of 100 microseconds on a 6-MHz 1-wait-state IBM PC/AT. 2. This is the standard environment when quoting speeds; it's both the common case and the worst case. Machines that don't run at this speed run faster. 2 If the semaphore is already claimed and the caller must block or if another thread is waiting on the semaphore, the semaphore dynlink package must enter kernel mode.
System semaphores, on the other hand, use a data structure that is
kept in system memory outside the address space of any process. Therefore, system semaphore operations are slower than RAM semaphore operations, on the order of 350 microseconds for an uncontested semaphore claim. Some important advantages offset this operating speed however. System semaphores support mechanisms that prevent deadlock by crashing programs, and system semaphores support exclusivity and counting features. As a general rule, you should use RAM semaphores when the requirement is wholly contained within one process. When multiple processes may be involved, use system semaphores.
The first step, regardless of the type or use of the semaphore, is to
create it. An application creates RAM semaphores simply by allocating a 4- byte area of memory initialized to zero. The far address of this area is the RAM semaphore handle. The DosCreateSem call creates system semaphores. (The DosCreateSem call takes an exclusivity argument, which we'll discuss later.) Although semaphores control thread execution, semaphore handles are owned by the process. Once a semaphore is created and its handle obtained, all threads in that process can use that handle. Other processes must open the semaphore via DosOpenSem. There is no explicit open for a RAM semaphore. To be useful for IPC, the RAM semaphore must be in a shared memory segment so that another process can access it; the other process simply learns the far address of the RAM semaphore. A RAM semaphore is initialized by zeroing out its 4-byte memory area.
Except for opening and closing, RAM and system semaphores use exactly
the same OS/2 semaphore calls. Each semaphore call takes a semaphore handle as an argument. A RAM semaphore's handle is its address; a system semaphore's handle was returned by the create or open call. The OS/2 semaphore routines can distinguish between RAM and system semaphores by examining the handle they are passed. Because system semaphores and their names are kept in an internal OS/2 data area, they are a finite resource; the number of RAM semaphores is limited only by the amount of available RAM to hold them.
The most common use of semaphores is to protect critical sections. To
reiterate, a critical section is a body of code that manipulates a data resource in a nonreentrant way. In other words, a critical section will screw up if two threads call it at the same time on the same data resource. A critical section can cover more than one section of code; if one subroutine adds entries to a table and another subroutine removes entries, both subroutines are in the table's critical section. A critical section is much like an airplane washroom, and the semaphore is like the sign that says "Occupied." The first user sets the semaphore and starts manipulating the resource; meanwhile others arrive, see that the semaphore is set, and block (that is, wait) outside. When the critical section becomes available and the semaphore is cleared, only one of the waiting threads gets to claim it; the others keep on waiting.
Using semaphores to protect critical sections is straightforward. At
the top of a section of code that will manipulate the critical resource, insert a call to DosSemRequest. When this call returns, the semaphore is claimed, and the code can proceed. When the code is finished and the critical section is "clean," call DosSemClear. DosSemClear releases the semaphore and reactivates any thread waiting on it.
System semaphores are different from RAM semaphores in this
application in one critical respect. If a system semaphore is created for exclusive use, it can be used as a counting semaphore. Exclusive use means that only the thread that set the semaphore can clear it; 3. This is a departure from the principle of resource ownership by process, not by thread. The thread, not the process, owns the privilege to clear a set "exclusive use" semaphore. 3 this is expected when protecting critical sections. A counting semaphore can be set many times but must be released an equal number of times before it becomes free. For example, an application contains function A and function B, each of which manipulates the same critical section. Each claims the semaphore at its beginning and releases it at its end. However, under some circumstances, function A may need to call function B. Function A can't release the semaphore before it calls B because it's still in the critical section and the data is in an inconsistent state. But when B issues DosSemRequest on the semaphore, it blocks because the semaphore was already set by A.
A counting semaphore solves this problem. When function B makes the
second, redundant DosSemRequest call, OS/2 recognizes it as the same thread that already owns the semaphore, and instead of blocking the thread, it increments a counter to show that the semaphore has been claimed twice. Later, when function B releases the semaphore, OS/2 decrements the counter. Because the counter is not at zero, the semaphore is not really clear and thus not released. The semaphore is truly released only after function B returns to function A, and A, finishing its work, releases the semaphore a second time.
A second major use of semaphores is signaling (unrelated to the signal
facility of OS/2). Signaling is using semaphores to notify threads that certain events or activities have taken place. For example, consider a multithreaded application that uses one thread to communicate over a serial port and another thread to compute with the results of that communication. The computing thread tells the communication thread to send a message and get a reply, and then it goes about its own business. Later, the computing thread wants to block until the reply is received but only if the reply hasn't already been received--it may have already arrived, in which case the computing thread doesn't want to block.
You can handle this by using a semaphore as a flag. The computing
thread sets the semaphore via DosSemSet before it gives the order to the communications thread. When the computing thread is ready to wait for the reply, it does a DosSemWait on the semaphore it set earlier. When the communications thread receives the reply, it clears the semaphore. When the computing thread calls DosSemWait, it will continue without delay if the semaphore is already clear. Otherwise, the computing thread blocks until the semaphore is cleared. In this example, we aren't protecting a critical section; we're using the semaphore transition from set to clear to flag an event between multiple threads. Our needs are the opposite of a critical section semaphore: We don't want the semaphore to be exclusively owned; if it were, the communications thread couldn't release it. We also don't want the semaphore to be counting. If it counts, the computing thread won't block when it does the DosSemWait; OS/2 would recognize that it did the DosSemSet earlier and would increment the semaphore counter.
OS/2 itself uses semaphore signaling in this fashion when asynchronous
communication is needed. For example, asynchronous I/O uses semaphores in the signaling mode to indicate that an I/O operation has completed. The system timer services use semaphores in the signaling mode to indicate that the specified time has elapsed. OS/2 supports a special form of semaphore waiting, called DosMuxSemWait, which allows a thread to wait on more than one semaphore at one time. As soon as any specified semaphore becomes clear, DosMuxSemWait returns. DosMuxSemWait, like DosSemWait, only waits for a semaphore to become clear; it doesn't set or claim the semaphore as does DosSemRequest. DosMuxSemWait allows a thread to wait on a variety of events and to wake up whenever one of those events occurs.
11.2.1 Semaphore Recovery
We discussed earlier some difficulties that can arise if a semaphore is
left set "orphaned" when its owner terminates unexpectedly. We'll review
the topic because it's critical that applications handle the situation
correctly and because that correctness generally has to be demonstrable by
inspection. It's very difficult to demonstrate and fix timing-related bugs
by just testing a program.
Semaphores can become orphaned in at least four ways:
1. An incoming signal can divert the CPU, and the signal handler can fail to return to the point of interruption.
2. A process can kill another process without warning.
3. A process can incur a GP fault, which is fatal.
4. A process can malfunction because of a coding error and fail to release a semaphore.
The action to take in such events depends on how semaphores are being
used. In some situations, no action is needed. Our example of the computing and communications threads is such a situation. If the process dies, the semaphore and all its users die. Special treatment is necessary only if the application uses DosExitList to run code that needs to use the semaphore. This should rarely be necessary because semaphores are used within a process to coordinate multiple threads and only one thread remains when the exitlist is activated. Likewise, a process can receive signals only if it has asked for them, so an application that does not use signals need not worry about their interrupting its critical sections. An application that does use signals can use DosHoldSignal, always return from a signal handler, or prevent thread 1 (the signal-handling thread) from entering critical sections.
In other situations, the semaphore can protect a recoverable resource.
For example, you can use a system semaphore to protect access to a printer that for some reason is being dealt with directly by applications rather than by the system spooler. If the owner of the "I'm using the printer" system semaphore dies unexpectedly, the next thread that tries to claim the semaphore will be able to do so but will receive a special error code that says, "The owner of this semaphore died while holding it." In such a case, the application can simply write a form feed or two to the printer and continue. Other possible actions are to clean up the protected resource or to execute a process that will do so. Finally, an application can display a message to the user saying, "Gee, this database is corrupt! You better do something," and then terminate. In this case, the application should deliberately terminate while holding the semaphore so that any other threads waiting on it will receive the "owner died" message. Once the "owner died" code is received, that state is cleared; so if the recipient of the code releases the semaphore without fixing the inconsistencies in the critical section, problems will result.
Additional matters must be considered if a process intends to clean up
its own semaphores by means of a DosExitList handler. First, exclusive (that is, counting) semaphores must be used. Although an exitlist routine can tell that a RAM or nonexclusive system semaphore is reserved, it cannot tell whether it is the process that reserved it. You may be tempted simply to keep a flag byte that is set each time the semaphore is claimed and cleared each time the semaphore is released, but that solution contains a potentially deadly window of failure. If the thread sets the "I own it" flag before it calls DosSemRequest, the thread could terminate between setting the flag and receiving the semaphore. In that case, the exitlist routine would believe, wrongly, that it owns the semaphore and would therefore release it--a very unpleasant surprise for the true owner of the semaphore. Conversely, if the thread claims the semaphore and then sets the flag, a window exists in which the semaphore is claimed but the flag does not say so. This is also disastrous.
Using exclusive system semaphores solves these problems. As I
mentioned earlier, when the thread that has set a system semaphore dies with the semaphore set, the semaphore is placed into a special "owner died" state so that the next thread to attempt to claim the semaphore is informed of its orphan status. There is an extra twist to this for exclusive-use system semaphores. Should the process die due to an external cause or due to a DosExit call and that process has a DosExitList handler, all orphaned system semaphores are placed in a special "owner died" state so that only that process's remaining thread--the one executing the DosExitList handlers--can claim the semaphore. When it does so, it still receives the special "owner died" code. The exitlist handler can use DosSemWait with a timeout value of 0 to see if the semaphore is set. If the "owner died" code is returned, then the DosExitList handler cleans up the resource and then issues DosSemClear to clear the semaphore. If a thread terminates by explicitly calling DosExit with the "terminate this thread" subcode, any exclusive-use system semaphores that it has set will not enter this special "owner died" state but will instead assume the general "owner died" state that allows any thread in the system to claim the semaphore and receive the "owner died" code. Likewise, any semaphores in the special "owner died" state that are not cleared by the DosExitList handlers become normal "owner died" semaphores when the process completely terminates.
11.2.2 Semaphore Scheduling
Although multiple threads can wait for a semaphore, only one thread gets
the semaphore when it becomes available. OS/2 schedules semaphore grants
based on CPU priority: The highest-priority waiting thread claims the
semaphore. If several waiting threads are at the highest priority, OS/2
distributes the grants among them evenly.
11.3 Named Pipes
We've already discussed anonymous pipes--stream oriented IPC mechanisms that work via the DosRead and DosWrite calls. Two processes can communicate via anonymous pipes only if one is a descendant of the other and if the descendant has inherited the parent's handle to the pipe. Anonymous pipes are used almost exclusively to transfer input and output data to and from a child process or to and from a subtree of child processes.
OS/2 supports another form of pipes called named pipes. Named pipes
are not available in OS/2 version 1.0; they will be available in a later release. I discuss them here because of their importance in the system architecture. Also, because of the extensible nature of OS/2, it's possible that named pipe functionality will be added to the system by including the function in some other Microsoft system software package that, when it runs under OS/2, installs the capability. In such a case, application programs will be unable to distinguish the "add-on" named pipe facility from the "built-in" version that will eventually be included in OS/2.
Named pipes are much like anonymous pipes in that they're a serial
communications channel between two processes and they use the DosRead and DosWrite interface. They are different, however, in several important ways.
þ Named pipes have names in the file system name space. Users of a named pipe need not be related; they need only know the name of a pipe to access it.
þ Because named pipes use the file system name space and because that name space can describe machines on a network, named pipes work both locally (within a single machine) and remotely (across a network).
þ An anonymous pipe is a byte-stream mechanism. The system considers the data sent through an anonymous pipe as an undifferentiated stream of bytes. The writer can write a 100-byte block of data, and the reader can read the data with two 30-byte reads and one 40-byte read. If the byte stream contains individual messages, the recipient must determine where they start and stop. Named pipes can be used in this byte-stream mode, but named pipes also support support message mode, in which processes read and write streams of messages. When the named pipe is in message mode, OS/2 (figuratively!) separates the messages from each other with pieces of waxed paper so that the reader can ask for "the next message" rather than for "the next 100 bytes."
þ Named pipes are full duplex, whereas anonymous pipes are actually a pair of pipes, each half duplex. When an anonymous pipe is created, two handles are returned--a read handle and a write handle. An open of a named pipe returns a single handle, which may (depending on the mode of the DosOpen) be both read and written. Although a full duplex named pipe is accessed via a single handle, the data moving in each direction is kept totally separate. A named pipe should be viewed as two separate pipes between the reader and the writer--one holds data going in, the other holds data coming back. For example, if a thread writes to a named pipe handle and then reads from that handle, the thread will not read back the data it just wrote. The data the thread just wrote in is in the outgoing side; the read reads from the incoming side.
þ Named pipes are frequently used to communicate with processes that provide a service to one or more clients, usually simultaneously. The named pipe API contains special functions to facilitate such use: pipe reusability, multiple pipes with identical names, and so on. These are discussed below.
þ Named pipes support transaction I/O calls that provide an efficient way to implement local and remote procedure call dialogs between processes.
þ Programs running on MS-DOS version 3.x workstations can access named pipes on an OS/2 server to conduct dialogs with server applications because, to a client, a named pipe looks exactly like a file.
You'll recall that the creator of an anonymous pipe uses a special
interface (DosMakePipe) to create the pipe but that the client process can use the DosRead and DosWrite functions, remaining ignorant of the nature of the handle. The same holds true for named pipes when they are used in stream mode. The creator of a named pipe uses a special API to set it up, but its clients can use the pipe while remaining ignorant of its nature as long as that use is serial. 4. Random access, using DosSeek, is not supported for pipes and will cause an error code to be returned. 4 Named pipes are created by the DosMakeNmPipe call. Once the pipe is created, one of the serving process's threads must wait via the DosConnectNmPipe call for the client to open the pipe. The client cannot successfully open the pipe until a DosConnectNmPipe has been issued to it by the server process.
Although the serving process understands that it's using a named pipe
and can therefore call a special named pipe API, the client process need not be aware that it's using a named pipe because the normal DosOpen call is used to open the pipe. Because named pipes appear in the file system name space, the client can, for example, open a file called \PIPE\STATUS, unaware that it's a named pipe being managed by another process. The DosMakeNmPipe call returns a handle to the serving end of the pipe; the DosOpen call returns a handle to the client end. As soon as the client opens a pipe, the DosConnectNmPipe call returns to the serving process.
Communication over a named pipe is similar to that over an anonymous
pipe: The client and server each issue reads and writes to the handle, as appropriate for the mode of the open. When a process at one end of the pipe closes it, the process at the other end gets an error code in response to write operations and an EOF indication in response to read operations.
The scenario just described is simple enough, but that's the problem:
It's too simple. In real life, a serving process probably stays around so that it can serve the next client. This is the purpose behind the DosConnectNmPipe call. After the first client closes its end of the named pipe and the server end sees the EOF on the pipe, the server end issues a DosDisconnectNmPipe call to acknowledge that the client has closed the pipe (either explicitly or via termination). It can then issue another DosConnectNmPipe call to reenable that pipe for reopening by another client or by the same client. In other words, the connect and disconnect operations allow a server to let clients, one by one, connect to it via a single named pipe. The DosDisconnectNmPipe call can be used to forcibly disconnect a client. This action is appropriate if a client makes an invalid request or otherwise shows signs of ill health.
We can serve multiple clients, one at a time, but what about serving
them in parallel? As we've described it so far, our serving process handles only one client. A client's DosOpen call fails if the named pipe already has a client user or if the server process hasn't issued the DosConnectNmPipe call. This is where the instancing parameter, supplied to DosMakeNmPipe, comes in.
When a named pipe is first opened,
5. Like other non-file-system resident named objects, a named pipe remains known to the system only as long as a process has it open. When all handles to a named pipe are closed, OS/2 forgets all information concerning the named pipe. The next DosMakeNmPipe call recreates the named pipe from ground zero. 5 the instance count parameter is specified in the pipe flag's word. If this count is greater than 1, the pipe can be opened by a server process more than once. Additional opens are done via DosMakeNmPipe, which returns another handle to access the new instance of the pipe. Obviously the pipe isn't being "made" for the second and subsequent calls to DosMakeNmPipe, but the DosOpen call can't be used instead because it opens the client end of the named pipe, not the server end. The instance count argument is ignored for the second and subsequent DosMakeNmPipe calls. Extra instances of a named pipe can be created by the same process that created the first instance, or they can be created by other processes. Figure 11-1 illustrates multiple instances of a named pipe.
ÚÄÄÄÄÄÄÄÄÄÄ¿ \\pipe\pipename
³ Client ÃÄÄÄÄÄ¿ � ÚÄÄÄÄÄÄÄÄÄÄÄ¿
³ A ³ ³ °°°°°°°°°°°°°°°°°°°°°° ³ ³
ÀÄÄÄÄÄÄÄÄÄÄÙ ³ ÚÄÄÄÄÄÄÄÄÄÄ¿ ³ ³
ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ³
ÚÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÁÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ Client ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ³ ³ ³ B ³ ÚÄÄÁÄÄÄÄÄÄÄ¿ ³ ³ ³ Server ³ ÀÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄ´ ³ ³ ÃÄÄÄÄÄ´ Process ³
³ ÚÄÄÁÄÄÄÄÄÄÄ¿ ³ ÃÄÄÙ ³ ³
ÚÄÄÄÄÄÄÄÄÄÄ¿ ³ ÚÄÄÄ´ ³ ³ ÃÄÄÄÄÄÄÄÄ´ ³ ³ Client ÃÄÄÄÄÄÙ ³ ³ ³ ÃÄÄÙ ³ ³ ³ C ³ ³ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄ´ ³ ÀÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ÃÄÄÙ ³ ³
³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³
ÚÄÄÄÄÄÄÄÄÄÄ¿ ³ ÀÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ Client ÃÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ³ D ³ ÀÄÄÄÄÄÄÄÄÄÄÙ
Figure 11-1. Multiple instances of a named pipe.
When a client process does a DosOpen on a named pipe that has multiple
instances, OS/2 connects it to any server instance of the pipe that has issued a DosConnectNmPipe call. If no instances are available and enabled, the client receives an error code. OS/2 makes no guarantees about distributing the incoming work evenly across all server instances; it assumes that all server threads that issued a DosConnectNmPipe call are equal.
The multiple instance capability allows a single server process or
perhaps multiple server processes to handle many clients simultaneously. One process using four threads can serve four clients as rapidly as four processes, each with one thread, can do the job. As long as threads don't interfere with one another by blocking on critical sections, a multiprocess server has no inherent efficiency advantage over a multithread server.
The OS/2 named pipe package includes some composite operations for
client processes: DosTransactNmPipe and DosCallNmPipe. DosTransactNmPipe is much like a DosWrite followed by a DosRead: It sends a message to the server end of the named pipe and then reads a reply. DosCallNmPipe does the same on an unopened named pipe: It has the combined effect of a DosOpen, a DosTransactNmPipe, and a DosClose. These calls are of little value if the client and server processes are on the same machine; the client could easily build such subroutines itself by appropriately combining DosOpen, DosClose, DosRead, and DosWrite. These calls are in the named pipe package because they provide significant performance savings in a networked environment. If the server process is on a different machine from the client process, OS/2 and the network transport can use a datagramlike mechanism to implement these calls in a network-efficient fashion. Because named pipes work invisibly across the network, any client process that performs these types of operations should use these composite calls, even if the author of the program didn't anticipate the program being used in a networked environment. Using the composite calls will
ensure the performance gains if a user decides to use a server process
located across the network. Readers familiar with network architecture will recognize the DosCallNmPipe function as a form of remote procedure call. In effect, it allows a process to make a procedure call to another process, even a process on another machine.
The OS/2 named pipe facility contains a great many features, as befits
its importance in realizing the OS/2 tool-based architecture. This book is not intended to provide an exhaustive coverage of features, but a few other miscellaneous items merit mention.
Our above discussion concentrated on stream-based communications,
which can be convenient because they allow a client process to use a named pipe while ignorant of its nature. For example, you can write a spooler package for a device not supported by the system spoolers--say, for a plotter device. Input to the spooler can be via a named pipe, perhaps \PIPE\PLOTOUT. An application could then be told to write its plotter output to a file named \PIPE\PLOTOUT or even \\PLOTMACH\PIPE\PLOTOUT (across a network). The application will then use the spooler at the other end of the named pipe.
Sometimes, though, the client process does understand that it's
talking to a named pipe, and the information exchanged is a series of messages rather than a long stream of plotter data. In this case, the named pipe can be configured as a message stream in which each message is indivisible and atomic at the interface. In other words, when a process reads from a named pipe, it gets only one message per read, and it gets the entire message. Messages can queue up in the pipe, but OS/2 remembers the message boundaries so that it can split them apart as they are read. Message streams can be used effectively in a networking environment because the network transport can better judge how to assemble packets.
Although our examples have shown the client and server processes
issuing calls and blocking until they are done, named pipes can be configured to operate in a nonblocking fashion. This allows a server or a client to test a pipe to see if it's ready for a particular operation, thereby guaranteeing that the process won't be held up for some period waiting for a request to complete. Processes can also use DosPeekNmPipe, a related facility that returns a peek at any data (without consuming the data) currently waiting to be read in the pipe interface. Servers can use this to scan a client's request to see if they're interested in handling it at that time.
Finally, we mentioned that a process that attempts a DosOpen to a
named pipe without any available instances is returned an error code. Typically, a client in this situation wants to wait for service to become available, and it doesn't want to sit in a polling loop periodically testing for server availability. The DosWaitNmPipe call is provided for this situation; it allows a client to block until an instance of the named pipe becomes available. When DosWaitNmPipe returns, the client must still do a DosOpen. The DosOpen can fail, however, if another process has taken the pipe instance in the time between the "wait" and the "open" calls. But because multiple waiters for a named pipe are serviced in priority order, such a "race" condition is uncommon.
11.4 Queues
Queues are another form of IPC. In many ways they are similar to named pipes, but they are also significantly different. Like named pipes, they use the file system name space, and they pass messages rather than byte streams. Unlike named pipes, queues allow multiple writes to a single queue because the messages bring with them information about their sending process that enables the queue reader to distinguish between messages from different senders. Named pipes are strictly FIFO, whereas queue messages can be read in a variety of orders. Finally, queues use shared memory as a transfer mechanism; so although they're faster than named pipes for higher volume data transfers on a single machine, they don't work across the network.
The interface to the queue package is similar but not identical to
that of the named pipe interface. Like named pipes, each queue has a single owner that creates it. Clients open and close the queue while the owner, typically, lives on. Unlike named pipes, the client process must use a special queue API (DosReadQueue, DosWriteQueue, and so on) and thus must be written especially to use the queue package. Although each queue has a single owner, each queue can have multiple clients; so the queue mechanism doesn't need a facility to have multiple queues of the same name, nor does it need a DosWaitNmPipe equivalent.
Queue messages are somewhat different from named pipe messages. In
addition to carrying the body of the message, each queue message carries two additional pieces of information. One is the PID of the sender; OS/2 provides this information, and the sender cannot affect it. The other is a word value that the sender supplied and that OS/2 and the queue package do not interpret. Queue servers and clients can use this information as they wish to facilitate communication.
The queue package also contains a peek facility, similar to that of
named pipes but with an interesting twist. If a process peeks the named pipe and then later reads from it, it can be sure that the message it reads is the same one that it peeked because named pipes are always FIFO. Queues, however, allow records to be read in different orders of priority. If a queue is being read in priority order, a process might well peek a message, but by the time the process issues the queue read, some other message of higher priority may have arrived and thus be at the front of the list. To get around this problem, when the queue package peeks a message, it returns a magic cookie to the caller along with the message. The caller can supply this cookie to a subsequent DosReadQueue call to ensure that the peeked message is the one read, overriding the normal message-ranking process. This magic cookie can also be supplied to the DosPeekQueue call to peek the second and subsequent records in the queue.
Finally, one extremely important difference between queues and named
pipes is that named pipes transfer (that is, copy) the data from the client to the server process. Queues transfer only the address of the data; the queue package does not touch the data itself. Thus, the data body of the queue message must be addressable to both the client and the serving process. This is straightforward if both the client and serving threads belong to the same process. If the client and serving threads are from different processes, however, the data body of the queue message must be in a shared memory segment that is addressable to both the client and the server.
A related issue is buffer reusability. An application can reuse a
memory area immediately after its thread returns from the named pipe call that wrote the data from that area; but when using a queue, the sender must not overwrite the message area until it's sure the reading process is finished with the message.
One way to kill both these birds--the shared memory and the memory
reuse problems--with one stone is to use the memory suballocation package. Both the client and the queue server need to have shared access to a memory segment that is then managed by the memory suballocation package. The client allocates a memory object to hold the queue message and write it to the queue. The queue server can address that queue message because it's in the shared memory segment. When the queue manager is finished with the message, it calls the memory suballocator to release the memory object. The client need not worry about when the server is finished with the message because the client allocates a new message buffer for each new message, relying on the server to return the messages fast enough so that the memory suballocator doesn't run out of available space.
A similar technique on a segment level is to use giveaway shared
memory. The client allocates a giveaway segment for each message content, creates the message, gives away a shared addressability to the segment to the server process, and then writes the message (actually, the message's address) to the queue. Note that the sender uses the recipient's selector as the data address in this case, not its own selector. When the thread returns from that DosWriteQueue call, the client releases its access to the segment via DosFreeSeg. When the server process is finished with the message, it also releases the memory segment. Because the queue server is the last process with access to that segment, the segment is then returned to the free pool.
Software designers need to consider carefully the tradeoffs between
queues, named pipes, and other forms of IPC. Queues are potentially very fast because only addresses are copied, not the data itself; but the work involved in managing and reusing the shared memory may consume the time savings if the messages are small. In general, small messages that are always read FIFO should go by named pipes, as should applications that communicate with clients and servers across a network. Very large or high data rate messages may be better suited to queues.
11.5 Dynamic Data Exchange (DDE)
DDE is a form of IPC available to processes that use the presentation manager API. The presentation manager's interface is message oriented; that is, the primary means of communication between a process and the presentation manager is the passing of messages. The presentation manager message interface allows applications to define private messages that have a unique meaning throughout the PC. DDE is, strictly speaking, a protocol that defines new messages for communication between applications that use it.
DDE messages can be directed at a particular recipient or broadcast to
all presentation manager applications on a particular PC. Typically, a client process broadcasts a message that says, "Does anyone out there have this information?" or "Does anyone out there provide this service?" If no response is received, the answer is taken to be no. If a response is received, it contains an identifying code 6.A window handle. 6 that allows the two processes to communicate privately.
DDE's broadcast mechanism and message orientation gives it a lot of
flexibility in a multiprocessing environment. For example, a specialized application might be scanning stock quotes that are arriving via a special link. A spreadsheet program could use DDE to tell this scanner application to notify it whenever the quotes change for certain stocks that are mentioned in its spreadsheet. Another application, perhaps called Market Alert, might ask the scanner to notify it of trades in a different set of stocks so that the alert program can flash a banner if those stocks trade outside a prescribed range. DDEs can be used only by presentation manager applications to communicate with the same.
11.6 Signaling
Signals are asynchronous notification mechanisms that operate in a fashion analogous to hardware interrupts. Like hardware interrupts, when a signal arrives at a process, that process's thread 1 stops after the instruction it is executing and begins executing at a specified handler address. The many special considerations to take into account when using signals are discussed in Chapter 12. This section discusses their use as a form of IPC.
Processes typically receive signals in response to external events
that must be serviced immediately. Examples of such events are the user pressing Ctrl-C or a process being killed. Three signals (flag A, flag B, and flag C), however, are caused by another process 7. This is the typical case; but like all other system calls that affect processes, a thread can make such a call to affect its own process. 7 issuing an explicit DosFlagProcess API. DosFlagProcess is a unique form of IPC because it's asynchronous. The recipient doesn't have to block or poll waiting for the event; it finds out about it (by discovering itself to be executing the signal handler) as soon as the scheduler gives it CPU time.
DosFlagProcess, however, has some unique drawbacks. First, a signal
carries little information with it: only the number of the signal and a single argument word. Second, signals can interrupt and interfere with some system calls. Third, OS/2 views signals more as events than as messages; so if signals are sent faster than the recipient can process them, OS/2 discards some of the overrun. These disadvantages (discussed in Chapter 12) restrict signals to a rather specialized role as an IPC mechanism.
11.7 Combining IPC Forms
We've discussed each form of IPC, listing its strengths and weaknesses. If you use forms in conjunction, however, you benefit from their combined strengths. For example, a process can use named pipes or DDE to establish contact with another process and then agree with it to send a high volume of data via shared memory. An application that provides an IPC interface should also provide a dynlink package to hide the details of the IPC. This gives designers the flexibility to improve the IPC component of their package in future releases while still maintaining interface compatibility with their clients.
12 Signals
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
The OS/2 signal mechanism is similar, but not identical, to the UNIX signal mechanism. A signal is much like a hardware interrupt except that it is initiated and implemented in software. Just as a hardware interrupt causes the CS, IP, and Flags registers to be saved on the stack and execution to begin at a handler address, a signal causes the application's CS, IP, and Flags registers to be saved on the stack and execution to begin at a signal-handler address. An IRET instruction returns control to the interrupted address in both cases. Signals are different from hardware interrupts in that they are a software construct and don't involve privilege transitions, stack switches, or ring 0 code.
OS/2 supports six signals--three common signals (Ctrl-C, Ctrl-Break,
and program termination) and three general-purpose signals. The Ctrl-C and Ctrl-Break signals occur in response to keyboard activity; the program termination signal occurs when a process is killed via the DosKill call. 1. The process termination signal handler is not called under all conditions of process termination, only in response to DosKill. Normal exits, GP faults, and so on do not activate the process termination signal handler. 1 The three general-purpose signals are generated by an explicit call from a thread, typically a thread from another process. A signal handler is in the form of a far subroutine, that is, a subroutine that returns with a far return instruction. When a signal arrives, the process's thread 1 is interrupted from its current location and made to call the signal-handler procedure with an argument provided by the signal generator. The signal- handler code can return, 2. OS/2 interposes a code thunk, so the signal handler need not concern itself with executing an IRET instruction, which language compilers usually won't generate. When the signal handler is entered, its return address points to a piece of OS/2 code that contains the IRET instruction. 2 in which case the CPU returns from where it was interrupted, or the signal handler can clean its stack and jump into the process's code at some other spot, as in the C language's longjmp facility.
The analogy between signals and hardware interrupts holds still
further. As it does in a hardware interrupt, the system blocks further interrupts from the same source so that the signal handler won't be arbitrarily reentered. The signal handler must issue a special form of DosSetSigHandler to dismiss the signal and allow further signals to occur. Typically this is done at the end of the signal-handling routine unless the signal handler is reentrant. Also, like hardware interrupts, the equivalent of the CLI instruction--the DosHoldSignal call--is used to protect critical sections from being interrupted via a signal.
Unlike hardware interrupts, signals have no interrupt priority. As
each enabled signal occurs, the signal handler is entered, even if another signal handler must be interrupted. New signal events that come in while that signal is still being processed from an earlier event--before the signal has been dismissed by the handler--are held until the previous signal event has been dismissed. Like hardware interrupts, this is a pending-signal flag, not a counter. If three signals of the same kind are held off, only one signal event occurs when that signal becomes reenabled.
A signal event occurs in the context of a process whose thread of
execution is interrupted for the signal handler; a signal doesn't cause the CPU to stop executing another process in order to execute the first process's signal handler. When OS/2 "posts" a signal to a process, it simply makes a mark that says, "The next time we run this guy, store his CS, IP, and Flags values on the stack and start executing here instead." The system uses its regular priority rules to assign the CPU to threads; when the scheduler next runs the signaled thread, the dispatcher code that sends the CPU into the application's code reads the "posted signal" mark and does the required work.
Because a signal "pseudo interrupt" is merely a trick of the
dispatcher, signal handlers don't run in ring 0 as do hardware interrupt handlers; they run in ring 3 as do all application threads. In general, as far as OS/2 is concerned, the process isn't in any sort of special state when it's executing a signal handler, and no special rules govern what a thread can and cannot do in a signal handler.
Receiving a signal when thread 1 is executing an application or
dynlink code is straightforward: The system saves CS, IP, and Flags, and the signal handler saves the rest. The full register complement can be restored after the signal has been processed, and thread 1's normal execution resumes without incident. If thread 1 is executing a system call that takes the CPU inside the kernel, the situation is more complex. OS/2 can't emulate an interrupt from the system ring 0 code to the application's ring 3 code, nor can OS/2 take the chance that the signal handler never returns from the signal 3.It's acceptable for a signal handler to clean up thread 1's stack, dismiss the signal, jump to another part of the application, and never return from the signal. For example, an application can jump into its "prompt and command loop" in response to the press of Ctrl-C. 3 and therefore leaves OS/2's internals in an intermediate state. Instead, when a signal is posted and thread 1 is executing ring 0 OS/2 code, the system either completes its operations before recognizing the signal or aborts the operation and then recognizes the signal. If the operation is expected to take place "quickly," the system completes the operation, and the signal is recognized at the point where the CPU resumes executing the application's ring 3 code.
All non-I/O operations are deemed to complete "quickly," with the
exception of the explicit blocking operations such as DosSleep, DosSemWait, and so on. I/O operations depend on the specific device. Disk I/O completes quickly, but keyboard and serial I/O generally do not. Clearly, if we wait for the user to finish typing a line before we recognize a signal, we might never recognize it--especially if the signal is Ctrl-C! In the case of "slow devices," OS/2 or the device driver terminates the operation and returns to the application with an error code. The signal is recognized when the CPU is about to resume executing the ring 3 application code that follows the system call that was interrupted.
Although the application is given an error code to explain that the
system call was interrupted, the application may be unable to reissue the system call to complete the work. In the case of device I/O, the application typically can't tell how much, if any, of the requested output or input took place before the signal interrupted the operation. If an output operation is not reissued, some data at the end of the write may be missing. If an output operation is restarted, then some data at the beginning of the write may be written twice. In the case of DosSleep, the application cannot tell how much of the requested sleep has elapsed. These issues are not usually a problem; it's typically keyboard input that is interrupted. In the case of the common signals (Ctrl-C, Ctrl-Break, and process killed) the application typically flushes partial keyboard input anyway. Applications that use other "slow" devices or the IPC flag signals need to deal with this, however.
Although a process can have multiple threads, only thread 1 is used to
execute the signal handler. 4. For this reason, a process should not terminate thread 1 and continue executing with others; then it cannot receive signals. 4 This leads to an obvious solution to the interrupted system call problem: Applications that will be inconvenienced by interrupted system calls due to signals should dedicate thread 1 to work that doesn't make interruptible system calls and use other thread(s) for that work. In the worst case, thread 1 can be totally dedicated to waiting for signals: It can block on a RAM semaphore that is never released, or it can execute a DosSleep loop.
A couple of practical details about signals are worth noting. First,
the user of a high-level language such as C need not worry about saving the registers inside the signal-handler routine. The language runtimes typically provide code to handle all these details; as far as the application program is concerned, the signal handler is asynchronously far called, and it can return from the signal by the return() statement. Also, no application can receive a signal without first requesting it, so you need not worry about setting up signal handlers if your application doesn't explicitly ask to use them. A process can have only one signal-handling address for each signal, so general-purpose dynlink routines (ones that might be called by applications that aren't bundled with the dynlink package) should never set a signal handler; doing so might override a handler established by the client program code.
Signals interact with critical sections in much the same way as
interrupts do. If a signal arrives while thread 1 is executing a critical section that is protected by a semaphore and if that signal handler never returns to the interrupted location, the critical section's semaphore will be left jammed on. Even if the signal handler eventually returns, deadlock occurs if it attempts to enter the critical section during processing of the signal (perhaps it called a dynlink package, unaware that the package contained a critical section). Dynlink packages must deal with this problem by means of the DosHoldSignal call, which is analogous to the CLI/STI instructions for hardware interrupts: The DosHoldSignal holds off arriving signals until they are released. Held-off signals should be released within a second or two so that the user won't be pounding Ctrl-C and thinking that the application has crashed. Applications can use DosHoldSignal, or they can simply ensure that thread 1 never enters critical sections, perhaps by reserving it for signal handling, as discussed above.
Ctrl-C and Ctrl-Break are special, device-specific operations. Setting
a signal handler for these signals is a form of I/O to the keyboard device; applications must never do this until they have verified that they have been assigned the keyboard device. See Chapter 14, Interactive Programs.
13 The Presentation Manager and VIO
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
In the early chapters of this book, I emphasized the importance of a high- powered, high-bandwidth graphical user interface. It's a lot of work for an application to manage graphical rendition, windowing, menus, and so on, and it's hard for the user to learn a completely different interface for each application. Therefore, OS/2 contains a subsystem called the presentation manager (PM) that provides these services and more. The presentation manager is implemented as a dynlink subsystem and daemon process combination, and it provides:
þ High-performance graphical windowing.
þ A powerful user interface model, including drop-down menus, scroll bars, icons, and mouse and keyboard interfaces. Most of these facilities are optional to the application; it can choose the standard services or "roll its own."
þ Device independence. The presentation manager contains a sophisticated multilevel device interface so that as much work as possible is pushed down to "smart" graphics cards to optimize performance.
Interfacing an application with the presentation manager involves a
degree of effort that not all programmers may want to put forth. The interface to the application may be so simple that the presentation manager's features are of little value, or the programmer may want to port an MS-DOS application to OS/2 with the minimum degree of change. For these reasons, OS/2 provides a second interface package called VIO, 1. VIO is a convenience term that encompasses three dynlink subsystems: KBD (keyboard), VIO (Video I/O; the display adapter), and MOU (mouse). 1 which is primarily character oriented and looks much like the MS-DOS ROM BIOS video interface. The initial release of OS/2 contains only VIO, implemented as a separate package. The next release will contain the presentation manager, and VIO will then become an alternate interface to the presentation manager.
Fundamentally, the presentation manager and VIO are the equivalent of
device drivers. They are implemented as dynlink packages because they are device dependent and need to be replaced if different devices are used. Dynlinks are used instead of true device drivers because they can provide high throughput for the screen device: A simple call is made directly to the code that paints the pixels on the screen. Also, the dynlink interface allows the presentation manager to be implemented partially as a dynlink subsystem and partially as a daemon process accessed by that subsystem.
These packages are complex; explaining them in detail is beyond the
scope of this book. Instead, I will discuss from a general perspective the special issues for users of this package.
VIO is essentially character oriented. It supports graphics-based
applications, but only to the extent of allowing them to manipulate the display controller directly so that they can "go around" VIO and provide special interfaces related to screen switching of graphics applications (see below). The base VIO package plays a role similar to that of the ROM BIOS INT 10/INT 16 interface used in MS-DOS. It contains some useful enhancements but in general is a superset of the ROM BIOS functions, so INT 10-based real mode applications can be quickly adjusted to use VIO instead. VIO is replaceable, in whole or in part, to allow applications being run with VIO to be managed later by the presentation manager package.
The presentation manager is entirely different from VIO. It offers an
extremely rich and powerful set of functions that support windowing, and it offers a full, device-independent graphics facility. Its message-oriented architecture is well suited to interactive applications. Once the presentation manager programming model is learned and the key elements of its complex interface are understood, a programmer can take advantage of a very sexy user interface with comparatively little effort. The presentation manager also replaces the existing VIO/KBD/MOU calls in order to support older programs that use these interfaces.
13.1 Choosing Between PM and VIO
The roles of VIO and the presentation manager sometimes cause confusion: Which should you use for your application? The default interface for a new application should be the presentation manager. It's not in OS/2 version 1.0 because of scheduling restrictions; owners of version 1.0 will receive the presentation manager as soon as it is available, and all future releases will be bundled with the presentation manager. The presentation manager will be present on essentially all personal computer OS/2 installations, so you are not restricting the potential market for an application if you write it for the presentation manager.
The presentation manager interface allows an application to utilize a
powerful, graphical user interface. In general form it's standardized for ease of use, but it can be customized in a specific implementation so that an application can provide important value-added features. On the other hand, if you are porting an application from the real mode environment, you will find it easier to use the VIO interface. Naturally, such programs run well under the presentation manager, but they forgo the ability to use graphics and to interact with the presentation manager. The user can still "window" the VIO application's screen image, but without the application's knowledge or cooperation. To summarize, you have three choices when writing an application:
1. Only use the VIO interface in character mode. This works well in a presentation manager environment and is a good choice for ported real mode applications. The VIO interface is also supported by the Family API mechanism. This mode is compatible with the Family API facility.
2. Use the special VIO interface facilities to sidestep VIO and directly manipulate the display screen in either character or graphics mode. This also works in a presentation manager environment, but the application will not be able to run in a window. This approach can be compatible with the Family API if it is carefully implemented.
3. Use the presentation manager interface--the most sophisticated interface for the least effort. The presentation manager interface provides a way to "operate" applications that will become a widely known user standard because of the capabilities of the interface, because of the support it receives from key software vendors, and because it's bundled with OS/2. The user is obviously at an advantage if he or she does not have to spend time learning a new interface and operational metaphors to use your application. Finally, Microsoft is a strong believer in the power of a graphical user interface; future releases of OS/2 will contain "more-faster-better" presentation manager features. Many of these improvements will apply to existing presentation manager applications; others will expand the interface API. The standard of performance for application interfaces, as well as for application performance, continues to evolve. The rudimentary interfaces and function of the first-generation PC software are no longer considered competitive. Although OS/2 can do nothing to alleviate the developer's burden of keeping an application's function competitive, the presentation manager is a great help in keeping the application's interface state of the art.
Clearly, using the presentation manager interface is the best
strategy for new or extensively reworked applications. The presentation manager API will be expanded and improved; the INT 10-like VIO functions and the VIO direct screen access capabilities will be supported for the foreseeable future, but they're an evolutionary dead end. Given that, you may want to use the VIO mechanism or the Family API facilities to quickly port an application from a real mode version and then use the presentation manager in a product upgrade release.
13.2 Background I/O
A process is in the background when it is no longer interacting directly with the user. In a presentation manager environment, this means that none of the process's windows are the keyboard focus. The windows themselves may still be visible, or they may be obscured or iconic. In a VIO environment, a process is in the background when the user has selected another screen group. In this case, the application's screen display is not visible.
A presentation manager application can easily continue to update its
window displays when it is in the background; the application can continue to call the presentation manager to change its window contents in any way it wishes. A presentation manager application can arrange to be informed when it enters and leaves the background (actually, receives and loses the keyboard focus), or it can simply carry on with its work, oblivious to the issue. Background I/O can continue regardless of whether I/O form is character or graphics based.
VIO applications can continue to do I/O in background mode as well.
The VIO package maintains a logical video buffer for each screen group; when VIO calls are made to update the display of a screen group that is in the background, VIO makes the requested changes to the logical video buffer. When the screen group is restored to the foreground, the updated contents of the logical video buffer are copied to the display's physical video buffer.
13.3 Graphics Under VIO
VIO is a character-oriented package and provides character mode applications with a variety of services. As we have just seen, when a screen switch takes place, VIO automatically handles saving the old screen image and restoring the new. VIO does provide a mechanism to allow an application to sidestep VIO and directly manipulate the physical video buffer, where it is then free to use any graphical capability of the hardware. There are two major disadvantages to sidestepping VIO for graphics rather than using the presentation manager services:
1. The application is device dependent because it must manipulate the video display hardware directly.
2. VIO can no longer save or restore the state of the physical video buffer during screen switch operations. The application must use a special VIO interface to provide these functions itself.
The following discussion applies only to applications that want to
sidestep the presentation manager and VIO interfaces and interact directly with the display hardware.
Gaining access to the video hardware is easy; the VIO call VioGetBuf
provides a selector to the video buffer and also gives the application's ring 2 code segments, if any, permission to program the video controller's registers. The complication arises from the screen-switching capabilities of OS/2. When the user switches the application into a background screen group, the contents of the video memory belong to someone else; the application's video memory is stored somewhere in RAM. It is disastrous when an application doesn't pay attention to this process and accidentally updates the video RAM or the video controller while they are assigned to another screen group.
Two important issues are connected with screen switching: (1) How does
an application find out that it's in background mode? (2) Who saves and restores its screen image and where? The VioScrLock call handles screen access. Before every access to the display memory or the display controller, an application must first issue the VioScrLock call. While the call is in effect, OS/2 cannot perform any screen switches. Naturally, the application must do its work and quickly release the screen switch lockout. Failure to release the lock in a timely fashion has the effect of hanging the system, not only for a user's explicit screen switch commands, but also for other facilities that use the screen switch mechanism, such as the hard error handler. Hard errors can't be presented to the user while the screen lock is in effect. If OS/2 needs to switch screens, and an aberrant application has the screen lock set, OS/2 will cancel the lock and perform the screen switch after a period (currently 30 seconds). This is still a disaster scenario, although a mollified one, because the application that was summarily "delocked" will probably end up with a trashed screen image. The screen lock and unlock calls execute relatively rapidly, so they can be called frequently to protect only the actual write-to-screen operation, leaving the screen unlocked during computation. Basically, an application should use VioScrLock to protect a block of I/O that can be written, in its entirety, without significant recomputation. Examples of such blocks are a screen scroll, a screen erase, and a write to a cell in a spreadsheet program.
VioScrLock must be used to protect code sequences that program the
display hardware as well as code sequences that write to video memory. Some peripheral programming sequences are noninterruptible. For example, a two- step programming sequence in which the first I/O write selects a multiplexed register and the second write modifies that register is uninterruptible because the first write placed the peripheral device into a special state. Such sequences must be protected within one lock/unlock pair.
Sometimes when an application calls VioScrLock, it receives a special
error code that says, "The screen is unavailable." This means that the screen has been switched into the background and that the application may not--and must not--manipulate the display hardware. Typically, the program issues a blocking form of VioScrLock that suspends the thread until the screen is again in the foreground and the video display buffers contain that process's image.
An application that directly manipulates the video hardware must do
more than simply lay low when it is in the background. It must also save and restore the entire video state--the contents of the display buffer and the modes, palates, cursors, and so on of the display controller. VIO does not provide this service to direct-screen manipulation processes for two reasons. First, the process is very likely using the display in a graphics mode. Some display cards contain a vast amount of video memory, and VIO would be forced to save it all just in case the application was using it all. Second, many popular display controllers such as the EGA and compatibles contain many write-only control registers. This means that VIO cannot read the controller state back from the card in order to save it for a later restoration. The only entity that understands the state of the card, and therefore the only entity that can restore that state, is the code that programmed it--the application itself.
But how does the system notify the process when it's time to save or
restore? Processes can call the system in many ways, but the system can't call processes. OS/2 deals with this situation by inverting the usual meaning of call and return. When a process first decides to refresh its own screen, it creates an extra thread and uses that thread to call the VioSavRedrawWait function. The thread doesn't return from this call right away; instead, VIO holds the thread "captive" until it's time for a screen switch. To notify the process that it must now save its screen image, VIO allows the captive thread to return from the VioSavRedrawWait call. The process then saves the display state and screen contents, typically using the returned thread. When the save operation is complete, VioSavRedrawWait is called again. This notifies VIO that the save is complete and that the screen can now be switched; it also resets the cycle so that the process can again be notified when it's time to restore its saved screen image. In effect, this mechanism makes the return from VioSavRedrawWait analogous to a system-to-process call, and it makes the later call to VioSavRedrawWait analogous to a return from process to system.
The design of OS/2 generally avoids features in which the system calls
a process to help a system activity such as screen switching. This is because a tenet of the OS/2 design religion is that an aberrant process should not be able to crash the system. Clearly, we're vulnerable to that in this case. VIO postpones the screen switch until the process saves its screen image, but what if the process somehow hangs up and doesn't complete the save? The screen is in an indeterminate state, and no process can use the screen and keyboard. As far as the user is concerned, the system has crashed. True, other processes in the system are alive and well, but if the user can't get to them, even to save his or her work, their continued health is of little comfort.
The designers of OS/2 were stuck here, between a rock and a hard
place: Applications had to be able to save their screen image if they were to have direct video access, but such a facility violated the "no crashing" tenet of the design religion. Because the video access had to be supported and the system had to be crash resistant, we found a two-part workaround.
The first part concerns the most common cause of a process hanging up
in its screen-save operation: hard errors. When a hard error occurs, the hard error daemon uses the screen switch mechanism to take control of the screen and the keyboard. The hard error daemon saves the existing screen image and keeps the application that was in the foreground at the time of the hard error from fighting with the daemon over control of the screen and the keyboard. However, if the hard error daemon uses the screen-switching mechanism and if the screen-switching mechanism allows the foreground process to save its own screen image, that process might, while saving its screen image, try to use the device that has the hard error and thus deadlock the system. The device in error won't service more requests until the hard error is cleared, but the hard error can't be cleared until the daemon takes control. The daemon can't take control until the foreground process is through saving, and the foreground process can't complete saving until the device services its request. Note that this deadlock doesn't require an explicit I/O operation on the part of the foreground process; simply allocating memory or referencing a segment might cause swapping or loading activity on the device that is experiencing the hard error.
A two-part approach is used to solve this problem. First, deadlocks
involving the hard error daemon are managed by having the hard error screen switch do a partial screen save. When I said earlier that VIO would not save the video memory of direct access screen groups, I was lying a bit. When the system is doing a hard error screen switch, VIO will save the first part (typically 4 KB) of the video memory--enough to display a page of text. We don't have to worry about how much video RAM the application was using because the video display will be switched to character mode and the hard error daemon will overwrite only a small part of video memory. Naturally, this means that the hard error daemon must always restore the original screen group; it can't switch to a third screen group because the first one's video memory wasn't fully saved.
VIO and the hard error daemon keep enough free RAM around to save this
piece of the video memory so that a hard error screen switch can always take place without the need for memory swapping. When the hard error daemon is finished with the screen, the overwritten video memory is restored from the buffer. As we discussed above, however, VIO can't restore the state of the video controller itself; only the application can do that. The VioModeWait function is used to notify the application that it must restore the screen state.
In summary, any application that directly accesses the video hardware
must provide captive threads to VioSavRedrawWait and to VioModeWait. VioSavRedrawWait will return when the application is to save or to restore the video memory. VioModeWait will return when the application is to restore the state of the video controller from the application's own record of the controller's state.
The second part of the "application hangs while saving screen and
hangs system" solution is unfortunately ad hoc: If the application does not complete its screen save operation within approximately 30 seconds, the system considers it hung and switches the screen anyway. The hung process is suspended while it's in background so that it won't suddenly "come alive" and manipulate the screen. When the process is again in the foreground, the system unsuspends it and hopes that it will straighten itself out. In such a case, the application's screen image may be trashed. At best, the user can enter a "repaint screen" command to the application and all will be well; at worst, the application is hung up, but the system itself is alive and well. Building a system that can detect and correct errors on the part of an application is impossible; the best that we can hope to do is to keep an aberrant application from damaging the rest of the system.
I hope that this long and involved discussion of rules, regulations,
doom, and disaster has not frightened you into contemplating programs that communicate by Morse code. You need be concerned with these issues only if you write applications that manipulate the display device directly, circumventing either VIO or the presentation manager interfaces. These concerns are not an issue when you are writing ordinary text applications that use VIO or the presentation manager or graphics applications that use the presentation manager. VIO and the presentation manager handle screen saving and support background display writing. Finally, I'll point out, as a curiosity, that even processes that use handle operations only to write to STDOUT use VIO or the presentation manager. When STDOUT points to the screen device, the operating system routes STDOUT writes to the VIO/presentation manager packages. This is, of course, invisible to the application; it need not concern itself with foreground/background, EGA screen modes, hard error screen restorations, and the like.
14 Interactive Programs
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
A great many applications interact with the user via the screen and the keyboard. Because the primary function of most desktop computers is to run interactive applications, OS/2 contains a variety of services that make interaction powerful and efficient.
As we've seen in earlier chapters, interactive programs can use the
presentation manager to manage their interface, or they can do it themselves, using VIO or direct video access for their I/O. The presentation manager provides a great deal of function and automatically solves a great many problems. For example, a presentation manager application doesn't have to concern itself with the sharing of the single keyboard among all processes in its screen group. The presentation manager takes care of that by handling the keyboard and by simply sending keyboard events to each process, as appropriate.
If you're writing an application that uses the presentation manager,
then you can skip this chapter. If you're writing an application that does not use the presentation manager but that may be used in an interactive fashion, it's very important that you understand the issues discussed in this chapter. They apply to all programs that use VIO or the STDIN/STDOUT handles to do interactive I/O, even if such programs are being run via the presentation manager.
14.1 I/O Architecture
Simply put, the system I/O architecture says that all programs read their main input from the STDIN handle and write their main output to the STDOUT handle. This applies to all non-presentation manager applications, but especially to interactive applications. The reason is that OS/2 and program-execution utilities such as CMD.EXE (shell programs) cooperate to use the STDIN/STDOUT mechanism to control access to the screen and keyboard. For example, if two processes read from the keyboard at the same time, some keys go to one process, and the rest go to the other in an unpredictable fashion. Likewise, it is a bad idea for more than one process to write to the screen at the same time. 1. Within the same screen group and/or window, of course. Applications that use different virtual screens can each write to their own screen without regard for other virtual screens. 1 Clearly, you don't want too many processes doing keyboard/screen I/O within a single screen group, but you also don't want too few. It would be embarrassing if a user terminated one interactive application in a screen group, such as a program run from CMD.EXE, and CMD.EXE failed to resume use of the keyboard/screen to print a prompt.
So how will we handle this? We might be running a great many processes
in a screen group. For example, you could use CMD.EXE to execute a spreadsheet program, which was told to execute a subshell--another copy of CMD.EXE. The user could then execute a program to interpret a special batch script, which in turn executes an editor. And this editor was told to run a copy of a C compiler to scan the source being edited for errors. Oh, yes, and we forgot to mention that the top level CMD.EXE was told to run a copy of the assembler in parallel with all these other operations (similar to the UNIX "&" operation).
Many processes are running in this screen group; some of them are
interactive, and some are not, and at any time only one is using the keyboard and the screen. Although it would be handy to declare that the most recently executed process will use the keyboard and the screen, you can't: The most recently executed program was the C compiler, and it's not even interactive. OS/2 cannot decide which process should be using the screen and keyboard because OS/2 lacks any knowledge of the function of each process. OS/2 knows only their child-parent relationships, and the situation can be far too complex for that information to be sufficient.
Because OS/2 can't determine which process should be using the screen
and the keyboard, it doesn't try. The processes themselves make the determination. The rule is simple: The process that is currently using the screen and the keyboard can grant access to a child process, or it can keep access for itself. If a process grants access to a child process, then it must keep off the screen and the keyboard until that child terminates. Once the child process is granted use of the screen and the keyboard, the child process is free to do as it wishes, perhaps granting access to its own children. Until that child process terminates, the parent must avoid device conflict by staying quiet.
Let's look at how this works in real life. For example, CMD.EXE, the
first process in the screen group, starts up with STDIN open on the keyboard and STDOUT open on the screen. (The system did this by magic.) When this copy of CMD.EXE is told to execute the spreadsheet program, CMD.EXE doesn't know if the spreadsheet program is interactive or not, so it lets the child process--the spreadsheet program--inherit its STDIN and STDOUT handles, which point to the keyboard and to the screen. Because CMD.EXE granted access to the screen and the keyboard to the child, CMD.EXE can't use STDIN or STDOUT until that child process terminates. Typically, at this point CMD.EXE would DosCWait on its child process.
Now the spreadsheet program comes alive. It writes to STDOUT, which is
the screen, and it reads from STDIN, which is the keyboard. When the spreadsheet program is instructed to run CMD.EXE, it does so, presuming, as did its parent, that CMD.EXE is interactive and therefore letting CMD.EXE inherit its STDIN and STDOUT handles. Now the spreadsheet must avoid any STDIN/STDOUT I/O until its child--CMD.EXE--terminates. As long as these processes continue to run interactive children, things are going to work out OK. When the children start to die and execution starts popping back up the tree, applications restart, using the screen and the keyboard in the proper order.
But what about the detached assembly that CMD.EXE started before it
ran the spreadsheet? In this case, the user has explicitly told CMD.EXE that it wants the application run "detached" from the keyboard. If the user specified a STDIN for the assembler--perhaps a file--then CMD.EXE sets that up for the child's STDIN. If the user didn't specify an alternate STDIN, CMD.EXE opens STDIN on the NULL device so that an application that reads it will receive EOF. In this way, CMD.EXE (which knew that the application wasn't to use the keyboard because the user gave explicit instructions) did not let the child process inherit STDIN, so CMD.EXE continues to use it, printing a new prompt and reading a new command. Figure 14-1 shows a typical process tree. The shaded processes have inherited a STDIN, which points to the keyboard, and a STDOUT, which points to the screen. All such processes must lie on a single path if the rules are followed because each process has the option of allowing a maximum of one child to inherit its STDIN and STDOUT handles unchanged.
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³°°°°°°°°°°°°°°³ ³°°°°°°°°°°°°°°³ ÀÄÄÂÄÄÄÄÄÄÄÄÂÄÄÙ ÚÄÄÄÄ¿ Processes ÚÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄ¿ ³°°°°³ = using the ³ ³ ÀÄÄÄÄÙ keyboard ÚÄÄÄÁÄÄ¿ ÚÄÄÁÄÄÄ¿ ³ ³ ³°°°°°°³ ³ ³ ³°°°°°°³ ÀÄÂÄÄÄÄÙ ÀÄÂÄÄÂÄÙ ÚÄÄÙ ÚÄÄÙ ÀÄÄ¿ ÚÄÄÄÁÄÄ¿ ÚÄÄÁÄ¿ ÚÄÁÄÄ¿ ³ ³ ³°°°°³ ³ ³ ³ ³ ÀÂÄÄÂÙ ÀÄÄÄÂÙ ÀÄÂÄÄÂÄÙ ÚÄÄÙ ÀÄÄ¿ ÀÄÄ¿ ÚÄÄÙ ÀÄÄ¿ ÚÄÄÁÄ¿ ÚÄÁÄÄ¿ ÚÄÁÄÄ¿
ÚÄÄÁÄ¿ ÚÄÁÄÄ¿ ³°°°°³ ³ ³ ³ ³ ³ ³ ³ ³ ÀÂÄÄÂÙ ÀÄÄÄÄÙ ÀÄÄÄÄÙ ÀÄÄÄÄÙ ÀÄÄÄÄÙ ÚÄÄÙ ÀÄÄ¿
ÚÄÄÁÄ¿ ÚÄÁÄÄ¿ ³ ³ ³°°°°³ ÀÄÄÄÄÙ ÀÄÄÄÄÙ
Figure 14-1. Processes using the keyboard.
You are undoubtedly becoming a bit concerned at this point: "Does this
mean I'm forced to use the limited, serial STDIN/STDOUT interface for my high-resolution graphics output?" I'm glad you asked. What we've been discussing is the architectural model that must be followed because it's used systemwide to avoid screen and keyboard conflicts. However, applications can and should use special services to optimize their interactive I/O as long as they do so according to the architectural model. Specifically, OS/2 provides the KBD, VIO, and MOU dynlink packages. These high-performance programs interface directly with the hardware, avoiding the STDIN/STDOUT limited interfaces. The key is "directly with the hardware": A process is welcome to use hardware-specific interfaces to optimize performance, but only after it has ensured that the architectural model grants it access to that device.
In practice, this is straightforward. Any interactive program that
wants to use KBD first ensures (via DosQHandType) that its STDIN handle is open on the keyboard. If STDIN is not open on the keyboard, the keyboard belongs to another program, and the interactive program must not burst in on the rightful owner by using KBD. All dynlink device interfaces are trusting souls and won't check your bona fides before they do their stuff, so the application must look before it leaps. The same applies to STDOUT, to the keyboard device, and to the VIO package. All applications must verify that STDIN and STDOUT point to the keyboard and the screen before they use any device-direct interface, which includes VIO, KBD, MOU, and direct device access.
What's an interactive program to do if it finds that STDIN or STDOUT
doesn't point to the keyboard and screen devices? I don't know, but the author of the application does. Some applications might not be truly interactive and therefore would work fine. For example, Microsoft Macro Assembler (MASM) can prompt the user for the names of source, object, and listing files. Although MASM is technically interacting with the user, MASM is not an interactive application because it doesn't depend on the ability to interact to do its work. If STDIN points to a file, MASM is perfectly happy reading the filenames from that file. MASM doesn't need to see if STDIN points to the keyboard because MASM doesn't need to use the KBD package. Instead, MASM reads its names from STDIN and takes what it gets.
Other programs may not require an interactive interface, but when they
are interacting, they may want to use KBD or VIO to improve performance. Such applications should test STDIN and STDOUT to see if they point to the appropriate devices. If they do, applications can circumvent the STDIN/STDOUT limitations and use KBD and VIO. If they don't, the applications are stuck with STDIN and STDOUT. Finally, many interactive applications make no sense at all in a noninteractive environment. These applications need to check STDIN and STDOUT, and, if they don't point to the devices, the applications should write an error message to STDERR and terminate. Admittedly, the user is in error if he or she attempts to run an interactive application, such as a WYSIWYG editor, detached, but printing an error message is far better than trashing the display screen and fighting with CMD.EXE over the keyboard. The screen group would then be totally unusable, and the user might not even be able to terminate the editor if he or she can't get the terminate command through the keyboard contention.
It's technically possible, although highly unusual, for an application
to inherit access to the keyboard yet not have access to the screen. More commonly, an application has access to the screen but not to the keyboard. Although most users would find it confusing, power users can detach programs such as compilers so that any output summary or error messages they produce appear on the screen. Although the user may end up with intermingled output, he or she may like the instant notification. Each application that wants to use VIO, KBD, or the environment manager needs to check STDIN and STDOUT individually for access to the appropriate device.
Earlier in this section, we talked about how applications work when
they create children that inherit the screen and the keyboard, and it probably sounded complicated. In practice, it can be simple. For example, the technique used to DosExecPgm a child that will inherit the keyboard can be used when the parent itself doesn't have the keyboard and thus can't bequeath it. Therefore, the parent doesn't need to check its STDIN status during the DosExecPgm. To summarize, here are the rules:
Executing Programs
þ If the child process is to inherit the STDIN handle, the parent process must not access that handle any further until the child process terminates.
þ If the child process is not to inherit the STDIN handle (so that your program can continue to interact), then the child process STDIN must be opened on a file or on the NULL device. Don't rely on the child not to use the handle; the child might DosExecPgm a grandchild that is not so well mannered.
þ A process can let only one child at a time inherit its STDIN. If a process is going to run multiple child processes in parallel, only one can inherit STDIN; the others must use alternative STDIN sources.
þ All these rules apply to STDINs open on pipes and files as well as to KBD, so your application needn't check the source of STDIN.
All Processes
þ Verify that the STDIN handle points to the keyboard before using KBD, SIGBRK, or SIGCTLC (see below). You must not use these direct device facilities if STDIN is not open on the keyboard.
þ Verify that the STDOUT handle points to the screen before using VIO. You must not use direct device facilities if STDOUT is not open on the screen.
þ If the process executes any child that inherits its STDIN, it must not terminate itself until that child process terminates. This is because the parent will assume that the termination of the direct child means that the STDIN handle is now available.
14.2 Ctrl-C and Ctrl-Break Handling
Just when you think that it's safe to go back into the operating system, one more device and process tree issue needs to be discussed: the handling of Ctrl-C and Ctrl-Break. (Once again, this discussion applies only to programs that don't explicitly use the presentation manager facility. Those applications that do use the presentation manager have all these issues handled for them automatically.) These two events are tied to the keyboard hardware, so their routing has a great deal in common with the above discussion. The fundamental problem is simple: When the user presses Ctrl-C or Ctrl-Break, what's the operating system to do? Clearly, a process or processes or perhaps an entire subtree of processes must be killed or signaled. But do we kill or signal? And which one(s)?
OS/2 defines a convention that allows the processes themselves to
decide. Consider a type of application--a "command application"--that runs in command mode. In command mode, a command application reads a command, executes the command, and then typically returns to command mode. Furthermore, when the user presses Ctrl-Break, the command application doesn't want to terminate but to stop what it's doing and return to command mode. This is the style of most interactive applications but not that of most noninteractive applications. For example, if the user types MASM to CMD.EXE, the CMD.EXE program runs MASM as a child process. CMD.EXE is a command application, but MASM is not. The distinction between "command application" and "noncommand application" is not made by OS/2 but is merely descriptive terminology that is useful in this discussion.
The system convention is that Ctrl-C and Ctrl-Break mean "Stop what
you're doing." OS/2 generates signals in response to Ctrl-C and Ctrl-Break; it never directly kills a process. OS/2 can easily decide which process to signal when Ctrl-C or Ctrl-Break is pressed: It signals the lowest command process in the process tree in that screen group. At first glance, this may not seem easy. How can OS/2 distinguish command processes, and how can it determine the "lowest"? The total process tree in a screen group may be very complex; some processes in it may have died, creating multiple now- independent "treelets."
The process tree may be complex, but the tree of processes using the
keyboard is simpler because a process can't let multiple children simultaneously inherit its STDIN. A process can only inherit the keyboard, 2. Actually, a process should only inherit the keyboard. The keyboard device can be opened explicitly, but doing so when a process's inherited STDIN doesn't point to the keyboard device would be a serious error. 2 not open it explicitly; so a single path down the tree must intersect (or contain) all command processes. This single path can't be fragmented because of missing processes due to child death because a process that has let a child inherit its STDIN must not terminate until the child does. So, all OS/2 needs is to find any command process in the command subtree and then look at its descendants for another command process and so on. The bottommost process receives the signal. 3. Actually, OS/2 uses a more efficient algorithm than this; I'm merely illustrating that finding the lowest command process is not difficult. 3
Figure 14-2 illustrates a possible process tree. The shaded processes
have inherited handles to the keyboard and screen; those marked with C are command processes.
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³°°°°°°°°°°°°°°³ ³°°°°°°c°°°°°°°³ ÀÄÄÂÄÄÄÄÄÄÄÄÂÄÄÙ ÚÄÄÄÄ¿ Processes ÚÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄ¿ ³°°°°³ = using the ³ ³ ÀÄÄÄÄÙ keyboard ÚÄÄÄÁÄÄ¿ ÚÄÄÁÄÄÄ¿ ³ ³ ³°°°°°°³ ³ ³ ³°°c°°°³ ÀÄÂÄÄÄÄÙ ÀÄÂÄÄÂÄÙ ÚÄÄÙ ÚÄÄÙ ÀÄÄ¿ ÚÄÄÄÁÄÄ¿ ÚÄÄÁÄ¿ ÚÄÁÄÄ¿ ³ ³ ³°c°°³ ³ ³ ³ ³ ÀÂÄÄÂÙ ÀÄÄÄÂÙ ÀÄÂÄÄÂÄÙ ÚÄÄÙ ÀÄÄ¿ ÀÄÄ¿ ÚÄÄÙ ÀÄÄ¿ ÚÄÄÁÄ¿ ÚÄÁÄÄ¿ ÚÄÁÄÄ¿
ÚÄÄÁÄ¿ ÚÄÁÄÄ¿ ³°c°°³ ³ ³ ³ ³ ³ ³ ³ ³ ÀÂÄÄÂÙ ÀÄÄÄÄÙ ÀÄÄÄÄÙ ÀÄÄÄÄÙ ÀÄÄÄÄÙ ÚÄÄÙ ÀÄÄ¿
ÚÄÄÁÄ¿ ÚÄÁÄÄ¿ ³ ³ ³°°°°³ ÀÄÄÄÄÙ ÀÄÄÄÄÙ
Figure 14-2. Ctrl-C routing in a process tree.
This now begs the final question: How can OS/2 tell if an application
is a command process or not? It can tell because all command processes/command applications do something that other processes never do. By definition, a command process doesn't want to be summarily killed when the user presses Ctrl-C or Ctrl-Break, so all command processes establish signal handlers for Ctrl-C and Ctrl-Break. Because all command processes intercept Ctrl-C and Ctrl-Break, all we need now is to establish the convention that only command processes intercept Ctrl-C and Ctrl-Break. This hearkens back to our earlier discussion of checking STDIN before directly using the keyboard device. Telling the keyboard device that you want the Ctrl-C or Ctrl-Break signals routed to your process is a form of I/O with the keyboard device, and it must only be done if your program has verified that STDIN points to the keyboard device. Furthermore, intercepting Ctrl-C or Ctrl-Break just so that your program can clean up during unexpected termination is unnecessary and insufficient. The SIGTERM signal or, better, the exitlist mechanism provides this capability and covers causes of death other than the keyboard. So all processes that intercept Ctrl-C and Ctrl-Break have access to the keyboard, and they want to do something other than die when the user presses Ctrl-C or Ctrl-Break. They fit the command process definition.
Now that we've exhaustively shown how OS/2 finds which process to send
a Ctrl-C signal, what should the process do when it gets the signal? Obey the system convention and stop what it's doing as quickly as is wise. If the application isn't working on a command, the application typically flushes the keyboard type-ahead buffer and reprompts. If the application is working on a command that is implemented in code within the application, the application jumps from the signal handler to its command loop or, more commonly, sets a flag to terminate the current command prematurely. 4. Occasionally, terminating a command halfway through could leave the user's work trashed. In such a case, finishing the command is prudent. 4
Finally, if the application is running a child process, it typically
stops what it's doing by issuing a DosKill on that child command subtree. This, then, is how Ctrl-C can kill a program such as MASM. Ctrl-C is sent to MASM's closest ancestor that is a command process, 5. Often, CMD.EXE is MASM's direct ancestor, but other programs, such as a build utility, could have come between CMD.EXE and MASM. 5 which in turn issues a DosKill on MASM's subtree. MASM does any exitlist cleanup that it wishes (probably deleting scratch files) and then terminates. When the command process that ran MASM, typically CMD.EXE, sees that MASM has terminated, it prints ^C on the screen, followed by a new prompt.
15 The File System
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
The file system in OS/2 version 1.0 is little changed from that of MS-DOS, partially in an effort to preserve compatibility with MS-DOS programs and partially due to limitations imposed by the project's schedule. When the schedule for an "all singing, all dancing" OS/2 was shown to be too long, planned file system improvements were moved to a future release. To explain the rationale for postponing something so useful, I'll digress a little.
The microcomputer industry developed around the dual concepts of mass
market software and standards. Because software is mass marketed, you can buy some very sophisticated and useful programs for a modest sum of money-- at least modest in comparison to the development cost, which is often measured in millions of dollars. Mass marketing encourages standards because users don't want to buy machines, peripherals, and systems that don't run these programs. Likewise, the acceptance of the standards encourages the development of mass market software because standards make it possible for a single binary program to execute correctly on a great many machines and thus provide a market big enough to repay the development costs of a major application.
This synergy, or positive feedback, between standards and mass market
software affected the process of developing operating systems. At first glance, adding new features to an operating system seems straightforward. The developers create new features in a new release, and then applications are written to use those new features. However, with mass market software, it doesn't work that way. Microsoft could indeed release a new version of OS/2 with new features (and, in fact, we certainly will do so), but initially few new applications will use said new features. This is because of the initial limited market penetration of the new release. For example, let's assume that at a certain time after the availability of a new release, 10 percent of OS/2 users have upgraded. An ISV (Independent Software Vendor) is planning its next new product--one it hopes will be a bestseller. The ISV must decide whether to use the new feature and automatically lock itself out of 90 percent of the potential market or to use the common subset of features contained in the earlier OS/2 release and be able to run on all machines, including the 10 percent running the OS/2 upgrade. In general, ISVs won't use a nonvital feature until the great majority of existing systems support that feature.
The key to introducing new features in an operating system isn't that
they be available and useful; it's that the release which contains those new features sees widespread use as quickly as possible. If this doesn't happen, then the new feature pretty much dies stillborn. This is why each MS-DOS release that contained major new functionality coincided with a release that was required to use new hardware. MS-DOS version 2.0 was required for the IBM XT product line; MS-DOS version 3.0 was required for the IBM AT product line. And OS/2 is no exception: It wasn't required for a new "box," but it was required to bring out the protect mode machine lying fallow inside 80286-based machines. If a new release of a system doesn't provide a new feature that makes people want it or need it badly, then market penetration will be slow. People will pay the cost and endure the hassle of upgrading only if their applications require it, and those applications dare require it only if most people have already upgraded.
Because of this, the initial release of OS/2 is "magical" in the eyes
of its developers. It provides a window of opportunity in which to introduce new features into the PC operating system standard, a window that won't be open quite as wide again for a long time. And this postponed major file system enhancements: The file system can be enhanced in a later release and benefit existing applications without any change on their part, whereas many other OS/2 features needed to be in the first release or they might never be available.
15.1 The OS/2 File System
Although OS/2 version 1.0 contains little in the way of file system improvements, it does contain two that are significant. The first is asynchronous I/O. Asynchronous I/O consists of two functions, DosReadAsync and DosWriteAsync. These functions are identical to DosRead and DosWrite except that they return to the caller immediately, usually before the I/O operation has completed. Each takes the handle of a semaphore that is cleared when the I/O operation completes. The threads of the calling process can use this semaphore to poll for operation complete, or they can wait for the operation to complete. The DosMuxSemWait call is particularly useful in this regard because it allows a process to wait for several semaphore events, which can be asynchronous I/O events, IPC events, and timer events, intermingled as the programmer wishes.
The second file system feature is extended partitioning; it supports
dividing large physical disks into multiple sections, several of which may contain FAT file systems. In effect, it causes OS/2 to treat a large hard disk as two or more smaller ones, each of which meets the file system's size limits. It's widely believed that MS-DOS is limited to disks less than 32 MB in size. This isn't strictly true. The limitation is that a disk can have no more than 65,535 sectors; the standard sector size is 512 bytes, which gives the 32 MB value. Furthermore, each disk is limited to 32,768 clusters. A sector is the unit of disk storage; disks can read and write only integral sectors. A sector's size is established when the disk is formatted. A cluster is the unit of disk space allocation for files and directories. It may be as small as one sector, or it may be four sectors, eight sectors, or some other size. Because the MS-DOS file system supports a maximum of 65 KB sectors but only 32 KB clusters, a 32 MB disk must be allocated in two-sector (or bigger) clusters. It's possible to write a device driver that uses a sector size that is a multiple of 512 bytes, which gets around the 65 KB sector restriction and allows the use of a disk greater than 32 MB. This trick works for MS-DOS and for OS/2, but it's not optimal because it doesn't do anything to increase the maximum number of allocation clusters from the existing 32 KB value, 1. The FAT file system can deal with a maximum of 32 KB allocation units, or clusters. No matter what the size of the disk, all files must consume disk space in increments of no smaller than 1/32Kth of the total disk size. This means that a 60 MB disk, using 1024 byte sectors, allocates space in 2048-byte increments. 1 which means that because many disk files are small a lot of space is wasted due to internal fragmentation.
The OS/2 version 1.0 extended partitioning feature provides an interim
solution that is not quite as convenient as large sectors but that reduces the wastage from internal fragmentation: It allows more than one disk partition to contain a FAT file system. Multipartitioned disks are possible under MS-DOS, but only one partition can be an MS-DOS (that is, FAT) file system. This restriction has been relaxed in OS/2 so that, for example, a 60 MB disk can be partitioned into two separate logical disks (for example, C and D), each 30 MB.
15.2 Media Volume Management
The multitasking capability of OS/2 necessitated major file system enhancements in the area of volume management. A disk volume is the name given to the file system and files on a particular disk medium. A disk drive that contains a fixed medium always contains the same volume, but a disk drive from which the media (such as floppy disks) can be removed will contain whatever disk--whatever volume--the user has in it at the time. That volumes can change becomes a problem in a multitasking environment. For example, suppose a user is using a word processor to edit a file on a floppy disk in drive A. The editor has opened the file and is keeping it open for the duration of the edit. Without closing the file or terminating the editor, the user can switch to a screen group in which a spreadsheet program is running. The user might then need to insert a different disk into drive A--one that contains data needed by the spreadsheet. If the user then switches back to the word processor without remembering to change the floppy disk, disaster will strike. Pressing the Page Down key will cause the editor to try to read another sector from its already open disk file. The operating system knows--because of FAT information stored in RAM buffers_ that the next sector in the text file is sector N, and it will issue a read to sector N on the wrong medium--the spreadsheet floppy disk-- and return that to the word processor program as the next sector of the text file. And, at that, the user is getting off lightly; he or she might just as easily have given the word processor a command that caused it to write a sector to the disk, which would do double damage. A file on the spreadsheet floppy would be destroyed by the "random" write, and the text file would be corrupted as well because it's missing a sector write that it should have received.
We can't solve this problem by admonishing the user to be careful;
many programs read from and write to disk without direct user intervention. For example, the word processor might save work in progress to disk every two minutes. If this time interval elapses while the user is still working with the spreadsheet program on the spreadsheet floppy disk, our hypothetical "flawless" user is still S.O.L. 2. Severely out of luck. 2
OS/2 resolves these problems by recognizing that when an application
does I/O to an open file the I/O is not really aimed at drive A; it's aimed at a particular floppy disk volume--the one containing the open file. Each disk volume, removable or not, has a volume name stored in its root directory and a unique 32-bit volume identifier stored in its boot sector. Figure 15-1 illustrates the two volume names--one for computer use and one for human use. Each file handle is associated with a particular 32-bit volume ID. When an I/O request is made for a file handle, OS/2 checks to see if the proper volume is in the drive by comparing the 32-bit value of the request with that of the medium currently spinning. If they match, the operation completes. If the mounted volume is different from the requested volume, OS/2 uses the hard error daemon mechanism to prompt the user to insert the correct volume in the drive.
VOLUME LABELS
Sector Sector
0 N ÚÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ÀÄÄ�ÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄÄÄ�ÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄ 32-bit volume ID ÀÄÄÄÄÄÄÄ volume name in in boot sector home directory
Figure 15-1. Volume ID and volume name location.
Three problems must be overcome to make this scheme practical. First,
checking the volume ID of the medium must be fast. You can't afford to read the boot sector each time you do an I/O operation if doing so halves the speed of disk I/O. Second, you need assurance that volume IDs are unique. Third, you need a plan to deal with a volume that doesn't have a volume ID.
Keeping down the cost of volume verification is easy if you know when
a volume is changed; obviously, the ID is read from the boot sector only when a medium has been changed. But how can OS/2 tell that a media change has occurred? It can't; that's a device driver issue.
For starters, if the device contains a nonremovable medium, rechecking
its volume ID is never necessary. The device driver understands this, and when it is asked the status of the medium, it responds, "Unchanged." Some removable media drives have a flag bit that warns the driver that the door has been opened. In this case, when asked, the device driver tells OS/2 that the medium is "uncertain." The driver doesn't know for sure that it was really changed, but it may have been; so OS/2 rechecks the volume ID. Rechecking the volume ID is more difficult when a removable media device has no such indicator.
In this case, the author of the device driver uses device-specific
knowledge to decide on a minimum possible time to effect a media change. If the device is ready, yet less than the minimum possible time has elapsed since the last operation, the driver knows that the same medium must be in the drive. If more than the minimum possible time has elapsed, the driver returns "medium uncertain," and OS/2 rechecks the volume label. This time interval is typically 2 seconds for floppy disk drives, so effectively an extra disk read is done after every idle period; for any given episode of disk I/O, however, no extra reads are needed.
Ensuring that a volume ID is unique is another problem. Simply
lecturing the user on the wisdom of unique IDs is inadequate; the user will still label three disks "temp" or number them all as "10." And even the hypothetical perfect user might borrow from a neighbor a disk whose name is the same as one the user already owns. OS/2 deals with this problem by using a 32-bit randomized value for disk volume IDs. When a disk is formatted, the user enters a supposedly unique name. This name is checksummed, and the result, combined with the number of seconds between the present and 1980, is used to seed a random number generator. This generator returns a 32-bit volume ID. Although accidentally duplicating a volume ID is obviously possible, the four billion possible codes make it quite unlikely.
The name the user enters is used only to prompt the user to insert the
volume when necessary, so it need not be truly unique for the volume management system to work. If the user names several disks WORK, OS/2 still sees them as independent volumes because their volume IDs are different. If the user inserts the wrong WORK disk in response to a prompt, OS/2 recognizes it as the wrong disk and reissues the "Insert disk WORK" prompt. After trying each WORK volume in turn, the user will probably decide to relabel the disks!
The thorniest problem arises from unlabeled disks--disks formatted
with MS-DOS. Forcing the user to label these disks is unacceptable, as is having OS/2 automatically label them with volume IDs: The disk may be read- only, perhaps permanently so. Even if the disk is not read-only, the problem of low density and high density raises its ugly head. Low-density disks can be read in a high-density drive, but writes made to a low-density disk from a high-density drive can only be read on high-density drives. If a low-density disk is placed in a high-density drive and then labeled by OS/2, its boot sector is no longer readable when the disk is placed in a low-density drive.
For volumes without a proper volume ID, OS/2 attempts to create a
unique substitute volume ID by checksumming parts of the volume's root directory and its FAT table. OS/2 uses the existing volume name if one exists; if there is no volume name, OS/2 attempts to describe the disk. None of these techniques is foolproof, and they require extra disk operations every time the medium is identified. Therefore, software distributors and users should make every effort to label disks that OS/2 systems are to use. OS/2 labels are backward compatible with MS-DOS version 3.x labels.
The OS/2 DISKCOPY command makes a byte-by-byte verbatim copy of a
floppy disk, except that the duplicate disk has a different volume ID value in the boot sector (the volume label name is not changed). OS/2 users can't tell this, however, because the DISKCOMP utility lies, and if two disks are identical in every byte except for the volume ID, it reports that the disks are identical. However, if the user uses DISKCOPY to duplicate the disk under OS/2 and then compares the two with DISKCOMP under MS-DOS 3.x, a difference is reported.
Our discussion so far has centered on file reads and writes to an open
handle. Reads and writes are volume-oriented operations because they're aimed at the volume on which the file resides. DosOpens, on the other hand, are drive oriented because they search the default or specified drive for the file in question (or create it) regardless of the volume in the drive. All handle operations are volume oriented, and all name-based calls are drive oriented. Currently, you cannot specify that a given file is to be opened on or created on a specific volume. To ensure that a scratch or output file is created on a certain volume, arrange to have a file open on that volume and issue a write to that file immediately before doing the file open. The write operation followed by a DosBufReset will ensure that the particular medium is in the drive at that time.
15.3 I/O Efficiency
OS/2 provides full blocking and deblocking services for all disk I/O requests. A program can read or write any number of bytes, and OS/2 will read the proper sectors into internal buffers so that only the specified bytes are affected. Naturally, every DosRead or DosWrite call takes time to execute, so if your program makes few I/O calls, each for large amounts of data, it will execute faster.
I/O performance can be further improved by making sector aligned
calls, that is, by requesting a transfer of an integral multiple of 512 bytes to or from a file seek position that is itself a multiple of 512. OS/2 reads and writes entire disk sectors directly from and to the device hardware without an intermediate copy step through system buffers. Because the file system keeps logically adjacent sectors physically adjacent on the disk, disk seek times and rotational latency are such that one can read or write four sectors of data (2048 bytes) in essentially the same time needed to read or write one sector (512 bytes).
Even if the length or the file position of the request isn't a
multiple of 512, OS/2 performs the initial fraction of the request via its buffers, directly transfers any whole sectors out of the middle of the request, and uses the buffers for the fractional remainder. Even if your requests aren't sector aligned, making them as large as feasible is beneficial.
To summarize, I/O is most efficient when requests are large and sector
aligned. Even misaligned requests can be almost optimally serviced if they are large. Programs that cannot naturally make aligned requests and that are not I/O intensive should take advantage of the blocking and deblocking services that OS/2 provides. Likewise, programs that need to make large, unaligned requests should use OS/2's blocking management. Programs that need to make frequent, small, nonaligned requests will perform best if they read blocks of sectors into internal buffers and deblock the data themselves, avoiding the overhead of frequent DosRead or DosWrite calls.
16 Device Monitors, Data Integrity, and Timer Services
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
In discussing the design goals of OS/2, I mentioned continuing to support the kinds of functionality found in MS-DOS, even when that functionality was obtained by going around the operating system. A good example of such functionality is device data manipulation, a technique that usually involves hooking interrupt vectors and that is used by many application programs. For example, pop-up programs such as SideKick have become very popular. These programs get into memory via the terminate and stay resident mechanism and then edit the keyboard interrupt vector to point to their code. These programs examine each keystroke to see if it is their special activate key. If not, they transfer control to the original interrupt handler. If the keystroke is their special activate key, they retain control of the CPU and display, or "pop up," a message or a menu on the screen. Other programs hook the keyboard vector to provide spell checking or keyboard macro expansion. Some programs also hook the BIOS entry vector that commands the printer, either to substitute alternate printer driver code or to manipulate the data sent to the printer. Programs that turn a spreadsheet's output sideways are an example of this.
In general, these programs edit, or hook, the interrupt vectors that
receive device interrupts and communicate with device driver routines in the ROM BIOS. The functions provided by such programs and their evident popularity among users demonstrate a need for programs to be able to monitor and/or modify device data streams. The OS/2 mechanism that does this is called a device monitor.
16.1 Device Monitors
The design of device monitors had to meet the general requirements and religion of OS/2. Specifically, the MS-DOS technique of letting applications receive interrupts by editing the interrupt vectors could not be allowed because doing so would destroy the system's ability to provide a stable environment. Furthermore, unlike MS-DOS, OS/2 doesn't use the ROM BIOS as a form of device driver, so hooking the BIOS communication vectors would not provide access to the device data stream. In addition, allowing an application to arbitrarily interfere with a device driver's operation is contrary to OS/2 design principles; the device driver is the architectural embodiment of knowledge about the device, and it must be involved in and "aware" of any external manipulation of the data stream. The result is an OS/2 device monitor mechanism that allows processes, running in their normal ring 3 state, to monitor and edit device data streams with the prior permission and knowledge of the appropriate device driver.
Specifically, a process registers itself as a device monitor by
calling the appropriate device driver via a DosMonReg call. 1. Which is a dynlink package that eventually calls the device driver via a DosDevIOCtl call. 1 The process also provides two data buffers, one for incoming monitor data and another for outgoing monitor data. Processes can easily call OS/2, but OS/2 has no way to call processes. 2. Signals are a partial exception to this, but signals have limitations, as discussed earlier. 2 OS/2 gets around this by inverting the normal sense of a call and return sequence. When OS/2 needs to "call" a process, it requires that process to call OS/2 beforehand with one of its threads. OS/2 holds this thread captive until the callback event takes place. OS/2 then accomplishes a call to the process by releasing the thread so that it returns from the holding system call and resumes execution within the process. When the process is ready to "return" to OS/2, it recalls the holding entry point (see Figure 16-1).
Process calls OS/2 | OS/2 "calls" Process |
Process call | Process call FCN call
ÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄ | ÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ � � | � � � ³ ³ | ³ ³ ³ ³ ³ | ³ ³ ³ ³ ³ | ³ ³ ³ ³ ³ | ³ ³ ³ ³ ³ | ³ ³ ³ ³ ³ | ³ ³ ³ Ä Ä Ä Ä Å Ä Ä Ä Å Ä Ä Ä | Ä Ä Ä Ä Å Ä Ä Ä Å Ä Ä Ä Ä Ä Ä Å Ä Ä OS/2 ³ ³ | OS/2 ³ ³ ³ ³ ³ | ³ ³ ³ ³ ³ | ³ ³ ³ � � | � � � ³ ³ | ³ ³ ³ ³ FCN ³ | ³ ³ ³ ÀÄÄÄÄÄÄÄÙ | ÀÄÄ< <ÄÄÙ ÀÄÄÄ< Return | > > Return >
Figure 16-1. "Calling" a process from OS/2.
OS/2 uses this technique for monitors as well. A monitoring process is
required to call the OS/2 entry point directly after registering itself as a device monitor. OS/2 notifies the monitor process of the presence of data in the incoming buffer by allowing this thread to return to the process. Figure 16-2 illustrates a device with two monitoring processes, X and Y.
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿
³ Monitor ³ ³ Monitor ³
³ Process X ³ ³ Process Y ³
³ ³ ³ ³
³ DosMonRead DosMonWrite ³ ³ DosMonRead DosMonWrite ³
ÀÄÄÄÄÄ�ÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄ�ÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÙ
ÀÄ¿ ÀÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ ³ ³
ÚÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ ³ ³ ³ ÚÄÄÄÁÄÄÄÄ¿ Ú�ÄÄÄÄÄÄÁ¿ ÚÄÄÄÄ�ÄÄÄ¿ ³ ³ ³ Buffer ³ ³ Buffer ³ ³ Buffer ³ ³ ³ ³ 1 ³ ³ 2 ³ ³ 3 ³ ³ ³ ÀÄÄÄ�ÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÂÄÄÄÙ ³ ³ ÚÄÄÄÄÙ OS/2 ÀÄÄÄÄ¿ ³ ÀÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÙ
³ Data in Data out ³ ³ ³
ÚÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄ¿ ³ ³ Device driver � ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 16-2. Device monitors.
But we need to discuss a few additional details. First, because OS/2
strives to make processes see a consistent environment regardless of the presence of other processes, each device can have as many monitors as the device driver allows. OS/2 connects multiple device monitors into a chain so that the device data stream is passed through the first monitor in the chain, then through the second monitor, and so on. When a process registers itself as a monitor, it specifies whether it wants to be first in the chain or last in the chain; some applications are sensitive to this. The first monitor to register itself as first is truly first; the next monitor to ask for first actually becomes second, and so forth. The same algorithm applies to monitors that want to be last: The first to so request becomes the last, the second to request last becomes next to last, and so forth.
The actual format of the monitor data stream is device specific; the
device driver decrees the format. Some device drivers have special rules and requirements. For example, the keyboard device driver allows monitoring processes to insert the "screen switch" key sequence into the data stream, whereupon it is recognized as if the user had typed it at the physical keyboard. But the device driver will not pass such sequences that really were typed through the monitor chain; they are directly obeyed instead.
This approach prevents an amok keyboard monitor from effectively
crashing the system by intercepting and consuming all attempts by the user to switch screen groups. The screen device driver does not allow device monitors, not because the performance impact would be too big (as it, in fact, would be) but because the VIO and presentation manager dynlink packages totally circumvent the screen device driver so that it never sees any screen data being written.
The DosMonRead call holds the device's thread until incoming data is
available. The DosMonWrite call returns the CPU to the process as soon as it is able. The same thread that calls DosMonRead need not be the one to call DosMonWrite (see below).
Because monitors are an important component of OS/2, an application
must be very careful to use them properly; therefore, some caveats are in order. First, monitors are inserted into the device data chain, with obvious effects on the data throughput rate of the device. Each time the user presses a key, for example, a packet must pass through every monitor in the keyboard chain before the application can read the key and obey or echo it. Clearly, any sluggishness on the part of a monitor or the presence of too many monitors in a chain will adversely affect system response. The thread involved in reading, processing, and writing monitor data should be set at a high priority. We recommend the lowest of the force run priority categories. Furthermore, the monitor component of a monitoring application must contain no critical sections or other events that could slow or suspend its operation. In addition, if a monitor data stream will be extensively processed, a normal-priority thread must be used to handle that processing so that the high-priority thread can continue to transfer monitor data in and out without impediment. For example, an auxiliary thread and buffer must be used if a keyboard monitor is to write all keystrokes to a disk buffer.
Finally, if a monitor process terminates abnormally although OS/2
properly unlinks it from the monitor chain, the data in the process's monitor buffers is lost. Clearly, losing an unspecified amount of data without warning from the keyboard data stream or perhaps from printer output will upset the user no little amount. Monitoring processes must be written carefully so that they minimize this risk.
The device monitor feature threatens OS/2's fundamental architectural
principles more than any other. Thus, its presence in the system testifies to its importance. Specifically, device monitors violate the design principle of minimizing interference between processes, a.k.a. encapsulation. Clearly, a process that is monitoring a device's data stream can affect the output of or input to a great many processes other than itself. This is sometimes called a feature, not a bug. For example, the printer spooler uses monitors to intercept output aimed at the printer, storing it on disk, and to feed data from those disk files to the actual printer device. Clearly, spooling printer output interferes with another process, but the interference is valuable. Designers of monitoring applications must ensure that their applications damage neither the system's performance nor its stability.
16.2 Data Integrity
I've discussed data integrity in a multitasking environment several times. This section does not review that material in detail but brings together all the elements and introduces a few related system facilities.
The first problem in a multitasking system is that multiple processes,
or multiple threads within a process, may try to simultaneously manipulate the same resource--a file, a device, a data structure in memory, or even a single byte of memory. When the manipulation of a resource must be serialized to work correctly, that manipulation is called a critical section. This term refers to the act of manipulating the resource, but not particularly to the code that does so. Clearly, if any of four subroutines can manipulate a particular resource, entering any of the four is entering the critical section.
The problem is more pervasive than a programmer unfamiliar with the
issue might assume. For example, even the simple act of testing a word to see if it holds the value 4 and incrementing it if it doesn't is a critical section. If only one thread in one process can access this word, then the critical section is serialized. But if more than one thread can access the word, then more than one thread could be in the critical section at the same time, with disastrous results. Specifically, consider the assembly language sequence shown in Listing 16-1. It looks simple enough: Test to see if COUNT holds 4; if it doesn't, increment it; if it does, jump to the label COMPLETE. Listing 16-2 shows what might go wrong in a multithreaded environment: Thread A checks the value to see if it's 4, but it's 3. Right after the compare instruction, a context switch takes place, and thread B is executed. Thread B also performs the compare, sees the value as 3, and increments it. Later, thread A resumes execution, after the compare instruction, at a location where it believes the COUNT value to be 3; so it also increments the value of COUNT. The value is now 5 and will continue to be incremented way past the value of 4 that was supposed to be its upper limit. The label COMPLETE may never be reached.
COUNT DW 0 ; Event counter
. . CMP COUNT,4 ; is this the 4th? JE COMPLETE ; yes, we're done INC COUNT ; count event . .
Listing 16-1.
Thread A Thread B
. . CMP COUNT,4 [count is now 3] -------------------context switch---> CMP COUNT,4 [count is 3] JE COMPLETE INC COUNT [count is 4] . . <-----------context switch-------- JE COMPLETE [jmp not taken] INC COUNT [count is now 5]
Listing 16-2.
I apologize for again lecturing on this topic, but such problems are
very nonobvious, rarely turn up in testing, are nearly impossible to find in the field, and the very possibility of their existence is new with OS/2. Thus, "too much is not enough," caveat-wise. Now that we've reviewed the problems, let's look at the solutions.
A programmer inexperienced with a multitasking environment might
protest that this scenario is unlikely, and indeed it is. Maybe the chances are only 1 in 1 million that it would happen. But because a microprocessor executes 1 million instructions a second, it might not be all that long before the 1-in-1-million unlucky chance comes true. Furthermore, an incorrect program normally has multiple unprotected critical sections, many of which are larger than the 2-instruction window in our simple example.
The program must identify and protect all critical sections; a program
that fails to do so will randomly fail. You can't take solace in there being only one CPU and assuming that OS/2 probably won't context switch in the critical section. OS/2 can context switch at any time, and because context switching can be triggered by unpredictable external events, such as serial port I/O and rotational latency on a disk, no amount of testing can prove that an unprotected critical section is safe. In reality, a test environment is often relatively simple; context switching tends to occur at consistent intervals, which means that such problems tend not to turn up during program test. Instead, they turn up in the real world, and give your program a reputation for instability.
Naturally, testing has its place, but the only sure way to deal with
critical sections is to examine your code carefully while assuming that all threads in the system are executing simultaneously. 3. This is more than a Gedankenexperiment. Multiple processor machines will be built, and when they are, OS/2 will execute multiple threads, even within one process, truly simultaneously. 3 Furthermore, when examining a code sequence, always assume that the CPU will reschedule in the worst way. If there is any possible window, reality will find it.
16.2.1 Semaphores
The traditional solution for protecting critical sections is the semaphore.
The two OS/2 semaphores--RAM and system--each have advantages, and the
operation of each is guaranteed to be completely immune to critical section
problems. In the jargon, their operation is guaranteed atomic. Whenever a
thread is going to manipulate a critical resource, it first claims the
semaphore that protects the resource. Only after it controls the semaphore
does it look at the resource because the resource's values may have changed
between the time the semaphore was requested and the time it was granted.
After the thread completes its manipulation of the resource, it releases
the semaphore.
The semaphore mechanism protects well against all cooperating
4. Obviously, if some thread refuses to claim the semaphore, nothing can be done. 4 threads, whether they belong to the same process or to different processes. Another OS/2 mechanism, called DosEnterCritSec, can be used to protect a critical section that is accessed only by threads belonging to a single process. When a thread issues the DosEnterCritSec call, OS/2 suspends execution of all other threads in that process until a subsequent DosEnterCritSec call is issued. Naturally, only threads executing in application mode are suspended; threads executing inside the OS/2 kernel are not suspended until they attempt to return to application mode. 5. I leave as an exercise to the reader to explain why the DosEnterCritSec call is not safe unless all other threads in the process make use of it for that critical section as well. 5 The use of DosEnterCritSec is dangerous because the process's other threads may be suspended while they are holding a critical section. If the thread that issued the DosEnterCritSec then also tries to enter that critical section, the process will deadlock. If a dynlink package is involved, it may have created extra threads unbeknownst to the client process so that the client may not even be aware that such a critical section exists and might be in use. For this reason, DosEnterCritSec is safe only when used to protect short sections of code that can't block or deadlock and that don't call any dynlink modules.
Still another OS/2 critical section facility is file sharing and
record locking, which can be used to protect critical sections when they consist of files or parts of files. For example, a database program certainly considers its master database file a critical section, and it doesn't want anyone messing with it while the database application has it open. It can open the file with the file-sharing mode set to "allow no (other) readers, allow no writers." As long as the database application keeps the file open, OS/2 prevents any other process from opening (or deleting!) that file.
The record-locking mechanism can be used to provide a smaller
granularity of protection. A process can lock a range of bytes within a file, and while that lock is in effect, OS/2 prevents any other process from reading or writing those bytes. These two specialized forms of critical section protection are unique in that they protect a process against all other processes, even "uncooperating" ones that don't protect their own access to the critical section. Unfortunately, the file-sharing and record-locking mechanisms don't contain any provision for blocking until the conflict is released. Applications that want to wait for the conflict to clear must use a polling loop. Use DosSleep to block for at least a half second between each poll.
Unfortunately, although semaphores protect critical sections well,
sometimes they bring problems of their own. Specifically, what happens if an asynchronous event, such as program termination or a signal, pulls the CPU away from inside a critical section and the CPU never returns to release the semaphore? The answers range from "moot" to "disaster," depending on the circumstances. The possibilities are so manifold that I'll group some of them.
What can you do if the CPU is pulled away inside a critical
section?
þ Ignore it. This is fine if the critical section is wholly accessed by a single process and that process doesn't use signals to modify the normal path of execution and if neither the process nor its dynlink routines attempt to enter the critical section during DosExitList processing.
þ Clear the semaphore. This is an option if you know that the resource protected by the semaphore has no state, such as a semaphore that protects the right to be writing to the screen. The trick is to ensure that the interrupted thread set the semaphore and that you don't accidentally clear the semaphore when you don't set it. For example, if the semaphore is wholly used within a single process but that process's DosExitList handlers may use it, they can force the semaphore clear when they are entered.
þ Detect the situation and repair the critical section. This detection can be made for RAM semaphores only during process termination and only if the semaphore is solely used by that process. In such a case, you know that a thread in the process set the semaphore, and you know that the thread is no longer executing the critical section because all threads are terminated. You can test the semaphore by using a nonblocking DosSemSet; if it's set, "recover" the resource.
System semaphores are generally better suited for this. When the owning thread of a system semaphore dies, the semaphore is given a special mark. The next attempt to set the semaphore returns with a code that tells the new owner that the previous owner died within the critical section. The new owner has the option of cleaning up the resource.
Another possibility is to try to prevent the CPU from being yanked out
of a critical section. Signals can be momentarily delayed with the DosHoldSignal mechanism. Process termination that results from an external kill can be postponed by setting up a signal handler for the KILL signal and then using DosHoldSignal. This last technique doesn't protect you against termination due to GP fault and the like however.
16.2.2 DosBufReset
One remaining data integrity issue--disk data synchronization--is not
related to critical sections. Often, when a DosWrite call is made, OS/2
holds the data in a buffer rather than writing it immediately to disk.
Naturally, any subsequent calls made to read this data are satisfied
correctly, so an application cannot see that the data has not yet been
written unless the reading application uses direct physical access to the
volume (that is, raw media reads). This case explains why CHKDSK may
erroneously report errors that run on a volume that has open files.
OS/2 eventually writes the data to the disk, so this buffering is of
concern only when the system crashes with unwritten buffered data. Naturally, such crashes are expected to be rare, but some applications may find the possibility so threatening that they want to take protective steps. The two OS/2 functions for this purpose are flushing and writethroughs. The flush operation--DosBufReset--writes all dirty buffers-- those with changed but unwritten data in them--to the disk. When the call returns, the data is on the disk. Use this call sparingly; although its specification promises only that it will flush buffers associated with the specified file handle(s), for most file systems it writes all dirty buffers in the system to disk. Moreover, if file handles are open to a server machine on the network, most or all of that server's buffers get flushed, even those that were used by other client machines on the network. Because of these costs, applications should use this operation judiciously.
Note that it's not true that a flush operation simply causes a write
to be done sooner rather than later. A flush operation may also cause extra disk writes. For example, consider an application that is writing data 10 bytes at a time. In this case, OS/2 buffers the data until it has a full sector's worth. A series of buffer flush operations arriving at this time would cause the assembly buffer to be written to the disk many extra and unnecessary times.
16.2.3 Writethroughs
Buffer flushes are expensive, and unless they are used frequently, they
don't guarantee a particular write ordering. Some applications, such as
database managers, may want to guarantee that data be written to the disk
in exactly the same order in which it was given to OS/2 via DosWrite. For
example, an application may want to guarantee that the data is in place in
a database before the allocation chain is written and that the chain be
written before the database directory is updated. Such an ordering may make
it easy for the package to recover the database in case of a crash.
The OS/2 mechanism for doing this is called writethrough--a status bit
that can be set for individual file handles. If a writethrough is in effect for a handle to which the write is issued, OS/2 guarantees that the data will be written to the disk before the DosWrite operation returns. Obviously, applications using writethrough should write their data in large chunks; writing many small chunks of data to a file marked for writethrough is very inefficient.
Three caveats are associated with writethroughs:
þ If writethrough is set on a file after it is open, all subsequent writes are written through, but data from previous writes may still be in dirty buffers.
þ If a writethrough file is being shared by multiple processes or is open on multiple handles, all instances of that file should be marked writethrough. Data written to a handle not marked writethrough may go into the buffers.
þ The operation of data writethroughs has some nonintuitive surprises when used with the current FAT file system. Specifically, although this feature works as advertised to place the file's data sectors on the disk, it does not update the directory entry that specifies the size of the file. Thus, if you extend a file by 10 sectors and the system crashes before you close the file, the data in those 10 sectors is lost. If you had writethrough set, then those 10 sectors of data were indeed written to the disk; but because the directory entry wasn't updated, CHKDSK will return those sectors to the free list.
The writethrough operation protects the file's data but not the directory or allocation information. This is not a concern as long as you write over a portion of the file that has been already extended, but any writes that extend the file are not protected. The good news is that the data will be on the disk, as guaranteed, but the bad news is that the directory entry won't be updated; if the system crashes, file extensions cannot be recovered. The recommended solution is to use DosNewSize to extend the file as needed, followed by DosBufReset to update the directory information on the disk, and then to writethrough the data as needed. Overextending the file size is better than doing too many NewSize/BufReset combinations; if you overextend, you can always shrink the file before closing it with a final DosNewSize.
16.3 Timer Services
Frequently, applications want to keep track of the passage of real time. A game program may want events to occur asynchronously with the user's input; a telecommunications program may want to track how long a response takes and perhaps declare a link timed-out after some interval. Other programs may need to pace the display of a demonstration or assume a default action if the user doesn't respond in a reasonable amount of time. OS/2 provides several facilities to track the passage of real time; applications should use these facilities and shun polling and timing loops because the timing of such loops depends on the system's workload and the CPU's speed and because they totally lock out from execution any thread of a lower priority.
Time intervals in OS/2 are discussed in terms of milliseconds to
isolate the concept of a time interval from the physical mechanism (periodic clock interrupts) that measures time intervals. Although you can specify a time interval down to the millisecond, the system does not guarantee any such accuracy.
On most hardware, OS/2 version 1.0 uses a periodic system clock
interrupt of 32 Hz (32 times a second). This means that OS/2 measures time intervals with a quantum size of 31.25 milliseconds. As a result, any timeout value is subject to quantization error of this order. For example, if a process asks to sleep for 25 milliseconds, OS/2 knows that the request was made at some time after the most recent clock tick, but it cannot tell how long after, other than that less than 31.25 milliseconds had elapsed between the previous clock tick and the sleep request. After the sleep request is made, another clock tick occurs. Once again, OS/2 can't tell how much time has elapsed since the sleep request and the new clock tick, other than that it was less than 31.25 milliseconds. Lacking this knowledge, OS/2 uses a simple algorithm: At each clock tick, OS/2 decrements each timeout value in the system by the clock tick interval (generally 31.25 milliseconds). Thus, our 25-millisecond sleep request may come back in 1 millisecond or less or in 31.25 milliseconds. A request to block for 33 milliseconds could come back in 32 milliseconds or in 62.5 milliseconds.
Clearly, the OS/2 timer functions are intended for human-scale timing,
in which the 1/32-second quantization error is not noticeable, and not for high-precision timing of fast events. Regardless of the resolution of the timer, the system's preemptive scheduler prevents the implementation of high-accuracy short-interval timing. Even if a timer system call were to time out after a precise interval, the calling thread might not resume execution immediately because a higher-priority thread might be executing elsewhere.
One form of OS/2 timer services is built into some system calls. For
example, all semaphore blocking calls support an argument that allows the caller to specify a timeout value. When the specified time has elapsed, the call returns with a "call timed out" error code. Some threads use this facility to guard against being indefinitely locked out; if the semaphore call times out, the thread can give up, display an error message, or try another tactic. Other threads may use the facility expecting to be timed out: They use the timeout facility to perform periodic tasks and use the semaphore just as an emergency flag. Another thread in the system can provide an emergency wakeup for the timer thread simply by clearing the semaphore.
Blocking on a semaphore merely to delay for a specific interval is
unnecessary; the DosSleep call allows a thread to block unconditionally for an arbitrary length of time, subject, of course, to the timer's quantization error. 6. And subject to the fact that if thread 1 is doing the DosSleeping the sleep will be interrupted if a signal is taken. 6 DosSleep measures time intervals in a synchronous fashion: The thread is held inside the operating system until the time interval has elapsed. OS/2 provides an asynchronous timer service that allows timing to take place in parallel with a thread's normal execution. Specifically, the DosTimerAsync call is made with a timeout interval, such as DosSleep, and also with the handle of a system semaphore. 7. Unlike most semaphore applications, the timer functions work only with system semaphores. RAM semaphores may not be used because of the difficulty in posting a RAM semaphore at interrupt time; the RAM that contains the semaphore may be swapped out. 7 The DosTimerAsync call returns immediately; later, when the time interval has elapsed, the system semaphore is cleared. The process can poll the semaphore to see if the time is up, and/or it can block on the semaphore to wait for the time to elapse. Of course, if a process contains multiple threads, some can poll and others can block.
The DosTimerStart call is identical to the DosTimerAsync call except
that the semaphore is repeatedly cleared at the specified interval until a corresponding DosTimerStop call is made. DosTimerStart clears the semaphore; the process must set it again after it's been cleared.
None of the above-mentioned facilities is completely accurate for
tracking the time of day or the amount of elapsed time. As we mentioned, if a higher-priority thread is consuming enough CPU time, unpredictable delays occur. Even DosTimerStart is susceptible to losing ticks because if the CPU is unavailable for a long enough period the process won't be able to reset the semaphore soon enough to prevent missing its next clearing. Applications that want a precise measurement of elapsed time should use the time values stored in the global infoseg. We also recommend that applications with a critical need to manage timeouts, even if they are executing in the lower-priority background, dedicate a thread to managing the time-critical work and elevate that thread to a higher priority. This will ensure that time-critical events aren't missed because a high-priority foreground thread is going through a period of intensive CPU usage. Of course, such an application must be designed so that the high-priority timer event thread does not itself consume significant CPU time; it should simply log the timer events and rely on its fellow normal-priority threads to handle the major work involved.
17 Device Drivers and Hard Errors
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
The multitasking nature of OS/2 makes OS/2 device drivers considerably more complex than MS-DOS device drivers. Furthermore, whenever you have devices, you must deal with device failures--the infamous hard errors. The handling of hard errors in a multitasking environment is likewise considerably more complex than it was under MS-DOS.
17.1 Device Drivers
This section gives an overview of device drivers, paying special attention to their key architectural elements. Writing a device driver is a complex task that must be undertaken with considerable care; a great many caveats and "gotchas" lie in wait for the unsuspecting programmer. Many of these "gotchas" are of that most favorite breed: ones that never show up in testing, only in the field. This section is by no means an exhaustive discussion of device drivers, nor is it a how-to guide. Study the OS/2 device driver reference documentation carefully before setting out to write your own.
In Chapter 2 I briefly discussed device independence and the role
that device drivers play in bringing it about. I said that a device driver is a package of code that transforms I/O requests made in standard, device- independent fashion into the operations necessary to make a specific piece of hardware fulfill that request. A device driver takes data and status information from the hardware, in the hardware-specific format, and massages that information into the form that the operating system expects to receive.
The device driver architecture has two key elements. First, each
hardware device has its own device driver to hide the specific details of the device from the operating system. Second, device drivers are not hard- wired into the operating system when it is manufactured; they are dynamically installed at boot time. This second point is the interesting one. If all device drivers were hard-wired into OS/2, the technique of encapsulating device-dependent code into specific packages would be good engineering practice but of little interest to the user. OS/2 would run only on a system configured with a certain magic set of peripheral devices. But because device drivers are dynamically installable at boot time, OS/2 can work with a variety of devices, even ones that didn't exist when OS/2 was written, as long as a proper device driver for that device is installed at boot time. Note that device drivers can be installed only at boot time; they cannot be installed after the system has completed booting up. This is because in a future secure environment the ability to dynamically install a device driver would give any application the ability to violate system security.
Saying that device drivers merely translate between the operating
system and the device is a bit of oversimplification; in reality, they are responsible for encapsulating, or owning, nearly all device-specific knowledge about the device. Device drivers service the interrupts that their devices generate, and they work at task time (that is, at noninterrupt time). If a device monitor is necessary for a device, the device driver writer decides that and provides the necessary support. If an application needs direct access to a device's I/O ports or to its special mapped memory, 1. For example, the display memory of a CGA or an EGA card. 1 the driver offers those services to processes. The device driver also knows whether multiple processes should simultaneously use the device and either allows or disallows this.
17.1.1 Device Drivers and OS/2 Communication
Because device drivers need to call and be called by OS/2 efficiently and
because device drivers must handle hardware interrupts efficiently, they
must run at ring 0. This means that device drivers must be trusted and must
be trustworthy. A flaky device driver--or worse, a malicious one--can do
unlimited and nearly untraceable damage to any application or data file in
the system.
OS/2 can easily call a device driver. Because OS/2 loaded the device
driver into memory, it knows the address of its entry point and can call it directly. For the device driver to call OS/2 is trickier because the driver doesn't know the memory locations that OS/2 occupies nor does it have any control over the memory descriptor tables (LDT and GDT). When the device driver is initialized, OS/2 supplies the device driver with the address of the OS/2 DevHlp entry point. Device drivers call this address to access a variety of OS/2 services, called DevHlp services. The OS/2 DevHlp address references a GDT selector so that the DevHlp address is valid at all times-- in protected mode, in real mode, 2. A GDT selector cannot literally be valid in real mode because the GDT is not in use. OS/2 uses a technique called tiling so that the selector, when used as a segment address in real mode, addresses the same physical memory as does the protect mode segment. 2 at interrupt time, and during device driver initialization. Some DevHlp functions are only valid in certain modes, but the DevHlp facility is always available.
Why don't device drivers simply use dynamic links to access OS/2
services, the way that applications do? The OS/2 kernel dynlink interface is designed for processes running in user mode, at ring 3, to call the ring 0 kernel. In other words, it's designed for outsiders to call in, but device drivers are already inside. They run at ring 0, in kernel mode, and at interrupt time. One, of course, could kludge things so that device drivers make dynlink calls, and then special code at those OS/2 entry points would recognize a device driver request and do all the special handling. But every system call from a normal application would be slowed by this extra code, and every service call from a device driver would likewise be slowed. As a result, device drivers have their own private, high-efficiency "backdoor" entry into OS/2. Figure 17-1 illustrates the call linkages between OS/2 and a device driver. OS/2 calls only one entry point in the device driver, providing a function code that the device driver uses to address a dispatch table. OS/2 learns the address of thisentry point when it loads the device driver. The device driver in turn calls only one OS/2 address, the DevHlp entry point. It also supplies a function code that is used to address a dispatch table. The device driver is told this address when it receives its initialize call from OS/2. Not shown is the device driver's interrupt entry point.
OS/2 Device driver
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ Far call (FCN) ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Function ³ ³ DevHlp ³ ³ ³ Table ³ ³ Function Table ³ ³ ³ ÚÄÄÄÄÄÄ¿ ÚÄÄ�°°°°°° ³ ³ °°°°°°�ÄÄ¿ ÚÄÄÄÄÄÄ¿ ³ ÀÄÄÄÄÅÄ�³ ÃÄÄÙ ³ ³ ÀÄÄ´ ³�ÄÅÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ÃÄÄÄÄÄ�°°°°°° ³ ³ °°°°°°�ÄÄÄÄÄ´ ³ ³ DevHlp ³ ³ ³ ÃÄÄ¿ ³ ³ ÚÄÄ´ ³ ³ address ³ ³ ÀÄÄÄÄÄÄÙ ÀÄÄ�°°°°°° ³ ³ °°°°°°�ÄÄÙ ÀÄÄÄÄÄÄÙ ³ ³ ³ ³ ³ ³ ÀÄÄÄÄÄÄÄ´ ³ ³ ³ Far call ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ (DevHlp FCN) ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 17-1. Device driver call linkages.
17.1.2 Device Driver Programming Model
The programming model for device drivers under MS-DOS is simple. Device
drivers are called to perform a function, and they return when that
function is complete or they encounter an unrecoverable error. If the
device is interrupt driven, the CPU hangs in a loop inside the device
driver while waiting for the driver's interrupt handler to be entered; when
the operation is complete, the interrupt handler sets a private flag to
break the task-time CPU out of its wait loop.
The OS/2 device driver model is considerably more complicated because
OS/2 is a multitasking system. Even if the thread that calls the device driver with a request has nothing better to do than wait for the operation to complete, other threads in the system could make good use of the time. Another effect of the OS/2 multitasking architecture is that two or more threads can simultaneously call a device driver. To explore this last issue fully, we'll digress for a moment and discuss the OS/2 internal execution model.
By design, OS/2 acts more like a subroutine library than like a
process. The only dispatchable entities in the system are threads, and all threads belong to processes. When a process's thread calls OS/2, that thread executes OS/2's code. 3. The few exceptions to this don't affect the issues discussed here. 3 It's like walking up to the counter at a fast-food restaurant and placing your order. You then slip on an apron, run around behind the counter, and prepare your own order. When the food is ready, you take off the apron, run back around to the front of the counter, and pick up the food. The counter represents the boundary between ring 0 (kernel mode) and ring 3 (application mode), and the apron represents the privileged state necessary to work behind the counter.
Naturally, OS/2 is reentrant; at any one time many threads are
executing inside OS/2, but each is doing work for only one process--the process to whom that thread belongs. Behind the counter are several folks wearing aprons, but each is working only on his or her own order. This approach simplifies the internals of OS/2: Each instance of a section of code is doing only one thing for one client. If a section of code must wait for something, it simply blocks (analogous to a semaphore wait) as long as it has to and resumes when it can. Threads within the kernel that are competing for a single resource do so by internal semaphores, and they are given access to these semaphores on a priority basis, just as they are when executing in application mode.
OS/2 makes little distinction between a thread running inside the
kernel and one running outside, in the application's code itself: The process's LDT remains valid, and the thread, while inside the kernel, can access any memory location that was accessible to the process in application mode, in addition to being able to access restricted ring 0 memory. The only distinction the scheduler makes between threads inside and those outside the kernel is that the scheduler never preempts a thread running inside the kernel. This greatly relaxes the rigor with which kernel code needs to protect its critical sections: When the CPU is executing kernel code, the scheduler performs a context switch only when the CPU voluntarily blocks itself. As long as kernel code doesn't block itself, wait on a semaphore, or call a subroutine that waits on a semaphore, it needn't worry about any other thread entering its critical section. 4. Although hardware interrupts still occur; any critical section modified at interrupt time is still vulnerable. 4
When OS/2 calls a device driver at task time, it does so with the
thread that was executing OS/2--the thread that belongs to the client process and that made the original service call. Thus, the task-time part of a device driver is running, at ring 0, in the client's context. The client's LDT is active, all the client's addresses are active, and the device driver is immune from being preempted by other task-time threads (but not by interrupt service) until it blocks via a DevHlp function or returns to OS/2.
OS/2 device drivers are divided into two general categories: those for
character mode devices and those for block mode devices. This terminology is traditional, but don't take it too literally because character mode operations can be done to block mode devices. The actual distinction is that character mode device drivers do I/O synchronously; that is, they do operations in first in, first out order. Block mode device drivers can be asynchronous; they can perform I/O requests in an order different from the one in which they received them. A traditional serial character device, such as a printer, must not change the order of its requests; doing so scrambles the output. A block device, such as a disk, can reverse the order of two sector reads without problems.
Figure 17-2 shows an algorithm for character mode device drivers.
5. See the device driver reference manual for more details. 5 OS/2 calls the device driver with a request, as shown at the top of the figure. If the device driver is busy with another request, the new requesting thread should block on a RAM semaphore until the device is available. When the device is free, the task-time thread does the requested operation. Sometimes the work can be done at task time (such as an IOCTL call asking about the number of characters in an input buffer), but more frequently the task-time thread initiates the operation, and the work is completed at interrupt time. If the task-time thread needs to wait for interrupt service, it should block on a RAM semaphore 6. Located in the device driver data area. 6 that the interrupt- time code will clear. When the last associated device interrupt takes place and the operation is complete, the interrupt code releases the RAM semaphore. The task-time thread awakens and returns to OS/2 with the status bits properly set in the request block.
OS/2 code Device driver Device interrupt
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ Issue request to device driver. ÄÄÄÄÄÄ� Block until device is
available.
Peform request. Block until interrupts complete if necessary. ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ³ Device interrupt (if any) ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ³ Perform next step in I/O ³ operation. ³ ³ When done, use DevHlp ³ ProcRun to unblock task ³ time thread. ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ³ End of interrupt Complete request and ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ return to OS/2
Request now complete. Continue.
Figure 17-2. Simplified character mode device driver model.
Figure 17-3 shows an algorithm for block devices. The general outline
is the same as that for character devices but more complicated because of the asynchronous nature of random-access devices. Because requests can be processed in any order, most block device drivers maintain an internal work queue to which they add each new request. They usually use a special DevHlp function to sort the work queue in sector number order so that disk head motion is minimized.
OS/2 code Device driver Device interrupt
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ Issue request to device driver. ÄÄÄÄÄÄÄ� Add request to device
request list. Fire up device if not active.
Return to OS/2 with DONE clear. ÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ � ³ Device interrupt
OS/2 thread(s) block on ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ incomplete request when ³ Perform next step in they can proceed no ³ I/O operation. further without the ³ data. ³ If request is done
³ Pull request from ³ list
Any threads blocked ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ on this request are ³ Use DevHlp DevDone awakened. ³ to tell OS/2 that
³ the I/O is done. ³ ³ If further work on ³ queue, start work ³ on next item. ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ³ End of interrupt
Request now complete. ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ Continue.
Figure 17-3. Block mode device driver model.
The easiest way to understand this figure is to think of a block mode
device driver as being made up of N threads: one interrupt-time thread does the actual work, and all the others are task-time threads that queue the work. As each request comes into the driver via a task-time thread, that thread simply puts the request on the queue and returns to OS/2. Later, the device driver's interrupt service routine calls the DevHlp DevDone function to tell OS/2 that the operation is complete. Returning to OS/2 with the operation incomplete is permissible because the request block status bits show that the operation is incomplete.
Sometimes, OS/2 needs to wait for the operation (such as a read from a
directory); so when the device driver returns with the operation incomplete, OS/2 simply waits for it to finish. In other circumstances, such as flushing the cache buffers, OS/2 may not wait around for the operation to complete. It may go on about its business or even issue a new request to the driver, using, of course, a new request block because the old one is still in use. This design gives the system a great deal of parallelism and thereby improves throughput.
I said that a block mode device driver consists of several task-time
threads and one interrupt-time thread. The term interrupt-time thread is a bit misleading, however, because it's not a true thread managed by the scheduler but a pseudo thread created by the hardware interrupt mechanism. For example, a disk device driver has four requests queued up, and the READ operation for the first request is in progress. When it completes, the driver's interrupt service routine is entered by the hardware interrupt generated by the disk controller. That interrupt-time thread, executing the driver's interrupt service routine, checks the status, verifies that all is OK, and calls various DevHlp routines to post the request as complete and to remove it from the queue. It then notes that requests remain on the queue and starts work on the next one, which involves a seek operation. The driver's interrupt-time code issues the seek command to the hardware and then returns from the interrupt. When the disk stops seeking, another interrupt is generated; the interrupt-time code notes the successful seek, issues the read or write operation to the controller, and exits.
As you can see, the repeated activation of the device driver's
interrupt service routine is much like a thread, but with two major differences. First, every time an interrupt service routine is entered, it has a fresh stack. A task-time thread has register contents and a stack that are preserved by the system; neither is preserved for an interrupt service routine between interrupts. A task-time thread keeps track of what it was doing by its CS:IP address, its register contents, and its stack contents. An interrupt service routine must keep track of its work by means of static values stored in the device driver's data segment. Typically, interrupt service routines implement a state machine and maintain the current state in the driver's data segment. Second, a true thread remains in existence until explicitly terminated; an interrupt service thread is an illusion of a thread that is maintained by repeated interrupts. If any one execution of the interrupt service routine fails to give the hardware a command that will generate another interrupt, the interrupt pseudo thread will no longer exist after the interrupt service routine returns.
The block mode driver algorithm description left out a detail. If the
disk is idle when a new request comes in, the request is put on the queue, but there is no interrupt-time pseudo thread to service the request. Thus, both the task-time and interrupt-time parts of a device driver must be able to initiate an operation. The recommended approach is to use a software state machine to control the hardware and to ensure that the state machine, at least the start operation part of it, is callable at both task and interrupt time. The algorithm above is then modified so that after the task-time part of a block device driver puts its request on the driver's internal queue it verifies that the device (or state machine or interrupt pseudo thread) is active. If the device has been idle, the task-time thread in the device driver initiates the operation by calling the initial state of the state machine; it then returns to OS/2. This primes the pump; the interrupt pseudo thread now continues to run until the request queue is empty.
Figure 17-4 shows an overview of the OS/2 device driver architecture.
Each device driver consists of a task-time part, an interrupt-time part (if the device generates interrupts), and the start-operation code that is executed in either mode. The driver's data area typically contains state information, flags, and semaphores to handle communication between the task-time part and the interrupt. Figure 17-4 also shows that the task-time part of a device driver can have multiple instances. It can be called by several threads at the same time, just as a shared dynlink library routine might be. Unlike a dynlink library, a device driver has no instance data segment; the device driver's data segment is a global data segment, accessible to all execution instances of the device driver's task-time component. Just as a dynlink package uses semaphores to protect critical data areas in its global data segment, a device driver uses semaphores to protect the critical data values in its data segment. Unlike dynlink routines, the device driver has an additional special thread--the interrupt service thread. A device driver can't protect critical sections that are accessed at interrupt time by using semaphores because an interrupt service thread cannot block. It must complete the interrupt service and exit-- quickly, at that. When you write device drivers, you must minimize the critical sections that are entered by the interrupt service thread and protect them via the CLI/STI instruction sequence.
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ "D"³ ÚÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ "C"³ ³ ³ Device driver ³ ÚÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÃÄÄÙ ³ interrupt service ³ ³ "B"³ ³ ÀÄÂÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÙ
ÚÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÃÄÄÙ ³ ³ ³ Instance "A" ³ ³ ³ ³ ³ device driver ÃÄÄÙ ÚÄÄÄÄÄÄÄ�ÄÄÄ¿ ³ ³ task-time code ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ�³ Start I/O ³ ³ ÀÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÙ ³ code ³ ³
³ ÀÄÄÄÄÄÂÄÄÄÄÄÙ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ÚÄÄÄÄÄÄÄÄ�ÄÄÄÄÄÄ¿ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ�³ Device driver ³�ÄÄÄÄÄÙ ³ data ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 17-4. Device driver code structure.
17.1.3 Device Management
Device drivers do more than talk to the device; they also manage it for the
system. Device drivers are called each time a process opens or closes a
device; device drivers determine whether a device can be used by more than
one process simultaneously. Likewise, device drivers receive device monitor
requests from applications via the IOCTL interface and, when appropriate,
call OS/2 via the DevHlp interface to perform the bulk of the monitor work.
Finally, device drivers can grant processes access to the device's I/O
ports, to the device's mapped memory, and/or to special control areas in
the device driver's data area itself. Once again, processes ask for these
features via IOCTL; the device driver grants the requests via a DevHlp
dialog with OS/2. Some device drivers are degenerate; they don't actually
transfer data but exist solely to manage these other tasks. The screen
device driver is an example. Screen data is always written directly to the
display buffer by VIO, the application, or the presentation manager. The
screen device driver exists to grant direct access, manage screen groups,
and so on.
17.1.4 Dual Mode
The last key architectural feature of device drivers is that they are
written in dual mode: The driver code, both task time and interrupt time,
must be able to execute in protected mode and real mode. The process of
mode switching between protected mode and real mode is quite slow--about
800 microseconds. If we decreed that all device drivers run only in
protected mode and that service interrupts run only in protected mode, a
disk request from a real mode program might require six or more mode
switches--one for the request, and five for the interrupts--for a penalty
of almost 5 milliseconds. Consequently, device drivers must run in whatever
mode the CPU is in when the request comes along or the interrupt arrives.
At first glance, this seems easy enough: As long as the device driver
refrains from computing its own segment selectors, it can execute in either mode. The catch is that OS/2 may switch between modes at every call and/or interrupt, and the addresses of code and data items are different in each mode. A device driver might be called in protected mode with an address in the client process's address space. When the "data ready" interrupt arrives, however, the CPU may be running in real mode, and that client's address is no longer valid--for two reasons. One, the segment selector part of a memory address has a different meaning in real mode than it does in protected mode; and, two, the client's selector was in the LDT, and the LDT is invalid at interrupt time. 7. See the device driver reference manual for more details. 7 OS/2 helps device drivers deal with addressing in a dual mode environment in three ways:
1. Some addresses are the same in both modes and in either protected mode or real mode. The DevHlp entry point, the global infoseg address, the request packet address, and any addresses returned via the DevHlp GetDosVar function are valid at all times and in both modes.
2. Although the segment selector value for the device driver's code and data segments is different in each mode, OS/2 loads the proper values into CS and DS before it calls the device driver's task- time or interrupt-time entry points. As long as a device driver is careful not to "remember" and reuse these values, it won't notice that they (possibly) change at every call.
3. OS/2 provides a variety of DevHlp functions that allow a device driver to convert a selector:offset pair into a physical address and then later convert this physical address back into a selector:offset pair that is valid at that particular time. This allows device drivers to convert addresses that are outside their own segments into physical addresses and then, upon each task-time or interrupt-time call to the driver, convert that physical address back into one that is usable in the current mode. This avoids the problem of recording a selector:offset pair in protect mode and then trying to use it as a segment:offset pair in real mode.
17.2 Hard Errors
Sometimes the system encounters an error that it can neither ignore nor correct but which the user can correct. A classic example is the user leaving ajar the door to the floppy drive; the system can do nothing to access that floppy disk until someone closes the door. Such an error is called a hard error. The term originated to describe an error that won't go away when the operation is retried, but it also aptly describes the effort involved in the design of OS/2 to deal with such errors.
The manner in which MS-DOS handles hard errors is straightforward. In
our drive door example, MS-DOS discovers the problem when it is deep inside the bowels of the system, communicating with the disk driver. The driver reports the problem, and MS-DOS displays some text on the screen--the infamous "Abort, Retry, Ignore?" message. Typically, the user fixes the problem and replies; MS-DOS then takes the action specified by the user, finishes its work, and returns to the application. Often, applications didn't want the system to handle hard errors automatically. Perhaps they were concerned about data integrity and wanted to be aware of a disk writing problem, or they wanted to prevent the user from specifying "Ignore," 8. This response is classic. Sophisticated users understand the likely consequences of such a reply, but most users would interpret "Ignore" as "Make the problem go away"--an apparently ideal solution! 8 or they didn't want MS-DOS to write over their screen display without their knowing. To handle these situations, MS-DOS lets applications store the address of a hard error handler in the INT 24 vector; if a handler is present, MS-DOS calls it instead of its own handler.
The system is in an unusual state while processing an MS-DOS hard
error. The application originally calls MS-DOS via the INT 21 vector. MS- DOS then calls several levels deep within itself, whereupon an internal MS- DOS routine calls the hard error handler back in the application. Because MS-DOS is not generally reentrant, the application cannot recall MS-DOS via INT 21 at this point; doing so would mean that it has called MS-DOS twice at the same time. The application probably needs to do screen and keyboard I/O when handling the hard error, so MS-DOS was made partially reentrant. The original call involves disk I/O, so MS-DOS can be reentered via a screen/keyboard I/O call without problem.
Several problems prevented us from adopting a similar scheme for OS/2.
First, unlike the single-tasking MS-DOS, OS/2 cannot suspend operations while the operating system calls an application--a call that might not return for a long time. Second, major technical and security problems are involved with calling from ring 0 (the privileged kernel mode) to ring 3 (the application mode). Also, in the MS-DOS environment, deciding which process was responsible for the operation that triggered the hard error is easy: Only one application is running. OS/2 may have a hard time determining which process to alert because more than one process may have caused a disk FAT sector or a disk directory to be edited. The improved buffering techniques employed by OS/2 may cause a hard error to occur at a time when no process is doing any I/O. Finally, even if we solve all these problems, the application that triggers the hard error may be running in a background screen group and be unable to display a message or use the keyboard. Even if the application is in the foreground screen group, it can't use the screen and keyboard if it's not the process currently controlling them.
17.2.1 The Hard Error Daemon
This last problem yields a clue to the solution. OS/2 supports multiple
screen groups, and its screen group mechanism manages multiple simultaneous
use of the screen and the keyboard, keeping the current users--one in each
screen group--isolated from one another. Clearly, we need to use screen
groups to allow a hard error dialog to be completed with the user without
interfering with the current foreground application. Doing so solves the
problem of writing on another application's screen image and therefore
removes most of the need for notifying an application that a hard error has
occurred.
Specifically, OS/2 always has running a process called the hard error
daemon. When a hard error occurs, OS/2 doesn't attempt to figure out which process caused it; instead, it notifies the hard error daemon. The hard error daemon performs a special form of screen group switch to the reserved hard error screen group and then displays its message and reads its input. Because the hard error daemon is the only process in this screen group, screen and keyboard usage do not conflict. The previous foreground process is now temporarily in the background; the screen group mechanism keeps it at bay.
Meanwhile, the process thread that encountered the hard error in the
kernel is blocked there, waiting for the hard error daemon to get a response from the user. The thread that handles the hard error is never the thread that caused the hard error, and the kernel is already fully reentrant for different threads; so the hard error daemon thread is free to call OS/2 at will. When the user corrects the problem and responds to the hard error daemon, the hard error daemon sends the response back to the kernel, which allows the thread that encountered the error to take the specified action. That thread either retries the operation or produces an error code; the hard error daemon returns the system to the original screen group. The screen group code then does its usual trick of restoring the screen image to its previous state. Figure 17-5 illustrates the hard error handling sequence. A process thread encounters a hard error while in the OS/2 kernel. The thread blocks at that point while the hard error daemon's previously captured thread is released. The hard error daemon performs a special modified screen switch at (1), displays its message, gets the user's response, restores the application screen group at (2), and reenters the OS/2 kernel. The response code is then passed to the blocked application thread, which then resumes execution.
DosCall Process ÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄ
ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄ¿ ³ OS/2 kernel ³ ³ Return ³ ³ ³ Hard ³ to ³ ³ ³ error ³ process ³ ³ ÀÄÄÄÄÄÄ´ ÃÄÄÄ>Ä>ÄÄÄÄÙ ³ ³ < < ³ ³ ÃÄ¿ ÚÄ´ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
� � returns ³ ³ calls ³ ³ Hard error daemon ³ Display message ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ (1) Get response (2) TimeÄÄÄÄÄÄ�
Figure 17-5. Hard error handling.
Although the most common cause of hard errors is a disk problem for
example, an open drive door or a medium error--other events that require user intervention or user notification use the hard error mechanism. For example, the volume management package (see 15.2 Media Volume Management) uses the hard error mechanism to display its "Insert volume <name>" messages. As I mentioned earlier, MS-DOS applications running in the compatibility box can encounter problems, such as locked files, that they can't understand. Rather than have these applications fail mysteriously, OS/2 uses the hard error daemon mechanism to inform the user of the cause of the real mode application's difficulties. Although the application running in the compatibility box sees an operating system that acts like MS-DOS, the operating system is actually OS/2. Because of this, hard errors encountered by a real mode process are handled by an amalgam of the MS-DOS INT 24 mechanism and the OS/2 hard error daemon. See Chapter 19, The 3X Box.
17.2.2 Application Hard Error Handling
In some cases an application doesn't want the system to handle its hard
errors. For example, an application designed for unattended or remote
operation, such as a network server, may want to pass notification of hard
errors to a remote correspondent rather than hanging up forever with a
message on a screen that might not be read for hours. Another example is a
database program concerned about the integrity of its master file; it may
want to know about hard errors so that it can take some special action or
perhaps use an alternative master file on another device. OS/2 allows a
process to disable automatic hard error handling on a per file basis. Our
network example will want to disable hard error pop-ups for anything the
process does; our database example may want to disable hard error pop-ups
only for its master file, keeping their convenience for any other files
that it might access. When a hard error occurs on behalf of a process or a
handle that has hard error pop-ups disabled, OS/2 assumes that a FAIL
response was entered to a hypothetical hard error pop-up and returns to the
application with a special error code. The application must analyze the
code and take the necessary actions.
18 I/O Privilege Mechanism and Debugging/Ptrace
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
The earlier chapters of this book focused on the "captains and kings" of the operating system world, the major architectural features. But like any real world operating system, OS/2 contains a variety of miscellaneous facilities that have to be there to get the work done. Although these facilities may not be major elements in some architectural grand scheme, they still have to obey the principles of the design religion. Two of them are the I/O privilege mechanism and the debugging facility.
18.1 I/O Privilege Mechanism
Earlier I discussed the need for a mechanism that allows applications high- speed direct access to devices. But the mechanism must control access in such a way that the system's stability isn't jeopardized and in such a way that applications don't fight over device control. OS/2 meets this requirement with its I/O privilege mechanism. This facility allows a process to ask a device driver for direct access to the device's I/O ports and any dedicated or mapped memory locations it has. The I/O privilege mechanism can be used directly by an application, which necessarily makes it device dependent, or indirectly by a dynlink package. The dynlink package can act as a kind of device driver; a new version can be shipped with new hardware to maintain application compatibility. This pseudo device driver is normally much faster than a true device driver because of the customized procedural interface; not entering ring 0 and the OS/2 kernel code saves much time.
Unfortunately, this isn't a free lunch. Dynlink pseudo device drivers
can do everything that true device drivers can except handle interrupts. Because hardware interrupts must be handled at ring 0, the handler must be part of a true device driver. Frequently, a compromise is in order: Both a dynlink package and a true device driver are provided. The true device driver handles the interrupts, and the dynlink package does the rest of the work. The two typically communicate via shared memory and/or private IOCTLs. An example of such a compromise is the system KBD dynlink package. The system VIO package doesn't need a device driver to handle interrupts because the display device doesn't generate any.
The two components in the OS/2 I/O access model are access to the
device's memory and access to its I/O ports. Granting and controlling access to a device's mapped memory is easy because the 80286 protect mode supports powerful memory management facilities. First, a process asks the device driver for access to the device's memory, for example, to the memory buffer of a CGA board. Typically, a dynlink package, rather than an application, does this via the DosDevIOCtl call. If the device driver approves the request, it asks OS/2 via the DevHlp interface to set up an LDT memory descriptor to the proper physical memory locations. OS/2 returns the resultant selector to the device driver, which returns it to the calling process. This technique isn't limited to memory-mapped device memory; device drivers can use it to allow their companion dynlink packages direct access to a piece of the device driver's data segment. In this way, a combination dynlink/device driver device interface can optimize communication between the dynlink package and the device driver.
Providing I/O port access to a process is more difficult because it is
supported more modestly by the 80286 processor. The 80286 uses its ring protection mechanism to control I/O access; the system can grant code running at a certain ring privilege access to all I/O ports, but it can't grant access to only some I/O ports. It's too dangerous to grant an application access to all I/O ports simply because it uses VIO and VIO needs direct port access for the display adapter. This solution would mean that OS/2's I/O space is effectively unprotected because almost all programs use VIO or the presentation manager directly or indirectly.
Instead, OS/2 was designed to allow, upon request from the device
driver, any code segments marked 1. This is done via a special command to the linker. 1 to execute at ring 2 to have I/O access. The bad news is that access to all I/O ports must be granted indiscriminately, but the good news is that the system is vulnerable to program bugs only when those ring 2 segments are being executed. The capabilities of ring 2 code, as it's called, are restricted: Ring 2 code cannot issue dynlink calls to the system. This is partly a result of ring architecture (supporting ring 2 system calls would require significant additional overhead) and partly to discourage lazy programmers from flagging their entire process as ring 2 to avoid sequestering their I/O routines.
As I said, in OS/2 version 1.0 the ring mechanism can restrict I/O
access only to a limited degree. Any malicious program and some buggy programs can still damage system stability by manipulating the system's peripherals. Furthermore, a real mode application can issue any I/O instruction at any time. A future release of OS/2 that runs only on the 80386 processor will solve these problems. The 80386 hardware is specifically designed to allow processes access to some I/O ports but not to others through a bit map the system maintains. This map, which of course the application cannot directly change, tells the 80386 which port addresses may be accessed and which must be refused. This map applies equally to protect mode and real mode applications. 2. Actually, to "virtual real mode" applications. This is functionally the same as real mode on earlier processors. 2 OS/2 will use the port addresses supplied by the device driver to allow access only to the I/O ports associated with the device(s) to which the process has been granted access. This release will not support application code segments running at ring 2; any segments so marked will be loaded and run at ring 3. The change will be invisible to all applications that use only the proper I/O ports. Applications that request access to one device and then use their I/O permissions to program another device will fail.
18.2 Debugging/Ptrace
Because OS/2 goes to a great deal of effort to keep one application from interfering with another, special facilities were built to allow debugging programs to manipulate and examine a debuggee (the process being debugged). Because a debugger is available for OS/2 and writing your own is laborious, we expect few programmers to write debuggers. This discussion is included, nevertheless, because it further illuminates the OS/2 architectural approach.
The first concern of a debugger is that it be able to read and write
the debuggee's code and data segments as well as intercept traps, signals, breakpoints, and the like. All these capabilities are strictly in the domain of OS/2, so OS/2 must "export" them to the debugger program. A second concern is system security: Obviously, the debug interface provides a golden opportunity for "cracker" programs to manipulate any other program, thereby circumventing passwords, encryption, or any other protection scheme. OS/2 prevents this by requiring that the debuggee process be flagged as a debug target when it is initially executed; a debugger can't latch onto an already-running process. Furthermore, when secure versions of OS/2 are available, processes executed under control of a debugger will be shorn of any permissions they might have that are in excess of those owned by the debugger.
Before we examine the debugging interface, we should digress for a
moment and discuss the OS/2 approach to forcing actions upon threads and processes. Earlier I described the process of kernel execution. I mentioned that when a process thread makes a kernel request that thread itself enters kernel mode and services its own request. This arrangement simplified the design of the kernel because a function is coded to perform one action for one client in a serial, synchronous fashion. Furthermore, nothing is ever forced on a thread that is in kernel mode; any action taken on a thread in kernel mode is taken by that thread itself. For example, if a process is to be killed and one of its threads is in kernel mode, OS/2 doesn't terminate that thread; it sets a flag that says, "Please kill yourself at your earliest convenience." Consequently, OS/2 doesn't need special code to enumerate and release any internal flags or resources that a killed kernel mode thread might leave orphaned, and in general no thread need "understand" the state of any other. The thread to be killed cleans itself up, releasing resources, flags, and whatever before it obligingly commits suicide.
But when is the thread's "earliest convenience"? Thread termination is
a forced event, and all threads check for any pending forced events immediately before they leave kernel mode and reenter application mode. This transition takes place frequently: not only when a system call returns to the calling application, but also each time a context switch takes place.
Although it may appear that forced events might languish unprocessed,
they are serviced rapidly. For example, when a process issues a DosKill function on its child process, each thread in the child process is marked "kill yourself." Because the parent process had the CPU, obviously, when it issued the DosKill, each of the child's threads is in kernel mode, either because the thread is working on a system call or because it was artificially placed in kernel mode when the scheduler preempted it. Before any of those now-marked threads can execute even a single instruction of the child application's code, they must go through OS/2's dispatch routine. The "kill yourself" flag is noted, and the thread terminates itself instead of returning to application mode. As you can see, the final effect of this approach is far from slow: The DosKill takes effect immediately--not one more instruction of the child process is executed. 3. Excepting any SIGKILL handlers and DosExitList handlers, of course. 3 The only significant delay in recognizing a forced event occurs when a system call takes a long time to process. OS/2 is not very CPU bound, so any call that takes a "long time" (1 second or more) must be blocked for most of that time.
When a kernel thread issues a block call for an event that might take
a long time--such as waiting for a keystroke or waiting for a semaphore to clear--it uses a special form of block called an interruptible block. When OS/2 posts a force flag against a thread, it checks to see if that thread is blocking interruptibly. If it is, that thread is released from its block with a special code that says, "You were awakened not because the event has come to pass but because a forced event was posted." That thread must then finish the system call quickly (generally by declaring an error) so that the thread can go through the dispatch routine and recognize the force flag. I described this mechanism in Chapter 12 when I talked about another kind of forced event--the OS/2 signal mechanism. An incoming signal is a forced event for a process's thread 1; it therefore receives the same timely response and has the same effect of aborting a slow system call.
I've gone through this long discussion of forced events and how
they're processed because the internal debugging facility is based on one giant special forced event. When a process is placed in debug state, a trace force flag is permanently set for the initial thread of that process and for any other threads it creates. When any of those threads are in kernel mode--and they enter kernel mode whenever anything of interest takes place--they execute the debuggee half of the OS/2 trace code. The debugger half is executed by a debugger thread that issues special DosPtrace calls; the two halves of the package communicate through a shared memory area built into OS/2.
When the debuggee encounters a special event (for example, a Ctrl-C
signal or a GP fault), the trace force event takes precedence over any other, and the debuggee's thread executes the debuggee half of the DosPtrace code. This code writes a record describing the event into a communications buffer, wakes up the debugger thread, which is typically blocked in the debugger's part of the DosPtrace code, and blocks, awaiting a reply. The debugger's thread wakes up and returns to the debugger with the event information. When the debugger recalls DosPtrace with a command, the command is written into the communications area, and the debuggee is awakened to read and obey. The command might be "Resume normal execution," "Process the event as you normally would," or "Give me the contents of these locations in your address space," whereupon the debuggee thread replies and remains in the DosPtrace handler.
This approach is simple to implement, does the job well, and takes
advantage of existing OS/2 features. For example, no special code is needed to allow the debugger access to the debuggee's address space because the debuggee itself, unwittingly in the DosPtrace code, reads and writes its own address space. Credit goes to the UNIX ptrace facility, upon which this facility was closely modeled.
Finally, here are a few incidental facts that the readers of this
book, being likely users of debugging facilities, should know. OS/2 maintains a linkage between the debugger process and the debuggee process. When the debugger process terminates, the debuggee process also terminates if it has not already done so. The debuggee program need not be a direct child of the debugger; when the debugger process makes its initial DosPtrace call, OS/2 connects it to the last process that was executed with the special tracing option. If a process is executed with the tracing option but no debugger process subsequently issues a DosPtrace function, the jilted debuggee process is terminated in about two minutes.
19 The 3x Box
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
It's of critical importance that OS/2 do a good job of running existing MS- DOS applications, but as we've discussed, this is a difficult task. To offer the official MS-DOS interfaces under OS/2 and therefore claim upward compatibility would be easy; unfortunately, few popular applications would run successfully in such an environment. Most sophisticated applications take direct control of the machine environment and use MS-DOS for tasks the application doesn't want to bother with, such as file I/O, keyboard buffering, and so forth. If we're to run existing applications successfully, we must provide a close facsimile to a real mode PC running MS-DOS in all respects, not just the INT 21 program interface.
OS/2 provides such a highly compatible environment, called the real
mode screen group, the compatibility box, or simply the 3x box. The 3x box is an environment that emulates an 8086-based PC running MS-DOS version 3.3. 1. For OS/2 version 1.0, the 3x box is compatible with MS-DOS version 3.3. 1 MS-DOS programs execute in real mode, and because emulating real mode from within protected mode is prohibitively slow, OS/2 physically switches into real mode to execute MS-DOS applications. Because MS-DOS programs are well aware of the MS-DOS memory layout, this layout is replicated for the OS/2 3x box. The first N bytes (typically 640 KB) are reserved for the exclusive use of the low-memory parts of OS/2 and the 3x box; protected mode applications never use any of this memory. Thus, programs that are careless about memory allocation or that make single- tasking assumptions about the availability of memory can run in a multitasking environment. Figure 19-1 illustrates the OS/2 memory layout. The low bytes of memory are reserved for the device drivers and portions of OS/2 that must run in real mode. The remainder of the space, up to the RMSIZE value, is dedicated to the 3x box. Memory from 640 KB to 1 MB is reserved for ROMs and video display buffers. Memory above 1 MB holds the remainder of OS/2 and all protect mode applications. Nonswappable, fixed segments are kept at one end of this memory to reduce fragmentation.
N ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Protect ³ ³ mode ³ ³ applications ³ ³ (movable) ³ < < > > < < ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ Fixed ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ OS/2 ³ 1 MB ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
640 KB ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Real ³ ³ mode ³ ³ application ³ ³ ³ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ Additional ³ ³ device drivers ³ ~100k ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ³ Low OS/2 ³ ³ ³ 90:0 ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ Bios ROM ³ 0 ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 19-1. System memory layout.
OS/2 uses the screen group mechanism to provide a user interface to
the 3x box. One screen group is designated the real mode screen group; automatically, OS/2 executes COMMAND.COM in that screen group when it is first selected. The user accesses the real mode environment by selecting that screen group and returns to the protected mode environment by selecting another screen group. OS/2 version 1.0 supports a single real mode screen group because the real mode compatibility is provided by actually running the application in real mode. Thus, only one 640 KB area is reserved for all real mode applications, and adjudicating between the conflicting hardware manipulations of multiple real mode applications without any assistance from the 80286 microprocessor hardware would be prohibitively difficult. The 80386 microprocessor, however, provides a special hardware facility called virtual 8086 mode that will allow a future release of OS/2 to support multiple real mode screen groups, but only on an 80386-based machine.
The operating system that services the 3x application's INT 21
requests is not an exact copy of MS-DOS; it's actually a low-memory extension of OS/2 itself. Because OS/2 is derived from MS-DOS, OS/2 executes MS-DOS functions in a manner identical to that of the real MS-DOS. OS/2 supports the non-MS-DOS functions mentioned above by staying out of the way as much as possible and letting the 3x application "party hearty" with the hardware. For example, hooking most interrupt vectors is supported, as is hooking INT 21 and the ROM BIOS INT vectors. The ROM BIOS calls themselves are fully supported. Frequently, staying out of the way is not as easy as it may sound. For example, OS/2 must intercept and monitor real mode calls made to the disk driver part of the ROM BIOS so that it can prevent conflict with ongoing, asynchronous protect-mode disk I/O. OS/2 may find it necessary to momentarily block a real mode application's BIOS call until the protect mode device driver can release the hardware. Once the real mode application is in the BIOS, the same interlock mechanism prevents the protect mode device driver from entering the disk I/O critical section.
Hard errors encountered by the real mode application are handled by a
hybrid of the OS/2 hard error daemon and the 3x box INT 24 mechanism in a three-step process, as follows:
1: Hard error codes caused by events unique to the OS/2 environment-- such as a volume manager media change request--activate the hard error daemon so that the user can get an accurate explanation of the problem. The user's response to the hard error is saved but is not yet acted upon. Hard error codes, which are also present in MS-DOS version 3.3, skip this step and start at step 2.
2: If the real mode application has installed its own hard error handler via the INT 24 vector, it is called. If step 1 was skipped, the code should be known to the application, and it is presented unchanged. If step 1 was taken, the error code is transformed to ERROR_I24_GEN_FAILURE for this step. The response returned by the program, if valid for this class of hard error, is acted upon. This means that hard errors new to OS/2 can actually generate two pop-ups--one from the hard error daemon with an accurate message and one from the application itself with a General Failure message. This allows the user to understand the true cause of the hard error and yet notifies the application that a hard error has occurred. In such a case, the action specified by the application when it returned from its own hard error handler is the one taken, not the action specified by the user to the initial hard error daemon pop-up.
3: If the real mode application has not registered its hard error
handler via the INT 24 mechanism, OS/2 provides a default handler that uses the hard error daemon. If step 1 was taken and the hard error daemon has already run, it is not run again; OS/2 takes the action specified in response to the hard error pop-up that was displayed. If step 1 was not taken because the hard error code is MS-DOS 3.x compatible and if step 2 was not taken because the application did not provide its own handler, then OS/2 activates the hard error daemon in step 3 to present the message and receive a reply.
The 3x box supports only MS-DOS functionality; no new OS/2 features
are available to 3x box applications--no new API, no multiple threads, no IPC, no semaphores, and so on. 2. There are two exceptions. The OPEN function was extended, and an INT 2F multiplex function was added to notify real mode applications of screen switches. 2 This decision was made for two reasons. First, although any real mode application can damage the system's stability, allowing real mode applications to access some protect mode features may aggravate the problem. For example, terminate and stay resident programs may manipulate the CPU in such a way as to make it impossible for a real mode application to protect a critical section with semaphores and yet guarantee that it won't leave the semaphore orphaned. Second, because OS/2 has only one real mode box and it labors under a 640 KB memory ceiling, it doesn't make sense to develop new real mode applications that use new OS/2 functions and thus require OS/2.
The 3x box emulation extends to interrupts. OS/2 continues to context
switch the CPU when the 3x box is active; that is, the 3x box application is the foreground application. Because the foreground process receives a favorable priority, its CPU is preempted only when a time-critical protect mode application needs to run or when the real mode application blocks. If the CPU is running a protect mode application when a device interrupt comes in, OS/2 switches to real mode so that a real mode application that is hooking the interrupt vectors can receive the interrupt in real mode. When the interrupt is complete, OS/2 switches back to protected mode and resumes the protected application.
Although protected mode applications can continue to run when the 3x
box is in the foreground, the reverse is not true. When the 3x box screen group is in the background, all 3x box execution is suspended, including interrupts. Unlike protected mode applications, real mode applications cannot be trusted to refrain from manipulating the screen hardware when they are in a background screen group. Normally, a real mode application doesn't notice its suspension when it's in background mode; the only thing it might notice is that the system time-of-day has apparently "jumped forward." Because mode switching is a slow process and leaves interrupts disabled for almost 1 millisecond, mode switching can cause interrupt overruns on fast devices such as serial ports. The best way to deal with this is to switch the real mode application into a background screen group; with no more real mode programs to execute, OS/2 does no further mode switching.
Some OS/2 utility programs such as FIND are packaged as Family API
applications. A single binary can run in both protected mode and real mode, and the user is saved the inconvenience of switching from real mode to protected mode to do simple utility functions. This works well for simple utility programs without full screen or graphical interfaces and for programs that have modest memory demands and that in other ways have little need of OS/2's extended capabilities. Obviously, if an application can make good use of OS/2's protect mode features, it should be written to be protect mode only so that it can take advantage of those features.
20 Family API
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
When a new release of a PC operating system is announced, application writers face a decision: Should they write a new application to use some of the new features or should they use only the features in earlier releases? If they go for the sexy new features, their product might do more, be easier to write, or be more efficient; but when the program hits the market, only 10 percent of existing PCs may be running the new release. Not all of the existing machines have the proper processor to be able to run the new system, and, of those, many of their users haven't seen the need to go to the expense and endure the hassle of upgrading their operating system. If it's viable to write the new application so that it requires only the old operating system (and therefore runs in compatibility mode under the new operating system), then it's tempting to do so. Even though the product is not as good as it might be, it can sell to 100 percent of the installed base of machines--10 times as many as it would if it required the new operating system.
And here you have the classic "catch-22" of software standards: If
users don't see a need, they won't use the new system. If they don't use the new system, applications will not be written explicitly for it; so the users never see a need. Without some way to prime the pump, it will be a long time before a comprehensive set of applications are available that use the new system's features.
OS/2 tackles this problem in several ways. The software bundled with
OS/2 runs in protected mode, and OS/2 attempts to include as much additional user function as possible to increase its value to a user who initially owns no protected mode applications. The most important user acceptance feature of OS/2, however, is called Family API. Family API is a special subset of the OS/2 protected mode API. Using special tools included in the OS/2 developer's kit, you can build applications that use only the Family API. The resultant .EXE file(s) run unchanged in OS/2 protect mode or on an 8086 running MS-DOS 2.x or 3.x. 1. Of course, they also run in the MS-DOS 3.x compatible screen group under OS/2; but except for convenience utilities, it's generally a waste to dedicate the one real mode screen group to running an appl- cation that could run in any of the many protect mode screen groups. 1 Thus, developers don't have to choose between writing applications that are OS/2 protect mode and writing applications that are MS-DOS compatible; they can use the Family API mechanism and do both. Your applications will run as protected mode applications under OS/2 and as MS-DOS applications under a true MS-DOS system.
Clearly, the Family API is a noteworthy feature. It offers some OS/2
functions, together with the dynamic link system interface, to programs that run under MS-DOS without a copy of OS/2 anywhere in sight. It does this by providing an OS/2 compatibility library that accepts the OS/2 system interface calls and implements them itself, calling the underlying MS-DOS system via INT 21 as necessary. This information should give you a big head start in figuring out which OS/2 functions are included in the Family API: Clearly all functions that have similar INT 21 functions--such as DosOpen, DosRead, and DosAllocSeg--are supported. Also present are functions, such as DosSubAlloc, that can be supported directly by the special Family API library. Features that are extremely difficult to support in a true MS-DOS environment, such as multiple threads and asynchronous I/O, are not present in the Family API.
Where does this library come from? And how does it get loaded by MS-
DOS to satisfy the OS/2 executable's dynlink requests? It's all done with mirrors, as the expression goes, and the "mirrors" must be built into the application's .EXE file because that file is all that's present when a Family API application is executed under MS-DOS. Figure 20-1 shows the layout of a Family API .EXE file.
Family API .EXE ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³°°°°°°°°°°°°°°°°°³ ³°°°MS-DOS 3.x°°°°³ ³°°°.EXE header°ÄÄÅÄÄ¿ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³°°°°°°°°°°°°°°°°°³ ³ ³°°°Family API°°°°³ ³ Shaded area is ³°°°°°loader°°°°°°³ ³ read by MS-DOS as ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ a real mode ³°°°°°°°°°°°°°°°°°³ ³ application ³°°°Family API°°°°³ ³ ³°°°°°library°°°°°³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´�ÄÙ ³ ³ ³ OS/2 ³ ³ .EXE header ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ OS/2 ³ ³ application ³ ³ segments ³ Ã Ä Ä Ä Ä Ä Ä Ä Ä ´ ³ ³ ³ ³ Ã Ä Ä Ä Ä Ä Ä Ä Ä ´ ³ ³ ³ ³ Ã Ä Ä Ä Ä Ä Ä Ä Ä ´ ³ ³ ³ ³ Ã Ä Ä Ä Ä Ä Ä Ä Ä ´ ³ ³ ³ ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ ³ ³ Dynlink ³ ³ names ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
Figure 20-1. Family API executable (.EXE) format.
OS/2 needed to define a new .EXE file because the existing MS-DOS .EXE
file format contained too little information for the OS/2 protect mode segmented environment. Because, as we've discussed, the 8086 memory architecture is--despite the terminology normally used--a linear memory architecture, the MS-DOS .EXE format described only a single hunk of memory that was to be loaded contiguously. OS/2 needs each segment described separately, with information on its status: read only, code or data, demand load or preload, and so on. Naturally, OS/2 also needs a .EXE format with special records to describe loadtime dynamic links. This new .EXE format was defined so that its initial bytes look exactly like those of the old MS-DOS .EXE file header. A special flag bit is set in this fake .EXE
header that is ignored by all releases of MS-DOS but that OS/2 recognizes
to mean "It's not true. I'm really an OS/2 .EXE file. Seek to this location to find the true, new-style .EXE header."
When an MS-DOS system is told to load this .EXE file, it sees and
believes the old .EXE file header. This header does not describe the application itself but a body of special code built into the .EXE file before the actual application's code: the Family API loader and library. In other words, to MS-DOS this .EXE file looks like a valid, executable program, and that program is the Family API loader and library. The Family API loader and library are loaded into memory, and execution begins. MS-DOS doesn't load in the body of the application itself because it wasn't described as part of the load image in the special MS-DOS .EXE file header. As soon as it starts to execute, the Family API loader begins reading in the application's segments, performs a loader's general relocation chores, and fixes up dynlink references to the proper entry points in the Family API library package. When the application is loaded, the Family API loader block moves the application to its final execution address, which overlays most of the Family API loader to reclaim that space, and execution begins.
All OS/2 .EXE files have this fake MS-DOS .EXE format header. In non-
Family API executables, the Family API loader and library are missing, and by default the header describes an impossibly big MS-DOS executable. Should the application be accidentally run under a non-OS/2 system or in the OS/2 compatibility screen group, MS-DOS will refuse to load the program. Optionally, the programmer can link in a small stub program that goes where the Family API loader would and that prints a more meaningful error message. As we said earlier, the old-style .EXE headers on the front of the file contain a flag bit to alert OS/2 to the presence of a new-style .EXE header further into the file. Because this header doesn't describe the Family API loader and library parts of the file, OS/2 ignores their presence when it loads a Family API application in protected mode; the application's dynlink references are fixed up to the normal dynlink libraries, and the Family API versions of those libraries are ignored.
There Ain't No Such Thing As A Free Lunch, and the same unfortunately
applies to the Family API mechanism. First, although the Family API allows dynlink calls to be used in an MS-DOS environment, this is not true dynlinking; it's quasi dynlinking. Obviously, runtime dynlinking is not supported, but even loadtime dynlinking is special because the dynlink target library is bound into the .EXE file. One of the advantages of dynlinks is that the target code is not part of the .EXE file and can therefore be changed and upgraded without changing the .EXE file. This is not true of the dynlink emulation library used by the Family API because it is built into the .EXE file. Fortunately, this disadvantage isn't normally a problem. Dynlink libraries are updated either to improve their implementation or to add new features. The Family API library can't be improved very much because its environment--MS-DOS--is limited and unchanging. If new Family API features were added, loading that new library with preexisting Family API .EXE files would make no sense; those programs wouldn't be calling the new features.
A more significant drawback is the size and speed hit that the Family
API introduces. Clearly, the size of a Family API .EXE file is extended by the size of the Family API loader and the support library. The tools used to build Family API executables include only those library routines used by the program, but even so the library and the loader add up to a nontrivial amount of memory--typically 10 KB to 14 KB in the .EXE file and perhaps 9KB (the loader is not included) in RAM. Finally, loading a Family API application under MS-DOS is slower than loading a true MS-DOS .EXE file. Comparing loadtime against the loadtime of MS-DOS is tough for any operating system because loading faster than MS-DOS is difficult. The .EXE file consists of a single lump of contiguous data that can be read into memory in a single disk read operation. A relocation table must also be read, but it's typically very small. It's hard for any system to be faster than this. Clearly, loading a Family API application is slower because the loader and library must be loaded, and then they must, a segment at a time, bring in the body of the application.
Although the Family API makes dual environment applications possible,
it can't totally hide from an application the difference between the MS-DOS 3.x and the OS/2 execution environment. For example, the Family API supports only the DosFindFirst function for a single search handle at a time. An application that wants to perform multiple directory searches simultaneously should use DosGetMachineMode to determine its environment and then use the unrestricted DosFindFirst function if running in protect mode or use the INT 21 functions if running in real mode. Likewise, an application that wants to manipulate printer data needs to contain version- specific code to hook INT 17 or to use device monitors, depending on the environment.
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Part III The Future
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
21 The Future
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
This chapter is difficult to write because describing the second version of OS/2 becomes uninteresting when that version is released. Furthermore, preannouncing products is bad practice; the trade-offs between schedule and customer demand can accelerate the inclusion of some features and postpone others, often late in the development cycle. If we talk explicitly about future features, developers may plan their work around the availability of those features and be left high and dry if said features are postponed. As a result, this chapter is necessarily vague about both the functional details and the release schedule, discussing future goals for features rather than the features themselves. Design your application, not so that it depends on the features described here, but so that it is compatible with them.
OS/2 version 1.0 is the first standard MS-DOS-compatible operating
system that unlocks the memory-addressing potential of the 80286--a "train" that will "pull" a great many APIs into the standard. On the other hand, foreseeable future releases cannot expect such penetration, so the designers of OS/2 version 1.0 focused primarily on including a full set of APIs. Major performance improvements were postponed for future releases simply because such improvements can be easily added later, whereas new APIs cannot. Most of the planned work is to take further advantage of existing interfaces, not to create new ones.
21.1 File System
Clearly, the heart of the office automation environment is data--lots of data--searching for it, reading it, and, less frequently, writing it. A machine's raw number-crunching capacity is relatively uninteresting in this milieu; the important issue is how fast the machine can get the data and manipulate it. Certainly, raw CPU power is advantageous; it allows the use of a relatively compute bound graphical user interface, for example. But I/O performance is becoming the limiting factor, especially in a multitasking environment. Where does this data come from? If it's from the keyboard, no problem; human typing speeds are glacially slow to a computer. If the data is from a non-mass-storage device, OS/2's direct device access facilities should provide sufficient throughput. That leaves the file system for local disks and the network for remote data. The file system is a natural for a future release upgrade. Its interface is generic so that applications written for the first release will work compatibly with new file systems in subsequent releases.
Talking about pending file system improvements is relatively easy
because the weaknesses in the current FAT file system are obvious.
þ Large Disk Support Clearly, a new file system will support arbitrarily large disks without introducing prohibitive allocation fragmentation. Allocation fragmentation refers to the minimum amount of disk space that a file system can allocate to a small file--the allocation unit. If the allocation unit is size N, the average file on the disk is expected to waste N/2 bytes of disk space because each file has a last allocation unit and on the average that unit will be only half filled. Actually, if the allocation unit is large, say more than 2 KB, the average fragmentation loss is greater than this estimate because a disproportionate number of files are small. The existing MS-DOS FAT file system can handle large disks, but at the cost of using very large allocation units. Depending on the number and the size of the files, a 100 MB disk might be as much as 50 percent wasted by this fragmentation. The new Microsoft file system will support a very small allocation unit--probably 512 bytes--to reduce this fragmentation, and this small allocation unit size will not adversely affect the performance of the file system.
þ File Protection A new file system also must support file access protection as part of the move toward a fully secure environment. File protection is typically a feature of multiuser operating systems; the MS-DOS FAT file system was designed for a single-user environment and contains no protection facilities. So why do we need them now? One reason is that a networked PC is physically a single-user machine, but logically it's a multiuser machine because multiple users can access the same files over the network. Also, as we shall see, it is sometimes useful to be able to protect your own files from access by yourself. Today, most network installations consist of server machines and client machines, with client machines able to access only files on server machines. MSNET and PCNET servers have a rudimentary form of file protection, but it needs improvement (see below). In the future, as machines become bigger and as products improve, files on client machines will also be available across the network. Clearly, a strong protection mechanism is needed to eliminate risks to a client machine's files. Finally, a file protection mechanism can be useful even on a single-user machine that is not accessible from a network. Today a variety of "Trojan" programs claim to be one thing but actually are another. In a nonnetworked environment, these programs are generally examples of mindless vandalism; typically, they purge the contents of the victim's hard disk. In a future office environment, they might edit payroll files or send sensitive data to someone waiting across the network. If you, as a user, can put sensitive files under password protection, they are safe even from yourself when you unwittingly run a Trojan program. That program doesn't know the password, and you certainly will decline to supply it. Self-protection also prevents someone from sitting down at your PC while you are at lunch or on vacation and wreaking havoc with your files. Protection mechanisms take two general forms: capability tokens and access lists. capability token gives access to an object if the requestor can supply the proper token, which itself can take a variety of forms. A per-file or per- directory password, such as is available on existing MSNET and PCNET products, is a kind of capability token: If you can present the password, you can access the file. Note that the password is associated with the item, not the user. The front door key to your house is a good example of a capability token, and it shows the features and limitations of the approach very well. Access to your house depends on owning the capability token--the key--and not on who you are. If you don't have your key, you can't get in, even if it's your own house. Anybody that does have the key can get in, no matter who they are. A key can sometimes be handy: You can loan it to someone for a day and then get it back. You can give it to the plumber's office, for example, and the office can give it to the plumber, who can in turn give it to an assistant. Capability tokens are flexible because you can pass them around without notifying the owner of the protected object. This benefit is also the major drawback of capability token systems: The capabilities can be passed around willy-nilly and, like a key, can be duplicated. Once you give your key out, you never know if you've gotten "them" back again. You can't enumerate who has access to your house, and if they refuse to return a key or if they've duplicated it, you can't withdraw access to your house. The only way to regain control over your house is to change the lock, which means that you have to reissue keys to everybody who should get access. In the world of houses and keys, this isn't much of a problem because keys aren't given out that much and it's easy to contact the few people who should have them. Changing the capability "lock" on a computer file is much more difficult, however, because it may mean updating a great many programs that are allowed access, and they all have to be updated simultaneously so that none is accidentally locked out. And, of course, the distribution of the new capability token must be carried out securely; you must ensure that no "bad guy" gets a chance to see and copy the token. And, finally, because a separate capability token, or password, needs to be kept for each file or directory, you can't possibly memorize them all. Instead, they get built into programs, stored in files, entered into batch scripts, and so on. All these passwords--the ones that are difficult to change because of the hassle of updating everybody--are being kept around in "plain text" in standardized locations, an invitation for pilferage. And just as the lock on your door won't tell you how many keys exist, a capability token system won't be able to warn you that someone has stolen a copy of the capability token. An alternative approach is the access list mechanism. It is equivalent to the guard at the movie studio gate who has a list of people on his clipboard. Each protected object is associated with a list of who is allowed what kind of access. It's easy to see who has access--simply look at the list. It's easy to give or take away access--simply edit the list. Maintaining the list is easy because no change is made unless someone is to be added or removed, and the list can contain group names, such as "anyone from the production department" or "all vice presidents." The fly in this particular ointment--and the reason that MSNET didn't use this approach--is in authenticating the identification of the person who wants access. In our movie studio, a picture badge is probably sufficient.
1. Note that the photo on the badge, together with a hard-to duplicate design, keeps the badge from being just another capability token. 1 With the computer, we use
a personal password. This password doesn't show that you have access to a particular file; it shows that you are who you claim to be. Also, because you have only one password, you can memorize it; it needn't be written on any list. Finally, you can change the password frequently because only one person--the one changing it, you--needs to be notified. Once the computer system knows that you're truly Hiram G. Hornswoggle, it grants or refuses access based on whether you're on an access list or belong to a group that is on an access list. MS-DOS can't use this approach because it's an unprotected system; whatever flag it sets in memory to say that you have properly authenticated yourself can be set by a cheater program. OS/2 is a protect mode operating system and is secure from such manipulation provided that no untrusted real mode applications are executed.
2. A future OS/2 release will take advantage of the 80386 processor's virtual real mode facility to make it safe to run untrusted real mode programs on an 80386. 2 A networking environment provides an extra
challenge because you can write a program--perhaps running on an MS-DOS machine to avoid protection mechanisms--that "sniffs" the network, examining every communication. A client machine can't send a plain-text password over the network to authenticate its user because a sniffer could see it. And it certainly can't send a message saying, "I'm satisfied that this is really Hiram." The client machine may be running bogus software that will lie and say that when it isn't true. In other words, a network authentication protocol must assume that "bad guys" can read all net transmissions and can generate any transmission they wish. As should be clear by now, a future OS/2 file system will support per-object permission lists. OS/2 will be enhanced to support users' identifying themselves by means of personal passwords. Future network software will support a secure network authentication protocol.
A new file system will do more than support access lists; it will also
support filenames longer than the FAT 8.3 convention, and it will support extended file attributes. The FAT file system supports a very limited set of attributes, each of which are binary flags--system, hidden, read-only, and so on. Extended attributes allow an arbitrary set of attributes, represented as text strings, to be associated with each file. Individual applications will be able to define specific attributes, set them on files, and later query their values. Extended attributes can be used, for example, to name the application that created the file. This would allow a user to click the mouse over the filename on a directory display and have the presentation manager bring up the proper application on that file.
Finally, although this file system wish list looks pretty good, how do
we know that we've covered all the bases? And will our new file system work well with CD-ROM 3. Special versions of compact discs that contain digital data instead of digitized music. When accessed via a modified CD player, they provide approximately 600 MB of read-only storage. 3 disks and WORM drives? The answers are "We don't" and "It doesn't," so a future OS/2 release will support installable file systems. An installable file system is similar to an installable device driver. When the system is initialized, not only device drivers but new file system management packages can be installed into OS/2. This will allow specialized file systems to handle specialized devices such as CD-ROMs and WORM, as well as providing an easy interface to media written on foreign file systems that are on non-MS-DOS or non-OS/2 systems.
21.2 The 80386
Throughout this book, the name 80386 keeps cropping up, almost as a kind of magical incantation. To a system designer, it is a magical device. It provides the protection facilities of the 80286, but it also provides three other key features.
21.2.1 Large Segments
The 80386 has a segmented architecture very much like that of the 80286,
but 80286 segments are limited to 64 KB. On the 80386, segments can be as
large as 4 million KB; segments can be so large that an entire program can
run in 2 segments (one code and one data) and essentially ignore the
segmentation facilities of the processor. This is called flat model.
Writing programs that deal with large structures is easier using flat
model, and because compilers have a hard time generating optimal segmented
code, converting 8086/80286 large model programs to 80386 flat model can
produce dramatic increases in execution speed.
Although a future release of OS/2 will certainly support large
segments and applications that use flat model internally, OS/2 will not necessarily provide a flat model API. The system API for 32-bit applications may continue to use segmented (that is, 48-bit) addresses.
21.2.2 Multiple Real Mode Boxes
The 80386 provides a mode of execution called virtual real mode. Processes
that run in this mode execute instructions exactly as they would in real
mode, but they are not truly in real mode; they are in a special 8086-
compatible protected mode. The additional memory management and protection
facilities that this mode provides allow a future version of OS/2 to
support more than one real mode box at the same time; multiple real mode
applications will be able to execute simultaneously and to continue
executing while in background mode. The virtual real mode eliminates the
need for mode switching; thus, the user can execute real mode applications
while running communications applications that have a high interrupt
rate.
21.2.3 Full Protection Capability
The virtual real mode capability, coupled with the 80386's ability to
allow/disallow I/O access on a port-by-port basis, provides the hardware
foundation for a future OS/2 that is fully secure. In a fully secure OS/2,
the modules loaded in during bootup--the operating system itself, device
drivers, installable file systems, and so on--must be trusted, but no other
program can accidentally or deliberately damage others, read protected
files, or otherwise access or damage restricted data. The only damage an
aberrant or malicious program will be able to do is to slow down the
machine by hogging the resources, such as consuming most of the RAM or CPU
time. This is relatively harmless; the user can simply kill the offending
program and not run it anymore.
21.2.4 Other Features
The 80386 contains other significant features besides speed, such as paged
virtual memory, that don't appear in an API or in a specific user benefit.
For this reason, we won't discuss them here other than to state the
obvious: An 80386 machine is generally considerably faster than an 80286-
based one.
So what do these 80386 features mean for the 80286? What role will it
play in the near and far future? Should a developer write for the 80286 or the 80386? First, OS/2 for the 80386 4. A product still under development at the time of this writing. All releases of OS/2 will run on the 80386, but the initial OS/2 release treats the 80386 as a "fast 80286." The only 80386 feature it uses is the faster mode-switching capability. 4 is the same operating system, essentially, as OS/2 for the 80286. The only new API in 80386 OS/2 will be the 32-bit wide one for 32-bit mode 80386-only binaries. The other features--such as virtual memory, I/O permission mapping, and multiple real mode boxes--are of value to the user but don't present any new APIs and therefore are compatible with all applications. Certainly, taking advantage of the 80386's new instruction order codes and 2^32-byte-length segments will require a new API; in fact, a program must be specially written and compiled for that environment. Only applications that can't function at all using the smaller 80286-compatible segments need to become 80386 dependent; 80286 protect mode programs will run without change and without any disadvantage on the 80386, taking advantage of its improved speed.
To summarize, there is only one operating system, OS/2. OS/2 supports
16-bit protected mode applications that run on all machines, and OS/2 will support 32-bit protected mode applications that will run only on 80386 machines. A developer should consider writing an application for the 32-bit model 5. When it is announced and documented. 5 only if the application performs so poorly in the 16-bit model that a 16-bit version is worthless. Otherwise, one should develop applications for the 16-bit model; such applications will run well on all existing OS/2-compatible machines and on all OS/2 releases. Later, when the 80386 and OS/2-386 have sufficient market penetration, you may want to release higher-performance upgrades to products that require the 80386.
21.3 The Next Ten Years
Microsoft believes that OS/2 will be a major influence in the personal computer industry for roughly the next ten years. The standardization of computing environments that mass market software brings about gives such standards abnormal longevity, while the incredible rate of hardware improvements brings on great pressure to change. As a result, we expect OS/2 to live long and prosper, where long is a relative term in an industry in which nothing can survive more than a decade. What might OS/2's successor system look like? If we could answer that today, a successor system would be unnecessary. Clearly, the increases in CPU performance will continue. Personal computers will undoubtedly follow in the footsteps of their supercomputer brethren and become used for more than calculation, but also for simulation, modeling, and expert systems, not only in the workplace but also in the home. The future will become clearer, over time, as this most wonderful of tools continues to change its users.
The development of OS/2 is, to date, the largest project that
Microsoft has ever taken on. From an initially very small group of Microsoft engineers, to a still small joint Microsoft-IBM design team, to finally a great many developers, builders, testers, and documenters from both Microsoft and IBM, the project became known affectionately as the "Black Hole."
As I write this, OS/2 is just weeks away from retail sale. It's been a
great pleasure for me and for the people who worked with me to see our black hole begin to give back to our customers the fruits of the labors that were poured into it.
Glossary
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
anonymous pipe a data storage buffer that OS/2 maintains in RAM; used for interprocess communications.
Applications Program Interface (API) the set of calls a program uses to obtain services from the operating system. The term API denotes a service interface, whatever its form.
background category a classification of processes that consists of those associated with a screen group not currently being displayed.
call gate a special LDT or GDT entry that describes a subroutine entry point rather than a memory segment. A far call to a call gate selector will cause a transfer to the entry point specified in the call gate. This is a feature of the 80286/80386 hardware and is normally used to provide a transition from a lower privilege state to a higher one.
captive thread a thread that has been created by a dynlink package and that stays within the dynlink code, never transferring back to the client process's code; also a thread that is used to call a service entry point and that will never return or that will return only if some specific event occurs.
child process a process created by another process (its parent process).
closed system hardware or software design that cannot be enhanced in the field by third- party suppliers.
command subtree a process and all its descendants.
context switch the act of switching the CPU from the execution of one thread to another, which may belong to the same process or to a different one.
cooked mode a mode established by programs for keyboard input. In cooked mode, OS/2 handles the line-editing characters such as the back space.
critical section a body of code that manipulates a data resource in a non-reentrant way.
daemon program a process that performs a utility function without interaction with the user. For example, the swapper process is a daemon program.
debuggee the program being debugged.
debugger a program that helps the programmer locate the source of problems found during runtime testing of a program.
device driver a program that transforms I/O requests made in a standard, device- independent fashion into the operations necessary to make a specific piece of hardware fulfill that request.
device monitor a mechanism that allows processes to track and/or modify device data streams.
disjoint LDT space the LDT selectors reserved for memory objects that are shared or that may be shared among processes.
dynamic link a method of postponing the resolution of external references until loadtime or runtime. A dynamic link allows the called subroutines to be packaged, dist ributed, and maintained independently of their callers. OS/2 extends the dynamic link (or dynlink) mechanism to serve as the primary method by which all system and nonsystem services are obtained.
dynlink see dynamic link.
dynlink library a file, in a special format, that contains the binary code for a group of dynamically linked subroutines.
dynlink routine see dynamic link.
dynlink subsystem a dynlink module that provides a set of services built around a resource.
encapsulation the principle of hiding the internal implementation of a program, function, or service so that its clients can tell what it does but not how it does it.
environment strings a series of user-definable and program-definable strings that are associated with each process. The initial values of environment strings are established by a process's parent.
exitlist a list of subroutines that OS/2 calls when a process has terminated. The exitlist is executed after process termination but before the process is actually destroyed.
Family Applications Program Interface (Family API) a standard execution environment under MS-DOS versions 2.x and 3.x and OS/2. The programmer can use the Family API to create an application that uses a subset of OS/2 functions (but a superset of MS-DOS 3.x functions) and that runs in a binary-compatible fashion under MS-DOS versions 2.x and 3.x and OS/2.
file handle a binary value that represents an open file; used in all file I/O calls.
file locking an OS/2 facility that allows one program to temporarily prevent other programs from reading and/or writing a particular file.
file system name space names that have the format of filenames. All such names will eventually represent disk "files"--data or special. Initially, some of these names are kept in internal OS/2 RAM tables and are not present on any disk volume.
forced event an event or action that is forced upon a thread or a process from an external source; for example, a Ctrl-C or a DosKill command.
foreground category a classification of processes that consists of those associated with the currently active screen group.
GDT see global descriptor table.
general priority category the OS/2 classification of threads that consists of three subcategories: background, foreground, and interactive.
general protection (GP) fault an error that occurs when a program accesses invalid memory locations or accesses valid locations in an invalid way (such as writing into read-only memory areas).
giveaway shared memory a shared memory mechanism in which a process that already has access to the segment can grant access to another process. Processes cannot obtain access for themselves; access must be granted by another process that already has access.
global data segment a data segment that is shared among all instances of a dynlink routine; in other words, a single segment that is accessible to all processes that call a particular dynlink routine.
global descriptor table (GDT) an element of the 80286/80386 memory management hardware. The GDT holds the descriptions of as many as 4095 global segments. A global segment is accessible to all processes.
global subsystem initialization a facility that allows a dynlink routine to specify that its initialize entry point should be called when the dynlink package is loaded on behalf of its first client.
grandparent process the parent process of a process that created a process.
handle an arbitrary integer value that OS/2 returns to a process so that the process can return it to OS/2 on subsequent calls; known to programmers as a magic cookie.
hard error an error that the system detects but which it cannot correct without user intervention.
hard error daemon a daemon process that services hard errors. The hard error daemon may be an independent process, or it may be a thread that belongs to the session manager or to the presentation manager.
huge segments a software technique that allows the creation and use of pseudo segments larger than 65 KB.
installable file system (IFS) a body of code that OS/2 loads at boot time and that provides the software to manage a file system on a storage device, including the ability to create and maintain directories, allocate disk space, and so on.
instance data segment a memory segment that holds data specific to each instance of the dynlink routine.
instance subsystem initialization a service that dynlink routines can request. A dynlink routine's initialize entry point is called each time a new client is linked to the routine.
interactive category a classification of processes that consists of the process currently interacting with the keyboard.
interactive program a program whose function is to obey commands from a user, such as an editor or a spreadsheet program. Programs such as compilers may literally interact by asking for filenames and compilation options, but they are considered noninteractive because their function is to compile a source program, not to provide answers to user-entered commands.
interprocess communications (IPC) the ability of processes and threads to transfer data and messages among themselves; used to offer services to and receive services from other programs.
interruptible block a special form of a blocking operation used inside the OS/2 kernel so that events such as process kill and Ctrl-C can interrupt a thread that is waiting, inside OS/2, for an event.
I/O privilege mechanism a facility that allows a process to ask a device driver for direct access to the device's I/O ports and any dedicated or mapped memory locations it has. The I/O privilege mechanism can be used directly by an application or indirectly by a dynlink package.
IPC see interprocess communications.
KBD an abbreviated name for the dynlink package that manages the keyboard device. All its entry points start with Kbd.
kernel the central part of OS/2. It resides permanently in fixed memory locations and executes in the privileged ring 0 state.
LDT see local descriptor table.
loadtime dynamic linking the act of connecting a client process to dynamic link libraries when the process is first loaded into memory.
local descriptor table (LDT) an element of the 80286/80386 memory management hardware. The LDT holds the descriptions of as many as 4095 local segments. Each process has its own LDT and cannot access the LDTs of other processes.
logical device a symbolic name for a device that the user can cause to be mapped to any physical (actual) device.
logical directory a symbolic name for a directory that the user can cause to be mapped to any actual drive and directory.
low priority category a classification of processes that consists of processes that get CPU time only when no other thread in the other categories needs it; this category is lower in priority than the general priority category.
magic cookie see handle.
memory manager the section of OS/2 that allocates both physical memory and virtual memory.
memory overcommit allocating more memory to the running program than physically exists.
memory suballocation the OS/2 facility that allocates pieces of memory from within an application's segment.
MOU an abbreviated name for the dynlink package that manages the mouse device. All its entry points start with Mou.
multitasking operating system an operating system in which two or more programs/threads can execute simultaneously.
named pipe a data storage buffer that OS/2 maintains in RAM; used for interprocess communication.
named shared memory a memory segment that can be accessed simultaneously by more than one process. Its name allows processes to request access to it.
open system hardware or software design that allows third-party additions and upgrades in the field.
object name buffer the area in which OS/2 returns a character string if the DosExecPgm function fails.
parallel multitasking the process whereby programs execute simultaneously.
parent process a process that creates another process, which is called the child process.
physical memory the RAM (Random Access Memory) physically present inside the machine.
PID (Process Identification Number) a unique code that OS/2 assigns to a process when the process is created. The PID may be any value except 0.
pipe see anonymous pipe; named pipe.
presentation manager the graphical user interface for OS/2.
priority (also known as CPU priority) the numeric value assigned to each runnable thread in the system. Threads with a higher priority are assigned the CPU in preference to those with a lower priority.
privilege mode a special execution mode (also known as ring 0) supported by the 80286/80386 hardware. Code executing in this mode can execute restricted instructions that are used to manipulate key system structures and tables. Only the OS/2 kernel and device drivers run in this mode.
process the executing instance of a binary file. In OS/2, the terms task and process are used interchangeably. A process is the unit of ownership, and processes own resources such as memory, open files, dynlink libraries, and semaphores.
protect mode the operating mode of the 80286 microprocessor that allows the operating system to use features that protect one application from another; also called protected mode.
queue an orderly list of elements waiting for processing.
RAM semaphore a kind of semaphore that is based in memory accessible to a thread; fast, but with limited functionality. See system semaphore.
raw mode a mode established by programs for keyboard input. In raw mode OS/2 passes to the caller each character typed immediately as it is typed. The caller is responsible for handling line-editing characters such as the back space.
real mode the operating mode of the 80286 microprocessor that runs programs designed for the 8086/8088 microprocessor.
record locking the mechanism that allows a process to lock a range of bytes within a file. While the lock is in effect, no other process can read or write those bytes.
ring 3 the privilege level that is used to run applications. Code executing at this level cannot modify critical system structures.
runtime dynamic linking the act of establishing a dynamic link after a process has begun execution. This is done by providing OS/2 with the module and entry point names; OS/2 returns the address of the routine.
scheduler the part of OS/2 that decides which thread to run and how long to run it before assigning the CPU to another thread; also, the part of OS/2 that determines the priority value for each thread.
screen group a group of one or more processes that share (generally in a serial fashion) a single logical screen and keyboard.
semaphore a software flag or signal used to coordinate the activities of two or more threads; commonly used to protect a critical section.
serial multitasking the process whereby multiple programs execute, but only one at a time.
session manager a system utility that manages screen group switching. The session manager is used only in the absence of the presentation manager; the presentation manager replaces the session manager.
shared memory a memory segment that can be accessed simultaneously by more than one process.
signaling using semaphores to notify threads that certain events or activities have taken place.
signals notification mechanisms implemented in software that operate in a fashion analogous to hardware interrupts.
software tools approach a design philosophy in which each program and application in a package is dedicated to performing a specific task and doing that task very well. See also encapsulation.
stack frame a portion of a thread's stack that contains a procedure's local variables and parameters.
static linking the combining of multiple compilands into a single executable file, thereby resolving undefined external references.
single-tasking a computer environment in which only one program runs at a time.
swapping the technique by which some code or data in memory is written to a disk file, thus allowing the memory it was using to be reused for another purpose.
system semaphore a semaphore that is implemented in OS/2's internal memory area; somewhat slower than RAM semaphores, but providing more features.
System File Table (SFT) an internal OS/2 table that contains an entry for every file currently open.
task see process.
thread the OS/2 mechanism that allows more than one path of execution through the same instance of an application program.
thread ID the handle of a particular thread within a process.
thread of execution the passage of the CPU through the instruction sequence.
time-critical priority a classification of processes that may be interactive or noninteractive, in the foreground or background screen group, which have a higher priority than any non-time-critical thread in the system.
time slice the amount of execution time that the scheduler will give a thread before reassigning the CPU to another thread of equal priority.
VIO an abbreviated name of the dynlink package that manages the display device. All its entry points start with Vio.
virtual memory the memory space allocated to and used by a process. At the time it is being referenced, the virtual memory must be present in physical memory, but otherwise it may be swapped to a disk file.
virtualization the general technique of hiding a complicated actual situation behind a simple, standard interface.
writethrough an option available when a file write operation is performed which specifies that the normal caching mechanism is to be sidestepped and the data is to be written through to the disk surface immediately.
3x box the OS/2 environment that emulates an 8086-based PC running MS-DOS versions 2.x or 3.x.
Index
Symbols
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
3x box
80286 processor
bugs in I/O access control in segmented architecture of size of segments
80386 processor
I/O access control in key features of
8080 processor 8086/8088 processor
memory limitations of real mode
A
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Abort, Retry, Ignore message (MS-DOS)
access lists
addresses
invalid subroutine
addressing, huge model address offsets address space, linear allocation. See also memory
file memory
anonymous pipes API
80386 processor Family memory management
Apple Macintosh application environment. See environment application mode applications
``combo command communicating between compatibility with MS-DOS designing for both OS/2 and MS-DOS device monitors and dual mode I/O-bound protecting real mode running MS-DOS time-critical
Applications Program Interface. See API
Family
architecture
80386 processor design concepts of OS/2 device driver I/O segmented
arguments
DosCWait DosExecPgm
ASCII text strings ASCIIZ strings asynchronous I/O asynchronous processing atomic operation
B
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
background
category I/O processing threads and applications
base segment BAT files, MS-DOS BIOS entry vector, hooking the blocking services block mode
device drivers driver algorithm
blocks, interruptible boot process boot time, installing device drivers at breakthroughs, technological buffer reusability buffers
flushing monitor
bugs
80286 processor program
byte-stream mechanism
C
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
call gate
call and return sequence, OS/2
call statements, writing for dynamic link routines
call timed out error
capability tokens
captive threads
categories, priority
Central Processing Unit. See CPU
character mode
device driver model for
child processes
controlling
CHKDSK circular references CLI instruction client processes closed system clusters CMD.EXE
I/O architecture and logical device and directory names
code, swapping CodeView command application COMMAND.COM command mode command processes command subtrees
controlling
command threads communication, interprocess compatibility
downward functional levels of MS-DOS name generation and VIO and presentation manager
compatibility box compatibility issues, OS/2 compatibility mode computers
mental work and multiple CPU networked (see also networks) Von Neumann
CONFIG.SYS file consistency, design context switching conventions, system cooked mode CP/M, compatibility with CP/M-80 CPU See also 8086/8088 processor, 80286 processor, 80386 processor
priority
crashes, system critical sections
protecting signals and
CS register Ctrl-Break and Ctrl-C customized environment
D ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ daemon, hard error daemon interfaces, dynamic links as daemon program data
global handling in dynamic linking instance instructions and swapped-out
data integrity data segments, executing from data streams, monitoring debuggee debugger
system
debugging demand loading demand load segment densities, disk design, concepts of OS/2 design goals DevHlp device data streams device drivers
architecture of block mode character mode code structure definition of dynamic link pseudo OS/2 communication and programming model for
device independence
definition of
device management device monitors device names devices
direct access of logical
device-specific code, encapsulating Digital Research direct device access directories
ISAM logical working
directory names directory tree hierarchy disjoint LDT space disk data synchronization disk I/O requests disk seek times disk space, allocation of DISKCOMP DISKCOPY disks
laser unlabeled
dispatcher display device, manipulating the display memory .DLL files DosAllocHuge DosAllocSeg DosAllocShrSeg DosBufReset DosCallNmPipe DosCalls DosClose DosConnectNmPipe DosCreateCSAlias DosCreateSem DosCreateThread DosCWait DosDevIOCtl DosDupHandle DosEnterCritSec DosErrClass DosError DosExecPgm DosExit DosExitCritSec DosExitList DosFindFirst DosFindNext DosFlagProcess DosFreeModule DosFreeSeg DosGetHugeShift DosGetMachineMode DosGetProcAddr DosGiveSeg DosHoldSignal DosKill DosKillProcess DosLoadModule DosMakeNmPipe DosMakePipe DosMonRead DosMonReq DosMonWrite DosMuxSemWait DosNewSize DosOpen
named pipes and
DosPeekNmPipe DosPtrace DosRead
named pipes and
DosReadQueue DosReallocHuge DosResumeThread DosScanEnv DosSearchPath DosSemClear DosSemRequest DosSemSet DosSemWait DosSetFHandState DosSetPrty DosSetSigHandler DosSetVerify DosSleep DosSubAlloc DosSubFrees DosSuspendThread DosTimerAsync DosTimerStart DosTimerStop DosTransactNmPipe DosWrite
named pipes and
DosWriteQueue downward compatibility DPATH drive-oriented operations drives, high- and low-density dual mode Dynamic Data Exchange (DDE) dynamic linking
Family API use of loadtime runtime
dynamic link libraries dynamic link routines, calling dynamic links
architectural role of circular references in details on implementing device drivers and interfaces to other processes naming side effects of using for pseudo device drivers
dynlink See also dynamic links
routines
E
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
encapsulation
device driver
entry ordinals entry point name environment
customized dual mode protected single-tasking single-tasking versus multitasking stable stand-alone virtualizing the
environment block environment strings EnvPointer EnvString error codes, compatibility mode errors
general protection fault hard localization of program timed out
.EXE files
executing Family API
EXEC function (MS-DOS) execute threads executing programs execution speed exitlist expandability extended file attributes extended partitioning extension, filename external name, dynamic link
F
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
Family API
drawbacks of
Family Applications Program Interface (Family API) far return instruction fault, memory not present fault errors, general protection faults, GP features
additional introducing new operating system
file
allocation, improving attributes, extended handles I/O limits, floppy disk locking protection seek position sharing system
File Allocation Table filenames, OS/2 and MS-DOS files
.DLL .EXE linking naming .OBJ
file system
future of hierarchical installable
file system name space
named pipes and
file utilization FIND utility program flags register flat model, 80386 processor flexibility, maximum flush operation, buffer forced events foreground
category processing threads and applications
fprintf fragmentation
disk internal
functions
addition of resident
future versions of OS/2
G
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
garbage collection
garbage handles
general failure error
general priority category
general protection fault (GP fault)
errors
GetDosVar giveaway shared memory global data global data segments Global Descriptor Table (GDT) global subsystem initialization glossary goals
design OS/2
GP faults grandparent processes graphical user interface
standard
graphics, VIO and graphics devices graphics drivers, device-independent
H
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
handles
closing closing with dynamic links duplicated and inherited garbage semaphore
hard error daemon hard errors
handling in real mode
hardware, nonstandard hardware devices. See devices hardware interrupts
device drivers and
hardware-specific interfaces Hayes modems heap algorithm heap objects Hercules Graphics Card hierarchical file system hooking the keyboard vector huge memory huge model addressing huge segments
I
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
IBM PC/AT
INCLUDE
Independent Software Vendors (ISVs)
industrial revolution, second
infosegs
inheritance
INIT
initialization
device driver global subsystem instance subsystem
initialization entry points, dynamic link input/output. See I/O installable file system (IFS) instance data
segment
instance subsystem initialization instructions
sequence of
insufficient memory INT 21 INT 2F multiplex function integrated applications integrity, data Intel. See also 8086/8088 processor, 80286 processor, 80386 processor
80286 processor 80386 processor 8080 processor 8086/8088 processor
interactive
category processes programs
interface, user interlocking internal fragmentation interprocess communication (IPC) interrupt handling code interruptible block interrupts
3x box emulation hardware signals and
interrupt service routine, device driver interrupt-time thread interrupt vectors
hooking
I/O
architecture asynchronous background disk requests efficiency file port access privilege mechanism requesting an operation signals and video
I/O-bound applications IOCTL call IP register IPC See also interprocess communication
combining forms of using with dynamic links
IRET instruction
K
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
KBD
kernel
interfacing with dynamic links mode
keyboard, processes using the keyboard mode, cooked and raw keyboard vector, hooking the
L
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
label names, volume
large disk support
LAR instruction
latency, rotational
Least Recently Used (LRU) scheme
levels, compatibility
LIB
libraries
dynamic link sharing dynamic link subroutine and runtime
linear address space linking
dynamic static
loadtime dynamic linking local area networks. See networks Local Descriptor Table (LDT) locality of reference localization of errors local variables locking, file. See file and record locking logical
device and directory name facility devices directories disks, partitioning
low priority category LSL instruction
M
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
magic cookie
mass marketing, software and
media volume management
memory
display huge insufficient layout of system limitations of in 8088 processor named shared physical protection shared shared giveaway swapping swapping segments in swapped-out utilization video virtual
memory management
API hardware
memory manager
definition of
memory not present fault memory objects, tracking memory overcommit memory segments, allocating memory suballocation memory unit mental work, mechanizing message mode Microsoft, vision of Microsoft Macro Assembler (MASM) model, architectural modes
incompatible keyboard
modular tools module name monitors, device MOU MS-DOS
compatibility with environment list expandability of filenames in history of memory allocation in running applications in OS/2 version 1.0 version 2.0 version 3.0 version 4.0 version 5.0 versions of
multitasking
data integrity and definition of device drivers and full MS-DOS version parallel serial
multiuser systems, thrashing and
N
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
named pipes
multiple instances of
named shared memory name generation, compatibility and names, dynamic link name set network piggybacking networks
access to compatibility on file protection on importance of security on
network virtual circuit NUMADD program NUMARITH application
O
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
.OBJ files
obj namebuf
object name buffer
objects
defining file system name space and
office automation
objective of operating system for
offset registers OPEN function open system operating systems
multitasking other
ordinals, entry orphaned semaphores overlays, code
P
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
packet, request
paged virtual memory
paperless office
parallel multitasking
parent processes
partitioning, extended
passwords, file
Paterson, Tim
PATH
pathnames
peripherals, high-bandwidth
permissions
physical memory
PID
piggybacking
pipes
anonymous named
pointer, seek preemptive scheduler presentation manager
choosing between VIO and DDE and
priority
categories of time-critical
priority scheduler
semaphore
privilege mechanism, I/O privilege mode privilege transition process termination signal handler processes
calling from OS/2 child client command daemon dynamic linking and foreground and background grandparent interactive interfacing with dynamic links parent problems with multiple threads and
processing
asynchronous foreground and background
processing unit process tree programming model, device drivers programs
debugger executing interactive running on OS/2
program termination signal PROMPT protected environment protection
file memory model side-effects
protect mode
huge model addressing in real mode versus
protocol, DDE pseudo interrupts Ptrace pure segment
Q, R
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
queues
RAM See also memory
available semaphores
Random Access Memory. See RAM random selector values RAS information raw mode real mode
80386 processor 8086 processor applications compatibility box device drivers in hard errors in huge model addressing in protect mode versus screen group swapping in
real time, tracking passage of record locking redirector code (MS-DOS) reference locality registers
offset segment signals and
Reliability, Availability, and Serviceability (RAS) religion, OS/2 remote procedure call request packet resident functions resources, manipulating return() ring 3 ring transition ROM BIOS root directory rotational latency runtime dynamic linking runtime library
S
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
scheduler
preemptive priority semaphore
SCP-DOS screen device, handle for screen groups
multiple using in 3x box
screen images, manipulating screen-save operation screen switch screen updates, requirements for Seattle Computer Products sector aligned calls sectors
blocks of
security, dynamic link seek pointer segment arithmetic segment fault segments
80286 architecture 80386 architecture data dynamic link executing from data global data huge internally shared nonswappable pure sharing stack status and information
segment selectors segment swapping semaphore handles semaphores
data integrity and file system name space and orphaned RAM recovering scheduling system
serial multitasking session manager SetSignalHandler shared memory
giveaway named
sharing segments side effects
controlling dynamic link protection
signal handler address signal handlers signaling signals
holding
SIGTERM signal single-tasking single-tasking environment, containing errors in software design, OS/2 concepts of software tools approach speed execution stable environment stack frames stacks, thread stack segments standard error. See STDERR standard file handles standard input. See STDIN standard output. See STDOUT standards, software static data segments, adding static links status and information, segment STDERR STDIN
compatibility with presentation manager
STDIN/STDOUT mechanism, I/O STDOUT
compatibility with presentation manager
strings, environment suballocation, memory subroutine library subroutines, dynamic link packages as subsystems
dynamic link special support
subtask model subtrees, command
controlling
swapped-out memory SWAPPER.DAT swapping
segment
system
conventions crashes diagnosis File Table (SFT) memory layout security, debuggers and semaphores
system, hanging the systems
closed and open file
T
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
task See also processes
task-time thread
technological breakthroughs
TEMP
terminate and stay resident mechanism
thrashing
thread 1
thread death
thread of execution
thread ID
threads
background captive collisions of dynamic linking and foreground and background processing with foreground and interactive from different processes I/O-bound interrupt-time low priority multiple organizing programs using performance characteristics of priority categories of problems with multiple switching the CPU among task-time working sets and
thread stacks throughput balancing time-critical priority category time intervals timer services time slice timing, thread tools, software TOPS-10 TREE tree, process tree-structured file system Trojan programs
U
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
UNIX
naming conventions in ptrace facility
upper- and lowercase upward compatibility user interface
graphical presentation manager VIO
utilization
file memory
V
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
variables, local
versions, future OS/2
video hardware, accessing
VIO,
choosing between presentation manager and dynamic link entry points graphics under subsystem user interface
VioGetBuf VioModeWait VioSavRedrawWait VioScrLock virtual circuit, network virtual display device virtualization
device
virtualizing the environment virtual memory
concepts of paged
virtual real mode (80386) volume, disk volume ID volume-oriented operations Von Neumann, John
W
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
windowing interface
working directories
working set
WORM drives
writethrough
WYSIWYG
X
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
XENIX
Z
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
zero-terminated strings