THE QIO A fully configured RSX system has 80 system directives, but one stands alone as the most important. The QIO directive and its synchronous form, QIOW, is the only means of doing I/O on a RSX system. Unlike operating systems such as RSTS/E, RT-11, and UNIX which use different system calls for I/O, RSX has only the QIO. The QIO was a key component in the original RSX design. It has lasted, without any major changes from RSX-11D V1 to the latest release of VMS. You still use the same QIO that was used back in 1971 to open a file, read data, rewind a magtape, or write a message to the terminal. Most programmers equate QIO's with real-time devices such as A/D converters or LPA11's. Many RSX programmers have never needed to program a QIO directly. Instead the two I/O run-time system supplied with RSX, FCS and RMS, hide the actual QIO's. It is simple to issue a QIO from even high-level languages and thereby achieve greater throughput and device control than normally available. The simple Fortran-77 program CKSUM1 in figure 1 reads a fixed-length (512 byte), unformatted file and caculates the summation and exclusive-or checksum. On the 2048 block TEST.DAT file, the program takes 104 seconds on a PDP-11/24 with RL02's. Figure 2 shows how the program was converted to use double-buffered logic to read 16 disk blocks at a time. The new program, CKSUM2, now takes only 21 seconds, a 5-fold increase in performance. Note that performing just the caculation loop 2048*512 times takes 15.5 seconds. In effect, an I/O bound program has been made almost compute bound. To use QIO's directly, one must start by mastering its basic components and learning how to properly issue a QIO and test for success and errors. A QIO or QIOW consists of 6 standard parameters and 0-6 I/O specific optional parameters. The standard parameters are function code, logical unit, event flag, priority, I/O status address, and AST address. All but the first two are optional. An easy way to keep the parameters and their order straight is the memory aid 'FLEPIA'. The function code identifies the type of I/O operation. While supplied as a word, there are not 65535 seperate RSX I/O functions. Instead the function code is really treated as a two byte field. The high byte is the actual I/O function code and can range from 0-31. The low byte is the I/O function subcode and contains values specific to the actual device. Most of the 32 I/O function codes have standard assignments. For instance, the value 21 is a read virtual and a 22 is a write virtual. Note that the actual word values in octal are 10400 and 11000 respectively. In the sample program, notice how a PARAMETER statement is used to define the read virtual function. The low byte is used by specific devices to differentiate a particular function. For instance, a read logical request (code 2) to a terminal can be set not to echo input by setting the subcode to 20 octal. This makes 1020 the octal word equivalent of the I/O function. Macro-11 programmers should use the symbol names for the I/O function codes, i.e. IO.RVB for read virtual block. A complete list of I/O function code symbols and values is found in Appendix B of the RSX-11M/M-PLUS I/O Drivers Reference Manual. The manual also discusses what functions each device driver uses. One caution about I/O functions. Whenever possible the read/write virtual functions (IO.RVB/IO.WVB) should be used instead of the read/write logical (IO.RLB/IO.WLB). A common error made by a privilege program is to write some message intended for a terminal over the boot block on a disk. This is because RSX allows a privilege program to use logical I/O function to address absolute block numbers on the disk. If the logical unit was not properly assigned to a terminal, disaster can happen. Using the virtual form of the read/write I/O codes keeps all protection mechanisms in force at all times. However, if the function uses any subcode fields, the read/write logical must be used. The RSX executive zeros this byte when processing virtual requests. The next parameter in a QIO is the logical unit number. The logical unit is assigned to the hardware device. Logical units range from 1 to the limit determined when the task was linked (TKB UNITS option). It is important to remember that RSX has four types of device names: real, psuedo, logical, and TI:. A real device is exactly that, a hardware device on the system. For instance the name DL3: refers to RL01/02 drive 3. A psuedo device is a device that always points to a real device. Common psuedo devices include LB0: and SY0: for the system disk, SP0: for the spooling disk, and CL0: for the console listing. A logical device is a device name that maps to a real or psuedo device. What sometimes confuses people is that a logical name can be the same as a real device, but point to some completely different device. If I issue the command 'ASN DB1:=DL3:' and then assign DL3:, I am really doing I/O to DB1:. Finally, the name 'TI0:' always refers to the terminal at which the program is running. Logical unit assignments take place in a variety of ways. If you do nothing when building a program, the task is given 6 logical units. Numbers 1-4 default to device 'SY0:', logical unit 5 to 'TI0:', and logical unit 6 to 'CL0:'. Any higher logical units are left unassigned. TKB can make specific device assignments if you use the "ASG=ddn:unit" option. Note that all the devices named above are not real, instead they are psuedo devices and potential logical names. When you install a task, the INS program gets the device name set at task build time and first searches for a matching logical name. If match is found, the corresponding real device is assigned to the logical unit. Next the real and psuedo devices are searched. If no matching name is found, a warning message is output and the logical unit left unassigned. Once a task is installed, the logical unit assignments can be changed externally using the REAssign command. A running program can change its assignment by using the assign logical unit (ALUN$) directive. Note that RSX always assigns logical units to real devices, not logical device names. Once RSX makes an assignment, changing or deleting the logical device name has no effect. When the LUN command or RMDEMO task page is used to view a task's logical units, you sometimes see more units than expected. This is because features such as overlays and ODT add logical units beyond what would normally be supplied. One debugging technique is to use the LUN command to examine a task linked with ODT and then the REAssign command to assign the last two logical units to a different terminal. Now you keep the program's and ODT's I/O on seperate terminals. The event flag is the first optional QIO parameter. I/O takes time and the event flag is one means of determining when it is finished. If you specify an event flag, RSX clears it immediately and then sets the flag when the I/O completes. As shown in figure 2, the normal method of testing for I/O completion is to wait on the event flag (CALL WAITFR). When doing synchronous I/O, the QIOW directive is used to issue the I/O request and wait on completion. RSX does not require the event flag for the QIOW form, however if one is not named, you have simply issued a QIO and control is returned immediately to the program. A easy means of exhausting system pool is a loop which includes a QIOW with no event flag. The fourth parameter, priority, is left over from RSX-11D and remains valid for only IAS. These systems allowed a program to specify a priority of the QIO different from the issuing task priority. In RSX-11M, RSX-11M-Plus, and Micro/RSX this parameter is always ignored and is almost always left blank. RSX devices always return I/O status to a double-word block. The address of this block, if any, is named in the fifth parameter. Just like an event flag, the I/O status block is zeroed when the QIO is issued and set to the final status when the I/O completes. One simple way to check for I/O completion is to test the I/O status for a non-zero value. It is critical that the I/O status information be processed in pieces. You first test the low byte of the first word for success or error. Success is always greater than zero, an error is negative. Device specific status is sometimes returned in the high byte of the first word. The second word is usually a transfer count. For example, the number of bytes successfully read in a read logical/virtual request is stored in the second word. Note that an I/O can transfer data and still get an error return in the first byte. A IO.RVB for five disk blocks to a four block file will return an end-of-file error (IE.EOF) and a transfer count of 4000 octal. In the example, the transfer count is used to test for I/O success or failure. If no data is transfered (IOST(2) = 0), the program assumes it has reached the end of the file. The last standard QIO parameter is the AST address. AST stands for asynchronous system trap. It is a powerful mechanism in RSX that allows a task execution to be interrupted and control passsed to some routine to service an event. The QIO AST is triggered when the I/O completes so your service code processes the results of the I/O. You cannot set the AST address from a high-level language, so the parameter is not even present. Further discussion of AST's and I/O programming will come in a future issue. After the six standard parameters, come up to six I/O function specific parameters. There is an almost infinite variety to format and uses of these parameters. One common set of parameters are those used for output to terminals, printers, and such unit-record equipment. Only three parameters are used, a buffer address, buffer length, and carriage control. The latter is a character that specifies how many blank lines follow the text. The standard Fortran formatted control characters are used, space for one line, 1 for page eject, and so forth. A zero value means no carriage control should be supplied. Disk reads and writes take a slightly different format. The first two parameters are still a buffer address and buffer length. But the third parameter is ignored and the next two specify the starting disk block as a 24-bit number. The low part is in parameter 5 and the high byte in parameter 4. The final I/O parameter is not used. If you use the read/write virtual functions (IO.RVB/IO.WVB), the disk block number is within the file which you previously haved opened. Disk files start with block 1. But if you are using the read/write logical functions (IO.RLB/IO.RLB), you are addressing absolute disk blocks, starting with block 0. Your program must be privileged or the disk mounted foreign to do such I/O. Looking at each part of CKSUM2, you can see how simple it is to use QIO's for disk I/O. The two parameter statements are used simply to define the number of disk blocks per I/O and the read virual block function. PARAMETER IOBLK=16 PARAMETER IORVB="21*"400 The next section declares the QIO I/O status blocks, I/O parameters, and input buffers for both QIO's. A good practice is to always initialize the parameter blocks to zero. INTEGER*2 IOST1(2),IOPRM1(6),BUF1(256*IOBLK) INTEGER*2 IOST2(2),IOPRM2(6),BUF2(256*IOBLK) DATA IOPRM1/6*0/,IOPRM2/6*0/ Fortran has a special feature that allows a program to declare it will be performing its own I/O to a file. A minus one value to the BUFFERCOUNT keyword disables normal Fortran I/O. OPEN (UNIT=1,...,BUFFERCOUNT=-1) Before a QIO is issued, the I/O parameter array must be initialized. As mentioned previously, the first parameter in a disk I/O is the address of the buffer. Fortran has no syntax for setting a variable to the address of another, so the system library routine GETADR is used. It simply stores the address of the second argument into the first. The second array element is set to the size of the buffer in bytes. CALL GETADR(IOPRM1(1),BUF1(1)) IOPRM1(2) = 512*IOBLK CALL GETADR(IOPRM2(1),BUF2(1)) IOPRM2(2) = 512*IOBLK The two initial QIO's are now issued, using the system library routine QIO. The first reads from the beginning of the file (block 1) and the second is offset by the blocking factor. Note the QIO's use the same logical unit as the OPEN statement and separate event flags so the program can tell when each QIO finishes. IOPRM1(5) = 1 CALL QIO(IORVB,1,1,,IOST1,IOPRM1) IOPRM2(5) = IOPRM1(5) + IOBLK CALL QIO(IORVB,1,2,,IOST2,IOPRM2) The program now enters its main loop. It waits for the first I/O to finish. Success or error is tested by checking the transfer count. In this simple example, it is assumed that a zero transfer means the end-of-file has been reached. BUF1 is checksummed and the QIO reissued. We know the starting block number will be whatever the last block used in the second QIO plus the blocking factor constant IOBLK. 100 CALL WAITFR(1) IF (IOST1(2) .EQ. 0) GOTO 999 DO 110 I = 1,IOST1(2)/2 SUMADD = SUMADD + BUF1(I) SUMEOR = IEOR(SUMEOR,BUF1(I)) 110 CONTINUE IOPRM1(5) = IOPRM2(5) + IOBLK CALL QIO(IORVB,1,1,,IOST1,IOPRM1) When finished with BUF1, the program waits for the second QIO to finish and repeats the steps above. There are a few caveats to QIO's and disk I/O. A QIO is a directive and returns directive status. If the QIO fails as a directive, waiting on an event flag is meaningless because the I/O has never been issued. In a more rigorous program example, the CALL QIO statements would add the directive return and test for error. CALL QIO(IORVB,1,1,,IOST1,IOPRM1,IDS) IF (IDS .NE. 1) THEN ...error processing code... RSX also uses two end-of-file definitions. When using FCS or RMS, they keep an EOF position to a byte within a block. The file may actually have blocks allocated beyond this position. The file system considers its end-of-file to be the last block actually allocated in the file. When using QIO's to access disk blocks, you are using the file system's EOF. I have only touched the surface on QIO's. In future articles I will examing AST's, high-performance terminal I/O, and more details on the file system. Read Chapter 1 of the RSX-11M/RSX-11M-Plus I/O Driver Reference Manual for an introduction to RSX input/output and Chapter 5 for details on disk QIO's.