EXCP I/O – Introduction

EXCP is the facility that allows us the ability to perform device-level I/O without having to the heavy lifting.  It handles the details like pagefixing buffers, scheduling an I/O request, and processing interrupts so we don’t have to worry about these things.  We can focus on building our channel programs that instruct the device.

To use EXCP we need an open DCB.  The DCB is the connection between our program and the actual device we are communicating with.  There is very little useful information in a EXCP DCB except that it points to a Data Extent Block (DEB).  The DEB is created as part of the open process and it authorizes our access to the device.  For example the DEB indicates if we can write to the device or if our access is read-only.  For DASD devices it also indicates which tracks we have access to.  The DEB also points to the UCB providing the link to the physical device.

+-----+     +-----+     +-----+     +-----+     +-----+
| UCB |<====| DEB |<====| DCB |<====| IOB |====>| CCW |
+-----+     +-----+     +-----+     +-----+     +-----+

An EXCP request is defined by an Input/Output Block (IOB).  The IOB points to the DCB as well as the Channel Command Word (CCW) chain.  A CCW is an instruction to an I/O device (Channel, Control Unit, or Device).  CCWs may be chained so that as soon as execution of a CCW has successfully completed anohter CCW will be processed.  If a CCW fails then no further CCWs in the chain are executed. Information about the success or failure of a CCW program is returned in the IOB.  This includes status information from the Channel Status Word (CSW).  The CSW is the communication area between the CPU and the I/O subsystem (Channels, Control Units, and Devices).

Most of the interesting part of EXCP programming centers around building the CCW chains.  We will start with looking at the CCW.

CCW

A CWW is 64 bits (8 bytes) in length.  Here is the layout of the CCW

 0        8        16       24       32       40       48       56
+--------+--------+--------+--------+--------+--------+--------+--------+
|  CMD   |    Data Addr (24 bits)   | FLAGS  |00000000|    Length       |
+--------+--------+--------+--------+--------+--------+--------+--------+

Command Code (Bits 0-7) Specifies the I/O operation to be performed.

Data Address (Bits 8-31) Specifies the location in main storage for the data associated with the I/O operation.

Chain Data Flag (CD) (Bit 32) When set to one indicates the chaining of data. It causes the storage area of the next CCW to be used with the current I/O operation. The use of data chaining allows data to be transferred to or from noncontiguous areas of storage in a single I/O operation.

Command Chain (CC) (Bit 33) When set to one, and when the CD flag is set to zero, indicates the chaining of commands. When the operation of the current CCW is complete and Command Chaining is active, the next CCW will become the new current I/O operation.

Suppress Length Indication (SLI) (Bit 34) When set to one and the CD flag is set to zero, the incorrect length indication is suppressed.

Skip (SKIP) (Bit 35) When set to one the transfer of information is suppressed for read and sense operations.

Program Controlled Interrupt (PCI) (Bit 36) When set to one the channel will generate an interruption condition when the CCW begins execution in the channel.

Indirect Data Address (IDA) (Bit 37) When set to one indicates indirect addressing. The data address of the CCW points to an Indirect Address List instead of directly to the data.

Count (Bits 48-63) Specifies the number of bytes to be used for the I/O operation.

Bits 38-47 should always be set to zeros.

Channel command codes are specific to each individual device but fall into general categories: Write, Read, Read Backward, Control and Sense. Write transfers data from the processor storage to the device, read and read backward transfer data from the device to processor storage. Control operations are specific to each device and generally do not involve data transfer but the data address in the CCW may point to an area in storage related to the control operation.

DASD I/O Programming

Now we can look specifically at CCW programming to access DASD devices.  For this discussion I will work with the 3350 device.  The concepts here can be applied to other types of DASD devices.  The reason I am using the 3350 is because this device is natively supported in MVS 3.8 and does not require any additional usermod support.  It is also the device I have used the most over the years.

In programming for DASD devices we have to think about how the devices were originally constructed.  There were a series of round platters stacked on about the other.  There was an access arm the contained read/write heads with one head for each platter.  The arm could move inward and outward from the center of the platter to the outer edge in specific increments.  Each of these increments defined a circular track at some offset on the platter.  The collection of tracks (one for each platter) made up a cylinder.  If you are not familiar with this concept there are sevearl sources of documents that explain it better using pictures.

With a basic understanding of the structure of the DASD device we can understand how a specific location (data record) is referenced.  Each track has a unique address that consists of a two byte cylinder address and a two byte head address.  These addresses are fixed by the physical layout and construction of the device.  Each track may contain a variable number of data records based on the size of the records and the size of the track.  Therefore to identify a specific record we have an address in the form CCHHR (2 byte cylinder address, 2 byte head address, 1 byte record address).

Each track contains some special records that are created when the device is formatted and normally there is no need to accees them.  These include the Home Address (HA) and Record Zero.  Data records begin with record number one.

The 3350 has 555 cylinders.  Each cylinder has 19 tracks (heads).  Each track has a maximum capacity of 19,069 bytes.  This infomation will be needed when writing CCWs to access the device.  This will be different for different types of DASD devices.

To read a specific record we have to first position the heads to the correct cylinder this is done with the SEEK command.  The command code for SEEK is X’07’ and it expects six bytes of data containing the seek address in the format 00CCHH where 00 is two bytes of binary zeros, CC is the two byte cylinder address and HH is the two byte head address.

SEEKCCW  DC    X'07',AL3(SEEKADDR),X'40',x'00',AL2(6)
            :
            :
*                  C C H H 
SEEKADDR DC    X'0001000001'    Cyl=256, H=1

This CCW command would cause the heads to be positioned at cylinder 256 and select the head for track one.  Notice that the X’40’ flag bit is set indicating Command Chaining.  Simply seeking to a specific cylinder would not accomplish much so we need to follow up with additional CCW commands to actually do something useful.  Setting the Command Chain bit tells the channel that another CCW immediately follows this one.

When using EXCP to access a DASD device we don’t issue a SEEK command.  In fact MVS will prevent us from issuing a SEEK command.  This is because there are usually multiple datasets contained on a DASD volume.  MVS keeps us from reading or writing to areas of the device we are not authorized to access.  We will quickly review how this is accomplished.

Each dataset is made up of one or more extents.  An extent is a set of contiguous tracks or cylinders.  The VTOC is a special file on each volume that keeps track of where datasets are located.  This information is maintained in a Dataset Control Block (DSCB).  The DSCB has a list of the extents that make up the dataset.  Open processing reads the DSCB and uses the extent information to build the DEB.  The DEB is contained in protected storage and can not be modified by a non-authorized user.

When an EXCP request is submitted for a DASD device MVS gets the seek address from the IOB, validates it against the DEB to verify it is contained within an extent of the dataset.  If the seek address is not contained within a valid dataset extent the request is rejected.  If the seek address is valid then MVS builds a SEEK CCW using the IOB seek address.  A SET FILE MASK CCW is then chained on to the SEEK CCW.  The SET FILE MASK specifies what types of CCW commands may follow.

         DC    X'1F',AL3(FILEMASK),X'40',x'00',AL2(1)         --- Set File Mask CCW
             :
             :
FIELMASK DC  X'file-mask-bits'

File Mask
00......  - Do not allow Write Home Address and Write Record Zero
01......  - Do not allow any write commands
10......  - Do not allow Write Format commands
11......  - Allow all write commands
...00...  - Allow all Seek commands
...01...  - Allow Seek Cylinder and Seek Head commands 
...10...  - Allow Seek Head commands
...11...  - Do not allow Seek commands or head switching
......0.  - Do not allow Diagnostic Write commands
......1.  - Allow Diagnostic Write commands
.......0  - Not PCI Fetch mode
.......1  - PCI Fetch mode
..0..0..  - Must always be Zero

There may only be one SET FILE MASK command in a CCW sequence. This keeps the user CCW program from accessing tracks outside the dataset extents. It also enforces read-only for datasets opened for input.  The users CCWs follow the SET FILE MASK command.

Once the correct cylinder and head have been selected by the SEEK command we need to position for the record we wish to read or write.  This is done with a SEARCH command.  To understand how to use SEARCH we have to understand the physical layout of the track.

As we said earlier the track always begins with two special records, the Home Address and Record Zero.  Data records are made up of three components, the count, the key, and the data area.  The key is optional an may be omitted.  The count is always required.  It is made up of 8 bytes in the format CCHHRKDL.  CC is the cylinder number, HH is the head, R is the record number, K is the length of the key area, and DL is two-byte length of the data area.  If the key length is zero then there is no key area for the record.  If the key length and data length of the block are both zero the record is an end of file indicator.

+---+---+---+---+---+---+---+---+ 
| C   C | H   H | R | K |  DL   |
+---+---+---+---+---+---+---+---+

Below is the layout of a track. The track begins at the Index Point which physically identifies the beginning of every track. Immediately following the index point is the Homa Address (HA).  The Home Address contains track information and is used to indicate if an alternate track is in use.  Alternate tracks are special reserved tracks used to replace a defective track.  A special record called Record Zero (R0) is always present immediately following the Home Address.  R0 does not contain any user data but is part of the standard track formatting.  It is necessary to allow track positioning using the SEARCH CCW command.  The HA and R0 are written using special CCW commands when the volume is formatted.  There is not normally any reason to read or write these special records.  User data begins with Record One (R1) and may be followed by additional user records.  An empty track contains only the HA and R0.

[IX] [HA] [R0-CKD] [R1-CKD] [R2-CKD] ... [Rn-CKD]

When setting our position around a track (a position to a specific record) we use the SEARCH CCW command.  The SERACH command compares the serach value (specified by the SRARCH CCW) against the next record that passes the read heads.  If there is not a match the CCW completes normally.  If the values to match the CCW completes but also sets the Status Modifier bit in the CSW.  This tells the channel that a match has occured and causes the channel to skip over the next CCW immediately following the SEARCH.  This allows us to set up a loop in our channel program to locate the desired record:

        CCW   SEARCH
        CCW   TIC,*-8
        CCW   READ

In this example we have three CCW commands. The first is a SEARCH followed by a Transfer In Channel (TIC) CCW. A TIC is an unconditional branch in a channel program. The address portion of the TIC CCW points to the next CCW to be executed by the channel. In this case the TIC will branch back to the SEARCH (*-8 refers to the previous CCW since each is eight bytes in length). When we start this channel program the first CCW executed is the SEARCH. If the search does not match then the channel will fall through to the next command, a TIC that branches back to the SEARCH. This will be repeated until the SERACH matches a record on the track at which point the channel will skip the command immediately following the SEARCH (in this case the TIC) and the READ CCW will be executed by the channel to read the selected record.

We do have to deal with the situation where the SEARCH never matches. In this case the Status Modifier bit will never be set and it would appar that we would be in a infinite loop that never ends. The loop is broken when the dasd device detects two Index Points during an uninterrupted search CCW sequence. By the time the Index Point has passed the read heads twice the deivce knows that every record on the track has been checked at least once. When this happens the CCW is terminated abnormally and the Unit Check bit is set in the CSW. This causes the channel to end the current program.

There are two types of SERACH commands, one searches by ID which is the CCHHR in the count area.  The othter type searches by key data contents.  Both of the types search for either an equal condition, high condition, or a equal or high condition.  The following table shows the results of various combinations of the SEARCH ID CCW in relation to a record.  The values are in the format CC-HH-R. (The SEARCH ID command may search for R0).


Search CCW Value | Count Area for Record | ID Equal | ID High | ID Equal or High
--------------------------------------------------------------------------------
   01-01-3       |      01-01-1          |  False   |  False  |      False
   01-01-3       |      01-01-3          |  True    |  False  |      False
   01-01-3       |      01-01-4          |  False   |  True   |      True

There are also three SEARCH KEY commands (key equal, key high, key high or equal) that compare the search contents of the CCW against the data in the key area of the record.

When we indicated what happens when two Index Points are encountered is true for the normal SEARCH commands but there is a second type of each of the SEARCH commands that are Multi-Track commands.  If the SEARCH is a multi-track request and the end of the track is reached (two Index Points encountered) the device will switch to the next head and continue the search.  This allows an entire cylinder to be searched using one CCW program and an single I/O request.  If the end of the last trrack of a cylinder is detected and the search has not been satsified the CCW terminates with the Unit Check bit set in the CSW.  It is important to note that the multi-track CCWs can only be used if allowed by the settings in the SET MASK instruction that preceeded the SEARCH.  MVS sets the authorization in the SET MASK based on how the dataset was allocated.  If it was allocated in cylinders then MVS will allow SEEK HEAD and Multi-Track operations.  If the dataset was allocated in tracks then SEEK HEAD and Multi-Track operations are not allowed so that the user program can never access tracks not belonging to the data set.

Below are the various CCW command codes for the SEARCH commands:


   CCW Command     Normal  Multi-Track
Search ID Equal      31         B1
Search ID High       51         D1
Search ID Eq/High    71         F1

Search Key Equal     29         A9
Search KEY High      49         C9
Search KEY Eq/High   69         E9

Now one thing we do have to consider is which record to search for.  If we want to read or write the Key  or Data area of a record we search for the ID of the record we want to access.  If we want to read or write the Count area we must position based on the ID of the previous record.  This is because once the device has read the count area for a Search ID equal the count area for that record is now past the read/write heads.  So if we want to read the Count, Key and Data area for record number two we would have to do a search ID for record number one.  If we just want to read the Data area for record number two we would do a search for record number two.  This can get a little confusing but it does make great sense when you consider what is physically happening on a dasd device.  Using Hercules we need to think in terms of how an actual 3350 device was constructed and how it operated since that behaviour is emulated.

Here are additional CCW Command Codes for accessing dasd devices:


   CCW Command     Normal  Multi-Track
Read Count           12         91
Read R0              16         96
Read Data            06         86
Read Key & Data      0E         8E
Read CKD             1E         9E
Read Multiple CKD    5E

Write CKD            1D         -
Write Data           05         -
Write Key & Data     0D         -

Erase                11         -

Writing An EXCP Program

Now we are ready to take on the rest of the code necessary to execute our own channel programs using EXCP.  An I/O request is represented by an Input Output Block (IOB).  Here is the layout of the IOB.

IOB      DS    0F
IOBFLAGS DS    XL2                 IOB FLAGS                                     
*                                                          
IOBSENSE DS    XL2                 SENSE BYTES 0 AND 1
IOBECBAD DS    A                   ADDRESS OF ECB          
IOBCSW   DS    A                   CHANNEL STATUS WORD     
IOBCSWFG DS    XL2                 CSW FLAGS               
IOBRESDL DS    H                   RESIDUAL COUNT          
IOBCCWAD DS    A                   ADDRESS OF CCW CHAIN    
IOBDCBAD DS    A                   ADDRESS OF DCB          
         DS    2A                                          
IOBSEEK  DS    XL3                 SEEK ADDRESS   MBBCCHHR 
IOBSRCH  DS    XL5                 SEARCH ADDRESS    CCHHR

The IOB is mapped by the system macro IEZIOB but it is much less confusing to define the IOB ourselves sicne we are only concerned with the fields related to EXCP processing.

IOBFLAGS       Two bytes of flag bits that provide information about our I/O request to the EXCP code.  Some of the flags in the first byte we are interested in are:

IOBDATCH EQU   X'80'    - Data Chaining Used In CCW Program
IOBCMDCH EQU   X'40'    - Command Chaining Used In CCW Program
IOBUNREL EQU   X'02'    - Unrelated (Non-Sequential) Request
IOBSPSVC EQU   X'01'    - Do Not Use BSAM, BPAM, QSAM I/O Appendages

IOBSENSE      Two byte area where EXCP returns the first two sense bytes from the device if execution of the channel program does not complete successfully and Unit Check is returned in the CSW.
IOBECBAD    Address of an ECB that is posted when the EXCP request completes. The high order byte will contain the completion for the request.

7F - Normal I/O Completion
42 - Extent Error
41 - I/O Completed With Error/Exceptional Status

There are other completion codes that may be returned but these what we normally expect to encounter.
IOBCSW        CCW address from the CSW.
IOBCSWFG  Device Status and Channel Status bytes from the CSW.
IOBRESDL   Residual Count from the CSW.
IOBCCWAD Is the address of the first CCW in the channel program.
IOBDCBAD  Address of the DCB.
IOBSEEK      DASD Seek address (MBBCCHHR)

Here are the highlights of a program that reads a dsad device using excp.  I don’t include all of the program here, just the parts directly related to performing an EXCP.  Please refer to program EXCP01 in the downloads.

         OPEN  (EXCPDCB,INPUT)  
         TM    EXCPDCB+48,X'10' 
         BO    OPEN020

We begin by opening our EXCP DCB. Here we check the DCB open flags to verify the DCB open request was successful.

EXCPDCB  DCB   DSORG=PS,MACRF=E,DDNAME=EXCP

Here is the DCB definition. Note the MACRF type of ‘E’ for EXCP.

IOB      DS    0F                      
IOBFLAGS DC    XL2'4300'               
IOBSENSE DC    XL2'0000'               
IOBECBAD DC    A(ECB)                  
IOBCSW   DC    A(0)                    
IOBCSWFL DC    XL2'0000'               
IOBRESDL DC    H'00'                   
IOBCCWAD DC    A(CCWSRCH)              
IOBDCBAD DC    A(EXCPDCB)              
         DC    2A(0)                   
IOBSEEK  DC    XL3'000000'       MBB   
IOBSRCH  DC    XL5'0000000000'   CCHHR

Here is our IOB. Note the IOBFLAGS value. The IOB points to an ECB, our opened DCB, and our CCW chain.

ECB      DC    F'0'                                 
*                                                   
*                                                   
         DS    0D                                   
CCWSRCH  DC    X'31',AL3(IOBSRCH),X'40',X'00',AL2(5)
CCWTIC   DC    X'08',AL3(CCWSRCH),X'40',X'00',AL2(0)
CCWREAD  DC    X'06',AL3(IOBUF),X'00',X'00',AL2(80)

Here is our ECB and our CCW chain. The first CCW is a SEARCH for ID Equal. It is follow by a TIC that points back to the Search. Finally we have a READ DATA CCW that attempts to read 80 bytes.

Now we have to deal with getting the actual disk address (MBBCCHHR) of the record we want to read. We will refer to our dataset using realative track address that are converted to physical address. A relative address simply refers to a dasd dataset as a collection of tracks. It doesn’t deal with cylinders or extents. The first relative track is zero, followed by track one, etc. MVS does provide a routine to convert from relative address (TTR – Track/Record) to physical address (MBBCCHHR). We are familiar with the CCHHR part of the address. The BB is always two bytes of zeros. The M is the extent number (beginning with zero) of the dataset extent containing the track.

*                                                                      
***********************************************************************
*    CONVERT RELATIVE TRAK TO CCHH                                     
*       R1 = RELATIVE TRACK (TT)                                       
*       RESULT STORED IN MBBCCHHR                                      
*       (MODIFIED R1,R2,R3,R4,R5,R15)                                  
***********************************************************************
*                                                                      
GETCCHH  DS    0H                                                      
         ST    R14,XTGET

Here is the begenning of our converstion routine. On entry R1 contains the relative track number. The physical address will be calculated and stored in a field called MBBCCHHR. This routine ignores the record number since it does not require any conversion.

         LA    R2,EXCPDCB              POINT TO DCB     
         L     R2,44(,R2)              POINT TO DEB     
         SLR   R3,R3                                    
         IC    R3,16(,R2)              NUMBER OF EXTENTS
         LA    R5,32(,R2)              FIRST EXTENT INFO
         SLR   R4,R4                                    
         SLR   R2,R2

First we get the DEB address from the DCB. Next we get the number of dataset extents from the DEB. I am using offsets into the control blocks but you can use the mapping macros (IEZDEB). The first extent begins at offset 32 in the DEB. At this point R3 contains the number of extents and R5 has the address of the first extent data.

GET010   DS    0H                                                
         ICM   R4,B'0011',14(R5)       NUMBER OF TRACKS IN EXTENT
         CR    R1,R4                   DOES IT FIT IN THIS EXTENT
         BL    GET020                     YES - BRANCH

Here is the main loop of our conversion routine. Here we get the number of tracks in this extent. We compare it to the relative track nubmer to see if the relative track is in this extent. If so we can exit the loop.

         SR    R1,R4                   ADJUST RELATIVE TRACK  
         LA    R5,16(,R5)              POINT TO NEXT EXTENT   
         LA    R2,1(,R2)               BUMP EXTENT COUNT      
         BCT   R3,GET010               LOOP BACK              
*                                                             
         B     GETRC12                 TRACK NOT VALID

If the relative track number is larger than the tracks in the current extent we subtract the number of tracks in the current extent from the relative track number. We then increment our pointer to the next extent entry in the DEB. We also keep track of the extent number in R2. We use a BCT instruction to loop back as long as we have more extents to process. If we exhaust all extents then the relative track is not contained within any dataset extent and we return with a RC of 12 in R15.

GET020   DS    0H                                          
         XC    MBBCCHHR(0),MBBCCHHR    CLEAR RESULT AREA   
         STC   R2,MBBCCHHR             SAVE "M" (EXTENT #) 
         LH    R2,6(,R5)               START CC            
         AH    R1,8(,R5)               ADD IN START HH     
         SLR   R0,R0                                       
         L     R15,=F'30'             3350 - 30 TRKS/CYL   
         DR    R0,R15                                      
         AR    R2,R1                   UPDATE CC           
         STCM  R2,B'0011',MBBCCHHR+3   SAVE CC
         STCM  R0,B'0011',MBBCCHHR+5   SAVE HH

Once the proper extent is located we complete the conversion process. We begin by clearing the result area and storing the extent number in the “M” byte. We get the beginning cylinder of the extent in R2. We then add the beginning head (track) of the extent to our remaining relative track value. We divide this total track value by the number of tracks in a cylinder. For a 3350 this number is 30. This code must be adjusted for other device types. We add the beginning cylinder of the extent to the result to get our “CC” value. The remainder is our “TT” value. These values are saved into the result area.

         SLR   R15,R15                 SET RC 
         L     R14,XTGET                      
         BR    R14                            
*                                             
*                                             
GETRC12  DS    0H                             
         LA    R15,12                  SET RC 
         L     R14,XTGET                      
         BR    R14

And we finish up with our exit code.

         LA    R6,1          RECORD NUMBER, INIT TO ONE 
         LA    R1,0          RELATIVE TRACK NUMBER      
         BAL   R14,GETCCHH   CONVERT TO MBBCCHHR        
         LTR   R15,R15            CHECK RETURN CODE     
         BZ    READ010            - BRANCH IF GOOD      
*                                                       
         WTO   'TTR CONVERSION FAILED',ROUTCDE=(1,11)   
         B     EXIT

Now we are read to read some records using EXCP. We will keep our current record number in R6. We start with 1 since that is the first data record (remember R0 is not used for data). We set our relative track number (zero) in R1 and call our conversion routine to get the physical address. We do check the return code to verify the result is valid.

READ010  DS    0H                                           
         MVC   IOBSEEK(8),MBBCCHHR                          
         STC   R6,IOBSEEK+7  PUT IN RECORD NUMBER           
*                                                           
         XC    ECB,ECB            CLEAR ECB                 
         EXCP  IOB                ISSUE I/O REQUEST         
*                                                           
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE  
*                                                           
         BAL   R14,PRINTIOB       GO FORMAT IOB             
*                                                           
         CLI   IOBECBAD,X'7F'     NORMAL COMPLETION         
         BNE   EXIT                                         
*                                                           
         LA    R6,1(,R6)          NEXT RECORD NUMBER        
         B     READ010

Here is the main read loop. First we copy the physical track address into our IOB. We then store in our current record number. Next we clear our ECB, issue the EXCP, and wait for the I/O to complete. I have written a routine called PRINTIOB that will format various fields from the IOB and print them (see the EXCP01 program for the code). We then check the completion code. If there were no exceptional conditions we increment the record number and issue the EXCP again. We continue until an exceptional condition results.

//GENR   EXEC  PGM=IEBGENER                 
//SYSPRINT DD  SYSOUT=*                     
//SYSIN    DD  DUMMY                        
//SYSUT2   DD  DSN=TCS3.EXCP01.DATA,        
//         DISP=(NEW,CATLG),                
//         UNIT=SYSDA,VOL=SER=WORK01,       
//         SPACE=(CYL,(2,2)),               
//         DCB=(BLKSIZE=80,LRECL=80,RECFM=F)
//SYSUT1   DD  *                            
0001                                        
0002                                        
0003

I created a test dataset to read using IEBGENER. It is a simple non-blocked 80/80 dataset containing three records. Here is the output from our EXCP01 program when run against this test data.

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 095E70 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 0000 (      )
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000001AE000001                                           

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 095E70 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 0000 (      )
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000001AE000002                                           

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 095E70 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 0000 (      )
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000001AE000003                                           

I/O REQUEST                                                          
   COMPLETION CODE = 41                                              
   CSW = 095E60 DEV STAT = 0E CHAN STAT = 40 RESIDUAL = 0005 (     5)
   --- DEVICE STATUS  = CE DE UC                                     
   --- CHANNEL STATUS = IL                                           
   SENSE = 0008                                                      
   SEEK = 00000001AE000004

Here we see four I/O requests.  The first three have a completion code of X’7F’ indicating no exceptional condition.  Next the CSW is formatted.  The CSW points past the end of the last CCW executed (we have to back up 8 bytes to get the CCW address).  The Device Status and Channel Status are displayed as hex as well as being formatted on the next two lines.  The residual length is zero indicating we have read 80 bytes of data as expected.  Finally we format the SEEK address and we can see the record number increasing for each request.  Since we wrote three records using IEBGENER we expect to read three records.  When we attempt to read record number four we get a completion code of X’41’ indicating an exceptional condition has occured.  We also notice that in addition to Channel End and Device End the Unit Status bit is set.  We also have Incorrect Length set in the Channel Status, a residual length of 5, and Sense bytes containing X’0008′.  The CCW address points to the TIC CCW so the failing CCW is 8 bytes before it – the Search ID Equal CCW.  This is why the residual length is 5.  The Sense data tells us why we received an Unit Exception.

Sense Byte 0

1... .... - Command Reject - Invalid command or command sequence 
.1.. .... - Intervention Required - Device not ready 
..1. .... - Bus Out Parity Check - Parity check occured transfering data 
...1 .... - Equipment Check - Unusual hardware error 
.... 1... - Data Check - Error reading from device 
.... .1.. - Overrun - Data received later than expected 
.... ..00 - Not used, zero

Sense Byte 1 

1... .... - Permanent Error - Unrecoverbale error has occured 
.1.. .... - Invalid Track Format - Write exceedes track capacity 
..1. .... - End of Cylinder - Multitrack opeartion has encountered cylinder end 
...0 .... - Not used, zero 
.... 1... - No Record Found - Two index points encoutered without interveaning read 
.... .1.. - File Protected - File Mask violated 
.... ..1. - Write Inhibited 
.... ...1 - Operation Incomplete

Since we have a Sense value of 0008 we see that no record was found.  This is exactly what we should expect since we wrote three records to the data set.  That is unless you were expecting the EOF mark to be the fourth record.  The EOF is in our dataset, but it is the first record on the next track.

In Search of EOF

When I first ran the EXCP01 program I expected the EOF to be written on the same track as the data blocks.  There is no reason the EOF could not be contained on the same track.  Over the years I have learned that EOF marks don’t always appear exactly where you might expect to find them.  In fact sometimes there is no EOF record.  If all tracks in all extents are full and there is no room for an EOF record then end of file is assumed to occur after the last block on the last track.  What we do see from our first example is that just because we read a short track (a track that could contain more blocks) it doesn’t mean we shouldn’t continue on with the next track.

Here we will modify the EXCP01 program to continue reading until the EOF record is encountered.  After a great amount of time and consideration I have decided to name it EXCP02.  See the downloads for the full program.

Track Balance

Before continuing we need to consider how many blocks will fit on a track.  I normally think of a 3350 as having a track size of 19,069 bytes.  This is a number that has been etched into my brain.  This is not the track size, it is actually the size of the largest non-keyed block you can write on a track.  The 3350 track size is 19254.  This leads to the next question – why the difference?  To answer the question we need to go back to the physical layout of a track.

[HA] [R0-C] [R0-D] [R1-C] [R1-K] [R1-D] ... [Rn-C] [Rn-K] [Rn-D]

A track always begins with the Home Address record followed by the special Record Zero. The read of the track is used for data records which consist of a Count area, an optional Key area, and a Data area. What we failed to show was the gaps between the records. A gap is some physical space between records that is necessary for the electronics of the read/write heads to function.

^Ix^ {G1} [HA] {G2} [R0-C] {G2} [R0-D] {G3} --->
[R1-C] {G2} [R1-K] {G2} [R1-D] {G3} ... --->
[Rn-C] {G2} [Rn-K] {G2} [Rn-D] {G4}

Here is a more detailed picture of a track.  It begins with the Index Mark.  Next is a Gap-1 that occurs between the index mark and the home address.  Next is the Home Address which is followed by a Gap-2.  A Gap-2 occurs inbetween the count, key, and data areas of a record.  A Gap-3 occurs inbetween records (after a data area and before the next count area).  Finally a Gap-4 occurs after the last data record on the track.  Gap-1, Gap-2, and Gap-3 all have specific sizes.  Gap-4 is always as large as the unused space after the last block.

When we are writing blocks to a track we need to remember that we must account for the size of the gaps in addition to the size of the blocks.  Earlier I said the 3350 track size was 19,254.  Actually that is the size after accounting for the Home Address, Record Zero, and their associated gaps.  For non-keyed records there is a overhad size of 185 bytes for each record.  This 185 bytes is for the count area and gaps.  If we take our useable track size (19,254) and subtract the overhead for one block (185) we get the maximum record size 19,069.  For keyed blocks we have to add in the key length and use an overhead of 267 to account for the additional gap.

Now we can calculate how much space remains on a track as we write blocks to it.  Here is an example using 4,096 blocks with no keys.


TrackBal  Blocksize Overhead New TrkBal
 19,254      -4096     -185    14,973
 14,973      -4096     -185    10,692
 10,692      -4096     -185     6,411
  6,411      -4096     -185     2,130
  2,130         *        *        *

We start with our maximum useable track size, subtract out the blocksize and overhead and get a new Track Balance of 14,973.  We continue until we get a result less than the blocksize + overhead and at that point no additional records will fit on the track.  Here we find that four 4K blocks will fit on one 3350 track.  For fixed length blocks we can do a one-time calcuation.  For variable length blocks we must calculate as we go.

Reading A Block of Unknown Blocksize

In our first program we read blocks with a blocksize of 80.  We knew what the blocksize was and was able to specify it in our CCW.  What if we don’t know the size of the block?  We can issue a read for a size greater than the largest block (19,069 for a 3350) and see how much data we get back.  The only problem is if the size of the block read does not match the size specified in the Read CCW we will get an Incorrect Length exceptional condition.  We can cause the channel to ignore this condition by setting the Supress Length Indication (SLI) bit our Read CCW.  When the read completes the residual length in the CSW will tell us the number of bytes NOT transfered by the Read command.  In other words it will contain the value of the CCW count minus the size of the data transfered.  We can use this number to calculate how many bytes were read.  We simply subtract the residual count from the CCW length value.

         DS    0D                                   
CCWSRCH  DC    X'31',AL3(IOBSRCH),X'40',X'00',AL2(5)
CCWTIC   DC    X'08',AL3(CCWSRCH),X'40',X'00',AL2(0)
CCWREAD  DC    X'06',AL3(*-*),X'20',X'00',AL2(32768)

Here is our updated CCW channel program. Notice I set the size on the Read CCW to 32,768. For a 3350 we can use any number greater than 19,069. Also note that I used zero A(*-*) for the address in the Reac CCW. This is because I will getmain a 32K buffer and update the CCW with the address returned from GETMAIN.

         GETMAIN R,LV=32768        
*                                  
         STCM  R1,B'0111',CCWREAD+1

Now all that is left is to calculate the actual size of the record we just read.

         SLR   R1,R1                                             
         ICM   R1,B'0011',CCWREAD+6         DATA LENGTH FROM CCW 
         SLR   R2,R2                                             
         ICM   R2,B'0011',IOBRESDL          RESIDUAL LEN FROM CSW
         SR    R1,R2                        CALC BYTES READ

And now we can format and print this value as part of our IOB formatting.

READ020  DS    0H                                          
         CLI   IOBECBAD,X'41'     DEVICE STATUS AVAILABLE ?
         BNE   EXIT               NO - BRANCH              
*                                                          
         TM    IOBCSWFL,X'02'     UNIT CHECK ?             
         BNO   EXIT                 NO - BRANCH            
*                                                          
         TM    IOBSENSE+1,X'08'   NO RECORD FOUND          
         BNO   EXIT                 NO - BRANCH            
*                                                          
         LA    R7,1(,R7)          NEXT TRACK               
         B     READ000            LOOP BACK TO READ        

Here we modify our code to deal with a No Record Found condition. If we get a Unit Check we look at the sense bytes to see if it was a No Reocrd Found. If so we increment our relative track number and loop back to process the next track. If the exception is anything else we end execution.

I wanted to use a larger blocksize for testing.  I could still use blocked records with IEBGENER but IEBDG makes more sense.

//IEBDG  EXEC  PGM=IEBDG                               
//SYSPRINT DD  SYSOUT=*                                
//SEQOUT   DD  DSN=TCS3.EXCP02.DATA,                   
//         DISP=(NEW,CATLG),                           
//         UNIT=SYSDA,VOL=SER=WORK01,                  
//         SPACE=(CYL,(2,2)),                          
//         DCB=(BLKSIZE=4096,LRECL=4096,RECFM=F)       
//SYSIN    DD  *                                       
  DSD OUTPUT=(SEQOUT)                                  
  FD NAME=FLD1,LENGTH=8,STARTLOC=1,FORMAT=AL,ACTION=TL 
  CREATE QUANTITY=12,NAME=(FLD1),FILL=X'00'            
  END

bla

                                                                      
I/O REQUEST                                                           
   COMPLETION CODE = 7F                                               
   CSW = 095EC0 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 7000 (28,672) 
   --- DEVICE STATUS  = CE DE                                         
   --- CHANNEL STATUS =                                               
   SENSE = 0000                                                       
   SEEK = 00000001AE000001                                            
   BYTES READ = 1000 ( 4,096)                                         

I/O REQUEST                                                           
   COMPLETION CODE = 7F                                               
   CSW = 095EC0 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 7000 (28,672) 
   --- DEVICE STATUS  = CE DE                                         
   --- CHANNEL STATUS =                                               
   SENSE = 0000                                                       
   SEEK = 00000001AE000002                                            
   BYTES READ = 1000 ( 4,096)                                         

I/O REQUEST                                                           
   COMPLETION CODE = 7F                                               
   CSW = 095EC0 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 7000 (28,672) 
   --- DEVICE STATUS  = CE DE                                         
   --- CHANNEL STATUS =                                               
   SENSE = 0000                                                       
   SEEK = 00000001AE000003                                            
   BYTES READ = 1000 ( 4,096)                                         

I/O REQUEST                                                           
   COMPLETION CODE = 7F                                               
   CSW = 095EC0 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 7000 (28,672) 
   --- DEVICE STATUS  = CE DE                                         
   --- CHANNEL STATUS =                                               
   SENSE = 0000                                                       
   SEEK = 00000001AE000004                                            
   BYTES READ = 1000 ( 4,096)                                         

I/O REQUEST                                                           
   COMPLETION CODE = 41                                               
   CSW = 095EB0 DEV STAT = 0E CHAN STAT = 40 RESIDUAL = 0005 (     5) 
   --- DEVICE STATUS  = CE DE UC                                      
   --- CHANNEL STATUS = IL                                            
   SENSE = 0008                                                       
   SEEK = 00000001AE000005                                            

I/O REQUEST                                                           
   COMPLETION CODE = 7F                                               
   CSW = 095EC0 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 7000 (28,672) 
   --- DEVICE STATUS  = CE DE                                         
   --- CHANNEL STATUS =                                               
   SENSE = 0000                                                       
   SEEK = 00000001AE000101                                            
   BYTES READ = 1000 ( 4,096)                                         

I/O REQUEST                                                           
   COMPLETION CODE = 41                                               
   CSW = 095EB0 DEV STAT = 0E CHAN STAT = 40 RESIDUAL = 0005 (     5) 
   --- DEVICE STATUS  = CE DE UC                                      
   --- CHANNEL STATUS = IL                                            
   SENSE = 0008                                                       
   SEEK = 00000001AE000102                                            

I/O REQUEST                                                           
   COMPLETION CODE = 41                                              
   CSW = 095EC0 DEV STAT = 0D CHAN STAT = 00 RESIDUAL = 8000 (32,768)
   --- DEVICE STATUS  = CE DE UE                                     
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000001AE000201

Here is a summary of what happened –

  • Four records were successfully read from Cyl=01A3 Trk=0000
  • The attempt to read a fifth record terminated with No Record Found
  • We incremented the track to Cyl=01A3 Trk=0001
  • We successfully read one record
  • The attempt to read a second record terminated with No Record Found
  • We incremented the track to Cyl=01A3 Trk=0002
  • The attempt to read the first record terminated with Unit Exception

Unit Execption indicates a record with a data length of zero was read which indicates a End Of File.  We are successful in our quest to find the EOF record!

Reading A Complete Cylinder With One I/O

Now that we can read from a DASD device we can look at some advanced channel programming for reading. Frist we will begin with reading all the records in a cylinder at once. When reading a dataset sequentially a big performance boost could be realized by reading a cylinder at a time. This discussion applies to a physical 3350 (not an emulated 3350 on Hercules).

A platter in a 3350 turned at a rate of 6,000 rpm. Doing a little math we can determine that it takes 16.7 ms (0.000167 seconds) for the platter to make one complete revolution. When we issue a Seek for a specific record we have to wait for that record to reach the read/write heads. The length of the wait for the record to arrive depends on where it is when we issue the Search. If we are very lucky and the record is exactly in the right place our wait time is zero. If we are very unlucky and our record has just bearly passed the r/w heads we have to wait for the platter to go around a full turn – 16.7 ms. If we read a lot of records it averages out over time to (Max-Min)/2 or 16.7 / 2 which equals 8.4 ms. This delay time is called latency.

The time it takes to read a single record is the total of the Seek Time (the time it takes to position the heads if they are not on the correct cylinder) + Latency + Read Time. Seek time for a 3350 is anywhere from 10 ms to seek from one track to the next to a maximum of 50 ms to seek from the lowest to the highest cylinder.

A typical read of a 4K block could take 25 ms Seek + 8.4 ms Latency + 4.2 ms to read the block for a total of 37.6 ms. Now we can calculate the time required to read a full cylinder of 4k blocks reading them one at a time.  If there are no other datasets being accessed on the dasd device we only have the Seek delay for the first seek to the cylinder. To read a full cylinder of 4K blocks (120 blocks total) would take 25 ms Seek + (120 * (8.4 ms + 4.2 ms)) = 1537 ms.

If we read a complete cylinder with one I/O command we can cut out the latency for all except the first block.  We also guarentee that only one Seek is required.  The time to read a complete cylinder of 4k blocks is: 25 ms Seek + (30 * 16.7 ms) + 8.4 ms = 524 ms.

This is quite an improvement in time – almost 3 times faster.

Now that we know why it might be desirable to read an entire cylinder with one I/O request we can write a program to do exactly this. You can find the complete program as EXCP03 in the downloads.

CCWSRCH DC X'31',AL3(IOBSRCH),X'40',X'00',AL2(5)
CCWTIC   DC    X'08',AL3(CCWSRCH),X'40',X'00',AL2(0)   BLK CYL
CCWREAD  DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)     1   1
         DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)     2   1
         DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)     3   1
         DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)     4   1
         DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)     5   2
                                :
                                :
                                :
         DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)  115   29
         DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)  116   39
         DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)  117   30
         DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)  118   30
         DC    X'86',AL3(*-*),X'40',X'00',AL2(4096)  119   30
         DC    X'86',AL3(*-*),X'00',X'00',AL2(4096)  120   30

First we starrt with a lot of CCWs. Since there are four 4K blocks per track and 30 tracks per cylinder we need 120 read CCWs. Also note that instead of the X’06’ Read Data command we are using X’86’ Read Data Multi-Track. This will cause automatic head switching at the end of each track. We have Command Chaining set for every CCW except the last read.

         L     R3,=F'4096'        4K                               
         SLR   R2,R2              CLEAR FOR MULTIPLY               
         M     R2,=F'4'           4 BLOCKS/TRACK                   
         M     R2,=F'30'          30 TRACKS/CYL                    
*                                                                  
         GETMAIN R,LV=(R3)                                         
*                                                                  
         LA    R2,CCWREAD                                          
         LA    R3,120             120 BLOCKS/CYL                   
*                                                                  
OPEN030  DS    0H                                                  
         STCM  R1,B'0111',1(R2)   SAVE BUFFER ADDRESS INTO READ CCW
         A     R1,=F'4096'        NEXT BUFFER                      
         LA    R2,8(,R2)          NEXT CCW                         
         BCT   R3,OPEN030         LOOP BACK

We also need 120 4K buffers which is 480K that we obtain with a GETMAIN. We can then set up a loop to plug the buffer addresses into the CCWs.

         L     R1,IOBCSW          GET CSW ADDRESS           
         LA    R2,CCWREAD         FIRST READ CCW            
         SR    R1,R2                                        
         CLI   IOBECBAD,X'7F'     ALL READS COMPLETE        
         BE    READ020                                      
         S     R1,=F'8'           BACK UP FOR INCOMPLETE CCW
READ020  DS    0H

When the I/O completes we need to determine how many records were read in case we encountered an End of File before the end of the cylinder. We can determine this by subtracting the address of the first Read CCW from the address in the CSW and dividing by 8. We do need one additional step where we check to see if all CCWs completed normally and subtract out the length of the CCW that did not complete.

          C     R1,=F'0'           CHECK FOR NEG VALUE     
          BH    READ030              - NO READS SUCCESSFUL 
          SLR   R1,R1                                      
 READ030  DS    0H                                         
          SRL   R1,3               DIVIDE BY 8

We also need to check for a negative value that would indicate the Channel Program ended before the first Read CCW. In this case we set the read count to zero. Finally we divide by 8 (the number of bytes in a CCW) to get the number of Read CCWs that completed successfully.
If we can read an entire cylinder with a single I/O request it will take 25 ms Seek + 8.4 ms Latency + 501 ms to read the data for a total of 534.4 ms. We can see that this is about 3 times faster and this is if the heads don’t move. If other dataset are being accessed on the dasd drive our Seek time would greatly increase.

Here are the results when we read a dataset containing 120 records.

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 095EC0 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 0000 (     0)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000001AE000001                                           
   BYTES READ = 1000 ( 4,096)                                        
   BLOCKS READ = (   120)

Here are the results when the dataset only contains 111 records.

I/O REQUEST                                                           
   COMPLETION CODE = 41                                               
   CSW = 095E80 DEV STAT = 0D CHAN STAT = 40 RESIDUAL = 1000 ( 4,096) 
   --- DEVICE STATUS  = CE DE UE                                      
   --- CHANNEL STATUS = IL                                            
   SENSE = 0000                                                       
   SEEK = 00000001AE000001                                            
   BLOCKS READ = (   111)

Finally here are the results when the dataset is empty (contains only an EOF record).

I/O REQUEST                                                           
   COMPLETION CODE = 41                                               
   CSW = 095B08 DEV STAT = 0D CHAN STAT = 40 RESIDUAL = 1000 ( 4,096) 
   --- DEVICE STATUS  = CE DE UE                                      
   --- CHANNEL STATUS = IL                                            
   SENSE = 0000                                                       
   SEEK = 00000001AE000001                                            
   BLOCKS READ = (     0)

Reading Count, Key and Data

Read Count

Now we will take a quick look at some of the other read commands.  First is the Read Count (X’12’) which reads the 8-byte count area (CCHHRKDL).  CCHHR is the 5-byte cylinder, head, record address of the record, K is the one-btye key length, and DL is the 2-byte record data length.  If we repace the X’06’ Read Data with X’12’ Read Count we get the following results.

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 095B10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 7FF8 (32,760)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000D5000001                                           
   BYTES READ = 0008 (     8)                                        

----- READ COUNT -----                                               
0000 00D50000 02080100

We have dumped the contents of the contents of I/O buffer containing the count area just read.  CCHHR = 00D5000002 KL = 08 DL = 0100.  The first thing to note is our Seek Address was for record one but we read the count area for record two.  This is because the count area for record one has already passed the r/w heads (during the Search Command) so the next count area available to be read is for record 2.  We also see that the data block is 256 bytes long with an 8-byte key (you may recognize this as the format for a PDS directory block).

Read Key and Data

Next we will issue a Read Key and Data (X’0E’).  Here are the results.

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 095B10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 7EF8 (32,504)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000D5000001                                           
   BYTES READ = 0108 (   264)                                        

----- READ KEY AND DATA -----                                        
0000 C1C8D3E6 E6D9C9E3 00FEC1C3 C3D6E4D5  E34000FA 1B2C00FA 21000000 0000C2E2
0020 00105810 58000000 88000001 0000C1C8  D3C3E6D9 C9E300A2 042C00A2 0A000000
0040 000002E2 0020A820 A8000000 88000001  0100C1C8 D3C7E3C6 4040009F 042C00A0
0060 01000000 000002E2 005AC043 38000000  88000001 0100C1C8 D3C9E6D9 C9E300A1
0080 172C00A2 01000000 000002E2 001A681A  68000000 88000001 0100C1C8 D3E3D4D6
00A0 D54000A1 0E2C00A1 14000000 000002E2  000A380A 38000000 88000001 0100C1C8
00C0 D3E6E3C1 E2D200A1 052C00A1 0B000000  000002E2 000C400C 40000000 88000001
00E0 0100C1C8 D3E6E6D9 C9E300A2 0D2C00A3  05000000 000002E2 00222022 20000000
0100 88000001 01000000

Here we read 264 bytes ( 8 bytes of Key + 256 bytes of Data).  It is our responsibility to know how many bytes are for the key and how many for the data.

Read Count, Key and Data

Now for the Read Count, Key and Data (X’1E’).  Here are the results.

I/O REQUEST
   COMPLETION CODE = 7F
   CSW = 095B10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 7EF0 (32,496)
   --- DEVICE STATUS  = CE DE
   --- CHANNEL STATUS =
   SENSE = 0000
   SEEK = 00000000D5000001
   BYTES READ = 0110 (   272)
----- READ COUNT KEY AND DATA -----
0000 00D50000 02080100 C1D4C4D7 D9C3E5E3  00EEC1D4 C1D7E3C6 D3C50009 082E0009
0020 0D000000 000002E2 00212018 00000000  98000000 31970585 0100C1D4 C1D7E3C6
0040 F0F10009 122E000A 01000000 000002E2  000E680E 68000000 98000000 31970593
0060 0100C1D4 C1D7E3C6 F0F2000A 042E000A  09000000 000002E2 00020002 00000000
0080 98000000 31991261 0100C1D4 C1E2D7E9  C1D7000E 082E000F 01000000 000002E2
00A0 00357814 90000000 98000000 01960105  0101C1D4 C2D3C9E2 E34000CD 032C00CE
00C0 01000000 0000C2E2 00952043 B0000000  88000001 0000C1D4 C4D7D9C3 E5E30010
00E0 152E0010 1B000000 000002E2 0012000D  20000000 98000000 82150403 01000000
0100 00000000 00000000 00000000 00000000

Thsi time we read 272 bytes (8 bytes of count, 8 bytes of  key, and 256 bytes of data).  Also note that just like Read Count, the Read Count Key and Data command returned the next record following the record located with our Search Command.

Read Multiple Count Key and Data

This is a favorite or mine.  The Read Multiple Count Key and Data command can be used to read an entire track without knowing in advance the size or number of records on the track.  Because this is a Read Count type of command we need to Search to Record Zero.

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 095B10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 3FE0 (16,352)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000001AE000000                                           
   BYTES READ = 4020 (16,416)                                        

----- READ MULTIPLE COUNT KEY AND DATA -----
0000 01AE0000 01001000 C1C2C3C4 C5C6C7C8  00000000 00000000 00000000 00000000
0020 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000
              LINES 00096040-00096FE0 SAME AS ABOVE                          
1000 00000000 00000000 01AE0000 02001000  40C2C3C4 C5C6C7C8 00000000 00000000
1020 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000
              LINES 00097040-00097FE0 SAME AS ABOVE                          
2000 00000000 00000000 00000000 00000000  01AE0000 03001000 4040C3C4 C5C6C7C8
2020 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000
              LINES 00098040-00098FE0 SAME AS ABOVE                          
3000 00000000 00000000 00000000 00000000  00000000 00000000 01AE0000 04001000
3020 404040C4 C5C6C7C8 00000000 00000000  00000000 00000000 00000000 00000000
3040 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000
              LINES 00099060-00099FE0 SAME AS ABOVE                          
4000 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000

Here I am reading a track containing four 4K blocks.  We see the buffer begins with the count area for record 1.  The key (if any) begins immediately following the count.  The data begins immediately following the key.  We determine the start of the next record by adding the Count length (8) to the Key length and the Data length.  Record 1 begins at offset 0.  It has no key and the data is 4096 (X’1000′) bytes.  Record 2 should then start at offset X’1008′.

This is a great way to read an entire track when the blocks are not all the same size and we don’t know how many blocks are on the track.

Reading A PDS With EXCP

Now we can look at using EXCP channel programming to read a partitioned dataset.  We will start with the structure of the directory.  The directory consists of a set of 256 byte blocks, each with an 8 byte key.  The number of directory blocks is determined when the PDS is initially allocated.  An EOF mark occurs immediately following the last directory block.

+-+-----+   +-+-----+   +-+-----+   +-+-----+     +---+
|K| Dir |   |K| Dir |   |K| Dir |   |K| Dir | ... |EOF|
+-+-----+   +-+-----+   +-+-----+   +-+-----+     +---+

Each directory block begins with a 2 byte value indicating how many bytes in the block are used for directory information (including the 2 bytes for the length). Member entries begin immediately following the length.

+--+--------+--------+--------+     +--------+-------+
|LL|Mbr-1   |Mbr-2   |Mbr-3   | ... |Mbr-n   |00...00|
+--+--------+--------+--------+     +--------+-------+

Member entries are variable in length. There is a fixed portion which is always present followed by optional User Data. The user data area is between 0 and 62 bytes.

+--------+---+-+-----------+
|Mbr-Name|TTR|C| User Data |
+--------+---+-+-----------+

The fixed portion of the member entry is 12 bytes. The first eight bytes contain the member name. The next three bytes contain a TTR pointer to the beginning block of the entry. The C field contains the size of the user data (in half words) in bits 3 through 7 (x’1F’). If bit 0 is set the entry is an Alias. To get the size of the user data we locially AND the C field with a mask of x’1F’ and then multiply by 2.

Member entries are stored in ascending sequence.  The key contains the highest member name contained in the block.  The logical end of the directory is marked with a member name of x’FFFFFFFFFFFFFFFF’.

The key allows us to locate the directory block containing a specific entry without having to read through the contents of every block.  By using a Search Key High or Equal  the correct directory block can be quickly located.

         DC    X'69',AL3(MBRNAME),X'40',X'40',AL2(8)       Search Key Equal or High
         DC    X'08',AL3(*-8),X'40',x'00',AL2(0)           TIC back to Search
         DC    X'06',AL3(IOBUF),X'00',X'00',AL2(256)       Read Data

If we run this channel program against a PDS we should expect the I/O to either complete successfully and read a block of data or it should terminate with a No Record Found condition. Anything else should indicate some type of error condition. This technique depends on the logical EOF in the PDS directory (x’FFFFFFFFFFFFFFFF’) since the Search CCW will not detect the EOF record.

If the record is not found we need to continue our search on the next track of the dataset. We can update our Seek address (MBBCCHHR) and reissue the I/O. This should continue until a record is located and read or an exceptional condition occurs (indicating the PDS structure is damaged).

If our dataset is allocated on a cylinder boundry we can use the multi-track seach command to serach an entire cylinder at a time.

         DC    X'E9',AL3(MBRNAME),X'40',X'40',AL2(8)       Search Key Equal or High Multi-Track
         DC    X'08',AL3(*-8),X'40',x'00',AL2(0)           TIC back to Search
         DC    X'06',AL3(IOBUF),X'00',X'00',AL2(256)       Read Data

If the I/O terminates with a No Record Found condition we update our Seek address to the next cylinder of the dataset. To do this we would add the number of tracks in a cylinder to our relative track address (in the case of a 3350 we would add 30 tracks).

This brings up the question of how do we know if our dataset is allocated in tracks or cylinders. The answer is found in the DEB extent information. Our routine to convert from TTR to MBBCCHHR uses the DEB extent information but I did not present any details, just the code to do the conversion. Here is the format of a DEB extent entry.

         X    - File Mask
         AL3  - UCB Address
         XL2  - BIN
         XL2  - Extent Begin CC
         XL2  - Extent Begin HH
         XL2  - Extent End CC
         XL2  - Extent End HH
         FL2  - Number of Tracks In This Extent

The File Mask is the parameter for a Set File Mask CCW. We have already looked at the Set File Mask parameters but here is a quick refresh of the bits we are interested in.

01.. .... - Do Not Allow Write
...1 1... - Do Not Allow Seek Cyl or Seek Head
...1 0... - Do Not Allow Seek Cyl (Seek Head allowed)

Below is an extent entry from a DEB.

 58001E68 000000EB 001000EB 0019000A

The file mask tells us the dataset is opened for input only (write commands not allowed). Because Seek Cyl/Head is not allowed we can tell our dataset was allocated in tracks and multi-track commands are not allowed. The UCB address of the dasd device is 001E68. The BIN is always x’0000′. The extent begins at Cylinder x’00EB’ Track x’0010′ and ends at Cylinder x’00EB’ Track x’0019′ and there are 10 (x’000A’) tracks in the extent.

Here is the extent entry for a dataset allocated in cylinders.

 50001E68 000001AE 000001AE 001E001E

Notice that here the file mask allows Seek Head so we can use multi-track commands.

EXCP05 – Read PDS Member

Find Block Containing Member Entry

***********************************************************************
*                       FIND DIRECTORY BLOCK                           
*                                                                      
*    ENTRY: R1 = A(CL8'MEMBER NAME')                                   
*                                                                      
*    EXIT:  R15 = RC                                                   
*                 0 - ENTRY LOCATED                                    
*                 8 - ERROR                                            
*           R1  = A(DIR BLOCK)                                         
*                                                                      
***********************************************************************

Will will call our routine with register 1 containing the address of an 8-byte member name. On exit register 15 will contain a return of 0 if a block was located that may contain the member entry. A return code of zero does not mean the directory entry exists. It only means that IF the entry exists it will be in the block returned. Any other condition returns a value of 8 as the return code and indicates some type of error has occured. Register 1 will contain a pointer to the buffer containing the block that may contain the entry.

FINDBLK  DS    0H                                                
         ST    R14,XTFIBLK        SAVE RETURN ADDRESS            
*                                                                
         MVC   SRCHNAME,0(R1)     COPY MEMBER NAME TO SEARCH PARM

We begin by saving the return address and then moving the member name to our search key area used by our Search CCW.

         MVI   DIRSRCH,X'69' SEARCH KEY HI/EQ              
         MVI   MULTITRK,0    CLEAR MULTITRACK FLAG         
         LA    R2,EXCPDCB              POINT TO DCB        
         L     R2,44(,R2)              POINT TO DEB        
         LA    R2,32(,R2)              FIRST EXTENT INFO   
         SLR   R1,R1                                       
         IC    R1,0(R2)      GET FILE MASK                 
         N     R1,=A(X'18')  ISOLATE SEEK HEAD             
         C     R1,=A(X'10')  ALLOW SEEK HEAD               
         NOP   FINDB010        NO - BRANCH                 
         BNE   FINDB010        NO - BRANCH                 
*                                                          
         MVI   MULTITRK,1    INDICATE MULTI-TRACK ALLOWED  
         MVI   DIRSRCH,X'E9' SEARCH KEY HI/EQ M/T          
FINDB010 DS    0H

Now we determine if we can use the Multi-Track form of our search. We begin by defaulting to the non-multitrack command code. We also clear the flag byte that indicates multitrack in use. We check the file mask in the DEB to determine if Seek Head is allowed and if so we update the flag byte and the Search command code.

Notice the NOP instruction. I included this so I could change it to an unconditional branch that forces the search to operate in non-multitrack mode for testing even if the dataset is allocated in cylinders.

         LA    R7,0          RELATIVE TRACK NUMBER        
*                                                         
FINDB020 DS    0H                                         
         LR    R1,R7         RELATIVE TRACK NUMBER        
         BAL   R14,GETCCHH   CONVERT TO MBBCCHHR          
         LTR   R15,R15            CHECK RETURN CODE       
         BNZ   FINDBXT8           - BRANCH IF NO GOOD     
*                                                         
         MVC   IOBSEEK(8),MBBCCHHR                        
         MVI   IOBSEEK+7,0        PUT IN RECORD NUMBER    
*                                                         
         LA    R1,DIRSRCH         POINT TO CHANNEL PROGRAM
         ST    R1,IOBCCWAD        SAVE INTO IOB           
*                                                         
         XC    ECB,ECB            CLEAR ECB               
         EXCP  IOB                ISSUE I/O REQUEST       
*                                                         
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE
*                                                         
         BAL   R14,PRINTIOB       GO FORMAT IOB

We being by setting our relative track number to zero. The PDS directory always beings on the first track of the dataset. We then calculate the MBBCCHHR and update the IOB seek address. We put the address of our channel program into the IOB. This is necessary since we use the same IOB with different channel programs for reading directory blocks and data blocks. We then schedule the I/O request and wait for it to complete.

DIRSRCH  DC    X'69',AL3(SRCHNAME),X'40',X'00',AL2(8)   
DIRTIC   DC    X'08',AL3(DIRSRCH),X'40',X'00',AL2(0)    
DIRREAD  DC    X'06',AL3(DIRBLKIO),X'00',X'00',AL2(256)

Here is our channel program to find and read the directory block. Note that we do not set the Suppress Length Indication (SLI) bit in the Read CCW. Directory blocks are always 256 bytes in length and an incorrect length condition indicates some type of error.

*                                                        
         CLI   IOBECBAD,X'7F'     NORMAL COMPLETION      
         BE    FINDBXT0             YES - FOUND OUR BLOCK
*                                                        
         CLI   IOBECBAD,X'41'     DEVICE STATUS AVAILABLE
         BNE   FINDBXT8            NO - ERROR            
*                                                        
         TM    IOBCSWFL,X'02'     UNIT CHECK             
         BNO   FINDBXT8            NO - ERROR            
*                                                        
         TM    IOBSENSE+1,X'08'   NO RECORD FOUND        
         BNO   FINDBXT8             NO - ERROR           
*                                                        
         CLI   MULTITRK,1         MULTITRACK ENABLED     
         BNE   FINDB030              NO - NEXT TRACK     
*                                                        
         A     R7,=F'30'          3350 HAS 30 TRK/CYL    
         B     FINDB020           GO SEARCH NEXT CYL     
*                                                 
*                                                 
FINDB030 DS    0H                                 
         A     R7,=F'1'           SEARCH NEXT     
         B     FINDB020                      TRACK

If the I/O completed normally then we have successfuly located and read a directory block. We can return the block to the caller. Otherwise we check for Unit Check and No Record Found indicating we reached the end of a track or cylinder without locating the requested block. If we are in multitrack mode we update our relative track address to the next cylinder. Here I have hardcoded a value of 30 since a 3350 has 30 tracks/cyl. If we are not in multitrack mode we update to the next track and continue our search.

A PDS directory that exceedes one cylinder would be very large. A 3350 track will hold 36 directory blocks. Since there are 30 tracks/cyl a cylinder would contain 1,080 directory blocks.

FINDBXT0 DS    0H                                       
         LA    R15,0              SET RC                
         LA    R1,DIRBLKIO        POINT TO I/O BUF      
         B     FINDBEXT           AND EXIT              
*                                                       
*                                                       
FINDBXT8 DS    0H                                       
         LA    R15,8              SET RC                
         B     FINDBEXT           AND EXIT              
*                                                       
*                                                       
FINDBEXT DS    0H                                       
         L     R14,XTFIBLK        RESTORE RETURN ADDRESS
         BR    R14

And finally our exit points. For a successful read we set the return code to zero and load the address of the I/O buffer into register 1.

Find Directory Entry In Directory Block

***********************************************************************
*                       FIND DIRECTORY ENTRY                           
*                                                                      
*    ENTRY: R1 = A(CL8'MEMBER NAME')                                   
*                                                                      
*    EXIT:  R15 = RC                                                   
*                 0 - ENTRY LOCATED                                    
*                 4 - ENTRY NOT LOCATED                                
*                 8 - ERROR                                            
*           R1  = A(DIR ENTRY)                                         
*                                                                      
***********************************************************************

Here is our routine to locate a directory entry in a PDS directory block. On entry register 1 contains a pointer to the member name. A return code of zero indicates the entry was found and the address of the entry is returned in register 1. A return code of 4 indicates the entry does not exist and a return code of 8 indicates an error has occured.

FINDMBR  DS    0H                                    
         ST    R14,XTFIMBR        SAVE RETURN ADDRESS
*                                                    
         BAL   R14,FINDBLK        GO FIND DIR BLOCK  
         LTR   R15,R15            CHECK RC           
         BNZ   FINDMXT4           BRANCH IF ERROR

We being by saveing the return address. Next we call the FINDBLK routine to locate the block that will contain the entry if it exists.

         SLR   R3,R3                                          
         ICM   R3,B'0011',0(R1)   GET LENGTH OF DIRECTORY DATA
         S     R3,=F'2'           ADJUST OUT LENGTH FLD       
         LA    R2,2(,R1)          POINT TO FIRST ENTRY

We begin the search of the directory block by loading the number of bytes remaining into register 3 and loading register 2 with a pointer to the first member entry. As we continue processing the block R3 will contain the remainding number of bytes and R2 will point to the current member entry being processed.

FINDM010 DS    0H                                                 
         LTR   R3,R3              END OF DATA                     
         BNP   FINDMXT4             YES - ENTRY NOT FOUND         
*                                                                 
         CLC   0(8,R2),SRCHNAME   ENTRY WE ARE LOOKING FOR?       
         BE    FINDMXT0             YES - BRANCH                  
*                                                                 
         SLR   R1,R1                                              
         ICM   R1,B'0001',11(R2)  GET C FIELD                     
         N     R1,=A(X'1F')       LENGTH OF USER DATA IN HALFWORDS
         SLL   R1,1               MULTIPLY BY 2                   
         A     R1,=F'12'          ADD IN FIXED SIZE               
*                                                                 
         AR    R2,R1              NEXT ENTRY                      
         SR    R3,R1              ADJUST LENGTH                   
         B     FINDM010           LOOP BACK

Here is the loop that searches for the member entry. We begin by checking to see if there are any more entries to process. If not then the entry was not found. Next the current entry name is compared to the search name which was saved in the FINDBLK routine. If it matches we found the entry and we are done. If not match we load the C field of the TTRC field into R1. We AND against a mask to keep only the length of the user data and use a SLL to multiply the value by 2 since it is in half words. Next the fixed size is added to obtain the total length of the entry. We adjust our current entry pointer and length remaining and then loop back.

FINDMXT0 DS    0H                                       
         LA    R15,0              SET RC                
         LR    R1,R2              POINT TO DIR ENTRY    
         B     FINDMEXT           AND EXIT              
*                                                       
*                                                       
FINDMXT4 DS    0H                                       
         LA    R15,4              SET RC                
         B     FINDMEXT           AND EXIT              
*                                                       
*                                                       
FINDMEXT DS    0H                                       
         L     R14,XTFIMBR        RESTORE RETURN ADDRESS
         BR    R14

Here are the exit points where the return code is set and if successful the address of the entry is returned in register 1.

Find Routine Driver

 FIND     DS    0H                                   
          ST    R14,XTFIND        SAVE RETURN ADDRESS
 *                                                   
          MVC   PL09MBR,0(R1)      SAVE MEMBER NAME  
          BAL   R14,FINDMBR        FIND DIR ENTRY    
          LTR   R15,R15              WAS IT LOCATED  
          BZ    FIND020                 YES - BRANCH

Here is a testing routine to drive our find member process. On entry R1 points to the member name to be located. The find member routine will be called and the results will be printed.

*                                                       
         MVI   PL09RC,C'4'       SET RC IN MESSAGE      
         C     R15,=F'4'         WAS RC=4               
         BE    FIND010                                  
*                                                       
         MVI   PL09RC,C'8'       SET RC=8               
FIND010  DS    0H                                       
         LA    R1,=C' '                                 
         LA    R15,1                                    
         BAL   R14,PRINT         PRINT BLANK LINE       
         LA    R1,PL09                                  
         LA    R15,PL09L                                
         BAL   R14,PRINT         PRINT RC LINE          
         SLR   R1,R1             INDICATE MBR NOT FOUND 
         B     FINDXT            EXIT

If the return code from FINDMBR was not zero we print a message with the member name and the RC.

FIND020  DS    0H                                          
         ST    R1,XBLKAD          SAVE PTR TO MEMBER ENTRY 
*                                                          
         SLR   R3,R3                                       
         ICM   R3,B'0001',11(R1)       C FIELD             
         N     R3,=A(X'1F')            USER                
         SLL   R3,1                        DATA LEN        
         LA    R3,12(,R3)                                  
         ST    R3,XBLKLEN              LENGTH TO DUMP

If found we save the pointer to the entry and calculate the entry length.

*                                                           
         LA    R1,=C' '                                     
         LA    R15,1                                        
         BAL   R14,PRINT          PRINT BLANK LINE          
*                                                           
         MVI   PL09RC,C'0'                                  
         LA    R1,PL09                                      
         LA    R15,PL09L                                    
         BAL   R14,PRINT          PRINT RETURN CODE LINE    
*                                                           
         LA    R1,XBLKAD                                    
         L     R15,=V(XDUMP)                                
         BALR  R14,R15            DUMP MEMBER ENTRY         
         L     R1,XBLKAD          RESTORE DIR ENTRY ADDRESS 
         B     FINDXT                                       
*                                                       
FINDXT   DS    0H                                       
         L     R14,XTFIND         RESTORE RETURN ADDRESS
         BR    R14

Finally a status message is printed and the directory entry is dumped.

Testing The Find Routine

         LA    R1,=CL8'ACCOUNT'              
         BAL   R14,FIND           FIND MEMBER
*                                            
         LA    R1,=CL8'MSTRJCL'              
         BAL   R14,FIND           FIND MEMBER
*                                            
         LA    R1,=CL8'ZZZZZZZZ'             
         BAL   R14,FIND           FIND MEMBER

We load R1 with the address of a member name and call the driver. I am using SYS1.LINKILB for testing since it has a large number of directory entries. ACCOUNT is located in the first directory block and MSTRJCL is located in the last. I also did a locate of ZZZZZZZZ which does not exist and should return RC=4.

Reading Member Data

***********************************************************************
*                       READ A BLOCK BY TTR                            
*                                                                      
*    ENTRY: R1 = 0TTR                                                  
*                                                                      
*    EXIT:  R15 = RC                                                   
*                 0 - BLOCK SUCCESSFULLY READ                          
*                 4 - NO RECORD FOUND                                  
*                 8 - EOF                                              
*                12 - ERROR                                            
*           R0  = BLOCK SIZE                                           
*           R1  = DATA                                                 
*                                                                      
***********************************************************************

On entry register 1 contains the TTR of the block to be read. On exit R0 will contain the size of the block read and R1 will point to the data in the I/O buffer. R15 contains a return code.

READTTR  DS    0H                                       
         STM   R14,R12,12(R13)    SAVE CALLERS REGISTERS
         LA    R14,SAVERTTR       CHAIN                 
         ST    R13,4(,R14)             ON               
         ST    R14,8(,R13)               SAVE           
         LR    R13,R14                       AREA       
*                                                       
         LR    R7,R1         SAVE TTR

We being by saving the caller’s registers and establishing a new save area. The TTR is copied into R7.

         LR    R1,R7         RELATIVE TRACK NUMBER   
         SRL   R1,8          SHIFT OFF R (KEEP TT)   
         BAL   R14,GETCCHH   CONVERT TO MBBCCHHR     
         LTR   R15,R15            CHECK RETURN CODE  
         BNZ   READTXTC           - BRANCH IF NO GOOD
*                                                         
         LA    R1,READSRCH        POINT TO CHANNEL PROGRAM
         ST    R1,IOBCCWAD        SAVE INTO IOB           
*                                                         
         MVC   IOBSEEK(8),MBBCCHHR                        
         STCM  R7,B'0001',IOBSEEK+7   PUT IN RECORD NUMBER

The relative track number is converted into a Seek Address. The address of the channel program for reading a block is placed in the IOB as is the full Seek address.

READSRCH DC    X'31',AL3(IOBSRCH),X'40',X'00',AL2(5) 
READTIC  DC    X'08',AL3(READSRCH),X'40',X'00',AL2(0)
READREAD DC    X'06',AL3(*-*),X'20',X'00',AL2(19069)

Here is the channel program to read a block. Notice we attempt to read a block of the maximum size for a 3350. The SLI bit is set. This will allow us to read a block of unknown length.

         XC    ECB,ECB            CLEAR ECB               
         EXCP  IOB                ISSUE I/O REQUEST       
*                                                         
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE
*                                                         
         BAL   R14,PRINTIOB       GO FORMAT IOB           
*                                                         
         CLI   IOBECBAD,X'7F'     NORMAL COMPLETION       
         BE    READTXT0             YES - FOUND OUR BLOCK

Now we issue the EXCP and wait for the I/O to complete. We then check see see if a block was successfully read.

          CLI   IOBECBAD,X'41'     DEVICE STATUS AVAILABLE 
          BNE   READTXTC            NO - ERROR             
 *                                                         
          TM    IOBCSWFL,X'01'     UNIT EXCEPTION          
          BO    READTXT8            YES - EOF RC=8         
 *                                                         
          TM    IOBCSWFL,X'02'     UNIT CHECK              
          BNO   READTXTC            NO - ERROR             
 *                                                         
          TM    IOBSENSE+1,X'08'   NO RECORD FOUND         
          BO    READTXT4             YES - RC=4            
          B     READTXTC           ELSE - ERROR

Next we check to see if device status is available in the IOB. If not we return an error condition. We then check for Unit Exception indicating an End Of File condition. Finally we check for a No Record Found condition. If none of these we return an error status.

READTXT0 DS    0H                                           
         LA    R15,0              SET RC                    
         SLR   R1,R1                                        
         SLR   R0,R0                                        
         ICM   R0,B'0011',READREAD+6  READ SIZE FROM CCW    
         ICM   R1,B'0011',IOBRESDL    RESIDUAL SIZE FROM CSW
         SR    R0,R1                  CALC RECORD SIZE READ 
         ICM   R1,B'0111',READREAD+1  DATA BUFFER ADDRESS   
         B     READTEXT           AND EXIT

If a record was read we calculate the size by subtracting the CWS Residual value from the size in the Read CCW.

READTXT4 DS    0H                         
         LA    R15,4              SET RC  
         B     READTEXT           AND EXIT
*                                         
*                                         
READTXT8 DS    0H                         
         LA    R15,8              SET RC  
         B     READTEXT           AND EXIT
*                                         
*                                         
READTXTC DS    0H                         
         LA    R15,12             SET RC  
         B     READTEXT           AND EXIT
*                                         
*                                         
READTEXT DS    0H                         
         L     R13,4(,R13)                
         L     R14,12(,R13)               
         LM    R2,R12,28(R13)             
         BR    R14

And finally the exit code.

Read Block Test Routine

         LA    R1,=CL8'IEFBR14'                   
         BAL   R14,FIND           FIND MEMBER     
*                                                 
         LTR   R1,R1              ANY DIR ENTRY   
         BZ    EXIT                NO - EXIT      
*                                                 
         SLR   R2,R2                              
         ICM   R2,B'0111',8(R1)   GET TTR         
         ST    R2,CURTTR          SAVE CURRENT TTR

We being by loacating the directory entry for a PDS memeber. I chose to use IEFBR14 for this test. We get the TTR pointer to the first block of the member from the directory entry.

RDMBR010 DS    0H                                  
         L     R1,CURTTR          CURRENT BLOCK TTR
         BAL   R14,READTTR        GO READ BLOCK    
*                                                  
         B     *+4(R15)           BRANCH ON RC     
         B     RDMBR020           - READ SUCCESSFUL
         B     RDMBR030           - NO RECORD FOUND
         B     RDMBR040           - EOF            
         B     RDMBR050           - ERROR

Next we call our READTTR routine to read a block by TTR. A branch table is used to evaluate the return code.

RDMBR020 DS    0H                                       
         ST    R0,XBLKLEN         SAVE BLOCK LEN        
         ST    R1,XBLKAD          SAVE BLOCK ADDRESS    
         LA    R1,=C' '                                 
         LA    R15,1                                    
         BAL   R14,PRINT          PRINT BLANK LINE      
         LA    R1,XBLKAD                                
         L     R15,=V(XDUMP)                            
         BALR  R14,R15            DUMP BLOCK            
*                                                       
         SLR   R1,R1                                    
         ICM   R1,B'0001',CURTTR+3  GET CURRENT RECORD  
         LA    R1,1(,R1)            INCREMENT RECORD    
         STCM  R1,B'0001',CURTTR+3  SAVE BACK           
         B     RDMBR010             GO READ NEXT RECORD

If a block was successfully read we save the ponter to the data and the data length so we can dump the block. We then increment the record number in our current TTR pointer.

RDMBR030 DS    0H                                       
         SLR   R1,R1                                    
         ICM   R1,B'0011',CURTTR+1  GET CURRENT TRACK   
         LA    R1,1(,R1)            INCREMENT TRACK     
         STCM  R1,B'0011',CURTTR+1  SAVE TRACK          
         MVI   CURTTR+3,1           RECORD 1            
         B     RDMBR010             GO READ NEXT RECORD

A No Record Found condition indicates we have read all the records on the current track. In this case we increment the current relative track number and reset the current record to one.

RDMBR040 DS    0H                                    
         LA    R1,=C' '                              
         LA    R15,1                                 
         BAL   R14,PRINT            PRINT BLANK LINE 
*                                                    
*                    1...5...10...15...20...25       
         LA    R1,=C'*** END OF FILE ***'            
         LA    R15,19                                
         BAL   R14,PRINT            PRINT EOF MESSAGE
         B     EXIT

For End Of File we just print a message and exit.

RDMBR050 DS    0H                                   
         LA    R1,=C' '                             
         LA    R15,1                                
         BAL   R14,PRINT            PRINT BLANK LINE
*                                                   
*                    1...5...10...15...20...25      
         LA    R1,=C'*** I/O ERROR ***'             
         LA    R15,17                               
         BAL   R14,PRINT            PRINT ERR MSG   
         B     EXIT

For an I/O Error we print a message and exit.

Program Execution Output

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 0969F8 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 0000 (     0)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000D5000000                                           
   BYTES READ = 0100 (   256)                                        

   FIND MEMBER = <ACCOUNT >   RC = 0                                  
0000 C1C3C3D6 E4D5E340 00FA1B2C 00FA2100  00000000 C2E20010 58105800 00008800
0020 00010000

Here is the eight-byte member name followed by the TTR (x’00FA1B’) and C (x’2C’).  The remainder is the user data section that contains information about the load module.

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 0969F8 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 0000 (     0)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000D5000000                                           
   BYTES READ = 0100 (   256)                                        

   FIND MEMBER = <MSTRJCL >  RC = 0   
0000 D4E2E3D9 D1C3D340 0122022C 01220700  00000000 03F20003 C003C000 00008800
0020 00010000

Here is the find output for MSTRJCL.

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 0969F8 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 0000 (     0)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000D5000000                                           
   BYTES READ = 0100 (   256)                                        

   FIND MEMBER = +ZZZZZZZZ+  RC = 4

Here is the output for a member name that does not exist.

I/O REQUEST                                                           
   COMPLETION CODE = 7F                                               
   CSW = 0969F8 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 0000 (     0) 
   --- DEVICE STATUS  = CE DE                                         
   --- CHANNEL STATUS =                                               
   SENSE = 0000                                                       
   SEEK = 00000000D5000000                                            
   BYTES READ = 0100 (   256)                                         

   FIND MEMBER =   RC = 0                                   
0000 C9C5C6C2 D9F1F440 0103162C 01040400  00000000 C3F20000 08000800 00008800
0020 00010000

Here is the find output for IEFBR14.

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 096A10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 4A55 (19,029)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000DD001316                                           
   BYTES READ = 0028 (    40)                                        

0000 20000000 00010020 00000000 00000000  07000000 00000000 C9C5C6C2 D9F1F440
0020 00000000 01000004                                                       

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 096A10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 4982 (18,818)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000DD001317                                           
   BYTES READ = 00FB (   251)                                        

0000 80FA0100 00000000 00000000 00000000  00000000 00000000 00000000 00000000
0020 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000
              LINES 000975C0-00097640 SAME AS ABOVE                          
00E0 00000000 00000000 00000000 00000000  00000000 00000000 000000           

I/O REQUEST                                                           
   COMPLETION CODE = 41                                               
   CSW = 096A00 DEV STAT = 0E CHAN STAT = 40 RESIDUAL = 0005 (     5) 
   --- DEVICE STATUS  = CE DE UC                                      
   --- CHANNEL STATUS = IL                                            
   SENSE = 0008                                                       
   SEEK = 00000000DD001318                                            

I/O REQUEST                                                           
   COMPLETION CODE = 7F                                               
   CSW = 096A10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 4A6B (19,051) 
   --- DEVICE STATUS  = CE DE                                         
   --- CHANNEL STATUS =                                               
   SENSE = 0000                                                       
   SEEK = 00000000DD001401                                            
   BYTES READ = 0012 (    18)                                         

0000 801102F5 F7F5F2E2 C3F1F0F4 40030873  346F

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 096A10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 4A69 (19,049)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000DD001402                                           
   BYTES READ = 0014 (    20)                                        

0000 80138800 0278349F 0BD9E2C9 F4F0F9F5  F0F2F8F4          

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 096A10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 4A69 (19,049)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000DD001403                                           
   BYTES READ = 0014 (    20)                                        

0000 0D000000 00040000 06000000 40000008  00020008          

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 096A10 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 4A75 (19,061)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000DD001404                                           
   BYTES READ = 0008 (     8)                                        

0000 1BFF07FE F0F4F1F4    

I/O REQUEST                                                           
   COMPLETION CODE = 41                                               
   CSW = 096A10 DEV STAT = 0D CHAN STAT = 00 RESIDUAL = 4A7D (19,069) 
   --- DEVICE STATUS  = CE DE UE                                      
   --- CHANNEL STATUS =                                               
   SENSE = 0000                                                       
   SEEK = 00000000DD001405                                            

*** END OF FILE ***

Here is the output from reading the data blocks for the PDS member IEFBR14.

                                                                     
I/O REQUEST                                                          
   COMPLETION CODE = 41                                              
   CSW = 0969E8 DEV STAT = 0E CHAN STAT = 40 RESIDUAL = 0008 (     8)
   --- DEVICE STATUS  = CE DE UC                                     
   --- CHANNEL STATUS = IL                                           
   SENSE = 0008                                                      
   SEEK = 00000000D5000000                                           

I/O REQUEST                                                          
   COMPLETION CODE = 41                                              
   CSW = 0969E8 DEV STAT = 0E CHAN STAT = 40 RESIDUAL = 0008 (     8)
   --- DEVICE STATUS  = CE DE UC                                     
   --- CHANNEL STATUS = IL                                           
   SENSE = 0008                                                      
   SEEK = 00000000D5000100                                           

I/O REQUEST                                                          
   COMPLETION CODE = 41                                              
   CSW = 0969E8 DEV STAT = 0E CHAN STAT = 40 RESIDUAL = 0008 (     8)
   --- DEVICE STATUS  = CE DE UC                                     
   --- CHANNEL STATUS = IL                                           
   SENSE = 0008                                                      
   SEEK = 00000000D5000200                                           

I/O REQUEST                                                          
   COMPLETION CODE = 7F                                              
   CSW = 0969F8 DEV STAT = 0C CHAN STAT = 00 RESIDUAL = 0000 (     0)
   --- DEVICE STATUS  = CE DE                                        
   --- CHANNEL STATUS =                                              
   SENSE = 0000                                                      
   SEEK = 00000000D5000300                                           
   BYTES READ = 0100 (   256)                                        

   FIND MEMBER =   RC = 0
0000 D4E2E3D9 D1C3D340 0122022C 01220700  00000000 03F20003 C003C000 00008800
0020 00010000

Here is the output for finding MSTRJCL when multitrack mode is disabled (remember the NOP to force non-multitrack). This time it takes four EXCP I/O requests to locate the block instead of the one EXCP when searching a cylinder at a time.

Writing To A DASD Device

Now that we can successfully read data it is time to look at writing.  There are two types of write commands, Write Format and Write Update.  The Write Update commands are used to update an existing block and include the Write Data and the Write Key and Data CCW commands.  The Write Count, Key and Data command is a formatting command.  It is used to add blocks to a track.  Use of this command may also result in the removal of blocks from a track.

When a block is added to a track any previously existing blocks following the point where the new block is added will be deleted.  For example if a track contained five blocks (block-1, block-2, block-3, block-4, block-5) and a Write CKD for block-3 is issued to the track the original blocks 3, 4, and 5 will be deleted and replaced with the new block-3.  The updated track will then contain only three blocks (block-1, block-2, and block-3).

As the name implies the Write CKD command will write the Count, Key and Data components of a record.  We must supple all three to the write command.  The Count Area is eight bytes and consists of the record address in the format CCHHR (5 bytes), a key length (1 byte), and the data length (2 bytes).  Immediately following the Count Area is the Key data (if the key length is non-zero).  The length of the key data is specified in the Count Area.  The record data follows the key data.  As with the key the length of the data is specified in the Count Area.  The total of the length of the Count, Key and Data is the length we specify as the CCW length.

Code To Write Data

It is important to remember that we are writing data and if not careful serious damage could be done by writing to the wrong dataset.  To minimize the chance of accidently destroying something I changed the DDNAME in the DCB to EXCPWRIT.  This serves as a reminder to me to not write over SYS1.LINKLIB or my source PDS.

Our initial program to write DASD blocks is EXCP06.  Much of the code is the same as in our previous examples.  Here we will just examine the code specific to this exercise.

         OPEN  (EXCPDCB,OUTPUT)

We have to specify OUTPUT on our OPEN macro. If we open the DCB in INPUT mode then write commands will be disabled in the Device Mask and all Write CCW Commands will fail.

         DS    0D                                      
CCWSRCH  DC    X'31',AL3(IOBSRCH),X'40',X'00',AL2(5)   
CCWTIC   DC    X'08',AL3(CCWSRCH),X'40',X'00',AL2(0)   
CCWWRITE DC    X'1D',AL3(CARDBUF1),X'40',X'00',AL2(88) 
         DC    X'1D',AL3(CARDBUF2),X'40',X'00',AL2(88) 
         DC    X'1D',AL3(CARDBUF3),X'40',X'00',AL2(88) 
         DC    X'1D',AL3(CARDBUF4),X'40',X'00',AL2(88) 
         DC    X'1D',AL3(CARDBUF5),X'00',X'00',AL2(88)

Here is our Channel Program we will use to write blocks. It is similar to our read Channel Programs. It begins with a Search ID Equal (for Record Zero) followed by a TIC. When then have a series of Write CKD CCWs to write 5 records. We are writing 80 byte records so our data length is 88 (80 bytes of Data + 8 bytes for the Count). These are non-keyed records (key length = 0). Note the Command Chain bit is set for all but the last write command.

*                C C H H     R       KL     DL             
CARDBUF1 DC    X'00000000',X'01',AL1(0),AL2(80),CL80'CARD1'
CARDBUF2 DC    X'00000000',X'02',AL1(0),AL2(80),CL80'CARD2'
CARDBUF3 DC    X'00000000',X'03',AL1(0),AL2(80),CL80'CARD3'
CARDBUF4 DC    X'00000000',X'04',AL1(0),AL2(80),CL80'CARD4'
CARDBUF5 DC    X'00000000',X'05',AL1(0),AL2(80),CL80'CARD5'

Here are the five data buffers for the records we will be writing. Each consists of 8 bytes for the Count and 80 bytes of data. For now the CCHH is set to zeros. When we calculate the actual Cylinder-Head address using the DEB extent data we will fill in these fields. Next is the record number followed by the key length and data length values. Finally we have the 80 bytes of data.

*                                                                      
***********************************************************************
*               WRITE 5 BLOCKS TO TRACK 0 WITH ONE I/O                 
***********************************************************************
*                                                                      
         LA    R1,0          RELATIVE TRACK NUMBER ZERO                
         BAL   R14,GETCCHH   CONVERT TO MBBCCHHR                       
         LTR   R15,R15            CHECK RETURN CODE                    
         BZ    WRITE010           - BRANCH IF GOOD                     
*                                                                      
WRITE000 DS    0H                                                      
         LOG   'TTR CONVERSION FAILED'                                 
         WTO   'TTR CONVERSION FAILED',ROUTCDE=(1,11)                  
         B     EXIT

We begin by calculating the actual Cylinder-Head address for relative track zero.

WRITE010 DS    0H                    
         MVC   IOBSEEK(8),MBBCCHHR   
         MVC   CARDBUF1(4),MBBCCHHR+3
         MVC   CARDBUF2(4),MBBCCHHR+3
         MVC   CARDBUF3(4),MBBCCHHR+3
         MVC   CARDBUF4(4),MBBCCHHR+3
         MVC   CARDBUF5(4),MBBCCHHR+3

We can then set the CCHH address into the IOB Seek field as well as in the Count Area of our I/O buffers.

         MVI   IOBSEEK+7,0        SEARCH FOR R0           
*                                                         
         XC    ECB,ECB            CLEAR ECB               
         EXCP  IOB                ISSUE I/O REQUEST       
*                                                         
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE
*                                                         
         LA    R14,EXIT                                   
         CLI   IOBECBAD,X'7F'     WAS IT SUCCESSFUL       
         BNE   PRINTIOB           GO FORMAT IOB AND EXIT

Since we want to write record number one we have to search for Record Zero. If the track is properly formatted it will always have a Record Zero which is not considered to be a data record. We can then issue the EXCP and wait for it to complete. If the EXCP request was not successful we print out the IOB status fields and exit.

***********************************************************************
*                WRITE 5 BLOCKS TO TRACK 1 ONE I/O                     
*                    REWRITE BLOCK 1 (WRITE CKD)                       
***********************************************************************
*                                                                      
         LA    R1,1          RELATIVE TRACK NUMBER ZERO                
         BAL   R14,GETCCHH   CONVERT TO MBBCCHHR                       
         LTR   R15,R15            CHECK RETURN CODE                    
         BNZ   WRITE000           - BRANCH IF FAIL                     
*                                                                      
         MVC   IOBSEEK(8),MBBCCHHR                                     
         MVC   CARDBUF1(4),MBBCCHHR+3                                  
         MVC   CARDBUF2(4),MBBCCHHR+3                                  
         MVC   CARDBUF3(4),MBBCCHHR+3                                  
         MVC   CARDBUF4(4),MBBCCHHR+3                                  
         MVC   CARDBUF5(4),MBBCCHHR+3                                  
*                                                                      
         MVI   IOBSEEK+7,0        SEARCH FOR R0                        
*                                                                      
         XC    ECB,ECB            CLEAR ECB                            
         EXCP  IOB                ISSUE I/O REQUEST                    
*                                                         
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE
*                                                         
         LA    R14,EXIT                                   
         CLI   IOBECBAD,X'7F'     WAS IT SUCCESSFUL       
         BNE   PRINTIOB           GO FORMAT IOB AND EXIT

We will now do the same thing for relative track 1 and write the same five records.

*                                                           
         MVI   CCWWRITE+4,0       CLEAR COMMAND CHAIN BIT   
         XC    ECB,ECB            CLEAR ECB                 
         EXCP  IOB                ISSUE I/O REQUEST         
*                                                           
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE  
*                                                           
         LA    R14,EXIT                                     
         CLI   IOBECBAD,X'7F'     WAS IT SUCCESSFUL         
         BNE   PRINTIOB           GO FORMAT IOB AND EXIT

Now we will write just one record to the same track. The Command Chain bit in the first Write CCW is cleared which will make it the last CCW in the Channel Program. This should result in only one record on the track since all previously existing records following the record being written are erased.

*                                                          
         LA    R1,2          RELATIVE TRACK NUMBER ONE     
         BAL   R14,GETCCHH   CONVERT TO MBBCCHHR           
         LTR   R15,R15            CHECK RETURN CODE        
         BNZ   WRITE000           - BRANCH IF FAIL         
*                                                          
         MVI   CCWWRITE+4,0            CLEAR COMMAND CHAIN 
*                                                          
         MVC   IOBSEEK(8),MBBCCHHR                         
         MVC   CARDBUF1(4),MBBCCHHR+3                      
         MVC   CARDBUF2(4),MBBCCHHR+3                      
         MVC   CARDBUF3(4),MBBCCHHR+3                      
         MVC   CARDBUF4(4),MBBCCHHR+3                      
         MVC   CARDBUF5(4),MBBCCHHR+3

Now we will write five records to relative track 2 but we will write them one at a time. We begin as before by calculating the CCHH address and updating the IOB Seek and Count fields in the I/O buffers.

*                                                          
         MVI   IOBSEEK+7,0        SERCH FOR R0             
         LA    R1,CARDBUF1        RECORD 1                 
         STCM  R1,B'0111',CCWWRITE+1  UPDATE RECORD        
*                                                          
         XC    ECB,ECB            CLEAR ECB                
         EXCP  IOB                ISSUE I/O REQUEST        
*                                                          
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE 
*                                                          
         LA    R14,EXIT                                    
         CLI   IOBECBAD,X'7F'     WAS IT SUCCESSFUL        
         BNE   PRINTIOB           GO FORMAT IOB AND EXIT

Now we search for record zero. We place the I/O buffer for record one in the Write CKD CCW.

*                                                          
         MVI   IOBSEEK+7,1        SERCH FOR R1             
         LA    R1,CARDBUF2        RECORD 2                 
         STCM  R1,B'0111',CCWWRITE+1  UPDATE RECORD        
*                                                          
         XC    ECB,ECB            CLEAR ECB                
         EXCP  IOB                ISSUE I/O REQUEST        
*                                                          
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE 
*                                                          
         LA    R14,EXIT                                    
         CLI   IOBECBAD,X'7F'     WAS IT SUCCESSFUL        
         BNE   PRINTIOB           GO FORMAT IOB AND EXIT

To write the second record we update the search address to record one (the record we just wrote). We also place the I/O buffer for record two into the Write CKD CCW.

*                                                         
         MVI   IOBSEEK+7,4        SERCH FOR R4            
         LA    R1,CARDBUF5        RECORD 5                
         STCM  R1,B'0111',CCWWRITE+1  UPDATE RECORD       
*                                                         
         XC    ECB,ECB            CLEAR ECB               
         EXCP  IOB                ISSUE I/O REQUEST       
*                                                         
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE
*                                                         
         LA    R14,EXIT                                   
         CLI   IOBECBAD,X'7F'     WAS IT SUCCESSFUL       
         BNE   PRINTIOB           GO FORMAT IOB AND EXIT

The code to write records 3, 4, and 5 is same – each time updating the serarch field and the I/O buffer address.

At this time we should have written three tracks to our dataset. The first track contains 5 blocks, the second contains 1 blocks, and the third contains 5 blocks. Now we need an EOF record. Instead of writing the EOF ourselves we will allow EXCP to do it for us during CLOSE processing. This will also update the last track used and track balance fields is the DSCB. We need to supply the address of the last block we have written as well as the space remaining (track balance) for the last track used. We do this by placing these values into the DCB prior to issuing the CLOSE macro.

         MVC   EXCPDCB+5(8),IOBSEEK        LAST MBBCCHHR USED
         MVC   EXCPDCB+8(5),CARDBUF5                         
*                                                            
         L     R1,=A(19254)       EMPTY TRACK BALANCE        
         S     R1,=A(185+80)      R1                         
         S     R1,=A(185+80)      R2                         
         S     R1,=A(185+80)      R3                         
         S     R1,=A(185+80)      R4                         
         S     R1,=A(185+80)      R5                         
         STCM  R1,B'0011',EXCPDCB+18            TRACK BALANCE

We need the MBBCCHHR of the last record. The MBB portion is contained in the IOBSEEK field. We then copy the CCHHR from the Count Area of our last I/O buffer (the last block written).

Now we have to calculate the track balance. We begin with the empty track valance value (19,254 for a 3350) and subtract out the values for the blocks written. A non-keyed record has an overhead of 185 bytes (which includes the Count Area length). We add this to the data length to get the number of bytes consumed by a record and subtract it from the track balance. This value is stored into the DCB. I am using fixed offset values into the DCB. These fields are mapped by the DCBD macro so you could choose to use symbolic names.

As part of this exercise I was looking for how to get these values from the DSCB when opening a dataset. I had expected to find them in the JFCB but they were now where to be found. It is possible they are available if various options are specifed (such as DISP=MOD) or Open for Update. Instead of trying various combinations to cause Open to provide the values I used RDJFCB to read get the JFCB for the open dataset. From the JFCB I can get the Dataset Name and the Volume Serial. With this information I can read the Format 1 DSCB using the OBTAIN macro. This then gives me access to the last track used and track balance values stored in the DSCB. Although these values are not used in this example I do include the code to obtain them.

Upon running the program we should have a dataset containing 11 records. I used TSO EDIT to view the contents of the dataset.

edit 'tcs3.excp06.data' cntl                                
 DATA SET 'TCS3.EXCP06.DATA' NOT LINE NUMBERED, USING NONUM 
 EDIT                                                       
list                                                        
 CARD1                                                      
 CARD2                                                      
 CARD3                                                      
 CARD4                                                      
 CARD5                                                      
 CARD1                                                      
 CARD1                                                      
 CARD2                                                      
 CARD3                                                      
 CARD4                                                      
 CARD5                                                      
 END OF DATA                                                
end nos                                                     
 READY

The results are exactly as expected. I was interested in peeking a little deeper into the contents of the tracks in the dataset. I created a utility program that will report on the blocks making up the tracks in a dataset. The source for this program (DSUINFO) is available in the downloads. I use the Read Multiple CKD CCW Command to read the tracks and then report some basic information for each track.

Here is the DSUINFO output for the dataset created by EXCP06.

                                                   ---------- DSUINFO ----------

   TCS3.EXCP06.DATA                                VOL=SER=WORK01    DEV TYPE=3350
     EXTENTS =  1      TRACKS =      3     BLKSIZE =     80     LRECL =     80     DSORG = PS      RECFM = F 
TT=    0 CC=0148 HH=000F BLKS=  5 EOF=  0 KL=  0/  0/  0 DL=    80/    80/    80
TT=    1 CC=0148 HH=0010 BLKS=  1 EOF=  0 KL=  0/  0/  0 DL=    80/    80/    80
TT=    2 CC=0148 HH=0011 BLKS=  5 EOF=  0 KL=  0/  0/  0 DL=    80/    80/    80
TT=    3 CC=0148 HH=0012 BLKS=  0 EOF=  1 KL=  0/  0/  0 DL=     0/     0/     0
TT=    4 CC=0148 HH=0013 BLKS=  0 EOF=  1 KL=  0/  0/  0 DL=     0/     0/     0

We can see there are 5 tracks in the dataset. It was allocated with SPACE=(TRK,(5,0)). The first track contains 5 data blocks and no EOF blocks. The Min/Max/Avg Key Lenght is 0 and the Min/Max/Avg Data length is 80. The second track contains one block and the third track contains 5 blocks. You may notice the EOF occurs on the fourth track. The last track simply contains whatever was left over from previous use. In this case it happens to be a single EOF block.

Now an interesting question might be what happens if the dataset is alloced with only three tracks – where will the EOF be located?

                                                   ---------- DSUINFO ----------

   TCS3.EXCP06.DATA                                VOL=SER=WORK01    DEV TYPE=3350
     EXTENTS =  1      TRACKS =      3     BLKSIZE =     80     LRECL =     80     DSORG = PS      RECFM = F  
TT=    0 CC=006A HH=001B BLKS=  5 EOF=  0 KL=  0/  0/  0 DL=    80/    80/    80
TT=    1 CC=006A HH=001C BLKS=  1 EOF=  0 KL=  0/  0/  0 DL=    80/    80/    80
TT=    2 CC=006A HH=001D BLKS=  5 EOF=  0 KL=  0/  0/  0 DL=    80/    80/    80

And here is the answer to that question. There is no EOF record written. If all tracks in all extents are read without ever encountering an EOF record then the EOF is implied to exist after the last record in the last extent.

Finally we can check the DSCB to verify the last track used and the track balance are valid.

Last Block Used TTR ... 000205          
Bytes Unused Last Trk . 4609     (17929)

Writing A PDS With EXCP

For the next exercise I chose to write a PDS dataset using EXCP. There are several restrictions to this program:

  1. Members must be added is ascending name sequence
  2. The first track will be used for directory blocks. It doesnt matter how many directory blocks are specified on the initial allocation, the directory will be overwritten.
  3. The blocksize is hard coded to 3280 and the logical record length is hard coded to 80.
  4. ISPF statistics are not reloaded
  5. The dataset is completely overwritten and any previously existing data is lost.

I chose to use an IEBUPDTE style input.  I created my test data using the LISTPDS program (from the CBT Tape) using PARM=’NOLIST,DECK,UPDTE’ and ran it against my MVSSP source PDS.

The program can be found in the source (available in the downloads) under the name EXCPDS.

Program Overview

Input cards are read from the input file. Control cards have the sequence “./” in columns one and two. The only control card accepted is “./ ADD NAME=” to add a member to the dataset.

The first track of the dataset is used for directory blocks. A full track of directory blocks and an EOF will be written on the first track. It doesn’t matter how many directory blocks were initially allocated to the dataset, the original directory will be overwritten.

Member data will be written beginning on the second track (relative track number one). When all the data for a member has been written an EOF is written to mark then end of the member. A directory entry for the member is then added to the current directory block I/O buffer. Directory blocks are not written until they are completely full. When a directory blcok is full it is written to the directory track (relative track zero) and a new directory block is started.

When the final member is written to the dataset the directory “logical EOF” entry is written. This is a directory entry with the value x’FFFFFFFFFFFFFFFF’ (8 bytes of x’FF’). This is always the last entry in a PDS directory and does not point to a member.

Once the EOF entry is added to the directory block it is written. Empty directory blocks are then written to fill out the track. The last block written to the directory track is an EOF record.

There is a very basic program and there are several things that should be done to make it more useful. One would be to save up the directory entries in memory and then sort them before writing any data blocks. This would allow members to be added in any sequence.

It might also be good to initially read the directory to determine how many directory blocks initially exist in the dataset. The direcory blocks could then be written with an update write (Write Key+Data) instead of formatting the track.

I would also be possible without too much effort to then add members to the existing dataset instead of overwriting all previously existing data.

If really large amounts of data was being added blocks could be buffered up and written out a track at a time to reduce the number of EXCPs (I/O requests) which would speed up operation. We could also use multiple buffers for output so while one track was writing the next tracks buffers could be under construction.

This would result in a high performance PDS loader.

EXCPDS Code

         OPEN  (SYSPRINT,OUTPUT)       OPEN SYSPRINT DCB 
         TM    SYSPRINT+48,X'10'       WAS IT SUCCESSFUL 
         BO    OPEN010                    YES - BRANCH   
*                                                        
         WTO   'SYSPRINT FAILED TO OPEN',ROUTCDE=(1,11)  
         B     EXIT                                      
*                                                        
OPEN010  DS    0H                                        
         OPEN  (EXCPDCB,OUTPUT)        OPEN EXCPWRIT DCB 
         TM    EXCPDCB+48,X'10'        WAS IT SUCCESSFUL 
         BO    OPEN020                    YES - BRANCH   
*                                                        
         LOG   'EXCPWRIT FAILED TO OPEN'                 
         WTO   'EXCPWRIT FAILED TO OPEN',ROUTCDE=(1,11)  
         B     EXIT                                      
*                                                        
OPEN020  DS    0H                                        
         OPEN  (SYSIN,INPUT)           OPEN SYSIN DCB    
         TM    EXCPDCB+48,X'10'        WAS IT SUCCESSFUL 
         BO    OPEN040                    YES - BRANCH   
*                                                      
         LOG   'SYSIN FAILED TO OPEN'                  
         WTO   'SYSIN FAILED TO OPEN',ROUTCDE=(1,11)   
         B     EXIT                                    
*                                                      
*                                                      
OPEN040  DS    0H

We begin by opening the three DCBs. One for SYSPRINT, one for SYSIN which contains the IEBUPDTE stream, and the EXCP DCB used for the output dataset. If any DCB fails to open a message is logged and execution is terminated.

         GETMAIN R,LV=3280        DATA BLOCK BUFFER                
         ST    R1,BLKBUF          SAVE ADDRESS                     
*                                                                  
         GETMAIN R,LV=264         INDEX BLOCK BUFFER               
         ST    R1,IXBLKBUF        SAVE ADDRESS                     
*                                                                  
*                                 INITIALIZE DIRECTORY BLOCK BUFFER
         XC    0(8,R1),0(R1)      CLEAR KEY                        
         XC    8(256,R1),8(R1)    CLEAR DATA                       
         MVC   8(2,R1),=X'0002'   BYTES USED                       
*                                                                  
         RDJFCB (EXCPDCB)         READ JFCB FOR EXCPWRIT OUTPUT DS 
*                                                                  
         OBTAIN CAMSRCH           GET DSCB FOR EXCPWRIT            
*                                                                  
*                                                                  
         SLR   R1,R1                                               
         ICM   R1,B'0111',DSCB+X'36'   GET TTR OF LAST BLOCK USED  
         STCM  R1,B'0111',LASTBLK                                  
*                                                                     
         SLR   R1,R1                                                  
         ICM   R1,B'0011',DSCB+X'39'   GET TRACKBAL OF LAST TRACK USED
         STCM  R1,B'0011',TRKBAL

Next two I/O buffers are allocated using GETMAIN. The first is for the data blocks and is 3280 bytes which is the blocksize we will use for the output. The other is for the directory blocks and it is 264 bytes (256 bytes for the data plus 8 bytes for the key).

Next the directory block is initialized by clearing the buffer and then initializing the bytes used value which is the first two bytes in the data portion. This value is set to x’0002′ because the length uses up two bytes.

Next the JFCB is read to get the dataset name and volser. This information is used to read the format one DSCB to get the TTR of the last used block and the track balance for the last track used. This information is not currently used but would be needed if the output dataset was being extended instead of replacing all the contents.

         GET   SYSIN,INREC        READ AN INPUT RECORD
*                                                     
*                                                     
         CLC   =C'./',INREC       CONTROL RECORD      
         BE    MBR010                YES - BRANCH     
*                                                     
         LOG  'INVALID INPUT RECORD - NO ./ ADD'      
         B     EXIT

Processing begins by reading the first record from SYSIN and verifying it is a control record (./ ADD NAME=). If it is not a control record an error is logged and execution is terminated.

MBR010   DS    0H                                          
         CLI   FIRSTIME,0         FIRST TIME HERE          
         BNE   MBR020               YES - DON'T ISSUE STOW 
*                                                          
         BAL   R14,BLKWRITE       GO WRITE A BLOCK         
         BAL   R14,EOFWRITE       GO WRITE AN EOF          
*                                                          
         LA    R1,DIRNAME                                  
         BAL   R14,STOW           ADD "EOF" MEMBER ENTRY   
*

This begins the main processing loop. Each time a control record is detected (“./” in the first two bytes) control is transfered here. A first time switch is used to skip over the code to flush the member currently being loaded. If it is not the first time the current data block is flushed (written) to the output dataset. It may be a short block (less than 3280 bytes) but it will always be a multiple of 80 bytes. Next an EOF record is written to mark the end of the current member. Finally the directory entry is added by calling the STOW routine (please note this is done without using the STOW macro).

MBR020   DS    0H                                           
         MVI   FIRSTIME,0         CLEAR SWITCH              
         LA    R1,78              MAX BUFFER LENGTH         
         LA    R2,INREC+2         SKIP OVER ./              
MBR030   DS    0H                                           
         CLI   0(R2),C' '         SKIP BLANKS               
         BNE   MBR040                                       
         LA    R2,1(,R2)          NEXT CHAR                 
         BCT   R1,MBR030          LOOP BACK                 
         B     MBR900               - INVALID CONTROL CARD  
*                                                           
*                                                           
MBR040   DS    0H                                           
         CLC   =C'ADD ',0(R2)     ADD CARD                  
         BNE   MBR900                NO - ERROR             
*                                                           
         LA    R2,3(,R2)          SKIP OVER                 
         S     R1,=F'3'           ADJUST LENGTH             
MBR042   DS    0H                                           
         CLI   0(R2),C' '         SKIP BLANKS               
         BNE   MBR044                                       
         LA    R2,1(,R2)          NEXT CHAR               
         BCT   R1,MBR042          LOOP BACK               
         B     MBR900               - INVALID CONTROL CARD
*                                                         
*                                                         
MBR044   DS    0H                                         
         CLC   =C'NAME=',0(R2)    FOUND NAME=             
         BNE   MBR900                NO - ERROR           
*                                                         
         LA    R2,5(,R2)          POINT TO NAME           
         S     R1,=F'5'           ADJUST LENGTH           
*                                                         
         LA    R3,8               MAX NAME LENGTH         
         LA    R4,DIRNAME         TARGET PTR FOR MBR NAME 
         MVC   DIRNAME,=CL8' '    CLEAR MEMBER NAME       
MBR050   DS    0H                                         
         CLI   0(R2),C' '         LOOK FOR END OF NAME    
         BE    MBR060                                     
         MVC   0(1,R4),0(R2)      COPY CHAR               
         LA    R2,1(,R2)            NEXT INPUT BYTE       
         LA    R4,1(,R4)            NEXT OUTPUT BYTE       
         S     R1,=F'1'           ADJUST SOURCE LEN        
         BNP   MBR060             END IF END OF BUFFER     
         BCT   R3,MBR050          LOOP BACK FOR NEXT CHAR  
*                                                          
         CLI   0(R2),C' '         EXPECTING TO FIND A BLANK
         BNE   MBR910               -- NAME TOO LONG       
MBR060   DS    0H

This section of code scans the control statement and obtains the member name. I think it is all pretty straight forward so there is no need for additional comments.

         MVC   DIRTTR,CURTTR      ADDRESS OF NEXT RECORD TO WRITE 
         MVI   DIRC,0             NO USER DATA (FOR NOW)          
*                                                                 
         L     R8,BLKBUF          I/O BUFFER ADDRESS              
         SLR   R9,R9              ZERO LENGTH USED

Here is the initial processing for a new member. The TTR of the next block to be written is moved to the directory entry and the user data length is set to zero. Register 8 is primed with the current offset into the I/O buffer and register 9, which is used to contain the number of bytes used in the buffer, is set to zero.

MBR070   DS    0H                                       
         GET   SYSIN,INREC        GET NEXT RECORD       
         CLC   =C'./',INREC       CONTROL RECORD        
         BE    MBR010                YES - LOOP BACK    
*                                                       
         MVC   0(80,R8),INREC     COPY DATA             
         LA    R8,80(,R8)         NEXT RECORD           
         LA    R9,80(,R9)         ADJUST LENGTH         
         C     R9,=F'3280'        COMPARE TO BLOCK SIZE 
         BL    MBR070             LOOP BACK             
*                                                       
         BAL   R14,BLKWRITE       GO WRITE A BLOCK      
         L     R8,BLKBUF          I/O BUFFER ADDRESS    
         SLR   R9,R9              ZERO LENGTH           
         B     MBR070             LOOP BACK

This is the inner loop for processing all the input records for a single PDS member. First a check is made for a control record. If a control record is indicated we branch back to the main loop to finish up the current member and start processing for the new member.

If the record is not a control record it is copied into the output buffer. The buffer position pointer and the buffer length registers are incremented. If the output block is not full we simply loop back and read another card.

When the buffer is full we call the BLKWRITE routine to write it to the dataset and reset registers 8 and 9 before looping back.

MBR900   DS    0H                            
         LOG  'EPDS010E INVALID CONTROL CARD'
         B     EXIT                          
*                                            
*                                            
MBR910   DS    0H                            
         LOG  'EPDS011E MEMBER NAME TOO LONG'
         B     EXIT

If an error is detected an error message is logged and execution is terminated.

SYSINEOF DS    0H                                                 
         BAL   R14,BLKWRITE       FLUSH DATA BUFFER               
         BAL   R14,EOFWRITE       GO WRITE AN EOF                 
*                                                                 
         LA    R1,DIRNAME         STOW THE                        
         BAL   R14,STOW                   FINAL MEMBER            
*                                                                 
         MVC   DIRNAME,=X'FFFFFFFFFFFFFFFF'  "EOF" DIRECTORY ENTRY
         MVC   DIRTTR,=X'000000'                                  
         MVC   DIRC,=X'00'                                        
         LA    R1,DIRNAME                                         
         BAL   R14,STOW           ADD "EOF" MEMBER ENTRY

When EOF is detected on the input dataset we branch here (using the EODAD=SYSINEOF parameter on the DCB). The current data buffer is flushed and an EOF is written to finish out the current member. The directory entry is added using the STOW routine. We then need to add the “Logical EOF” directory entry. A member name of x’FFFFFFFFFFFFFFFF’ indicates the end of the directory. We add it using the STOW routine.

DIRCL010 DS    0H                                    
         SLR   R1,R1                                 
         ICM   R1,B'0111',IXTTR   GET TTR FOR BLOCK  
         ST    R1,WPARM+0         SAVE INTO PARM LIST
         L     R1,IXBLKBUF        I/O BUFFER         
         ST    R1,WPARM+4                            
         LA    R1,256             DATA LEN           
         ST    R1,WPARM+8                            
         LA    R1,8               KEY LEN            
         ST    R1,WPARM+12                           
         LA    R1,WPARM                              
         BAL   R14,WRITEBLK       GO WRITE THE BLOCK

Now we start a loop to fill the first track of the dataset with directory blocks. We start by flushing the current directory block. This is done by calling the WRITEBLK (which is our low-level write routine). We build a parameter list containing the TTR of the block to write, the I/O buffer, the data length, and the key length.

*                                                  
         SLR   R1,R1                               
         IC    R1,IXTTR+2         GET RECORD NUMBER
         LA    R1,1(,R1)          ADD ONE          
         STC   R1,IXTTR+2         SAVE IT BACK

Now the record number in the TTR for the next index block is incremented.

         L     R1,IXTKBAL         GET INDEX TRACK TRK BALANCE
         S     R1,KEYOH           SUBTRACT OUT KEY OVERHEAD  
         S     R1,=F'264'         SUBTRACK BLOCK+KEY LENGTH  
         ST    R1,IXTKBAL         SAVE UPDATED TRACK BALANCE

Here the track balance (bytes remaining) on the index track is updated.

         L     R1,IXBLKBUF        INITILIZE DIRECTORY BLOCK BUFFER
         XC    0(8,R1),0(R1)      CLEAR KEY                       
         XC    8(246,R1),8(R1)    CLEAR DATA                      
         STCM  R7,B'0011',8(R1)   BYTES USED IN BLOCK

Next the I/O buffer for the index block is reset to an empty block.

         L     R1,IXTKBAL         GET TRACK BALANCE          
         LA    R2,264             IX BLK KEY+DATA LEN        
         A     R2,KEYOH           BLK OVERHEAD               
         A     R2,NKEYOH          EOF OVERHEAD               
         CR    R1,R2              ROOM FOR DATA BLOCK + EOF  
         BH    DIRCL010             YES - GO WRITE FILL BLOCK

Now we check to see if there is enough room on the track to write another directory block and an EOF record on the index track. If both blocks will fit we can loop back and write the empty directory block.

         SLR   R1,R1                                         
         ICM   R1,B'0111',IXTTR    GET TTR FOR BLOCK         
         ST    R1,WPARM+0         SAVE INTO PARM LIST        
         LA    R1,IXBLKBUF        I/O BUFFER                 
         ST    R1,WPARM+4                                    
         LA    R1,0               DATA LEN                   
         ST    R1,WPARM+8                                    
         LA    R1,0               KEY LEN                    
         ST    R1,WPARM+12                                   
         LA    R1,WPARM                                      
         BAL   R14,WRITEBLK       GO WRITE EOF FOR DIRECTORY 
*                                                            
         B     EXIT

If there is not enough room for a directory block and an EOF record we write the EOF to complete the directory portion of the PDS. We then clean up and exit. An EOF is simply a record with a key length of zero and a data length of zero.

BLKWRITE – Write A Data Block

This routine is called to write a data block (member data) to the PDS. This routine does not write on the directory track.

BLKWRITE DS    0H                                                  
         STM   R14,R12,12(R13)    SAVE CALLERS REGISTERS           
         LA    R14,SAVEA3         CHAIN                            
         ST    R13,4(,R14)             ON                          
         ST    R14,8(,R13)               SAVE                      
         LR    R13,R14                       AREA                  
*                                                                  
*                                                                  
         LR    R1,R9              BLOCK SIZE                       
         A     R1,NKEYOH          ADD IN BLOCK OVERHEAD            
         C     R1,CURTKBAL        COMPARE TO CURRENT TRACK BALANCE 
         BL    BLKW010              RECORD WILL FIT ON TRACK

We start by saving the caller’s registers.  Next we determine if there is sufficient room on the current track to contain the block.

         SLR   R1,R1                                       
         ICM   R1,B'0011',CURTTR  GET CURRENT TRACK        
         LA    R1,1(,R1)          NEXT TRACK               
         STCM  R1,B'0011',CURTTR  SAVE BACK                
         MVI   CURTTR+2,1         START BACK WITH RECORD 1 
         L     R1,ETRKBAL         EMPTY TRACK BALANCE VALUE
         ST    R1,CURTKBAL        NEW CURRENT TRACK BALANCE

If we need to start on a new track we increment the track number in the TTR and reset the record number to one. We also must reset the current track balance to the empty track size.

         SLR   R1,R1                                 
         ICM   R1,B'0111',CURTTR  GET TTR FOR BLOCK  
         ST    R1,WPARM+0         SAVE INTO PARM LIST
         L     R1,BLKBUF          I/O BUFFER         
         ST    R1,WPARM+4                            
         ST    R9,WPARM+8         DATA LEN           
         LA    R1,0               KEY LEN            
         ST    R1,WPARM+12                           
         LA    R1,WPARM                              
         BAL   R14,WRITEBLK       GO WRITE THE BLOCK 

The low-level WRITEBLK routine is called to write the block to the dataset. I could have probably chosen better names since WRITEBLK and BLKWRITE are easy to confuse. The Assembler doesn’t care but it really isn’t great coding style.

*                                                            
         SLR   R1,R1                                         
         IC    R1,CURTTR+2        GET RECORD NUMBER          
         LA    R1,1(,R1)          ADD ONE                    
         STC   R1,CURTTR+2        SAVE IT BACK               
*                                                            
         L     R1,CURTKBAL        GET TRACK TRK BALANCE      
         S     R1,NKEYOH          SUBTRACT OUT BLOCK OVERHEAD
         SR    R1,R9              SUBTRACK BLOCK LENGTH      
         ST    R1,CURTKBAL        SAVE UPDATED TRACK BALANCE 
*                                                            
         L     R13,4(,R13)        UNCHAIN SAVE AERA          
         LM    R14,R12,12(R13)    RESTORE REGS               
         BR    R14                RETURN                     

The record number in the TTR is incremented, the track balance is updated and then we return to the caller.

EOFWRITE – Write An EOF In Data Area

The EOFWRITE routine is just like the BLKWRITE routine. It writes an EOF record in the member data area. It does not write on the directory track.

EOFWRITE DS    0H                                                 
         STM   R14,R12,12(R13)    SAVE CALLERS REGISTERS          
         LA    R14,SAVEA3         CHAIN                           
         ST    R13,4(,R14)             ON                         
         ST    R14,8(,R13)               SAVE                     
         LR    R13,R14                       AREA                 
*                                                                 
         L     R1,NKEYOH          ADD IN BLOCK OVERHEAD FOR EOF   
         C     R1,CURTKBAL        COMPARE TO CURRENT TRACK BALANCE
         BL    EOFW010              RECORD WILL FIT ON TRACK      
*                                                                 
*** NEED TO WRITE EOF ON A NEW TRACK                              
*                                                                 
         SLR   R1,R1                                              
         ICM   R1,B'0011',CURTTR  GET CURRENT TRACK               
         LA    R1,1(,R1)          NEXT TRACK                      
         STCM  R1,B'0011',CURTTR  SAVE BACK                       
         MVI   CURTTR+2,1         START BACK WITH RECORD 1        
         L     R1,ETRKBAL         EMPTY TRACK BALANCE VALUE       
         ST    R1,CURTKBAL        NEW CURRENT TRACK BALANCE       
*                                                              
*                                                              
EOFW010  DS    0H                                              
         SLR   R1,R1                                           
         ICM   R1,B'0111',CURTTR  GET TTR FOR BLOCK            
         ST    R1,WPARM+0         SAVE INTO PARM LIST          
         L     R1,BLKBUF          I/O BUFFER                   
         ST    R1,WPARM+4                                      
         LA    R1,0               KL=DL=0                      
         ST    R1,WPARM+8         DATA LEN                     
         ST    R1,WPARM+12        KEY LEN                      
         LA    R1,WPARM                                        
         BAL   R14,WRITEBLK       GO WRITE THE BLOCK           
*                                                              
*** SAVE CURRENT BLOCK ADDRESS IN CASE THIS IS THE LAST MEMBER 
*                                                              
         MVC   EXCPDCB+5(8),IOBSEEK     COPY MBBCCHHR          
         MVC   EXCPDCB+5+7(1),CURTTR+2  MOVE IN RECORD         
*                                                              
         SLR   R1,R1                                           
         IC    R1,CURTTR+2        GET RECORD NUMBER          
         LA    R1,1(,R1)          ADD ONE                    
         STC   R1,CURTTR+2        SAVE IT BACK               
*                                                            
         L     R1,CURTKBAL        GET TRACK TRK BALANCE      
         S     R1,NKEYOH          SUBTRACT OUT BLOCK OVERHEAD
         ST    R1,CURTKBAL        SAVE UPDATED TRACK BALANCE 
         STCM  R1,B'0011',EXCPDCB+18                         
*                                                            
         L     R13,4(,R13)        UNCHAIN SAVE AERA          
         LM    R14,R12,12(R13)    RESTORE REGS               
         BR    R14                RETURN                     

This is almost exactly the same as the BLKWRITE routine except there is no need to pass a pointer to a data buffer or a block length. We also update the DCB with the MBBCCHHR of the EOF record in case this is the member entry. This will cause CLOSE processing to set the last used TTR value in the DSCB. We only need to update the value in the EOFWRITE because an EOF will always be the last block we write.

STOW – Store Directory Entry

STOW     DS    0H                                        
         STM   R14,R12,12(R13)    SAVE CALLERS REGISTERS 
         LA    R14,SAVEA3         CHAIN                  
         ST    R13,4(,R14)             ON                
         ST    R14,8(,R13)               SAVE            
         LR    R13,R14                       AREA        
*                                                        
         LR    R10,R1             POINT TO DIR ENTRY     
         SLR   R9,R9                                     
         IC    R9,11(R10)         GET USER DATA LENGTH   
         N     R9,=A(X'1F')                              
         SLL   R9,2               MULTIPLY BY 2          
         LA    R9,12(,R9)         CALC DIR ENTRY LENGTH  

The STOW routine places a directory entry into a directory block. We begin by saving the caller’s registers. Next we calculate the total length of the directory entry (12 + user data length).

         SLR   R7,R7              CLEAR REG                
         L     R1,IXBLKBUF        POINT TO IX BLOCK BUFFER 
         ICM   R7,B'0011',8(R1)   BYTES USED IN DIR BLK    
         LA    R8,256             MAX BYTES AVAILABLE      
         SR    R8,R7              TOTAL BYTES AVAILABLE    
         CR    R9,R8              WILL THE ENTRY FIT?      
         BNH   STOW010               YES - BRANCH          

Next we determine how many unused bytes are in the current directory block. This is accomplised by subtracting the bytes used from the total length. We can then determine if the entry will fit into the current block.

         SLR   R1,R1                                   
         ICM   R1,B'0111',IXTTR   GET TTR FOR BLOCK    
         ST    R1,WPARM+0         SAVE INTO PARM LIST  
         L     R1,IXBLKBUF        I/O BUFFER           
         ST    R1,WPARM+4                              
         LA    R1,256             DATA LEN             
         ST    R1,WPARM+8                              
         LA    R1,8               KEY LEN              
         ST    R1,WPARM+12                             
         LA    R1,WPARM                                
         BAL   R14,WRITEBLK       GO WRITE THE BLOCK   

If the block is full we must write it onto the directory track using the WRITEBLK (low-level write) routine.

         SLR   R1,R1                                         
         IC    R1,IXTTR+2         GET RECORD NUMBER          
         LA    R1,1(,R1)          ADD ONE                    
         STC   R1,IXTTR+2         SAVE IT BACK               
*                                                            
         L     R1,IXTKBAL         GET INDEX TRACK TRK BALANCE
         S     R1,KEYOH           SUBTRACT OUT KEY OVERHEAD  
         S     R1,=F'264'         SUBTRACK BLOCK+KEY LENGTH  
         ST    R1,IXTKBAL         SAVE UPDATED TRACK BALANCE 

Next we increment the record number and update the track balance value for the directory track.

         L     R1,IXBLKBUF        INITIALIZE DIR BLK I/O BUFFER 
         XC    0(8,R1),0(R1)      CLEAR KEY                     
         XC    8(256,R1),8(R1)    CLEAR DATA                    
         LA    R7,2               BYTES USED                    
         STCM  R7,B'0011',8(R1)   BYTES USED IN BLOCK           
         LA    R8,254             BYTES AVAILABLE               

Finally we initialize the directory block I/O buffer as an empty directory block.

STOW010  DS    0H                                      
         L     R2,IXBLKBUF        BUFFER               
         MVC   0(8,R2),0(R10)     UPDATE KEY           
         LA    R2,8(,R2)          POINT PAST KEY       
         AR    R2,R7              POINT PAST LAST ENTRY
         LR    R1,R9              SIZE OF DIR ENT      
         BCTR  R1,0                                    
         EX    R1,STOWMVC                              
STOWMVC  MVC   0(1,R2),0(R10)     COPY IN DIR ENTRY    
         AR    R7,R9              UPDATE BYTES USED    
         L     R1,IXBLKBUF        I/O BUFFER           
         STCM  R7,B'0011',8(R1)   UPDATE BYTES USED    

Now we can add the directory entry into the directory block. First we update the block key. We then point past the last used entry in the block. Now the directory entry can be copied into the I/O buffer. After updating the bytes used (the first two bytes of the block) we are done.

         L     R13,4(,R13)        UNCHAIN          
         LM    R14,R12,12(R13)           SAVE AREA 
         BR    R14                RETURN TO CALLER 

Just restore the caller’s registers and return.

WRITEBLK – Low-Level Write Routine

***********************************************************************
* WRITE A BLOCK                                                        
*                                                                      
*  R1 = A(PARM LIST)                                                   
*       +0    0TTR                                                     
*       +4    BUFFER ADDR                                              
*       +8    BLOCK LENGTH                                             
*      +12    KEY LENGTH                                               
*                                                                      
***********************************************************************

This routine expects the address of a 4-word parameter list to be passed in register 1. The parameter list contains the TTR of the block to be written, a pointer to the I/O buffer to write, the length of the block to write, and the length of the key for the block. The I/O buffer contains the key (if any) followed by the block data.

WRITEBLK DS    0H                                          
         STM   R14,R12,12(R13)     STORE CALLER'S REGISTERS
         LA    R14,SAVEA2          CHAIN                   
         ST    R13,4(,R14)              ON                 
         ST    R14,8(,R13)                SAVE             
         LR    R13,R14                        AREA         
*                                                          
         LR    R10,R1        SAVE PARAMETER POINTER        

As always we begin by saving the caller’s registers.

         L     R1,0(,R10)         TTR OF BLOCK TO WRITE   
         SRL   R1,8               SHIFT OFF RECORD NUMBER 
         BAL   R14,GETCCHH        CONVERT TO MBBCCHHR     
         LTR   R15,R15            CHECK RETURN CODE       
         BZ    WRITE010           - BRANCH IF GOOD        
*                                                         
WRITE000 DS    0H                                         
         LOG   'TTR CONVERSION FAILED'                    
         WTO   'TTR CONVERSION FAILED',ROUTCDE=(1,11)     
         B     EXIT                                       
*                                                         
*                                                         
WRITE010 DS    0H                                         

The relative track address is isolated from the TTR and the actual MBBCCHH is calculated.

WRITE010 DS    0H                                                
         MVC   IOBSEEK(8),MBBCCHHR      SET IOB SEEK ADDRESS     
         MVC   COUNTBUF(4),MBBCCHHR+3   SET CCHH INTO COUNT AREA 
*                                                                
         L     R1,0(,R10)         TTR OF BLOCK TO WRITE          
         STC   R1,COUNTR          PUT RECORD NUMBER INTO COUNT   

The MBBCCHH is moved to the IOB to provide the SEEK address. The CCHH is also copied into the Count buffer area and the record number is added.

         L     R2,8(,R10)         DATA BLOCK LENGTH        
         L     R3,12(,R10)        KEY LENGTH               
         STCM  R3,B'0001',COUNTKL SET KEY LEN IN COUNT     
         STCM  R2,B'0011',COUNTDL SET DATA LEN IN COUNT    
         AR    R3,R2              GET TOTAL LENGHT KEY+DATA
         STCM  R3,B'0011',CCWWRIT2+6  STORE INTO CCW            

The Key length and the Data length are then copied into the Count buffer area and then added together and placed into the CCW. Here I am using Data-Chaining so I can have separate buffer areas for the count and the key+data. Here is the Channel Program used.

         DS    0D                                                    
CCWSRCH  DC    X'31',AL3(IOBSRCH),X'40',X'00',AL2(5)    SERACH       
CCWTIC   DC    X'08',AL3(CCWSRCH),X'40',X'00',AL2(0)    TIC          
CCWWRITE DC    X'1D',AL3(COUNTBUF),X'80',X'00',AL2(8)   WRITE CKD    
CCWWRIT2 DC    X'00',AL3(*-*),X'00',X'00',AL2(*-*)      (DATA CHAIN) 

Looking at eh CCWWRITE we see a command code of X’1D’ for Write CKD. The data address points to the count buffer. In the flags Data-Chaining is set and the length is 8 (the length of the count area). Data chaining will cause the current operation to continue using the address and length fields of the next CCW. This keeps us from having to move data around to get everything into a single I/O buffer.

         LTR   R3,R3              IS IT AN EOF RECORD      
         BNZ   WRITE020              NO - BRANCH           
*                                                          
         NI    CCWWRITE+4,255-X'80'  TURN OFF DATA CHAIN   
WRITE020 DS    0H 

Now we check to see if there is any key and data to write. These values are zero to write an EOF record. If they are zero we need to turn off the data-chaining bit so the channel will not attempt to use the second CCW as part of the write.

         L     R1,4(,R10)         POINT TO DATA BUFFER
         STCM  R1,B'0111',CCWWRIT2+1  STORE INTO CCW  
*                                                     
         L     R1,0(,R10)         GET TTR             
         N     R1,=A(X'FF')       KEEP ONLY RECORD    
         BCTR  R1,0               SUBTRACT 1          
         STC   R1,IOBSEEK+7       SEARCH FOR RN-1     
*                                                     

Now the Key+Data buffer address is palced in the second write CCW. The Search address is set for one less than the record number we are writing.

         XC    ECB,ECB            CLEAR ECB               
         EXCP  IOB                ISSUE I/O REQUEST       
*                                                         
         WAIT  1,ECB=ECB          WAIT FOR I/O TO COMPLETE
*                                                         
         OI    CCWWRITE+4,X'80'   TURN ON DATA CHAIN      

Now we issue the I/O and wait for completion. We also reset the data-chaining bit in case we previously turn it off.

         CLI   IOBECBAD,X'7F'     WAS IT SUCCESSFUL     
         BE    WRITE900                                 
*                                                       
         BAL   R14,PRINTIOB       GO FORMAT IOB AND EXIT
         L     R13,4(R13)         UNCHAIN SAVE AREA     
         B     EXIT                                     
*                                                       
WRITE900 DS    0H                                       
         L     R13,4(,R13)        UNCHAIN SAVE AREA     
         LM    R14,R12,12(R13)    RESTORE REGISTERS     
         BR    R14                RETURN TO CALLER      

The completion code is checked. If the I/O was not completed successfully the information from the IOB is logged and exeuction is terminated. If all is well we restore the caller’s registers and return.

A Successful PDS Load

Here is the member listing from RPF.

EDIT       PDS : TCS3.EXCPDS.PDS----------------------------------------------
Cmd =>                                                                        
C Name     Newname  TTR    Userid  Date       Time  Lines Level  Members=00037
  #ASM              000101                                                    
  #BR14             000104                                                    
  #DSUINFO          000106                                                    
  #EXCP01           000108                                                    
  #EXCP02           00010A                                                    
  #EXCP03           00010C                                                    
  #EXCP04           00010E                                                    
  #EXCP05           000110                                                    
  #EXCP06           000112                                                    
  #LINK             000203                                                    
  #LINKSVC          000205                                                    
  #LINKSVX          000207                                                    
  #PUNCH            000209                                                    
  #PUNCH2           00020B                                                    
  #RUN              00020D                                                    
  #RUNSVX           00020F                                                    
  CIB01             000212                                                    
  DSUINFO           000307                                                    
  EXCP01            000805                                                    
  EXCP02            000A06                                                    
  EXCP03            000C07                                                    

And here is the XDUMP member. Everything looks good!!

Update XDUMP   : Trunc Xlate Top Nonum Nulls Asis -----------------------------
Cmd =>                                                  Scope 01,72 Scroll CSR 
...... ----+----1----+----2----+----3----+----4----+----5----+----6----+----7--
000100 XDUMP    CSECT ,                                                        
000200 *********************************************************************** 
000300 *  -- XDUMP --                                                        * 
000400 *                                                                     * 
000500 *     PRINT DATA IN DUMP FORMAT                                       * 
000600 *                                                                     * 
000700 *     ON INPUT R1 = A(DUMP PARM LIST)                                *  
000800 *           A  - ADDRESS OF DATA TO DUMP                              * 
000900 *           F  - LENGTH OF DATA                                       * 
001000 *           A  - PRINT ROUTINE                                        * 
001100 *           A  - USER WORD                                            * 
001200 *                                                                     * 
001300 *     ON ENTRY TO PRINT ROUTINE                                       * 
001400 *          R0  - USER WORD CONTENTS                                   * 
001500 *          R1  - PRINT LINE (CL133 FIRST BYTE IS BLANK)               * 
001600 *          R14 - RETURN ADDRESS                                       * 
001700 *          R15 - PRINT ROUTINE ENTRY ADDRESS                          * 
001800 *                                                                     * 
001900 *********************************************************************** 
002000 *                                                                       
002100          SAVE  (14,12),,*                                               

Concatenated PDS Datasets

MVS allows multiple PDS datasets to be concatenated and opened for input. This results in a DEB with extents for every dataset in the concatenation. To make this happen the DCB must have DSORG=PO specified (not DSORG=PS).

Below is a dump of a DEB created for concatenated PDS datasets.

0000 00AF1078 02AFD610 69000000 00001100  ......O.........
0010 05000000 FF000000 8F095BC0 04AFDE88  ..........$....H
0020 50001E68 00000178 00000178 001D001E  &...............
0030 50001E68 00000179 00000179 001D001E  &...............
0040 50001E68 00000176 00000176 001D001E  &...............
0050 50001E68 00000177 00000177 001D001E  &...............
0060 50001E68 00000175 00000175 001D001E  &...............
0070 02040000 00000000 00000000 00000000  ................

There are three datasets concatenated here. The first has two extents, the second has two extents and the third has a single extent.  The first thing we need to check is a bit that is not documented in the IEZDEB mapping macro.  We need to see if bit 7 (x’01’) is on in the DEBOFLGS field located at offset +08.  If this bit is set to one then we know we have concatenated datasets.

Next we need to locate the pointers to the beginning extent for each dataset.  The are located immediately following the last extent description in the DEB.  The DEBAMLNG field (one byte located at offset +4) indicates how many pointers there are.  There will be one less than the number of datasets concatenated.  The pointer to the first dataset is not included because it is always the first extent.

We can locate the pointers by using the DEBNMEXT field (one byte located at offset +16 [x’10’]) which contains the total number of extents.

In this example we see that the second dataset begins with extent number 2 (relative to zero – or the 3rd extent) and the third dataset begins with extent number 4 (relative to zero – or the 5th extent).

Each PDS directory is located at the beginning of the extent for the dataset.  It is important to remember the TTR pointers in the directory blocks are relative to the beginning of the dataset.  The TTR to MBBCCHHR conversion must take the concatenation number into account when calculating the physical address from a TTR pointer.

I have not written any example code but if you understand the basics of reading a PDS member it should not be too difficult to read from concatenated PDS datasets.