INTERTASK COMMUNICATION ----------------------- 1. Event Flags -------------- If all that is required is the ability to communicate the occurrence of one, or a small number of, events from one task to another the simplest way to do it is via Event Flags. Each task has access to 96 event flags as follows:- 1-32 Flags local to that task only 33-64 Flags global to all tasks 65-96 Flags global to tasks in the same UIC group These flags are simple on/off bits that can be tested/set/cleared by the task via Executive Directives. By convention flags 25-32 and 57-64 are reserved for system use. Thus if Task A wishes to inform Task B that an event has occurred (e.g. that it has written a record to a disk file) it could set event flag 33, and Task B, by monitoring that event flag, would know that the event had occurred. (Alternatively, if Task B was in the same group as Task A, it could set a Group Global Event Flag, e.g. 65, if they had been enabled). Obviously care must be exercised when using Global (or Group Global) event flags not to use the same flag to mean two different things in two different circumstances that might conflict. Event Flag Processing is handled by the directives:- CLEF$ - Clear Single Event Flag SETF$ - Set Single Event Flag WTSE$ - Wait for Single Event Flag to be Set WTLO$ - Wait for one of a set of Event Flags to be Set EXIF$ - Exit if Single Event Flag not set RDAF$ - Read All Event Flags (Local & Global) RDXF$ - Read Extended Event Flags (Local, Global & Group Global) CRGF$ - Create Group Global Event Flags ELGF$ - Eliminate Group Global Event Flags Each of these is described in the relevant part of the Executive Reference Manual, and Section 2.2 therein discusses Event Flags in general. FORTRAN-callable subroutines exist for each of the above, and programmers in other high-level languages may write their own MACRO-11 subroutines and call them. Note that Group Global Event Flags can also be Created/Deleted by the MCR command FLA described in the MCR Operations Manual. Note also that a system will only contain support for Group Global Event Flags if it is requested at System Generation Time. 2. Parent/Offspring Tasking --------------------------- Frequently the only 'communication' that a task requires with another one is to know when the other task has finished (and possibly whether it succeeded or not). There are two distinct types of circumstances in which this might arise - firstly where a task is invoked for a specific purpose, rather like a subroutine, and the invoking task merely wishes to know when it completes and whether it succeeded; secondly where a task is stalled because another task is using a non-shareable resource (e.g. a common data base) and wishes to know when that resource is free. Both situations can be handled using event flags, or other traditional methods, but with V3.2 RSX provides an easier mechanism in the form of Parent/Offspring Tasking. Currently this is implemented by means of three directives - SPWN$ - Spawn a task, optionally with a command line, and optionally requesting to know its exit status. CNCT$ - Connect to a running task, requesting to know its exit status EXST$ - Exit with status Thus in the first case discussed above, Task A can spawn Task B (with the SPWN$ directive), possibly passing it a command-line, and then wait for Task B to complete. Task B can then use the EXST$ directive to return status information (currently a single word) to the parent task. (Note that a status is returned even if Task B doesn't use the special EXST$ directive.) In the second case discussed above, Task A can connect to Task B (even if Task A didn't spawn Task B) and again wait for it to exit. Each of the directives is described in the relevant parts of the Executive Reference Manual, and Chapter 4 therein discusses Parent/Offspring Tasking in general. FORTRAN-callable subroutines exist for each of the above, and programmers in other high-level languages may write their own MACRO-11 subroutines and call them. Note that a major application of Parent/Offspring Tasking that is really outside the scope of this document is the ability to spawn a Command Line Interpreter (e.g. MCR...) passing it a command line - so that for instance a task can now dynamically install another task by passing the command line 'INS ...' to MCR... via a spawn directive. Note also that spawning in this case should not be confused with MCR's multi-user task 'spawning' (where ...PIP might be run as PIPT1). If the user attempts to spawn a multi-user task (e.g. ...PIP) via the SPWN$ directive, it will attempt only to activate ...PIP (and fail if it is already active) and will NOT create a version called PIPT1 or whatever. Note finally that a system will only contain support for Parent/ Offspring Tasking if it is requested at System Generation Time. 3. Send/Receive Mechanisms -------------------------- For tasks that require rather more information than available via the setting of an event flag, the Send & Receive Data Directives allow a mechanism for transferring fixed-length (13 word) packets of data from one task to another. In this case Task A constructs a 13-word area containing the data it wishes to send to Task B, and sends it to Task B using the SDAT$ directive. Task B can then receive it (via the RCVD$, RCVX$ or RCST$ directives), nominating a 15-word area into which the data is to be put (preceded by the 2-word taskname of Task A in RAD50). The three variants of the Receive Data Directive are - RCVD$ - Attempt to receive a packet, and continue processing if no packet is present. RCVX$ - Attempt to receive a packet, and exit if no packet is present. RCST$ - Attempt to receive a packet, and Stop if no packet is present. In each case a taskname may be specified to indicate interest only in packets from that task - if omitted packets from any task are eligible to be received. Note that there is NO directive to attempt to receive a packet and wait until one arrived if no packet was present (though the user could implement such a directive himself). This can lead to potential problems of synchronisation for the beginning user. The standard methods for two tasks communicating by means of packets is as follows - A. Task B executes completely for each packet. This is the case when some activity is initiated by a packet, and each packet is independent of each other. A simple example is the Print Spooler which spools files according to instructions received via packets. The processing here is - Task A Sends Packet to Task B (via SDAT$) Requests Task B to ensure it is running (via RQST$) Task B Does a Receive or Exit (RCVX$) on entry, and when it has processed that request loops back and does another. Note that the order of activity in Task A is vital - if it requested Task B before sending a packet, a context switch could occur allowing Task B to come in and exit before the packet was queued, thus leaving the packet queued until the next time Task B was requested. B. Task B is permanently active. This is possibly a more common case for user programs. Task B will process various requests as a response to data packets, but does not wish to exit in-between packets (possibly because files are open). The simplest method here is for Task B to do a Receive or Stop (RCST$) whenever it is looking for a packet. There are then two possibilities. Either Task A can issue an Unstop command (USTP$) after it sends the data packet (and possibly a Request as well if there is a chance Task B will not be active) or Task B can say (during initialisation) that it wishes an AST to be triggered when a packet arrives (using the SRDA$ directive) and can then Unstop himself in that AST routine. If the user has not generated Stop-Bit support into his system, a similar procedure to the above can be done using the Suspend/Resume directives (SPND$/RSUM$) or by using Event Flags (although of course a Global or Group Global event flag would have to be used if Task A was to set it). As, however, there is no Receive Data or Wait directive, slightly cleverer code is needed in Task B, such as - SETF$S flag ; for first time around 10$: WTSE$S flag ; wait for flag to be set CLEF$S flag ; clear flag for next time 20$: RCVD$S ... ; do the receive BCS 10$ ; If CS - no packet - wait again : : BR 20$ ; when processing finished (The initial SETF$S is, of course, unnecessary if the flag is being set by Task A.) Note that if the BR 20$ at the end is changed to BR 10$ problems can occur if two packets were already queued when the flag was cleared. Each of the above directives is described in the relevant part of the Executive Reference Manual, and sections 2.3.3/2.3.4 therein discuss ASTs in general. FORTRAN-callable subroutines exist for each of the above except those relating to ASTs (ASTs are not officially supported in the FORTRAN language definition - but may still be used via user-written MACRO-11 subroutines). Programmers in other high-level languages may write their own MACRO-11 subroutines and call them. Note that each packet is buffered in DSR until received by a task, and that packets will be queued to any installed task even if it is not active. Thus too great a throughput of packets in a system can cripple a user's DSR and cause the system to hang. When a task exits, its receive queue is flushed. Note that the Send/Receive directives and the Stop Bit directives will only be in the user's system if requested at System Generation (each set is a separate option). 4. Common Regions ----------------- For tasks which wish to share rather more data than can be handled by the above mechanisms, one possible method is to use a Common Region. In this method a separate partition in the user's system is defined as a Common Partition. A Common task must then be created to define the initial values for this Partition, and to define any labels in it. Such a task is built like an ordinary RSX11M task (except that in the case we are considering it will only contain data) with the exception that - i) It must have the /-HD (no header) and /PI (position independent) switches set. ii) It must have an option line of STACK=0 to remove the task stack. iii) It must have an option line of PAR=name:0:n where 'name' is the name of the common (and must be used for both task and partition), and 'n' is the size of the partition. iv) It must be built with a Symbol Definition File so that other tasks may link to it. By convention the task image and symbol definition files should reside in LB:[1,1]. Any task which wishes to access the common must include an option line to the Task Builder similar to - COMMON=name:access[:apr] where: name is the name of the partition access is either RW to say the task wants Read and Write Access to the region, or RO to say it only wants to read apr is the APR to be used by the task in mapping the region. For position independent commons (e.g. most of the type we are considering here) this can be omitted and will be allocated by the Task Builder. (LIBR,RESCOM,RESLIB are variant option names for COMMON.) The task can then access locations in the common in one of two ways. The easiest is simply to define global symbols in the common task image, and reference them in the user task - the Task Builder will automatically map them on to the common - but this is really only usable from MACRO-11 as most high-level languages do not support references to such 'external' symbols. The second method is to define a PSECT in the common task image with the GBL and OVR attributes, and the same PSECT in the user task - the Task Builder will then overlay the one on the other. As in FORTRAN the COMMON statement, and in BASIC-PLUS-2 the MAP statement, generate such PSECTs this method can be used to link high-level languages to the common. To avoid error messages from the Task Builder care should be taken to ensure that the attributes of the PSECTs match (RW,D,GBL,REL,OVR for FORTRAN COMMONs). As COBOL does not generate such PSECTs it is not possible directly to map a COBOL task to a COMMON region. COMMON regions and their use are described in detail in Chapter 3 of the Task Builder Reference Manual. Note that, in general, a task requires as many APRs allocated to the common region as will map it all - e.g. a 5KW common partition will need 2 APRs dedicated to it from all tasks that map to it. (The only common exception is the resident RMS-11K library which makes very clever use of memory-resident overlays and requires only 2 APRs to map 24KW.) Note also that in this, and all other methods of using a shared data base, the user must take his own precautions about two tasks simultaneously updating the data base and possibly corrupting it thereby. A common procedure is to use an Interlock Word which shows whether or not the data-base is in use. If this is done RSX11Ms ability to context-switch at any point must be borne in mind. For instance the following code TST LOCK ; Is the data base in use BNE ... ; If NE yes - go away INC LOCK ; No - it is now will not always work, as a context switch might occur between the TST and the INC allowing someone else to grab the data-base. One useful technique in MACRO-11 is to say that the relevant word is zero if the data-base is in use, and 1 otherwise, for then the code ASR LOCK ; Is the data base in use? BCC ... ; If CC yes - go away can be used, because the single instruction ASR both tests and sets the lock. 5. Dynamic Regions ------------------ The major drawback with using Common Regions is that usually the Common Partition has to be allocated permanently (even when the common is not in use) and that it is a fixed size. To circumvent this RSX11M introduced the notion of Dynamic Regions which could be created as part of an ordinary system partition. The general mechanism is that one task creates, and possibly initialises the region; any number of tasks may then access/modify the data in the region; and finally somebody, explicitly or implicitly, deletes the region. Because of the flexibility of the system, using it is naturally somewhat more complex than using a Common Region. All accessing is done by means of Executive Directives (confusingly called Memory Management Directives) which consist of the following - CRRG$ - Create a dynamic region (and optionally attach to it) ATRG$ - Attach to an existing dynamic region DTRG$ - Detach from a region CRAW$ - Create a window into a region (and optionally map it) MAP$ - Map an existing window into a region ELAW$ - Eliminate an address window (implicitly done by DTRG$) UMAP$ - Unmap an address window (implicitly done by ELAW$) These directives fall roughly into two categories - those concerned with regions and those concerned with Address Windows (though obviously they are closely related). Those concerned with regions (CRRG$, ATRG$, DTRG$) all take as an argument a Region Descriptor Block - an 8-word area that contains information such as the Region Name, its size and partition, and the user's access rights to the region (e.g. Read/Write/Delete). Those concerned with windows (CRAW$, MAP$, UMAP$, ELAW$) all take as argument a Window Descriptor Block - an 8-word area that contains information such as the Region ID (returned by ATRG$/CRRG$) of the region mapped on to, the offset into the region from which the mapping starts and length of the region to map, and the base APR to use for the mapping. Thus, once a region is created, a user accesses it by issuing an ATRG$ specifying Region Name (and access desired) followed by a CRAW$/MAP$ operation indicating which part he wishes to map and onto what part of his virtual task space he wishes to map it. Because these latter values have to be specified explicitly, it is generally impossible to use the mapping methods discussed previously (global symbols and PSECT names) to automatically map into a dynamic region. For the MACRO-11 programmer this is not too serious, as he can use the relevant virtual address explicitly in his program. For FORTRAN (or BASIC-PLUS-2) the method is slightly more complicated. He must define a COMMON in his program as before, but now must include in his task-build the line VSECT=name:base:length where name is the name of the COMMON base is the virtual address it is to be mapped to length is (optionally) the length of the COMMON This statement merely associates the given virtual address with the variables in the named COMMON. If the user then specifies the corresponding APR in his MAP statement, references to variables in the COMMON will automatically be mapped onto the relevant part of the Region. (If the user specifies a different APR he will probably get a Memory Protect Violation.) Note that both MACRO-11 and high-level programmers must include an option line to the Task Builder of the form WNDWS=n where n is the number of windows simultaneously mapped. Two directives (SREF$ and RREF$) also exist to allow one task easily to send another task information about which part of a region he is mapped to (using specially formatted Send/Receive packets). All the above directives are described in the relevant part of the Executive Reference Manual, and Chapter 3 therein discusses them in general, and the format of the two Descriptor Blocks in detail. Section 3.3 of the Task Builder Reference Manual discusses the Task Builder implications of them. FORTRAN-callable subroutines exist for each of the above, and programmers in high-level languages supporting PSECTs (e.g. NOT COBOL) may write their own MACRO-11 subroutines and call them. Note that a system will only contain support for these Memory Management Directives if they are requested at System Generation Time (SREF$ and RREF$ are a separate option). 6. Shared Files --------------- If the user requires rather more information to be shared than will fit in memory (or merely does not want to use up the memory) the easiest method of sharing data is to use shared files. Obviously this is entirely a user- designed method, and as such there are no special directives or routines in RSX11M to facilitate it (though the MACRO-11 programmer can make use of the block-locking facilities the Executive includes for RMS-11 with a little care). A few points should be borne in mind - 1. If more than one task requires update access to the same file it is usually necessary explicitly to say so in all programs that will access the file (even for read only) - e.g. setting the FA.SHR bit in MACRO-11. 2. The file systems generally do not distinguish between accesses from two different tasks, and accesses from two different channels (LUNs) in the same task. If locking of some kind is used great care must be taken to avoid the situation where a task sticks in a loop waiting for a lock to be cleared that it set itself. 3. Whether using FCS or RMS, file I/O results in a large amount of code in each task. Given this and the overhead of synchronising multiple tasks accessing the same database, it is frequently most efficient to have a single centralised File Manager Task that will pass the data back to the relevant task by one of the means described previously. 7. Buffering in DSR ------------------- Occasionally it is desirable to pass information between two privileged tasks (i.e. tasks mapped to the Executive). If this information is more than will fit in a Send Packet one possibility is to buffer it in DSR. All that would then be necessary would be for the first task to pass the address of the block (and possibly its length) to the second task via a Send Packet (or even as an Exit Status), which could then read it directly. Executive routines $ALOCB and $DEACB can be used to allocate and deallocate DSR. Note however that RSX11M is very sensitive about its pool, and it is vital that the second task deallocates exactly the same pool that the first task allocated. Naturally this technique is also restricted by the amount of free pool available. 8. Core-to-Core Driver ---------------------- It is often desirable frequently to move a large block of data from one task to another. While this can be done by the methods previously described they are often more relevant to shared data-bases than to such bulk movement. One solution would be to implement a Core-to-Core Driver. Drivers are traditionally thought of as methods of communicating with a physical device, but as has already been demonstrated by DEC's own Console Driver (CODRV) and Dave Elderkin's Virtual Terminal Driver (VTDRV) the I/O mechanism of RSX11M is far more flexible. It would be realtively easy to write a driver that would accept a read from one task and a write from another task and merely copy the data from the one to the other. 9. Executive Modifications -------------------------- Another option always open to the ingenious user is to modify the Executive to allow more functionality. While such modification can be very complicated and is often fraught with dangers, occasionally a fairly small modification can produce great results. For example, in the context of this document, it is fairly easy to determine that dynamic regions are identified in the system by the same sort of control block (PCB) as an ordinary task. Thus one would expect it to be easy to modify the Executive to allow ATRG$ and so on to work when specifying another task space. This is indeed so. By removing two checks that the partition specified is a common partition (in DRREG) and giving dynamic sub-partitions the name of the task running instead of the main partition name (by a mod to REQSB) it is possible for one task to map into another and, as long as he is given the relevant address, to access the other task's data structures. Naturally this approach can be problematic (what would happen if somebody tried to 'delete' the region?) and it is hard to be sure that something that appears to work will not break down in odd circumstances (what happens if the task is checkpointed?), but it is occasionally worth it for the great savings introduced. 10. Executive Extensions ------------------------ A recent concept has been creeping into RSX11M of implementing Executive Extensions rather than Executive Modifications. This has the double advantage of not requiring special treatment during System Generation, and of being likely to survive an update of the Executive (whether a new release or just published patches). The standard way to implement these Extensions is by means of the Illegal Instruction Trap. Generally when an illegal instruction is detected, a trap to the executive via a certain vector is made, and the executive proceeds to handle it (by aborting the task or whatever). It is relatively easy to intercept these traps (by changing the vector contents) by user-written code - which then by definition become extensions of the Executive. (Obviously care must be taken that the relevant code is mapped - either by putting it all in DSR, or by including a control block in DSR to map the relevant APR in a manner analogous to that used by Loadable Drivers.) The relevance of this method to the scope of this document is that one could, this way, implement instructions such as - Move n bytes from Task A Address n to Task B Address m because the code, being an extension of the Executive, would have access to all the Executive data structures and could directly manipulate APRs to map from one task to another. Obviously the implementation of such a system would require a large amount of work, but this could be justified in crucial situations (e.g. where a task is too large to spare an APR for most of the other methods). 11. Conclusions --------------- There is, obviously, no 'best way' to handle Intertask Communications - some methods will be applicable to some situations and not to others. In general it is worth remembering the following - 1. Any two tasks active at once in a system will be competing for resources. Do not allow a situation where Task A is waiting for Task B to do something while Task B is waiting for Task A to do something. 2. If there is a window in your code where a context switch could occur, or an event flag be set and missed or whatever, it is vital to work on the assumption that the undesired event will occur in that window and to find a way to close it. 3. Task A and Task B are generally totally independent - i.e. MACRO-11 tasks can talk to COBOL tasks, COBOL tasks can talk to BASIC-PLUS-2 tasks and so on - always providing, of course, that the relevant task supports that manner of 'talking'. 4. Ensure before determining what you are going to use that you have clear in your mind what your most crucial factor is - Core Memory, I/O time, Speed of Implementation and so on - methods efficient in one resource may be very expensive in another.