The person charging this material is re- sponsible for its return to the library from which it was withdrawn on or before the Latest Date stamped below. Theft, mutilation, and underlining of books are reasons for disciplinary action and may result in dismissal from the University. To renew call Telephone Center, 333-8400 UNIVERSITY OF ILLINOIS LIBRARY AT URBANA-CHAMPAIGN m 4 1593 L161— O-1096 UIUCDCS-R-80-1044 UILU-ENG 80 1741 THE EXECUTE STATEMENT: DESIGN, EXAMPLES, AND IMPLEMENTATION ALGORITHMS by December 1980 •5* • •ii Martin S. McKendry Roy H. Campbell I DEPARTMENT OF COMPUTER SCIENCE UNIVERSITY OF ILLINOIS AT URBANA-CHAMPAIGN URBANA, ILLINOIS UIUCDCS-R-80-1044 THE EXECUTE STATEMENT: DESIGN, EXAMPLES, AND IMPLEMENTATION ALGORITHMS by Martin S. McKendry Roy H. Campbell December 1980 DEPARTMENT OF COMPUTER SCIENCE UNIVERSITY OF ILLINOIS AT URB ANA- CHAMPAIGN URBANA, ILLINOIS 61801 Research supported in part by NASA Project NSG 1471. Digitized by the Internet Archive in 2013 http://archive.org/details/executestatement1044mcke ABSTRACT The Execute statement is a language mechanism that manages the interface between levels of processes in level structured operat- ing systems. It is a high level means of controlling instantia- tion and management of processes whose number and requirements are unknown when the operating system is compiled. The statement facilitates implementation of services such as memory management, interrupt management, and synchronization. Together with language capabilities, the Execute statement extends the benefits of high level languages to entire operating systems. This is done efficiently, and without additional hardware. The Execute statement makes explicit the interfaces between conceptual levels of abstraction, with inter-level protection enforced by static language constraints. Page 1 1 Introduction , Current high-level operating system languages only permit construction of systems in which all program code is compiled prior to loading of the operating system [Brinch-Hansen, 75] [Wirth, 77] [Campbell & Kolstad, 80]. Operating systems written in these languages use the 'kernel' approach — they consist of a kernel, coded in assembly language, that manages low-level func- tions such as processor scheduling and synchronization, and a set of higher level processes, compiled as a single unit in the operating system language, that handle functions such as device management and disk file organization. As a result, the languages are used mainly for dedicated applications, where a complete specification of all the programs that will ever run in the system is available when the system is built. In many systems, however, complete specifications are not known when the system is built. The most common example is an interactive or batch sys- tem, in which users prepare programs then execute them. The programs are prepared after the system is commissioned — consequently, they cannot be run by an operating system coded in a current high level operating system language unless the entire operating system is recompiled. User programs represent an extra 'level' of software above the basic operating system. This concept can be generalized [Dijkstra, 68]: many primi- tive functions, such as synchronization, memory management, or logical I/O, are at the system's lower levels, providing services to its higher levels. This paper presents a high level language construct, the Execute statement, that manages the interface between the levels of level structured systems. The Execute statement allows dynamic addition and deletion of processes, recognition of service requests between operating system levels, explicit Page 2 assignment of a processor to a process, and specification of information pass- ing, sharing, and protection between levels. Thus, arbitrary prograns can be loaded and executed, regardless of their time of compilation. The Execute statement retains static protection that arises from strong typing in high level languages such as Pascal [Jensen & Wirth, 74]. Dynamic protection is based on language capabilities [McKendry & Campbell, 80] which are used to control access between levels. The next chapter of this paper sets terminology for discussion of level structured systems. Chapter 3 characterizes the interface between sys- tem levels, then Chapter 4 presents the Execute statement with examples of its use. Chapter 5 discusses implementation algorithms. 2 Operating System Str uctures . Results of recent work in operating systems indicate that level struc- tures aid problem decomposition, verification, proving, and error containment [Dijkstra, 68] [Liskov, 72]. A level structured system is viewed as a series of abstra ct machines or level s of ab straction [Dijkstra, 681 [Horning & Ran- dell, 73]. At the most primitive level (level 0), the abstract machine corresponds to actual hardware. Level 1 implements services (such as schedul- ing and concurrency) not available in hardware. In general, level i (called a server) sup ports level i+1 and implements services for use by higher levels k: (k > i) (called users). Level i can request services implemented at lower levels j: (j < i), unless those services have been masked at intervening lev- els [Neumann et. al. , 77]. Page 3 hardware level 1 level 2 level 3 Figure 1: Process Structuring In this model, each level of abstraction is represented by a disjoint set of processes, said to implemen t the level (Figure 1). Each node in Figure 1 represents a process that has its own state vector and address space. Level i creates level i+1 by instantiating a set of processes to implement level i+1. A process that creates other processes is called the pare nt of the created processes. The created processes are called descendants or sub-processes of the parent. A parent is unable to distinguish between an immediate child and more distant descendants — transparency is such that they all appear to be a single process. By lifting the constraint that only one process at each level may have children, possible process structures extend to general trees. These are called execution trees. The Execute statement allows a parent to assign its processor to a child. Since the processor leaves the parent process, only one process at a time is running. Execution is said to be a_t this process. The path between it and the root of the tree is called the active br anch . While executing, a process can request any service offered on the active branch. The service will be provided by the nearest ancestor offering it. The Execute statement enables dynamic addition and deletion of processes from an execution tree, allocation of resources to those processes, Page 4 assignment of the processor to them, and recognition of requests for services implemented by ancestors. 3 Problem Spec ificatio n. 3 . 1 Requir ements . There are three requirements for managing the interface between lev- els: adding processes, assigning them a processor, and providing them with services. Provision of services further subdivides into two parts: recogniz- ing requests for service and, to satisfy those requests, enabling access between process address spaces. Consider the specific interface shown in Figure 2. in this exanple, a process at level n needs to load another process at level n+1, then run that process and provide the logical I/O service to it. level 1 root OS level n: logical I/O process I/O area access level n+1: if user program Figure 2: A Process Interface To fulfil Its function, the server process at level n must be able to Page 5 share the processor between user processes at level n-f-1. In turn, user processes must be able to relinquish the processor to request services. Pro- vision of services requires that a server process be able to access areas of a user's address space. Thus, at least while a service is being performed, some area must be common to both a server and a user. The essence of the interface between operating system levels is this common area and the processor sharing between server and user processes. 3.2 Prote ction. One of the features of high level languages is the protection that they provide [Morris, 73]. In managing the interface between levels of an operating system, it is desirable that this protection be retained. It would be violated if, for example, a process could access the entire address spaces of its descendants — because then it could alter code areas, raising the pos- sibility of access to illegal services, critical operating system code, and secure data areas. To protect against this, the Execute statement constrains access to be in accordance with data typing and language capabilities. Capabilities are used for access to the common areas shared between levels — access must con- form to the operations allowed on the item types of the capabilities. A secure loader manages these capabilities, controlling access by checking their types, establishing the safety of access, granting access when a service is requested, and revoking it again once a service has completed. Page 6 4 The Execut e Stat ement. An example Execute statement is: EXECUTE user UNTIL service name 1: service_procedure_ 1 (parameter^ list_ 1 ) ; service_name 2: service procedure_2 (parameter list_2), END (* execute *) The variable 'user' is a process state vector (psv) capability. Psv's contain the information needed to represent a process — here, the user is the child of the process containing the Execute statement (the server). Psv capa- bilities are managed [McKendry & Campbell, 80] in the root operating system (in terms of Figure 2), and so cannot be copied by general processes. ^or can they be used to alter the contents of process state vectors. TYPE psv = RECORD (* process status vector *) execute_link : ~psv; stack pointer : word; machine__regs : registerrecord ; page__map : page map_record; code_file : codefile record; END; - (* psv *) psv cap = CAPABILITY psv; END; (* no read, no write *) Ser vice names specify the services offered by a process. Together, these services are called a service list. Processes are constrained to at most one Execute statement, so the loader can bind process to services at load-time. Service procedure s are invoked when a service is requested. When the Execute statement is encountered, the processor encounter in;- it is immediately assigned to the user process. Execution leaves the server, staying with the user (at least, from the server's point of view) until the user (or one of its children) requests a service specified In the service list. Transparent to the server are both the specific process that makes the Page 7 request and requests to the server's ancestors. In a server, formal parameters to a service procedure are capabili- ties. Actual parameters are capability variables that are declared by the server program. The Execute mechanism sets these pointing into the user's address space when a user requests a service. They are revoked when the user is re-Executed, avoiding possible 'dangling pointer' problems — pointi-rs to non-existent or illegal data areas. Thus, the server has access to the memory space of the user for only as long as is needed to provide the service. To a user, service requests may appear to be hardware functions (e.^., page faults), language properties (e.g., logical I/O), or the result of expli- cit conventions introduced by a server (e.g., a message system). Services that result from explicit conventions are called utility services. A utility service is used in the same way as a procedure call and requires declaration by a user (like a 'forward' procedure declaration). Services that are language properties are called language services, and services that are hardware or operating system functions are called system services. For all services, the types of service procedure parameters are checked when a server is loaded and, in the case of utility services, when a user is loaded. Service parameters in a user must be declared as VAR parame- ters (call by reference), because they are pointers into the user's memory space. In a server, service parameters are declared as capabilities for the parameters declared by users. These capabilities are managed by the loader, which uses explicit type matching at load time to form a 'symbolic' capability manager. The loader uses information in the Execute statement itself and in the declaration by users of utility services to check that service parameters are properly declared. Page 8 Process loading is implemented as a utility service. Because the loader contains a manager for psv_caps, it is able to create and initialize process state vectors. To load a user process, a server declares a psv cap and requests the load service, specifying the secondary storage location where the binary code file for the user process is to be found: VAR user: psv cap; load (user, ); 4.1 Examp le J.: Logica l I/O. The first example of the Execute statement shows a server process for the language service depicted in Figure 2: logical I/O. In this example, ser- vice parameter types — 'io__area' and 'io cap' — are declared in the logical I/O server and checked by the loader. A file object, similar to the port object discussed in [McKendry & Campbell, 80], controls buffers and access to physical files. It is accessed via a file capability. The I/O process accesses files by exchanging buffers with physical I/O processes — it copies the contents of buffers to and from the I/O areas of descendants. Structurally, it is an infinite loop, repeatedly checking to see whether any Input has completed, then selecting a child to Execute. Children are selected on a round-robin basis. The example does not show their loading. Page 9 (* global declarations *) TYPE buffer = RECORD size: INTEGER; data: ARRAY [1. .79] OF CHAR; END; (* buffer *) buff_cap = CAPABILITY buffer; read, write END; (* managed 'symbolically' by the loader *) PROCESS logical_io; (* the logical I/O server process *) TYPE io_area = RECORD size: integer; data: ARRAY [1. .79] OF CHAR; END; io cap = CAPABILITY io area; read, write; imported END; child_rec = RECORD status : (ready, waiting); ps_vector: psv_cap ; io_vector: io_cap; io buffer: buffer_cap; END: (* child rec *) VAR child: ARRAY [l..n] OF childjrec; file : file cap; PROCEDURE initiateinput (VAR where: io cap); child [i]. status := waiting; file*. receive (child [i] .io_buf f er) ; (* This is a call on an object. The object gets the buffer * (from a physical I/O process) and sets 'io_buffer' pointing * to the buffer. The logical i/o process does not wait for completion. *) PROCEDURE initiate_output (VAR where: io_cap); file*.get_buffer (child [i] .io_buf fer ) ; WITH child [i] do FOR j := 1 to io_vector*.size do buf fer_cap* .data[ j] := io vector*. data [j]; (* copy data *) buf fer_cap*. size := io vector*. size ; file*. send (buffer cap); (* transmit buffer to file object *) Pago 10 PROCEDURE check_completions; (* check for input completions, and make data transfer *) FOR k := 1 to n do WITH child [k] DO IF status - waiting AND buffer_cap <> NIL THEM FOR j := 1 TO buffer_cap".size DO io vector". data [j] := buff er_cap" .data [j]; (* copy *) io_vec tor ".size := buffer cap". size; file" .release (buffer cap^ ; status := ready; (* main line of logical I/O server process *) DO FOREVER FOR i := 1 TO n DO IF child [i]. status = ready THEN EXECUTE child [i ] .ps_vector UNTIL io_in : initiate_input (child [i].io vector); io_out: initiate_output (child [i ] . io_ vector ) ; END; (* execute *) check__completions ; END. (* logical I/O process *) When the I/O server Executes child [i], execution passes to that child, staying there until the child (or one of its descendants) requests one of the services 'io in' or 'io out'. At that time, the capability child [i ] .io_vector is set to point to the io_area of the requesting process. The I/O server uses this capability to provide the service, then, when child [i] is again Executed, child [i ] .io_vector is revoked and execution returns to the requesting process. 4.2 Exa mple 2: Buffer Manager. The buffer manager is a capability manager [McKendry & Campbell, 80] that maintains a pool of buffers for use by its descendents. It illustrates utility services. The buffer manager allocates buffers to users when service requests are received. Since it issues user processes with buffer capabilities, the Page 11 buffer manager must access user space with a capability for a capability ( 'b cap cap'). The example assumes the same buffer declaration as is used in Example 1 : PROCESS buffer manager; TYPE b cap (* buffer request and release server *) = capability buffer; read, write END; (* managed by this process *) b_cap_cap = capability b_cap; read, write; imported END; (* managed by the 'system' *) VAR request^cap: b cap cap; child: psv cap; PROCEDURE buffjrequest (VAR buff: b_cap_cap); buff" := pointer to a buffer from the pool; PROCEDURE buffjrelease (VAR buff: b_cap_cap); release buff" to the pool; buff* := NIL; (* revoke access *) EXECUTE child UNTIL breq: buff request (request cap); b rel: buff_release (request cap); END; (* execute *) END. (* buffer manager *) A descendant of the buffer manager declares a buffer capability, then declares the services and uses them: VAR b_var: b cap; (* needed to use services *) PROCEDURE buffjrequest (VAR b: b_cap); SERVICE [bjreq]; (* declare services *) PROCEDURE buff release (VAR b: b cap); SERVICE [b rel]; buff_request (b_var); buff release (b var); (* use services *) Page 12 5 Imple mentatio n Algor i th ins. To implement the Execute statement, cooperation between the loader and the compiler is necessary. The loader, providing symbolic capability manage- ment, checks that parameters for service procedures are capabilities for the variables passed by users. The compiler assumes that this checking is correct, and that the loader declares psv's and psv capabilities properly. Since the type-checking algorithms are similar to those used by compilers, they are not discussed further. This chapter discusses algorithms for the Execute statement — particularly language and utility services — in some detail. The Execute statement lias two main purposes: First, it controls pro- cess context switching — as the result of an explicit request when a server Executes a user, and as the result of an implicit request when a user requests a service. Second, it performs the 'bookkeeping' operations needed to set a server's service capabilities pointing to the data areas in the user's space when a service is requested and revoke those capabilities when the service is complete. The explanation in this chapter assumes the reader to be familiar with Pascal — in particular, call-by-value, call-by-reference (by VAR in Pascal), and their underlying implementation representations. When a request is made, pointers must be coerced to capabilities. This is assumed to be automatic. Variables that were declared as capabilities are still called capabilities at run-time, although they may be implemented as pointers. Page 13 5 . 1 PjL ta Str uctures . The execution configuration depicted in Figure 3 (the solid lines are where a parent has actually Executed a child) is shown as a 'snapshot' of data structures in Figure 4. Here, the 'current process' pointer indicates that execution is at process C (stacks grow downward). process A process B process C Figure 3: Execution Configuration The execute stack records the current state of 'Execution'. It con- tains a pointer to each process on the active branch. If a process is not on this branch, either it has requested a service, one of its descendants has requested a service of one of its ancestors, or it has never been Executed. For a process not on the active branch, the 'execute link' of its psv record shows whether it has Executed a child, indicating the process to which execu- tion will actually return when this process is re-Executed. 5 . 2 Servic e Reques t Implementatio n . Whereas a service request can be coded in the source language like a procedure call, its actual machine code representation is as a pair: (level difference, service number). The level difference is the number of levels between the user and server, and the service number is a 'case index' into the service list in the server's Execute statement. The loader sets these values at load time — it retains a map of all the services offered through the Page 14 execute stack current process A's psv A's stack • execute link 11 stack pointer A" , B's psv B's stack ) f execute link C's psv C's stack execute link stack pointer Figure 4: Basic Execute Data Structures Page 15 execution tree. When a service request is encountered, several changes occur in the snapshot of Figure 4. Initially, machine code for a service call is the same as for a procedure call — procedure parameters are built on the user's stack. The procedure call is not made, however. Instead, the service number is pushed onto the stack (shown in Figure 5), and the processor switches contexts from the user to the server process. To make the context switch, the execute stack is popped according to the level difference, the processor transfers to the process now at the top of the execute stack (the server), and the server's execute link is set to NIL. Next, the service number, still on the user's stack, is popped and used to index the service list in the server's Execute statement. Code is then executed to build the actual parameters for the ser- vice procedure. Data structures at this point are shown in Figure 6. Here, the user's stack contains pointers to the service procedure's actual parameters — because they were declared as VARs. The server's stack contains pointers to the actual parameters in the server's space — because, according to the Exe- cute statement they are capabilities, and according to the use of capabilities as parameters [McKendry & Campbell, 80], they too were declared as VARs. Before the server procedure is called, two bookkeeping operations are needed. First, the parameter pointers on the user's stack are popped and copied into the server's actual parameters (establishing the link marked 4 in Figure 7). Then, the pointers on the server's stack are pushed onto the user's stack (establishing the link marked 3) — these pointers on the user's stack are visible to neither the user nor the server. They are needed later to automatically revoke the server's capability parameters. Page 16 server : execute stack current process user : -* t A's psv execute link A's stack stack pointer B's psv execute link C's psv C's stack execute link C's address space I/O area * stack pointer service m parameters service # Figure 5: User's Parameters Page 17 server : execute stack current process user : server s actual parameters A's psv service capability in A's address space A's stack execute link stack pointer server t parameters B's psv execute link C's psv C's stack execute link C's address space I/O area stack pointer service parameters Figure 6: User's and Server's Parameters Page 11 server : execute stack current process user : server s actual parameters: A's psv service capability in A's address space j\ a A's stack execute link stack pointer server ^ parameters 3's psv execute link C's psv C's stack execute link C's address space I/O area * stack pointer revocation pointer Figure 7: Service Capability and Revocation Pointer Page 19 With the situation now as shown in Figure 7, the request sequence is complete and the service procedure is ahout to be invoked. The execute link from the immediate parent (B) to the user (C) is still in place; it is used when the server re-Executes the user. At that time, execution returns to C, since it was C that requested the service. This supports the transparency described earlier — a server cannot distinguish the particular descendant that makes a request. Note that in the preceeding diagrams, C's address space will actually contain C's stack and the I/O area may be on the stack or on the heap. Simi- larly, A's service capability is probably on A's stack. The service call algorithm is given below. In this algorithm, 'current process' is an indirect pointer to a psv. It is added to or sub- tracted from to move it through the execute stack. Request (level^dif f , service_no) current_process := current_process + level_dif f erence; (pop execute stack) switch contexts from user to server stack current process"*. execute link := NIL; use user. top_of_s tack to index server's service list pop user stack execute server code for parameter construction (constructs addresses of actual parameters on stack) For each service parameter (copy actual parameters) copy parameter on user stack to parameter" on server stack pop user stack For each service parameter (copy revocation pointers) push parameter on server stack onto user stack ...call server procedure... 5.3 Execute Implemen tation. The code that implements the Execute sequence 'undoes' the service request, then leaves the processor executing the user's code. This requires that execute links be used to reconstruct the execute stack, and that the Page 20 service procedure's actual parameters be revoked: Execute (psv_cap) current process* ~.execute_link := psv_cap; while psv cap*. execute link <> NIL do (rebuild execute stack) push psv cap on execute stack psv cap := psv_cap~.execute_link; push psv cap onto execute stack; (this process gets the processor) switch contexts from server to user stack j for each service parameter parameter* := NIL; (revoke parameter capabilities) pop user stack; ...continue executing user code... 6 Conclusion. The Execute statement is a high level means of controlling instantia- tion and management of processes whose number and requirements are unknown at compile time. Together with capabilities, the Execute statement extends the benefits of high level languages and strong typing across operating systems and user programs, thus simplifying proving and verification. This is done efficiently and without additional hardware. The Execute statement explicitly specifies the interface between conceptual levels of abstraction. Static language constraints provide inter-level protection. In this paper, the operating system structures supported by the Exe- cute statement have been discussed. Language constructs and examples of their use have been shown, and implementation algorithms have been described. In [McKendry, 31], a more detailed description of the statement is to be found. Future research includes the issues of service parameter type import *nd export, the question of loader-correctness, implementation of system services such as interrupts and time allocation, and multiprocessor implementations of the Execute statement. Page 21 A pilot implementation of capabilities and the Execute statement based on Path Pascal [Campbell & Kolstad, 80] is currently under construction for the Prime 650. 7 Ack nowledgement . This work was undertaken as part of M. S. McKendry's Ph.D. Thesis research. Page 22 8 References. [Brinch Hansen, 75] 3rinch Hansen, P., "The Programming Language Concurrent Pascal," IEEE Trans, on SE., Vol. SE-1, pp. 199-207, June, 1975. [Campbell & Kolstad, 80] Campbell, R. H. and R. R. Kolstad, "An Overview of Path Pascal's Design," Sigplan Notices, Vol. 15, No. 9, pp. 13-14, Sep- tember, 1980. [Dijkstra, 68] Dijkstra, E. W. , "The Structure of the THE Multiprogramming System," CACM, Vol. 11, Mo. 5, pp. 341-346, May, 1968. [Horning & Randell, 73] Horning, J. J. and B. Randell, "Process Structuring," Computing Surveys, Vol. 5, No. 1, pp. 5-30, March, 1973. [Jensen & Wirth, 74] Jensen, K. , and N. Wirth, Pascal User Manual and_ Report, Springer-Verlag, 1974. [Liskov, 72] Liskov, B. H. , "The Design of the Venus Operating System," CACM, Vol. 15, No. 3, pp. 144-149, March, 1972. [McKendry & Campbell, 80] McKendry, M. S., and R. H. Campbell, "Capabilities for High Level Languages," University of Illinois, Department of Computer Science, TR UIUCDCS-R-80-1039, December, 1980. [McKendry, 81] McKendry, M. S., "Mechanisms for High Level Language Operating Systems," Ph.D. Thesis, University of Illinois at Urbana-Champaign. In preparation. [Morris, 73] Morris, J. H. , "Protection in Programming Languages," CACM, Vol. 16, No. 1, pp. 15-21, January, 1973. [Neumann, et al. , 77] Neumann, P. G. , et al. , "A Provably Secure Operating System: the System, its Applications, and Proofs," Stanford Research Institute Project 4332, Final Report, 1977. [Wirth, 77] Wirth, N. , "Modula, A Language for Modular Multiprogramming," Software Practise & Experience, Vol. 7, pp. 3-35, January, 1977. BIBLIOGRAPHIC DATA SHEET 1. Report No. UIUCDCS-R-80-1044 4. Title and Subtitle THE EXECUTE STATEMENT: DESIGN, EXAMPLES, AND IMPLEMENTATION ALGORITHMS 3. Recipient's Accession No. 5. Report Date December 1980 6. 7. Author(s) Martin S. McKendry and Roy H. Campbell 8. Performing Organization Rept. No. R-80-1044 9. Performing Organization Name and Address Department of Computer Science University of Illinois Urbana, IL 61801 10. Project/Task/Work Unit No. 11. Contract/Grant No. NASA NSG 1471 12. Sponsoring Organization Name and Address NASA Langley Research Center Hampton, VA 23665 13. Type of Report & Period Covered technical 14. 15. Supplementary Notes 16. Abstracts The Execute statement is a language mechanism that manages the interface between levels of processes in level structured operating systems. It is a high level means of controlling instantiation and management of processes whose number and requirements are unknown when the operating system is compiled. The statement facilitates implementation of services such as memory management, interrupt management, and synchronization. Together with language capabilities, the Execute statement extends the benefits of high level languages to entire operating systems, This is done efficiently, and without additional hardware. The Execute statement makes explicit the interfaces between conceptual levels of abstraction, with inter-level protection enforced by static language constraints. 17. Key Words and Document Analysis. 17a. Descriptors operating system language process management protection language capabilities 17b. Identifiers/Open-Ended Terms 17c. COSATI Field/Group 18. Availability Statement unlimited 19. Security Class (This Report) UNCLASSIFIED 20. Security Class (This Page UNCLASSIFIED 21. No. of Pages 26 22. Price FORM NTIS-35 (10-70) USCOMM-DC 40329-P7I