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

Terminal 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
58 Quotas and I/O

UCL
59 UCL Basics
60 Symbol Substitution
61 UCL Command execution
62 UCL Command execution, part 2
63 UCL Command Abbreviation

Glossary/Index


Download sources
Download binaries

Handles

Before we proceed further in our discussions of I/O operations, we need to discuss the concept of handles. Whenever a process wants to reference a UOS resource (whether a file, a device, or something else), there needs to be a way for it to uniquely identify that resource. This means of that unique identification is what we call a "handle". What do we use for the handle? We could use a string name for the resource, such as the name of the file. But passing strings across the processor rings has a lot of overhead associated with it. Besides that, if an application was to unknowingly use the same file twice (perhaps the user specified the same file for input and output) then it would need two handles. But if the filename was the handle, then the application would use the same handle twice and, thus, in the same context for both cases, which is almost certainly not what is desired. It would be better to use a unique integer value for handles.

We could use a simple integer value that is incremented for each new handle that is needed, but then we would need to translate that into whatever UOS class instance is associated with that handle value. This would require storing both the handle and the associated instance pointer, which requies a larger memory footprint (especially of concern on a system with thousands of active handles). In addition, that translation on every handle access would also cause additional processing overhead. So, instead we will use the address of the class instance as the handle value. Because a valid class instance will never be less than 256, we can use 0 to indicate an invalid handle, and other low values for special cases (such as the redirection handles like RH_SysOutput, discussed below). The disadvantage of this approach is that handle instance addresses (and thus handle values) tend to get reused over time. If a program closes a handle, but "hangs onto" the handle value and uses it again after the handle has been reused for another process, the error result would indicate a handle access violation. But if the handle wasn't reused, the error result from using it would indicate an invalid handle. The first condition could be somewhat confusing to someone trying to debug the application. The "inconsistency" between the two conditions could be avoided if we never reused a handle value in the executive - and some operating systems do this. For the aforementioned performance reasons, UOS does not take that approach. However, programs could use an interface that provides unique handle values and translates them to executive handle values. We will leave such an interface as an exercise for the reader.

First, let's add some new constants.

// Reserved Handles...
const RH_SysInput = 0 ; // SYS$INPUT
const RH_SysOutput = 1 ; // SYS$OUTPUT
const RH_SysCommand = 2 ; // SYS$COMMAND
const RH_SysError = 3 ; // SYS$ERROR
These redirection constants are used to indicate the process' current sys$input, sys$output, sys$command, and sys$error files. We will see how these are used a little later in the article.

When a new process is created, the standard redirection devices are assigned to the process' terminal. After we create the process in the kernel startup (as discussed in article 38), we call the FiP.Attach_Device method.

function TUOS_FiP.Attach_Device( Device : PChar ; PID : TPID ) : TUnified_Exception ;

var D : TDevice ;
    S : TString ;

begin
    // Setup and sanity check...
    Result := Set_Last_Error( nil ) ;
    D := Get_Device( string( Device ) ) ;
    if( D = nil ) then // Device not found
    begin
        Set_Last_Error( Create_Error( UOSErr_Device_Not_Found ) ) ;
        Result := Last_Error ;
        exit ;
    end ;
First we clear any pending exceptions. Next we obtain the device passed to us. If the device doesn't exist, we return an exception.

    // Assign device ownership to Process...
    Result := Assign_Device( Device, PID ) ;
    if( Result <> nil ) then
    begin
        exit ;
    end ;

    D.Attached := PID ; // Attach to PID...
Next we assign the device to the process. If there was an exception, we exit. Then we mark the device as being assigned to the process.

    // Create standard I/O logicals...
    S := TString.Create ;
    try
        S.P := Device ;
        S.Len := length( string( Device ) ) ;
        SSC.Set_Symbol( LNM_JOB, PID, 'sys$input', S ) ;
        SSC.Set_Symbol( LNM_JOB, PID, 'sys$output', S ) ;
        SSC.Set_Symbol( LNM_JOB, PID, 'sys$command', S ) ;
        SSC.Set_Symbol( LNM_JOB, PID, 'sys$error', S ) ;
        Assign( PID, 'sys$input:', RH_SysInput, 3, '', 0 ) ;
        Assign( PID, 'sys$output:', RH_SysOutput, 3, '', 0 ) ;
        Assign( PID, 'sys$error:', RH_SysError, 3, '', 0 ) ;
        Assign( PID, 'sys$command:', RH_SysError, 3, '', 0 ) ;
    finally
        S.Free ;
    end ;
end ; // TUOS_FiP.Attach_Device
Finally we define the four standard I/O logicals for the process (hence the use of the LNM_JOB table). Then we assign the passed device to each of the relocation handles.

The Assign method is used to assign a device to a process handle.

function TUOS_FiP.Assign( PID : TPID ; Name : string ; Channel : word ;
    Access : word ; Mailbox : string ; Flags : word ) : TUnified_Exception ;

var Resource : TResource ;

begin
    Result := nil ;
    case Channel of
        RH_SysInput, RH_SysOutput, RH_SysCommand, RH_SysError: ;
        else
            begin
                Set_Last_Error( Create_Error( UOSErr_Invalid_Channel ) ) ;
                exit ;
            end ;
    end ; // case Channel
The first thing we do is validate the handle. At this point, the only valid handles are the relocation handles. If any other value is passed, we return an error.

    Resource := Create_File_Handle( PID, PChar( Name ), Flags ) ;
    if( Resource = nil ) then
    begin
        Result := Last_Error ;
        exit ;
    end ;
Next we create a file handle and assign it to Resource. The TResource class is used as an indirection to the actual class instance for the device or file. Why this indirection? We will discuss the advantages of it in a future article. We'll look at the TResource class below.

    case Channel of
        RH_SysInput: // SYS$INPUT
            begin
                if( Resource._File.Write_Only ) then
                begin
                    Result := Set_Last_Error( Create_Error( UOSErr_Read_Only ) ) ;
                    exit ;
                end ;
                USC.Assign_Channel( PID, Channel, int64( Resource ) ) ;
            end ;
        RH_SysOutput: // SYS$OUTPUT
            begin
                if( Resource._File.Read_Only ) then
                begin
                    Result := Set_Last_Error( Create_Error( UOSErr_Read_Only ) ) ;
                    exit ;
                end ;
                USC.Assign_Channel( PID, Channel, int64( Resource ) ) ;
            end ;
        RH_SysCommand: // SYS$COMMAND
            begin
                if( Resource._File.Write_Only ) then
                begin
                    Result := Set_Last_Error( Create_Error( UOSErr_Read_Only ) ) ;
                    exit ;
                end ;
                USC.Assign_Channel( PID, Channel, int64( Resource ) ) ;
            end ;
        RH_SysError: // SYS$ERROR
            begin
                if( Resource._File.Read_Only ) then
                begin
                    Result := Set_Last_Error( Create_Error( UOSErr_Read_Only ) ) ;
                    exit ;
                end ;
                USC.Assign_Channel( PID, Channel, int64( Resource ) ) ;
            end ;
    end ; // case Channel
end ; // TUOS_FiP.Assign
Next we check the specific relocation handle and make sure we have the necessary access mode to the device (not read-only for sys$output and sys$error, and not write-only for sys$input and sys$command). Finally we assign the channel via the Assign_Channel method of the USC (discussed below).

Now let's look at the Create_File_Handle method.

function TUOS_FiP.Create_File_Handle( PID : TPID ; Name : PChar ;
    Flags : longint ) : TResource ;

var Device : TDevice ;
    Dummy : integer ;
    F : TUOS_File ;
    S : string ;

begin
    Result := nil ; // Assume failure
    Set_Last_Error( nil ) ;
    S := Resolve_Path( PID, string( Name ), True ) ;
    Dummy := pos( ':', S ) ;
    if( Dummy = 1 ) then // Start with colon
    begin
        Set_Last_Error( Create_Error( UOSErr_Invalid_Filename ) ) ;
        exit ;
    end ;
First, we resolve the passed path/name. If the name starts with a colon, the name is invalid and we exit with an error.

    if( Dummy = 0 ) then // No colon - a file on a file system
    begin 
        .
        .
        .
    end else
    begin
        Device := Get_Device( copy( S, 1, Dummy ) ) ;
        if( Device = nil ) then
        begin
            Set_Last_Error( Create_Error( UOSErr_Device_Not_Found ) ) ;
            exit ;
        end ;
If there is no colon at all, the passed name is a file name. We will handle that case in the future. Otherwise, it is a device and we get the device instance for that name. If it is invalid we exit with an error.

        S := copy( S, Dummy + 1, length( S ) ) ; // Get path/file after device
        if( Device.Device_Class = DFC_Stream ) then // Stream device
        begin
            if( Device._File <> nil ) then
            begin
                F := Device._File ;
            end else
            if( Device.Terminal <> nil ) then
            begin
                F := TFip_Terminal_File.Create( Kernel ) ;
                TFip_Terminal_File( F ).Terminal := Device.Terminal ;
                Device._File := F ;
            end else
            begin
                F := TFiP_Device_File.Create( Kernel ) ;
                TFiP_Device_File( F ).Device := Device ;
                Device._File := F ;
            end ;
        end else
Every device can have a file instance associated with it, although we don't make the association until there is a need. But if a file instance is ever associated, it remains asscciated from then on. So, the first thing we do is see if a file has already been associated. If so, we will use that file. Otherwise, we check to see if the device is a terminal. In that case, we create a TFiP_Terminal_File instance and associate it with the device. If no file is already associated and is not a terminal, we create a TFiP_Device_File instance and associate that with the device. The TFip_Device_File is used for generic access to devices, which we will discuss in a future article.

        if( not Device.Mounted ) then
        begin
            Set_Last_Error( Create_Error( UOSErr_Device_Not_Mounted ) ) ;
            exit ;
        end else
        if( Device.FS = nil ) then // No file structure on device
        begin
            if( ( Flags and FO_Block_Mode ) = 0 ) then // Opening in non-file-structured mode
            begin
                Set_Last_Error( Create_Error( UOSErr_Device_Not_File_Structured ) ) ;
                exit ;
            end ;
            if( Device._File <> nil ) then
            begin
                F := Device._File ;
            end else
            begin
                F := TFiP_Store_File.Create( Kernel ) ;
                TFiP_Store_File( F ).Store := Device.Store ; // Store
                Device._File := F ;
            end ;
        end else
If the device class isn't a stream, and the device is not mounted, we generate an exception. Otherwise, we see if there is a file system associated with the device. If not, then we assume the user wants to open the device in non-file-structured (or "block") mode. But if the user hasn't verified this by using the FO_Block_Mode flag, we generate an error and exit. Otherwise, we use the associated file, if any, or else we create a TFip_Store_File and associate that with the store.

        begin
            F := Open_File( PID, PChar( S ), Flags ) ; // Normal file...
            if( F = nil ) then // Error occurred
            begin
                exit ;
            end ;
        end ;
    end ;
On the other hand, if there is a file system on the device, we simply request an open on the file.

    Result := TResource.Create ;
    Result._File := TFile( F ) ;
    TFiP_File( F ).Add_Handle( Result ) ;
    USC.Add_Handle( PID, int64( Result ) ) ;
end ; // TUOS_FiP.Create_File_Handle
Once we have the proper file (in variable F), we create a resource instance and associate the file with the resource. Then we call USC.Add_Handle to add the handle to the process.
We will address the TFiP_Device_File and TFiP_Store_File classes in a later article.

Now let's look at the Assign_Channel method of the USC. First we need to add some instance data to the TProcess class:

public // Handles...
    _Sys_Input : THandle ; // Handle for sys$input
    _Sys_Output : THandle ; // Handle for sys$output
    _Sys_Command : THandle ; // Handle for sts$command
    _Sys_Error : THandle ; // Handle for sys$error
    _Handles : TInteger_List ; // Handles owned by process

Now the Assign_Channel method itself:
procedure TProcess.Assign_Channel( RC : word ; Handle : THandle ) ;

    procedure Update_Channel( var H : THandle ) ;

    begin
        if( H <> Handle ) then // Handle is being changed
        begin
            if( H <> 0 ) then // Have a handle
            begin
                FiP.Close_Handle( H ) ;
                _Handles.Remove( H ) ;
            end ;
            H := Handle ;
            if( Handle <> 0 ) then
            begin
                _Handles.Add( H ) ;
            end ;
        end ;
    end ;

begin
    case RC of
        RH_SysInput : Update_Channel( _Sys_Input ) ;
        RH_SysOutput : Update_Channel( _Sys_Output ) ;
        RH_SysCommand : Update_Channel( _Sys_Command ) ;
        RH_SysError : Update_Channel( _Sys_Error ) ;
    end ;
end ; // TProcess.Assign_Channel
Note that "channel", an older term, is synonomous with "handle". As you can see from the new instance data, we have a list of handles, and the four standard handles. The four standard handles are stored in the _Handles list, but we also have the handles in separate variables for quick access. We switch based on the relocation handle passed to us, calling the local Update_Channel procedure with the corresponding standard handle. We'll discuss Close_Handle below.
In Update_Channel, we first check that we are actually changing the channel value. If so and the passed handle is not 0, we close the handle and remove it from the handle list. In either case, we add the new handle to _Handles and set the standard handle value.
A THandle is a 64-bit integer value which contains the address of a TResource instance. You can think of it as simply a pointer to an instance.

procedure TUSC.Add_Handle( PID : TPID ; Handle : THandle ) ;

var Process : TProcess ;

begin
    if( PID = 0 ) then
    begin
        Process := Get_Process( Kernel.PID ) ;
    end else
    begin
        Process := Get_Process( PID ) ;
    end ;
    if( Process = nil ) then
    begin
        Set_Error( UOS_User_Security_Error_Invalid_PID ) ;
        exit ;
    end ;
    Process._Handles.Add( Handle ) ;
end ;
This method simply adds a handle to a process' _Handles list. The passed process ID is used, unless it is 0 - in which case, the current process is used. If an invalid process ID is passed, we generate an error.

Now let's look at TResource:

TResource = class( TCommon_COM_Interface )
                public // API...
                    PID : TPID ;
                    Position : int64 ; // Current position
                    _File : TFile ;
            end ;
The class has a process ID, a context (position), and the file object associated with the handle. As mentioned earlier, a TResource instance is an indirection to the actual file instance. This is to handle multiple processes accessing some files (a topic for a future article). Each TResource is owned by a single process*, but each file can be referenced by multiple TResources.

* it is possible for a process to share a handle under some circumstances.

Now let's look at the FiP's Close_Handle method.

function TUOS_FiP.Close_Handle( Handle : THandle ) : TUnified_Exception ;

var Resource : TResource ;

begin
    if( not USC.Remove_Handle( 0, Handle ) ) then
    begin
        Result := Set_Last_Error( Create_Error( UOSErr_Invalid_Handle ) ) ;
        exit ;
    end ;
    Result := Set_Last_Error( nil ) ;
    Resource := TResource( Handle ) ;
    TFiP_File( Resource._File ).Remove_Handle( Resource ) ;
    Resource.Free ;
end ;
The process of closing a handle gets rid of the TResource, releasing any memory for, and references to, the device. After that happens, the handle is no longer valid. The first thing we do is remove the handle from the process via the Remove_Handle method of the USC. If that fails, we return with an execption. Otherwise, we clear any pending exceptions, remove the handle's association with the file, and then we free the TResource instance.

Here is the Remove_Handle method.

function TUSC.Remove_Handle( PID : TPID ; Handle : THandle ) : boolean ;

var Index : integer ;
    Process : TProcess ;

begin
    Result := False ;
    if( PID = 0 ) then
    begin
        Process := Get_Process( Kernel.PID ) ;
    end else
    begin
        Process := Get_Process( PID ) ;
    end ;
    if( Process = nil ) then
    begin
        Set_Error( UOS_User_Security_Error_Invalid_PID ) ;
        exit ;
    end ;
    Index := Process._Handles.ItemIndex( Handle ) ;
    if( Index = -1 ) then // Handle not found
    begin
        exit ;
    end ;
    Result := True ;
    Process._Handles.Delete( Index ) ;
end ;
First we get the process instance and return an error if it is not found. Then we check the _Handles list for the process to see if the process owns that handle. It is possible that some invalid value could be passed to us from the user. If we treat an invalid value as a pointer to a class instance, we could crash the executive. Further, we don't want one process accessing the handles of another process (unless the first process has granted access to the second). Nor do we want to allow access to a handle that has been closed. For these reasons, whenever a handle is referenced, we will check against the process _Handles list to see if it is a valid handle for that process. If the passed handle is anything other than a valid handle in the list, we do nothing. Otherwise, we remove the handle from the list. Regardless of what happens to the handle after this point, it is no longer a valid handle for this process.

In the next article, we will look at the FiP file interfaces and the link between the application and the executive, which transfers output to TTerminal.