This document was made by OCR from a scan of the technical report. It has not been edited or proofread and is not meant for human consumption, but only for search engines.
On the Transfer of Control between Contexts
B. W. Lampson, J. G. Mitchell and E. H. Satterthwalto Xerox Research Center
3180 Porter Drive
Palo Alto, CA 94304, USA
We describe a single primitive mechanism for transferring control from one module to another, and show how this mechanism, together with suitable facilities for record handling and storage allocation, can be used to construct a variety of higher-level transfer disciplines. Procedure and function calls, coroutine linkages, non-local gotos, and signals can all be specified and Implemented in a compatible way. The conventions for storage allocation and name binding associated with control transfers are also under the programmer's control. Two new control disciplines are defined: a generalization of coroutines, and a facility for handling errors and unusual conditions which arise during program execution. Examples are drawn from the Modular Programming Language; in which all of the facilities described are being Implemented.
Transfers of control in programs can be divided into two classes. A local transfer stays within the same piece of program text, and does not change the naming environment. A goto which does not involve an exit
from a block has traditionally been the primitive local transfer operation, and other operations have been described by translating them into sequences of (possibly conditional) gotos and assignments. Recently there has' been a lot of effort to find a good set of higher-level local transfer operations, motivated by an awareness that the undisciplined use of the goto results in badly structured programs. The choice of if-then.-else, - for-while and case constructs, sometimes augmented by
loop and exit operations, has met with'wide acceptance. This is
not because of theoretical proofs- that they are sufficient to express any computation, but because many years of experimentation with the possibilities of the goto showed that it is most of fectively used in a few stylized
ways, from which these constructs were abstracted. In fact, the arguments for keeping goto available in programming languages are based on the observation that there are times when its use cannot readily be cast In one of these molds.
A global transfer, on the other hand, does more than alter the .sequential
flow of control. It usually invokes a new piece of program text, and it always affects the allocation of storage and the binding of names. This paper is about global transfers. In fact, it is an attempt to find a suitable primitive (which we will call transfer) and to describe higher-level global transfers or‑
control disciplines by translating them Into sequences of
transfers, assignments and other data-handling operations.
There are two reasons why this seems worthwhile. First, it is difficult to describe clearly how the control disciplines in existing languages work without resorting to the construction of a formal interpreter [Fisher]. Non-interpretive descriptions either contain large quantities of ambiguous English prose, or they involve operations (such as the Algol 60 copy rule for procedure calls) which may be precise but are certainly not clear. if a language can be used to describe Itself, by defining certain operations in terrns. of sequences of simpler operations In the language, the amount of conceptual baggage required to understand It can be reduced considerably.
Second, It is our opinion that much remains to be learned about the proper choice of global transfer operations. Until recently very few languages other than assemblers gave the programmer any choice of control operations. Simula [Hoare and Dahl] and the new crop of languages for artificial Intelligence have changed this situation to some extent, but it will be a long time before the possibilities for global transfers have been thoroughly explored. If programmers have the opportunity to create their own control disciplines they will certainly make a.lot of mistakes, but from this experimentation we can hope to learn what works well and what does not.
From this discussion the flavor of the paper should be clear. We will define a transfer primitive and then exploit the local expressive power of the language to describe some control disciplines which we happen to like. This
is not a trivial Job, since a 'good discipline must satisfy a number of constraints:
. programming generality [Dennis] - independently constructed modules can
work together without having to know each other's internal structure. In
particular, each module can choose its names and Its storage allocation
strategies independently of the others;
. compatibility - modules using different disciplines can still communicate, or the advantages of diversity will be completely overwhelmed by the drawbacks of Babel;
. robustness - it Is easy to get things set up, and restrictions and caveats are conspiclous by their absence;
. reconfigurability - connections betweeen modules can easily.be broken and reestablished, so that debugging facilities can be spliced In and the behavior of a module can be changed by attaching "adaptors" to its external connections.
This approach should not be misinterpreted. The fact that a construct can be explicated in terms of simpler ones does not mean that the programmer must have this decomposition In mind whenever he uses it. On the contrary, if he uses it as part of his working vocabulary he will normally think of it as an atomic concept. The' explication Is helpful in making the definition precise, and in answering questions about what will happen in unfamiliar situations; It should be thought of In that light.
Questions about the binding of names are, in our view, orthogonal to the study
Un Lne iranSrer 01- %AMU-LH UtilWUU411 l,UIILt AL
of transfers, and are not considered in this paper. in particular, rules for binding non-local variables and for linking separately compiled modules are not discussed.
2. The _host language
The facilities- described in this paper are implemented in a general purpose system programming language called the Modular Programming Language (NIPL), which is a component of a system for modular programming. MPL borrows much of its local character from Pascal [Wirth] and EL/1 [Wegbreit]. In particular, it is a typed language in which new types can be built out of old ones using record, array and pointer declarations. There Is
also a way to prevent components of a record from being accessed except by a group of procedures which are declared with it. Such a record is called closed, and the procedures are called its handles. Finally, it is
possible to declare a record type s as a direct extension of another record type r, by adding additional components and handles. Extension is
the transitive closure of direct extension, and the set of all extensions of r is the class of r. Closed records and classes were inspired by the
class mechanism of Simula [Hoare and Dahl]; that language, howeveir, encourages the restriction of access to handles, but does not enforce it... The control transfer operations use closed records and classes to construct and manipulate their data structures.
· A construct patterned of ter Pascal'S with is used heavily as syntactic sugar by the control disciplines. Any block can be prefixed by one or more clauses of the form: .
where p is a pointer to a record of type r. Within the block the names of the components of r can then be used without qualification. If c is such a component, then within the block c is short for p.c.
3. Contexts and frames
The entities between which global transfers occur we call contexts.
The definition which follows reflects our views about the properties which such entities ought to have. Although nearly all of the transfer operations of existing programming languages can be described within this framework, we are not making any claims for its universal applicability: Since we make use of its properties in constructing control disciplines, our constructions will not work in systems for which contexts-cannot be defined.
Within this framework we may restate the subject matter of thls.paper:
. the nature of contexts, and their creation and destruction;
. minimal primitives which are sufficient to describe any transfer of control between contexts;
. definition of good higher-level transfer disciplines;
. description of these disciplines in terms of the primitives.
A context consists of:
. a pointer to the text of a program, which we shall abstract as an array of objects called Instructions whose Internal structure and
properties are left undefined. We assume that the program is not modified during execution;
. a binding rule for names, which we shall abstract as a function mapping names into pointers;
some local storage, including an integer index into the program text called the piogram counter.
3.1 Representation of contexts
A context Is
represented by a frame, which is a record whose
components contain the information needed to define the context. More
precisely, a context base Is a closed record type
containing a program text pointer, a binding rule and a program counter; these
components are accessible only to the control transfer primitives, which are
the handles. A
context can almost be described as a member of the class of context bases,. Unfortunately, this description cannot quite be taken literally, because we need the transfer primitive to describe how procedures are called, and hence-this primitive cannot be defined as a procedure. The other operations on contexts, however, can properly be defined in this way.
It is interesting to compare this situation with what happens if we try to define as a class some other type which is normally taken as primitive. A 32 bit integer, for example, could be defined as a closed record containing a Boolean array with 32 elements, and we could (rather clumsily) write procedures to implement the standard arithmetic operations, without becoming involved in any circularity. As with any other closed record, these procedures must cooperate in maintaining the consistency of the representation: If add assumes that the integer is represented in 2's complement, then multiply had better not assume sign-magnitude representation. All the questions of consistency are
out in the open, however, since everything having to do with the closed record is expressed in the declaration of its handles.
For contexts the
consistency requirement has a new aspect. The procedure which creates a
context, f or instance, must build a data structure which Is consistent not
only with the other handles of the class context, but also with the transfer
primitive. This Is actually a rather strong requirement,
because the transfer primitive causes instructions to be executed from the .program text. When this text was constructed, some assumptions were made (by the compiler) about the environment which would be present during execution of the program. The creation procedure is responsible for setting up the environment so that these assumptions are satisfied. If they are not, chaos will result, since the foundation will be undermined on which the entire
the program is based. If
care is taken to satisfy the
assumptions of the transfer primitive, then, we may think of a context as a class, and the remaining discussion will proceed on that basis.
We will call a pointer to a context an lnport. The name is intended to suggest the main purpose of this type, which is to be an operand for the transfer primitive. A pointer to an inport we will call an outport,
with the idea that most of the control disciplines we are interested in need this extra level of indirection so that transfers into a context can be trapped when necessary.
3.2 Creation of contexts
We now proceed to explore In detail how contexts are created. Our discussion concentrates on the logical structure of the creation process, ignoring the details of the implementation, in which much of the work Is done at compile or link time, and many of the operations described are coalesced for ef ficienCy. We say a good deal about the treatment of the types of the various objects Involved in order to make it clear that everything we are doing is consistent with the constraints of a fully typed language.
Since a context is an instance of a class, there must be a single create primitive which takes some arguments and creates a context. The arguments to create are:
. a record called a program which contains
- an, array of program text,
- the type of the frame- record which the program expects;
. a frame record (which is not yet a context).
We will consider later where- these records come from.
With these inputs, create's Job is easy. It checks that the frame record actually presented is of the type specified by the program (using the facilities of the type system to find out what the frame's type Is). Then it inserts a pointer to the program text array In the frame, initializes the program counter to zero, and returns the frame record as a context.
A program must be derived eventually from a source file which has been compiled by the MPL compiler. The output of the compiler is an object file which contains the same information as the program record. There Is an operation called load which converts a file name Into a program record,
after checking as best it can that the file Is in fact a legitimate object file (the-type checking machinery cannot be expected to handle this situation perfectly, since It has no control over the-way In which Information Is stored In the file system). The only hard work filed has to do Is to find space for the program text array in the addressable memory of the machine on which the program Is running. How this is done depends on the details of the machine and is not relevant to this paper.
Constructing a' frame is more difficult. Again, we can break this operation down into two parts:
. obtaining storage for the frame;
. initializing this storage properly and returning it as the frame.
Any record type in MPL has a creation operation associated with it which is defined when the type is declared. This operation accepts a block of storage and perhaps some other parameters,- and produces a record of the proper type. it is the only way to make such a record. The create primitive for . contexts discussed above Is an example of such an operation.
An ordinary frame creator In MPL is a special case of this general mechanism, with two distinctive characteristics. First, it usually has a standard program text, for two reasons:
. frames tend to have a rather stylized form, so that the differences between them can be efficiently encoded Into a data structure called a, frame descriptor which can then be accepted as a parameter and Interpreted by the standard creator program.
. there is another circularity problem - someone has to create the frame
creator. A standard creator can Itself be created in a standard way which can be part of the initial system.
The frame descriptor is usually stored In the object file along with the program text. Use of this scheme Is not compulsory, however. All of the facilities for creating records can be used to create frames.
unusual thing about a frame creator is that it has to provide the binding
function for the context. Recall that this function maps the names used in the
program text into pointers to the objects which are bound to
names by this incarnation of the program. This is done by a generalization of . the display which Is often used to implement the binding rules of Algol. The names used by the program have the form rp.v, where rp is a pointer to the frame of some other context and v is a variable local to'that context. We call the set of frames r, s, t, ... referenced by a context in this way the
neighborhood of the
context; it Is defined by a collection rp, sp, tp, of
pointers to the frames. The context is automatically prefixed with a clause of the f orrn
USING rp, sp, tp, .
so that the program can refer to the variables without qualification, Just as it refers to non-local variables in Algol. One element of the neighborhood Is always the argument record. -
To define the binding function, then, the creator has to define the neighborhood, i.e: set the pointers rp, sp, tp, ... to the proper frames. The type of each frame is of course fixed by the declarations in the source program, but there may several frames of the-same type to choose from.' As far as the typing and control mechanisms of the language are concerned, the creator Is free to choose any of them. One familiar possibility is the Algol rule, which takes the unique textually enclosing occurrence [Wang and Dahl]. In a more complex control environment, however, it may be difficult to define such a unique occurrence, or the programmer may want more flexibility In defining the environment. In any event, the choice of binding rule Is entirely under the programmer's control and Is not relevant to the subject of this paper.
3.3 Storage allocation
remains the question of storage allocation for a frame. This must be
done with some care, since creating a context Is a rather common operation which Is required, for example, by every call of an Algol-like procedure. The standard solution Is to allocate frames from a stack; this works well in a control discipline which ensures that contexts are created and destroyed in last-in first-out fashion. Such a restriction would not be Incompatible with the basic control primitives, but it would-severely constrain the set of compatible higher-level disciplines which would be designed.
In order to avoid this problem, we have made a convention that frames are allocated on a heap; they can then be created and destroyed in any order. A standard non-compacting, coalescing free storage allocator [Knuth) is used, supplemented for speed by a vector of lists of available blocks for all the commonly used sizes. To keep the vector short, frame sizes are quantized by the complier so that they differ by about 10%. Thus the possible sizes might be 10, 12, 14, 16, 18, 20, 22, ..., 200, 220, etc. With this scheme only 40 sizes are required to span the range from 10 to 320, which is a much greater variation than Is likely to be encountered In practice. Furthermore, it Is
un the iransrer or control tretWeen Contexts
always possible to allocate a larger block than the one requested In order to reduce external fragmentation.
4. The transfer primitive
As-we have already seen, in order to handle transfers which change the environment we need at least one language feature orthogonal to that subset of the language,which is used for programs which run In a single environment.. This section describes a single primitive called transfer to meet this requirement.- We have tried to make this primitive do a minumum amount of work, leaving everything possible to be done by local code surrounding It In the two contexts which are involved in the transfer.
The basic transfer primitive, then, takes an Inport as its single argument.
Af ter it has been executed, the context which executed it is no longer running, and the context specified by the Inport has started running at the location specified by its program counter. In fact, this operation bears a striking similarity to the primitive used in Multics for switching control from one process to another [Seltzer], where the system scheduler, running within a user process, picks another process to run and transfers to it. The difference is that in Multics there is no relationship between the processes except for that established by the implementation of the scheduler.
In our case, however, we almost always want to pass some kind of return link and some arguments to the new context. We do this by establishing the convention that the link should be put into a global variable called link, and the argument into another global called ergs, before the
transfer is executed. The context being entered must use the values of these variables, if it cares, before doing another transfer. Since this convention is followed In all our examples, the remainder of the paper uses a three-argument primitive
transfer(destination inport, return outport, argument pointer). - as an abbreviation for
deit := destination inport; link := return outport;
ergs := argument pointer; transfer;
In the implementation the global variables deal, link and ergs are of course machine registers.
For obvious reasons we make ergs a pointer to the argumentrecord.
From the point of view of the type machinery, this will be a "universal" pointer which carries Its type with it. When the receiving context tries to use it, a run-time type check is needed to ensure that it actually has the proper type. In most cases, however, this check can be done at binding time, as we shall see later. . - '
Note that the transfer primitive says nothing about what is to be done
with the link or the arguments, and it does not create any contexts or allocate any storage. All of this is the responsibility of higher-level conventions or control disciplines, and the existing local features of the language, together with transfer, are sufficient to permit almost all of the transfer operations we know about to be programmed. An actual implementation, of course, may -favor certain disciplines by pre-defining them In a standard prologue and generating especially good code for them, as ours does for the port, procedure and signal disciplines described below.
5. Conventions for compatible transfers
In defining control disciplines, we would like to have as much compatibility. as possible, so that it Is possible to leave a context using one discipline and enter a second context using a' different one. To make this work, we must be careful about storage allocation and about the rules for handling the arguments and return liriks. We have already discussed a suitably general method for allocating -frames. This section considers the other general problems encountered in designing a fairly broad set of compatible control disclpllr.es.
The transfer primitive allows for a single argument, which Is normally a
pointer to the record containing the arguments which the user wanted to pass. The semantics of binding a formal parameter, say x, to an actual parameter, say 14, Is very simple. The sender of the argument record assigns the actual parameter to a suitably named component of the argument record (actualargs.x := 14). When he has finished constructing the record and is ready to transfer, he does
ergs := actuaiargs; transfer(destination).
The receiver does -formalargs := args,
and (automatically) prefixes his block with the clause
where formalargs is declared to have the type of the argument record he expects.
The effect of all this is that:
. the lowrlevel convention for passing arguments is very simple - one pointer is passed;
. the entire collection of arguments is treated as a unit, so that it can be passed on unchanged by a context which IS simply doing monitoring or tracing and Is not interested' in the Internal structure of the arguments;
. the receiver can reference the formals with the usual syntax;
. the language facilities for constructing and decomposing records are automatically available for arguments. These allow, among other things
- component values to be specified by name, by position or by default;
- a record to be decomposed by assigning it to an extractor; a
· syntactic construct which looks exactly like a record constructor except that all the components are treated as left-hand-sides of assignment operators;
- variable-length records.
In this way a fairly elaborate set of facilities Is made to do double duty without any need to introduce new semantics Into the language.
To preserve generality, we must ensure that the storage occupied by the
. argument record will not be reused until the receiver Is through with it. It is undesirable to put this storage in the sender's frame, as is customary in Algol implementations, because the sender's frame may not live as long as the receiver (e.g. when the sender Is a returning procedure; this case can be handled specially in Algol because of the restrictions on what a function can return). We therefore allocate separate storage for the arguments, and require the receiver to free this storage when he Is done with it.
Copying the entire argument into the receiver's frame would be another
alternative, but is is unattractive for variable length argument records and In situations where a receiver is not interested in the values of the arguments, but Is simply going to pass them on to someone else. Copying does work well f or short argument records, however, especially since the record can.be constructed in the machine's registers, and this strategy Is used for records of -less than 6 words. .
6. Coroutines and ports -
In this section we take up a pair of control disciplines which treat the two parties to a transfer as equals. In particular, this means that no creation of contexts or allocation of storage is Involved In a transfer, and that the relation between the parties is symmetric - each thinks that it is calling on the other one.
A coroutine (more or less as in Simula [Hoare and Dahl]; see also
[Mcilroy] and [Conway] ) is a context which, when entered, continues execution where it left off the last time it relinquished control. Local storage survives unchanged from exit to entry (as in a Fortran procedure, Interestingly enough). This is the simplest control discipline, and the easiest to describe. Each context Is pointed to by static inports set up at link time. Hence a transfer passes no return outport. The linkages are normally symmetric, as shown In figure 1.
are three problems with coroutines of this kind as a general-purpose control
discipline. One is that, because
of the fixed linkages, a coroutine
cannot be used to provide a service to more than one customer. A procedure, by contrast, is Ideally suited for this purpose, since it is created as a result of a call and destroyed when its work is done.
A second difficulty is that the control Is entirely anarchic. There is nothing to prevent control from entering a coroutine In an entirely unsymmetric way. For example, in figure 1 context 0 might gain control over inport a from line sl of P, even though its program counter Is at U. If subjected to an appropriate discipline this kind of control transfer might be useful, but no such discipline is present In the simple coroutine scheme.
6.2 initialization of coroutines
The third problem is proper initialization of a collection of coroutines. Recall that a transfer from context P to context Q does not change Q's program counter, but simply causes execution to resume at the point where it stopped, or at the beginning if Q has never run before. Since no buffering of ergs or link is provided by transfer, must save their values before doing another transfer. In general it will do this properlj, only If It is sitting immediately after a transfer to P. in figure 1, for example, If P is started first, it will transfer to Q at sl. but 0 will transfer to R and thus lose P's argument record.
This difficulty can be reduced by initializing more cautiously, as follows: (a) Start each context In turn by transfering to it, let It run up to Its
first transfer, and stop it before it sets up ergs and firth.
(b) Carefully choose one of the contexts and restart it by transferring to it.
Step (a) is unattractive because it requires a kind of control over the internal activities of the contexts which is quite different from what Is needed for normal transfers. Step (b) has more serious problems, which will become apparent on further examination.
Suppose in figure 1 that P is acting as a producer of data and Q as a consumer -who may occasionally return a reply. The fact that P and Q play different' roles Is concealed in the figure by the identical form of the skeletal program text. In figure 2 this difference has been brought out by expanding the argument handling associated with each transfer Into send and receive operations. The sequence of processing is: -
P: setup - send transfer - receive - compute - send - transfer -
Q: setup - transfer - receive - compute - send - transfer -
The two sequences: are identical except for the phase at initialization: in both cases there is a send - transfer - receive sequence which is the expansion of the simple transfer of ligure 1.
The differencein phase is quite Important, however, for step (b) of our cautious initializatioii procedure. If we choose P to restart, it will immediately transfer to 0, which will immediately transfer back, and Wsfirst
message will be lodt. If, on the other hand, we choose Q to restart, all will be well. Unfortunately, It is hard to see how to make the proper choice in more complex situations (If indeed it is always possible).
6.3 Processes and messages as a model.
Rather than making further attempts to patch up the simple coroutine discipline, we now turn to a much more powerful scheme: processes executing In parallel and communicating via event channels. This, of course, Is more power than we need or want, but by extracting the essential functions of the parallelism and message buffering we can design a control discipline with understandable properties which preserves the strengths of coroutines while avoiding their - problems
The idea of
processes executing in parallel we assume to be familiar [Dijkstra]. A message
channel is an object on which two basic actions can be performed by a process:
send a message and receive a message. A message is an arbitrary record, and_the channel can buffer an arbitrary
number of messages. An attempt to receive a message from an empty channel
causes the receiving process to wait until a message is sent to that channel.
There Is no constraint on the number of processes which can send or receive
messages on a given channel. This
facility is synthesized from two operating systems
[Lampson, Brinch Hansen]; we have suppressed many details which are Irrelevant to our purpose.
Any transfer operation can now be modeled by some combination of send and receive. We don't have to worry about losing messages, because of the buffering provided by the channels; each process will get around to processing Its messages In due course. Nor is the order in which
processes run of any importance; in fact, it is not even defined, except when processes must wait for messages. We still need-a convention which allows one process to provide service for many customers, however. We get it by analogy with the link parameter of the transfer primitive: an event channel on which to return a reply goes along with each message.
The process-channel model has added three essential features to the coroutirie discipline:
. parallel execution;
· buffering of messages;
. Indirect access to processes through message channels.
Figure 3 illustrates the structure of a symmetric connection. We now proceed to adapt these features to a sequential, unbuffered environment. The first step is to define a new type for symmetric transfer of control, called a port,-to replace the <channel, outport> pairs in figure 3 [Balzer, Krutar]. Each port is likewise a pair, consisting of an Inport IP and an outport OP. IP points to the context which will get control when a transfer is made through this port, and OP is where the return link will be stored.
We can avoid the need for parallel execution in a straightforward way, by modeling the notion of "a process waiting for a message on a channel" with the new concept of "a context being pending on an Inport". Since a process can only be waiting on one channel, we will insist that a context can only be pending on one inport. Now, if all transfers are to pending Inports, it will always be possible to run the context to which a transfer is directed, and there will be no need for parallel execution. A transfer which does not obey this rule will not be executed, but instead will cause a control fault, with consequences which we will explore shortly. .
Rather than explicitly associating the attribute "pending" with each inport, we can observe that an inport is a capability to start execution of a context, and interpret the pending rule as a requirement that only one non-null inport at a time should exist for each context. The inport components of all the other ports associated with a context Will be null, and a transfer to a null inport will cause a control fault. We thus complicate the semantics of transfer as little as possible.
Note that the pending rule has nothing to do with the transfer primitive, but is a convention which we introduce in order to construct a useful higher-level control discipline, that of ports. Even within this context, it may be proper to break the rule if It can be shown that no untoward consequences will result. Since the-rule is strictly internal to the port discipline, it stands or falls solely on the consistency of that discipline, and it is entirely Independent of the requirements of any other, separate convention for control transfers.
do, however, want it to be compatible with a procedure discipline; ‑
fortunately, this causes no trouble.
A context gets- to be pending on an Inport In the same way that a process gets to be waiting for a message on a particular channel: by executing a receive operation on the port containing that inport. There Is a definite relationship between the value of the program counter and the pending Inport: the program is at the point where-it expects control to arrive over that port.
As a result, there is no need for message buffering in a successful port transfer, since the receiving context is ready and willing to pick up the message at once.
6.5 Control faults and message buffering
During normal execution a control fault indicates an error, an attempt to transfer control to a context which was not interested in receiving it In that way. During initialization, on the other hand, a control fault may simply be an Indication that there is another context to start. When a fault occurs, therefore, control Is passed to the owner of the faulting context; the owning context must decide whether another context should be started. The mechanism by which this is done Is described In section 9. Here we confine ourselves to the local consequences of the fault. The argument used above to show that no message buffering is required depended on the absence of control faults. When a fault does occur, what action should be taken to ensure that no messages are lost?
First of all, If no message is being sent (I.e. ergs is null) there is.no need f or buffering. For instance, when two contexts have a strict producer-consumer relationship, transfers from the consumer to the producer involve no message. This explains why no special action was needed during the simple coroutine initialization (discussed In section 6.2 above) when we chose to restart the consumer.
When a control fault occurs during a transfer from P to Gl (see figure 6). and ergs is not null, we actually have to do something. We would like
not to Introduce any new kinds of objects, and not to complicate any existing operations. Since our repertoire of objects and operations is limited, things look unpromising at first sight. Fortunately, however, we do have contexts at our disposal, and within a context we can embed any kind of special processing and storage we want, as long as it interfaces properly to the rest of the world.
In particular, what we can do is to construct a buffer context B with a standard program text, and local storage within which we keep the argument. We want B to emit the argument the next time control is transferred to P through the port a. To get this effect, we put an Inport for B Into a's Inport component, and save the inport for P which normally is there in B's local storage. When B gets control, it will restore P's inport, transmit the saved argument and destroy itself. It does this by executing:
a.lnport := savedinport; transfer(DC, B, (a, savedargument));
where DC Is a system-provided context which destroys B and then does: transfer(a.outport, address(a.innort), savedargument);
The cost of all this machination is quite moderate (which is not actually very important, since control faults take place only at Initialization if there are no errors), and it has the great advantage that normal transfers are not complicated at all by the requirements of control faults. Figure 6 illustrates the successive stages of Initialization for our familiar two-context example.
6.6 Linkage faults
We also want to be able to do dynamic linking, as in Multics [Bensoussan et. - al], so that we must be prepared to deal with a transfer through an
outport which has not yet been defined. Fortunately, the techniques we have developed can handle this situation without difficulty. Undefined outports are initialized to point to a standard context which constructs a buffer context, if necessary, to save the argument of the transfer which caused the linkage
f ault,.and then passes the fault on to the owner of the faulting context. If the. owner fills in the outport and transfers to it, everything will proceed exactly as for a control fault. Indeed, It Is quite possible that a control fault will then occur.
6.7 Railroad switching
As we have already pointed out In passing, the outport component of a port is used to hold the return link passed by transfer. Figure 6 makes the purpose of this arrangement clearer. If context Q transfers through port q which is Joined to context R through port r, then r.outport Is set to q. A subsequent transfer through r will then return control to Q. If later P transfers through p to r, then r.outport will be reset to p, so that control will subsequently return to P. This action, which resembles the action of a spring-loaded railroad switch, allows many-to-one connections of ports, and provide's the memory required to return control correctly. Switching is done by the receiving context, since it is part of the port control discipline and has nothing to do with the transfer primitive. Often It produces no change, as for example In the transfers from R back to P or Q. To preserve compatibility with procedure returns (section 7.3) we make the convention' that a null link suppresses switching.
Procedures have semantics much like that of Algol procedures. The implementation makes use of almost all the facilities which have been described in the preceding sections.
7.1 Procedure calls
If p is declared as a procedure, then p(a,b,...) is a procedure call, just as It
would be a port call
if p had been declared as a port. There. are two -
a procedure p Is simply an outport; all procedure calls from a given context share a single inport In the frame, called the shared lnport. Since only one such call can be outstanding at a time (because of the pending rule), the pair (p, shared Inport) behaves exactly like a port. . . there is no switching done when control returns from a procedure call, because the call is regarded as a completed event, which may be repeated
. but cannot be resumed. .
Because of the way In which responsibility Is distributed during a transfer, these attributes of a procedure call are not visible to the
context which receives control, but are solely the local responsibility of the (
context making the call.
7.2 Procedure entry
Whenever a procedure Is entered, a context P must be created..This Is done
by another context C called the creator, as discussed In section 3. Since the transfer which results in creation of a procedure context is not special in any way, the creator must also take care to start the newly created context and pass It the argument supplied by the transfer. Furthermore, C must leave itself ready to create additional contexts, since the procedure may be entered recursively. Thus there must be a unique inport for C, and the behavior of C must be constant with respect to all transfers through that inport. On the other hand, C is basically an artifact Introduced to obtain a uniform control interface, and there Is no reason for it to be Involved in the return of control from P.
The consequence of these design constraints Is that the transfer operation which suspends the creator Is used In a somewhat unconventiontli manner. The link that was received in C-when It was started is simply passed on
to P. The inport through which control arrived in C remains unchanged, and C loops right after the transfer to P, so that it will execute the same code the next time it gets control.
The following, somewhat simplified code describes the body of a typical procedure creator:
start: q ALLOCATE(framesize); _
q.pc := initialpc; q.neighborhood := accesslink;
q.sharedport := NIL; q.startport := q;
transfer(q.startport, link, ergs);
When the creator is created, the values of the local variables frameslze and initialpc are extracted from the text of 0, and accesslink Is set up based on the rules for defining the binding function.
This code is misleadingly long in.the sense that a clever implementation can achieve the effect It describes-with just a few machine Instructions, and it Is too short In the sense that the "ALLOCATE" operator conceals some additional complexity; It has been discussed In section 3.
7.3 Procedure return
A procedure context has an outport called returnport into which it
it is entered. A first stab at its
return sequence would be:
transfer(returnport, NIL, returnargs);
but this won't do, because it ignores the fact that the context must be destroyed as part of the return. The caller cannot be expected to take care of this, since he doesn't necessarily know that he called a procedure. The actual return, then, Is more like the sequence-Used by a buffer context (section 8.5):
transfer(DP, self, (returnport, returnargs));
where DP is a standard context which destroys link and then does.
transfer(returnport, NIL, returnargs); -
Note that this, like the procedure call described In section 7.1, is fully compatible and does not, f or example, depend on any assumptions about the nature of the context pointed to by the returnport. Furthermore, an arbitrary. return record can be transmitted. The null link suppresses railroad
switc ig if the call was made through a port (see section 8.7).
Finally, we take up a control discipline designed to handle.. exceptional events' ef ficiently and conveniently. The basic elements of this discipline are:
· a set of names f or events, called signal codes (e.g. "out of storage", "overflow");
· for each context, an ordered set of outports called handlers;
. a system_ procedure called the signaller whose argument s Is a pair (signal code, argument record).
Anyone can signal the occurrence of an event by calling the signaller with the appropriate signal code as an argument, thus:
The second argument may be an arbitrary. record which can be used to pass additional Information to the handler. 'There is also an optional third argument which specifies thecontext in which the signal should be generated; usually this is the current context. An identifier declared as a signal code is treated by default like an identifier declared as a procedure: a search Is made, according to whatever binding rules are in force, for a definition which can be bound to the Identifier. The value of a signal code is simply an Integer, guaranteed tobe different for different codes. Its only purpose Is to "permit two signal codes to be compared for equality.
The signaller calls the first handler, passing it s. as an argument. If the handler returns to the signaller, Its result r is a pair (action, return record). If the action is reject, the signaller tries the next handler; If It Is
resume, the signaller returns the return record to its caller.
Usually each context supplies a handler, starting with the current context, and the handlers are ordered by a_pointer in each context called the signal port, which can be set by the=-user. The default choice of signal port for a procedure is the return link. Thus if all the contexts were Algol procedures, the effect would be to search up the stack, trying each procedure to see If it was interested In the signal.
Normally, handlers are declared in line with the program text of the context which will supply them, and there is convenient syntax for declaring a handler with each control transfer and with each block. If several handlers are -declared in a context, they are concatenated into a single one, using the same rule that the signaller uses. These declared handlers have the form of case statements which test the value of the signal code. By writing any as a case, however, the programmer can get hold of all the signals that go by and apply his own tests to them.
Thus, for example, one can write:
enabling OutOf Storage:
begin print("Storage exhausted"); exit computation end
enabling OutOfStorage(spaceneeded: integer): if tablespace > 1000 then return
GetTableSpace(spaceneeded) else reject );
if the OutOf
Storage signal is generated within the call of BuildTable, it will ‑
first be given to the handler associated with the call of BuildTable, and then to the. handier for the block. The first (innermost) handler checks to see If more space is available. If so, it obtains the space and returns it to the context ' which did the signalling. If not, It rejects the signal, and It Is passed to the handler for the block, which prints an error message and does a (structured) non-local goto. The consequences of this last action are discussed later.
The handlers have
the same semantics as ordinary procedures, differing only-In the syntax for
declaring them. Furthermore, the programmer is free to provide his own handler
f or a context; all he has to do is to put an outport into the component called
handier in the context's frame. The handlers declared with enabling have some
advantages, however. A great deal of trouble Is .
taken to make the cost of declaring a handler small, since it is assumed that signals are unusual, so that most declarations will never be Invoked. In fact, entering the scope of a declared handler does not cause any instructions to be executed. Instead, the compiler generates some recognizable instructions which do nothing, and distributes them strategically In the program text where the signaller can find them.
When the signaller gets to a context which has no explicit handler, then, it examines the program text for in-line handlers. If one is found, Its associated program text is located from the clues left by the compiler, and it Is called in the usual way.
-This scheme for handling signaFs has a good deal In common with the ON-condition facilities of PI/1. There are also a number of differences, however:
a) enabling a handier In MPL Is a declaration, not an executable statement;
b) the program has much greater control of signal handling than In PI/1. in particular:
· any and reject together .allow decisions about signal handling to be made in a very flexible way;
. if this isn't good enough, the user can write his own handler, rather than use enabling;
c) arguments can be passed with a. signal, and results can be returned, as in the example above;
d) the zero time-cost for enabling chandler makes the facility very attractive to use.
8.2 Unwinding .
Sometimes is is necessary to abandon a computation In mid-flight and restart from some earlier point. We call this operation unwinding. For example,
when an error is detected in a compiler, the current state becomes useless and we want to make a fresh start, perhaps with the input advanced to the next statement of the source program. In general when this happens there Is some collection of contexts which are no longer useful and should be destroyed. To
deal with this situation, we need:
a) a way of deciding which contexts should be destroyed;
b) a procedure for destroying each context in an orderly way; c) some place to send control when the unwinding Is complete.
If there are a lot of contexts around which are not related hierarchically, It is not at all clear who should be destroyed during unwinding. We therefore provide a standard procedure which does the right thing for nested procedure calls, and leave It to the programmer to write his own unwinder for more complex situations, using the operations of the next two paragraphs. The standard procedure is
unwind(from context, to context, signal),
and it destroys all the contexts encountered in propagating the signal between the two contexts, not including the end points. It is normally used in a handler, thus:
unwind(myself, myparent, mysignal).
The parent is passed to the handler when it is entered, along with the signal and signal argument.
Destroying a context is a two-step process. First It must be given a chance to put its house in order, i.e. to restore to a consistent state any non-local data structures which it may have been modifying. This is done by passing the signal cleanup to Its handler. If the context wants to get control before being destroyed, it should enable this signal. When the handler returns, the context is destroyed, using the same facilities which would be used to destroy any other record. With the destroy operation In hand, we can write a skeletal program for unwind:
c := fromcontext;
- for c := NextSignalkandier(c, signal) while c # tocontext do
Finally, we consider how to continue the computation, for the special case in which the context doing the unwind is an in-line handler of the one which Is to receive control. Since the handler knows about the program text of the destination in this case, it can simply set the destination's program counter to. the proper value, and then exit by destroying Itself, exactly like a buffer . context (section 6.5). The exit statement in the previous "OutOf Storage" example is syntactic sugar for:
unwind(myself, myparent, mysignal);
myparent.programcounter := ExitfromComputation;
transfer(DC, myself, (myparent, NIL) ).
The Muffles system [Organick] supports an unwind operation somewhat similar to what has Just been described.
9. Control faults
The discussion of control faults in section 6.6 left two questions open:
. who gets notified when a control fault occurs?
. how is the notice served?
The first question is handled like the similar problem for signals. Each context has an owner outport which defines who should be notified. By default this is set to the creator of the context, but .the user can establish any
relationships he likes by resetting it.
When a control fault occurs, it is simply converted Into a signal called controlfault which is started of f at the context specified by the owner outport of the f aulter, and then propagates In the usual way. This makes it reasonably convenient for the owner to differentiate a fault from a normal exit. During startup, when control faults are expected, each handler will probably. specify an exit to the next statement.
We have created an
environment f or describing global control disciplines, consisting of contexts within which execution takes
place, and a ‑
transfer primitive for passing control from one context to another.
Records and classes were used to create contexts and to handle arguments. We showed how to define the binding function for names In a fairly general way, and described a strategy which allocates storage for contexts. We established conventions for passing arguments and return links which can accomodate a wide variety of control disciplines in a compatible way.
Ports were Introduced as a non-hierarchical control discipline, and we saw how to Initialize a collection of contexts connected by ports, how to handle linkage faults, and how to switch port connections so that several contexts can use a single port. We showed how to handle Algol-like procedures without any new primitives, and compatibly with ports.
Finally, we introduced signals as a control discipline for dealing with
unusual events, described how to give the programmer complete control over signal propagation and how to:4mplement signal handlers efficiently, and used the signal mechanism to provide for orderly retreat from untenable situations.
Balzer, R. M., "PORTS - A Method f or Dynamic Interprogram Communication and Job Control," Proc AFIPS Conf. 39 (1971 SJCC)
Bensoussan, A. et. al, "The Multics Virtual Memory: Concepts and Design," Comm ACM 15, 5 (May 1972) -
Bobrow, D. G. and Wegbreit, B., "A Model and Stack Implementation of Multiple Environments," Comm. ACM 16, 10 (Oct 1973)
Brinch Hansen, P., "The Nucleus of a Multiprogramming Systeni," Comm. ACM 13, 4 (April 1970)
Conway, M. E., "Design of a Separable Transition-diagram Compiler," Comm. ACM 6, 7 (July 1963)
Dennis, J'. B., "Programming Generality, Parallelism and Computer Architecture," Proc. IF/P Congress 1968, North-Holland Publishing Co., Amsterdam, 1969
Dijkstra, E. W., "Cooperating Sequential Processes," in Programming Languages, Genuys, ed., Academic Press, New York, l967
Fisher, D. A., Control Structures for Programming Languages, Ph.D. Thesis, Carnegie-Mellon University, May 1970 (AD 708611)
Hoare, C. A. R. and Dahl, 0-J., "Hierarchical Program Structures," In Structured Programming, Academic Press, New York, 1972
Knuth, D., Fundamental Algorithms, Addison Wesley, Reading, Mass., 1968, p. 425
Krutar, R. A., "Conversational Systems Programming," In SIgplen Notices 6, 12 (Dec 1971)
Lampson, B. W., "On Reliable and Extendable Operating Systems," In The Fourth Generation, tnfotech, Maidenhead, Berks., 1971
Mcllroy, M. D., "Coroutines: Semantics in Search of a Syntax," Bell Telephone Laboratories, Murray Hill, N.J., unpublished report
Organick, E. 1., The Multics System: An Examination of Its Structure, MIT
Press, Cambridge, Mass., 1972
Seltzer, J. H., Traffic Control In a Multiplexed Computer System, Sc.D. Thesis, MIT, 1966 (MAC TR-30)
Wang, A. and Dahl, 0-J.., "CoreCitine Sequencing In a Block Structured Environment," BIT 11 (1971), p 426
Wegbreit, B., "The Treatment of Data Types In ELM," Comm. ACM 17, 4 (April 1974)
Wirth, N., "The Programming Language Pascal," Acta Informatica 1, 1 (1971)
Figure 1: Coroutine linkages
P: process Q: process
- . . . .. _ . 7-:-
-. Figure 3: Symmetric communication between processes_using message channels -
- - -
- - -
. - .- . ,
Figure 4: Symmetric communication between contexts using ports