1 Introduction
2 Ground Rules

Building a File System
3 File Systems
4 File Content Data Structure
5 Allocation Cluster Manager
6 Exceptions and Emancipation
7 Base Classes, Testing, and More
8 File Meta Data
9 Native File Class
10 Our File System
11 Allocation Table
12 File System Support Code
13 Initializing the File System
14 Contiguous Files
15 Rebuilding the File System
16 Native File System Support Methods
17 Lookups, Wildcards, and Unicode, Oh My
18 Finishing the File System Class

The Init Program
19 Hardware Abstraction and UOS Architecture
20 Init Command Mode
21 Using Our File System
22 Hardware and Device Lists
23 Fun with Stores: Partitions
24 Fun with Stores: RAID
25 Fun with Stores: RAM Disks
26 Init wrap-up

The Executive
27 Overview of The Executive
28 Starting the Kernel
29 The Kernel
30 Making a Store Bootable
31 The MMC
32 The HMC
33 Loading the components
34 Using the File Processor
35 Symbols and the SSC
36 The File Processor and Device Management
37 The File Processor and File System Management
38 Finishing Executive Startup

Users and Security
39 Introduction to Users and Security
40 More Fun With Stores: File Heaps
41 File Heaps, part 2
42 SysUAF
43 TUser
44 SysUAF API

I/O
45 Shells and UCL
46 UOS API, the Application Side
47 UOS API, the Executive Side
48 I/O Devices
49 Streams
50 Terminal Output Filters
51 The TTerminal Class
52 Handles
53 Putting it All Together
54 Getting Terminal Input
55 QIO
56 Cooking Terminal Input
57 Putting it all together, part 2

Glossary/Index


Download sources
Download binaries

QIO

QIO is the main I/O service of VMS and UOS. In UOS, it is a part of the File Processor component. Most I/O requests end up being processed by QIO. The "Q" is for "Queue", which refers to the ability of QIO to queue up several requests. We will discuss I/O queues in the future. For now, we will only look at what is necessary for Get_Command. The QIO parameters are: Event_Flags (for future discussion), the handle, the QIO function number, an IOSB (I/O Status Block) structure, Astadr (asynchronous response address, which we will discuss in the future along with Astprm), Astprm (asynchronous response parameters), and then six parameters whose meaning depends upon the QIO function number. Event_Flags, Astadr, and Astprm should always be 0 unless we are doing asychronous I/O. I should note that asynchronous I/O is not the same thing as the asychronous nature of terminals (and some other streams). In the context of QIO, it indicates that a program can queue up multiple I/O requests to the any given device. Normal (synchronous) operation indicates that the program issues a single I/O request and waits (blocks) until that operation completes.

The IOSB is a record that is passed to QIO, which fills the record with I/O status. IOSB is used in several different situations and so it has a morphable structure. Here is the IOSB definition:

type TIOSB = packed record
                 case boolean of // r_io_get
                     false:
                         ( r_io_64 : record
                                         w_status : longint ; // Final I/O status
                                         case boolean of // r_devdepend
                                             false:
                                                  ( r_bcnt_16 : record  // 16-bit byte count variant
                                                                    w_bcnt : word ; // 16-bit byte count
                                                                    case boolean of // r_l
                                                                        false: ( l_dev_depend : cardinal ) ; 
                                                                                 // 32-bit device dependent info
                                                                        true: ( l_pid : TPID ) ;
                                                                end // r_bcnt_16
                                                  ) ;
                                             true:
                                                 ( r_bcnt_32 : record // 32-bit byte count variant
                                                                   l_bcnt : cardinal ; // 32-bit byte count (unaligned)
                                                                   w_dev_depend_high : word ; // 16-bit device dependent info
                                                               end ; // r_bcnt_32
                                                 )
                                     end // r_io_64
                         ) ;
                     true:
                         ( r_get_64 : record
                                          case boolean of // r_l_status
                                              false: ( l_getxxi_status : cardinal ) ; // Final GETxxI status
                                              true: ( l_reg_status : cardinal ) ; // Final $Registry status
                                      end // r_get_64
                         ) ;
              end ; // TIOSB
Which items are used in the structure depends upon the specific QIO call. We will address this as we look at each QIO function in turn.

Here is the QIO function.

procedure TUOS_FiP.QIO( Event_Flags : cardinal ; Handle : THandle ; Func : cardinal ;
    var IOSB : TIOSB ; Astadr, Astprm, p1, p2, p3, p4, p5, p6 : int64 ) ;

var Resource : TResource ;
    B : string ;
    Prompt : TUOS_String ;
    SRB : TSRB ;

begin
    // Setup...
    fillchar( IOSB, sizeof( IOSB ), 0 ) ;
    Handle := USC.Translate_Handle( Kernel.PID, Handle ) ;
    if( not USC.Valid_Handle( Kernel.PID, Handle ) ) then
    begin
        Set_Last_Error( Create_Error( UOSErr_Invalid_Handle ) ) ;
        IOSB.r_io_64.w_status := UE_Error ;
        exit ;
    end ;
First, we initialize the IOSB structure to zeroes. Next we translate the handle (all QIO calls operate on handles). Then we verify that the handle is valid for the process.

    if( ( Func and IO_Function_Mask ) = IO_READPROMPT ) then
    begin
        Resource := TResource( Handle ) ;
        if( ( p6 {Prompt length} > 0 ) and ( TFiP_File( Resource._File ) is TFiP_Terminal_File ) ) then
        begin
            // Get and display prompt...
            SRB.Buffer := p5 ;
            SRB.Length := p6 ;
            Prompt := Get_User_String( Kernel, Kernel.PID, SRB, IOSB.r_io_64.w_status ) ;
            if( IOSB.r_io_64.w_status = UE_Error ) then
            begin
                exit ;
            end ;
            Write_File( RH_SysOutput, 0, Prompt.Contents, Prompt.Size, 0 ) ;
        end ;
        if( ( p1 = 0 ) or ( p2 = 0 ) ) then // Zero-length buffer passed
        begin
            exit ;
        end ;
        B := Read_File( Handle, 0, p2 {receiving buffer max length}, 0, IOSB ) ;
        if( IOSB.r_io_64.w_status = UE_Error ) then
        begin
            exit ;
        end ;
        SRB.Buffer := p1 ;
        SRB.Length := length( B ) ;
        IOSB.r_io_64.w_status := Set_User_String( Kernel, Kernel.PID, SRB, B ) ;
        exit ;
    end ; // IO_READPROMPT
end ; // TUOS_FiP.QIO
Since handles are merely addresses of TResource instance, we cast the handle to TResource, so we can work with it as one. In the case of the IO_READPROMPT function, we get the prompt from parameters p5 (prompt text) and p6 (prompt length). If the length of the prompt is zero, then we won't output anything. Further, the handle must be a terminal (if the resource's file is a TFiP_Terminal_File then we know that to be true). If it is a terminal and there is a prompt, we write get the prompt string from the user's address space with Get_User_String. Then, assuming there is no error, we write it out to sys$output.

Next we see if the receiving buffer size is zero. If it is, or if the buffer address is 0, we exit since there is no buffer to receive input. Otherwise, we call Read_File to get the input. That function sets fields in the IOSB. If the status (IOSB.r_io_64.w_status) indicates an error, we exit. Finally, we write the input data to the user's buffer.

Here is the Read_File method:

function TUOS_FiP.Read_File( Handle : THandle ; Stream : longint ;
    Length : int64 ; Flags : integer ; var IOSB : TIOSB ) : string ;

var Resource : TResource ;

begin
    // Setup...
    Set_Last_Error( nil ) ;
    Result := '' ;

    Resource := TResource( Handle ) ;
    setlength( Result, Length ) ;
    IOSB.r_io_64.r_bcnt_32.l_bcnt := 
        Resource._File.Read( Stream, Resource.Position, Length, PAnsiChar( Result )[ 0 ], Flags ) ;
    setlength( Result, IOSB.r_io_64.r_bcnt_32.l_bcnt ) ;
end ;
First we clear any exceptions and set the result to null. Note that we do not translate the handle or validate it against the process. This is because we already did so in QIO and why repeat what we already did? We want calls to the executive to finish as quickly as possible to reduce overhead. Therefore, we adopt a strategy of only validating data when it comes into the executive from outside (such as via an system call). But within the executive, we assume that the data is valid. If Read_File is called from any other API code then the handle will need to be translated and validated before calling this routine.
We then set the length of the result string to the passed length and call the Read method of the file associated with the resource (a terminal in this case). We pass the current resource position, although it really doesn't apply in this case.

The file being used is a TFiP_Terminal_File instance.

function TFiP_Terminal_File.Read( Stream : longint ;
    Position : TStore_Address64 ; Length : TStore_Size64 ; var Buff ;
    Flags : longint ) : TStore_Size64 ;

var UEC : TUnified_Exception ;

begin
    Result := Terminal.Read_Data( Buff, Length, UEC ) ;
    if( UEC <> nil ) then
    begin
        Set_Last_Error( UEC ) ;
    end ;
end ;
This simply calls the Read_Data method of the terminal associated with this file and sets the exception if there is an error.

Here is the Read_Data method.

function TTerminal.Read_Data( var Data ; _Size : TStore_Address64 ;
    var UEC : TUnified_Exception ) : TStore_Address64 ;

var S : string ;

begin
    Result := 0 ;
    if( Stream.Write_Only ) then
    begin
        UEC := Set_Last_Error( Create_Error( UOSErr_Write_Only ) ) ;
        exit ;
    end ;
    UEC := Set_Last_Error( nil ) ;
    S := Input_Filter.Read( _Size ) ;
    move( PAnsiChar( S )[ 0 ], Data, length( S ) ) ;
    Result := length( S ) ;
end ;
First we verify that the stream is readable. If not, we set an exception and exit. Otherwise we clear the exception, then call the input filter to read the input from the stream. After that, we copy the input to the passed Data parameter.

At this point, we've followed the input stack from the application program to the terminal's input filter. In the next article, we will look at the input filter.