THE SECRETS OF SYSTEM TABLES... REVEALED!
                       by Eugene Volokh, VESOFT
       Presented at 1985 HPIUG Conference, Washington, DC, USA
        Published by SUPERGROUP Magazine, Nov 1984/Apr 1985.
   Published in "Thoughts & Discourses on HP3000 Software", 3rd ed.


          The System Table is an Ornery Beast
          That defies investigation
          Unless one first acquaints oneself
          With the proper incantations.

          Long must one labor o'er
          Deep-hidden books of arcane lore
          Until one learns the secrets of
          EXCHANGEDB, MFDS, and more.

          And formats one must understand
          What's in word one, what's in word two
          What lurks inside the fourteenth bit
          And what the sixteenth SIR can do.

          A systems programmer's what Wilfred was,
          With stolid heart and trusty blade,
          And system tables he did read,
          And many useful programs made.

          Oh, listen to his fearsome tale
          Of magics dark and dragons fierce,
          And catch a glimpse of mysteries
          Not often told upon this earth.

           -- "The Saga of Wilfred,"
              Norse, ca. 11th century AD.


ABSTRACT

MPE's  System Tables contain a large variety  of data that can be very
useful  for  any task from performance  optimization to writing system
management  utilities to improving system security and adding power to
application  programs. Unfortunately, nowhere  is it clearly explained
(or  for that matter, explained any way at all) how one can access all
these  system  tables. The goal of this paper  is to shown how one can
access   data   segments,  file  labels,  absolute  memory  locations,
disc-resident  system tables -- in general, all forms of system tables
--  so that armed with the information  gleaned from here and a System
Tables  Manual,  a programmer can sit  down and start writing programs
that manipulate data segments.



INTRODUCTION


Just  like any other program, the  operating system must keep track of
its current state; information on all the things known to it -- files,
jobs, programs, everything -- must be kept somewhere. A "System Table"
is  just  a  name  for  a data structure  containing such data. System
tables  may  be  stored on disc, in memory  as a fixed array of memory
locations  (e.g.  locations %1000 to %1377 of  bank 0), in memory as a
data segment or a portion thereof, in the stack of some system process
or  even  a  user  process  --  any  data structure  maintained by the
operating system can be legitimately called a system table.

There are two problems with using system tables, which account for the
fact  that  few people know how to use  them safely -- one is that the
format  of the tables is little-known,  and is usually documented only
in  the System Tables Manual (not  the most readable document known to
man)  or,  worse,  only  in  the  source  code of  the procedures that
actually  maintain  the  data  structure.  Even  when  the  format  is
documented (e.g. "word 7 contains the flumbrug framastat" is a typical
example),   various   important  details  (just  what  is  a  flumbrug
framastat) are omitted. Equally bad is the other major problem -- even
if  one  knows  the format of a system  table, nowhere does it clearly
describe how to access it.

This  paper  will  try  to  address both these  problems. It will give
instructions  on  how  to access various system  tables, but will also
explain   what  the  various  system  tables  are  and  how  they  are
inter-related.  This,  coupled  with the precise  formats given in the
System  Tables Manual, should allow the  interested reader to learn to
manipulate  much  useful  system  information.  If you  don't have the
System  Tables  Manual,  you  can  order  it  from  HP as  part number
32002-90003 (MPE IV) or 32033-90010 (MPE V).

Note  that  although all the examples in  this paper are given in SPL,
there is no reason why you can't write system table-accessing programs
in  PASCAL, FORTRAN, or even COBOL. All  that is necessary is to write
simple  SPL routines for performing those  operations (such as MFDS or
TURNOFFTRAPS)  that can only be done in SPL, and then put the routines
in  an RL or an SL. That way,  the routines will be callable from your
favorite  language. System procedures like FLABIO, ATTACHIO, or GETSIR
can already be called from other languages.



"WAITER, THERE'S A SYSTEM TABLE IN MY STACK!"


It  might come as some surprise to you that one place the system hides
a  system  table  is  in  your  very own stack. Your  stack is a "data
segment," a contiguous chunk of memory no more than 32K words long, in
which  addressing  is  done relative to the  "data base" (DB) register
(not  to  be  confused  with IMAGE databases). If  you draw your stack
(with  addresses growing toward the top), it would look something like
this (see Section I of SPL Manual):

                       High memory
  Z >   ---------------------------------------------   ^
        |              unused space                 |   ^
  S >   ---------------------------------------------   ^
        | operands of machine instructions          |   ^
        | and local variables of the currently      |   ^
        | executing procedure                       |   ^
  Q >   ---------------------------------------------   ^
        | local variables of other procedures       |   ^
        | and stack markers indicating calls from   |   ^
        | one procedure to another                  |   ^
  Qi >  ---------------------------------------------  positive
        | global variables                          |  addresses
  DB >  ---------------------------------------------
        | "DB negative area," accessible only by    |  negative
        | SPL procedures (like V/3000) -- usable    |  addresses
        | for global storage                        |   v
  DL >  ---------------------------------------------   v
        | The Nether Regions, where mortals may     |   v
        | not stray and non-privileged accessors    |   v
        | are punished with bounds violations       |   v
        ---------------------------------------------   v
                       Low memory

At  the very top is the Z  register (stands for Zounds?), which is the
uppermost  boundary  of  the  stack  data  segment (note  that in some
manuals,  HP  draws  the  stack  upside down, with DL  on top and Z on
bottom  -- keep this in mind when comparing this picture with others).
Then,  below it is the S register, which marks the "top of the stack";
parameters to machine instructons are kept at and around S. Below S is
the  Q register, which indicates the top  of the stack at the time the
currently-executing   procedure  was  called;  the  procedure's  local
variables  are allocated above it, with the procedure's parameters and
the  stack  marker (indicating the place  from which the procedure was
called) immediately below it.

"Qi" is actually not a register, but rather the initial value of the Q
register  (which changes with every procedure call and every procedure
exit).  DB  is  the base register, from  which all addressing is done.
Word  address  6680 is actually "DB+6680";  any pointers that are kept
(even if they are pointers to local variables, which are between the Q
and  S  registers)  are relative to DB, since  DB is the only register
that  is  guaranteed  not  to  change. DL (which -  since it, like all
registers  save for DB itself, is expressed as a DB-relative address -
is  a  negative  address) marks the lowest point  in the stack that is
accessible  to user code; it, too,  can be moved to dynamically expand
or  contract  the  useful  DL-DB  area,  in which  SL procedures (like
V/3000) can store global data.

What  interests  us  is the "nether reaches,"  the area below DL. Your
mother  may  not  have  told you this, but there  is a whole wealth of
information  (for  many programs I've seen,  more interesting than the
stuff above DL) called the "PCBX" tucked away there. "PCBX" stands for
"PCB  eXtension" (the PCB being an  important system table that you'll
hear  more about later), and contains much of the information that the
system needs to know about the process -- what files it has open, what
session  it  belongs to, where other  tables that describe the session
more thoroughly (the JIT and JDT) are, what data segments this process
has allocated that must be deleted when it dies, and so on.

The  PCBX  is  divided  into  four  portions  (see Ch.7  of Sys.Tables
manual):

  - PXGLOB, which lists things like the capabilities of the person who
    is running the process, the DST indexes (aka data segment numbers,
    which  we'll  talk more about later)  of the Job Information Table
    (JIT)  and  Job Directory Table (JDT)  pertaining to the process's
    session, and so on.

  -  PXFIXED,  which describes some other  things about the process --
    what  data segments it has allocated,  what session it belongs to,
    what  the  values  of  its  registers are (if  it is not currently
    active;  if  it is currently activate,  i.e. being executed by the
    CPU,   the   register  values  are  actually  stored  in  the  CPU
    registers), and so on.

  - PXFILE, which describes the files that the process has opened.

  -  The pointer area, which points to the other portions. Unlike most
    other  pointers, these pointers are DL relative and are implied to
    be negative; i.e. a value of 364 points to DL-364.

They are arranged roughly as follows:

  DL >  ---------------------------------------------
  DL-1> | DL-relative pointer to PXGLOB             | -------------->
  DL-2> | DL-relative pointer to PXFIXED            | ---------->   |
  DL-3> | DL-relative pointer to PXFILE             | ------>   |   |
  DL-4> | Special count that we don't care about    |       |   |   |
        ---------------------------------------------       |   |   |
        |                                           |       |   |   |
        |                  PXFILE                   |       |   |   |
        |                                           |       |   |   |
        --------------------------------------------- <-----v   |   |
        |                                           |           |   |
        |                  PXFIXED                  |           |   |
        |                                           |           |   |
        --------------------------------------------- <---------v   |
        |                                           |               |
        |                  PXGLOB                   |               |
        |                                           |               |
        --------------------------------------------- <-------------v

Now,  how does one access these system  tables? Well, they are kept in
your  stack,  so  they  can  be accessed using  simple SPL DB-relative
pointers.  For instance, to get word 2 of the PXGLOB (which happens to
be, in both MPE IV and MPE V, the zeroth word of the user's capability
mask,  containing  information on whether the  user has SM capability,
AM,  etc. -- all the capabilities save for PM, PH, DS, MR, IA, and BA)
you would merely have to do the following:

  INTEGER POINTER DL;
  INTEGER CAPS;
  PUSH (DL);  << push the address of DL onto the stack >>
  @DL:=TOS;   << DL now points to the DL register >>
  GETPRIVMODE;  << all DL-negative addressing must be done in priv >>
  CAPS:=DL(
          -DL(-1)+   << -DL(-1) is the DL-relative ptr to PXGLOB >>
          2);        << add 2 to get to the caps word >>
  GETUSERMODE;

Voila!  Note  that we have to be in privileged mode to access ANY word
of  the PCBX. After all, if you could  do this in user mode, you could
change  the  line  "CAPS:=DL(-DL(-1)+2)" to  "DL(-DL(-1)+2):=CAPS" and
CHANGE  the capabilities word instead of just reading it... in fact, a
variation  of  this  is  exactly  what  VESOFT's  GOD  program  (which
temporarily  -- for the duration of the  session -- gives the user who
ran it all the capabilities and all the ALLOWs) does.

Similarly,  one  can  get  at all the other  portions of the PCBX. For
instance,  to  get  the Active File Table  (AFT) entry for file number
FNUM, one can do the following:

  INTEGER POINTER DL;
  INTEGER ARRAY COPY'OF'AFT'ENTRY(0:5);
  PUSH (DL);
  @DL:=TOS;
  GETPRIVMODE;
  MOVE COPY'OF'AFT'ENTRY:=DL(-6*FNUM-4),(6);
  GETUSERMODE;

Of  course, you must first know that the  AFT entry for file FNUM is 6
words long and always starts at location DL-6*FNUM-4 (in MPE V only --
this  is  different  from MPE IV). Also note  that the AFT is actually
known  as  either  the  Active File Table or  the Available File Table
(depending  on which chapter of the System Tables Manual you believe).
You can look up its format in chapters 6 and 7 of the manual.

If  you  want  a brief exercise in using  this access method, whip out
your  trusty  System Tables Manual and write  a small program to print
out   your  current  capability  word  (in  octal)  and  your  current
job/session  number  (HINT: it's in the  PXFIXED). While you're at it,
also   change  your  current  capability  word  to  give  yourself  SM
capability  (make sure you do not already  have it) and do a "LISTUSER
MANAGER.SYS" using the COMMAND intrinsic before and after this change.
You don't need to change it back since it is valid for the duration of
the process only, and when the process dies, the user is left with the
same capabilities as before. The solution to this problem (which we'll
call Problem 1) is in Appendix 1.

Of  course, when you're learning to  use these access methods, be sure
to  run all your test programs during off hours or on a non-production
computer.  I've  crashed my share of computers  when I was starting to
work  with  system  tables,  and  you'll  probably  crash  some,  too.
Fortunately,  once  you  get  acquainted  enough  with  the techniques
involved,  and  especially if you use some  of the safety devices I'll
describe  later, your programs will probably  be far more reliable. In
the meantime, expect some foul-ups.

Another  useful  trick  is using privileged mode  DEBUG to ensure that
your  program  is working right. Say  your program doesn't produce the
correct  result -- it could either be because you're trying to get the
wrong  value  (e.g. PXGLOB location 3  doesn't really contain what you
think  it contains) or that you're going for the right value, but your
method  of getting it is wrong or  you're outputting it wrong. To find
out  what  the  problem  is,  you  can use DEBUG to  look at the value
manually  --  if  it's  the  expected  value, then the  bug is in your
retrieval or output algorithms; if it's not the expected value, you're
trying to retrieve from the wrong place.

The  way  to get at values in  PXGLOB using privileged mode DEBUG (you
must  be  a  privileged user to do this!) is  to type a command of the
type  "DDL-address,length". Don't forget that DEBUG by default assumes
octal  input  and output; to input a  decimal number, prefix it with a
"#";  to make output come out in decimal, type "DDL-address,length,I".
Thus, to see the value of DL-1 (the PXGLOB pointer), type "DDL-1,1,I",
or  just "DDL-1,I" since 1 is the default length. To find the value of
PXGLOB   word  3,  you  can  either  first  do  a  "DDL-1,I",  and  do
"DDL-#xxx+3,I"  (where  xxx is the value that  the DDL-1 gave you), or
you  can say "DDL-'DL-1'+3,I" -- the  "'DL-1'" means "the value stored
at DL-1".

One  important note: although the access method I just described works
well enough, I favor a different approach that I'll discuss when I get
to  system  tables  kept  in  arbitrary  data  segments  and  the MFDS
instruction.



THERE'S DATA IN THEM THAR DATA SEGMENTS


If  you  peek  into  your  friendly neighborhood  System Tables manual
(which  at last count had 23 chapters),  you will find that about half
of chapter 7 is dedicated to tables that reside in stacks' DL-negative
areas,  and  you  might  start  to  wonder  what the  remaining 22 1/2
chapters  are about. Well, most of  the system tables described in the
manual  are  stored  in  operating  system  Data Segments,  and if you
thought  that DL-negative tables were tough to access, just wait until
you see these.

It  doesn't make sense to keep data in fixed locations in memory, e.g.
putting the table of currently active jobs in locations %10400 through
%10777, a process's stack in locations %533000 through %534377, and so
on.  For  one,  the very concept of  "virtual memory" demands that the
operating  system be able to swap data out from disk and then bring it
back  into a different location in memory; furthermore, the HP3000 is
still a 16-bit machine (yes, Virginia, there are still 16-bit machines
out  there,  and you're using one of  them), and it would have trouble
supporting addresses that are more than 16 bits long.

Rather,  the  data  is  broken  up  into many  "segments". Within each
segment, all addresses are relative to the base of the segment; if one
segment has to point to another, it would have both the segment number
and  the  offset  within  the  segment.  This  way,  a segment  can be
conveniently  swapped in and out of memory, since the physical address
of  the  segment  is  kept  in  only one table that  is managed by the
swapper;  also,  since  most  addresses  are segment-internal,  we can
usually get away with quick 16-bit addressing.

Consider  for  instance  your own process stack --  it, too, is a data
segment. Addresses within it are relative to the process's DB register
(a  slight difference from other segments,  in which the addresses are
relative to the segment's base); the stack contains all your data plus
the  tables in the DL-negative area that  we talked about. In case you
should wish to access another process's stack, you can -- all you need
is  its  "data  segment  number."  Similarly,  the  table  of  all the
currently  active  jobs  (called the "JMAT," for  Job MAster Table) is
also  kept  in  its  own data segment, as  is the table describing all
currently  active  processes  (the PCB --  Process Control Block). Any
such table can be accessed if you only know its data segment number.

Incidentally,  let me mention  a certain documentation inconsistencies
pertaining  to data segments. What I call the "data segment number" is
sometimes called:

  A  "DST index." All existing data segments are described in a system
  table  (which  itself is in a data  segment) called the Data Segment
  Table,  a k a DST. The number of  the data segment is thus the index
  into that Data Segment Table.

  A "DST number," a corruption of "DST index." Doesn't make much sense
  -- Data Segment Table number?

  A  "DST,"  a  further  corruption  of "DST index."  Although this is
  actually both ambiguous and technically improper, lots of people use
  it  (including  yours  truly). It's all HP's  fault that they didn't
  call  it  something  decent like "data segment  number" in the first
  place.

I shall make a daring attempt to CONSISTENTLY call these critters data
segment numbers throughout this document, but don't count on it.




ACCESSING DATA SEGMENTS -- ONE APPROACH

We  have  established  that most of the data  worth knowing is kept in
data  segments, and that a piece of data can be accessed by specifying
the  data  segment  number  and  the  offset within  the data segment,
without  caring  about the physical memory  address, which anyway will
probably  change  when  the  segment  is swapped in  and out. Now, the
question becomes: how do you get to it?

Well,  thought some unknown HP designer  when he was faced with making
all  this up back in '72 when the 3000 was being built, we can already
access  arbitrary DB-relative addresses in our  stacks -- why don't we
just  provide  some  way  of switching the DB  register to point to an
arbitrary  data segment? So, say that  someone wants to get location 3
of  data  segment 55 -- he'll just have  to switch to segment 55, grab
location  3  (note that he'll do it just  like he'd grab location 3 of
his  own  stack, except that now DB  points to segment 55), and switch
back to his stack, the value of the desired location in hand.

Thus, our program would look something like:

  << ATTENTION: People who're just looking at the pictures:
     THIS PROGRAM WON'T WORK!  DON'T EVEN TRY IT!  Read the text...
     >>
  $OPTION PRIVILEGED   << PM or no dice! >>
  BEGIN
  INTEGER
    DUMMY,  << location DB+0 >>
    I;      << location DB+1 >>

  << This is a system procedure that switches DB to point to the
     specified data segment, and returns the number of the
     data segment to which it now points.
     If the data segment number is 0, switches to the process's
     stack.  This is NOT the same as the documented privileged
     procedure SWITCHDB -- THEY CAN NOT BE USED INTERCHANGEABLY!
     SWITCHDB is only intended for accessing your own data segments
     that were allocated by your program using GETDSEG. >>
  INTEGER PROCEDURE EXCHANGEDB (DSEG'NUMBER);
  VALUE DSEG'NUMBER;
  INTEGER DSEG'NUMBER;
  OPTION EXTERNAL;

  DUMMY:=EXCHANGEDB (55);
  ASSEMBLE (LOAD DB+3);  << push the value at DB+3 onto the stack >>
  I:=TOS;  << pop it from the stack -- I now contains the value
              of the 3rd location in segment 55 >>
  DUMMY:=EXCHANGEDB (0);  << switch back to the stack >>
  << now, do whatever we want to do to I >>
  END.

What did we do? We set our DB register to point to data segment 55, we
got  the value at location DB+3 -- now location 3 of segment 55 -- and
moved  it into I, and then we switched back to our stack. Note that we
did  not use the value returned by  the first EXCHANGEDB, which is the
data  segment  number of the segment to  which DB pointed to before --
namely,  our  stack  --  as  the  data  segment  number in  the second
EXCHANGEDB.  That's because if we just  EXCHANGEDB to our stack's data
segment  number, DB will point to the  BASE of the stack data segment;
however,  specifying 0 as the data segment number is a special feature
that  switches  back to the process's stack  data segment and makes DB
point to the right place.

But  THIS  DOESN'T WORK. What's more, IT  FAILS IN A TRULY SPECTACULAR
WAY,  most likely crashing your system  or worse. Why? Because when we
do  an  EXCHANGEDB,  ALL THE DB-RELATIVE ADDRESSES  NOW POINT INTO THE
DATA  SEGMENT, including all of our DB-relative variables -- such as I
--   and  our  arrays,  either  DB-relative  (global),  or  Q-relative
(procedure local)! When we move the top of stack into I, we are moving
it to DB+1 -- the location in which I is stored -- and DB+1 now points
into  the data segment, too! In fact,  the only way in which the above
could  be  made  to  work  is by making sure  that whenever we use any
DB-relative  addressing while in "split-stack mode" -- i.e. when we've
EXCHANGEDBd  to another segment -- we  mean addressing relative to the
data  segment. For instance, we could leave the value of location 3 on
the  stack, EXCHANGEDB back to our stack data segment, and then safely
pick  the  value up into I; or, we  could put this in a procedure, and
make  I  a  Q-relative  variable. Remember,  Q-relative and S-relative
addressing  does not get changed;  however, ALL DB-relative addressing
does.

The  bottom  line  is  that,  for all practical  purposes, we can have
access  to  only one segment at a time  -- either our stack, in normal
mode,  or  one  alternate  data segment in  split-stack mode. We can't
really  work  with both, except when we're  in split-stack mode and we
use  S-relative and Q-relative addressing.  Remember, though that even
this  is  restricted  since most arrays, even  Q-relative ones, use DB
addressing  anyway  (because  they  have a pointer  that points to the
data,  and all these pointers are  always DB-relative). So, if for the
entirety  of  our  process's  life  we  want to be  dabbling with data
segment  55,  we have no problem; however,  if we usually want to work
with  our  stack  -- remember, data segment  55 probably does not have
room for our temporary variables, arrays, and whatever else is usually
kept  in  our  stack -- and only sometimes  get at data segment 55, we
have a problem.

Thus, the rules of the game for split-stack mode operation are:

  *  Always  be keenly conscious of when  you are in split-stack mode.
    Try  to  be  in  split-stack mode as little  as possible (I prefer
    never  to be in split-stack mode except when some operating system
    procedure  that  I'm  calling -- such  as DIRECSCAN, the directory
    traversal procedure -- requires it).

  * When in split-stack mode, remember that only:

    - By-value procedure parameters;

    - Simple (non-array) procedure-local variables;

    -  Variables  explicitly  declared to be  Q-relative or S-relative
      (using constructs like "INTEGER XYZZY = S-3;");

    -  Local arrays that are declared  to be Q-relative (e.g. "INTEGER
      ARRAY X(0:4)=Q;");

    refer  to data in your stack; all others refer to data in the data
    segment to which you've switched.



A CIVILIZED ALTERNATIVE

Fortunately,  there  is another way of  accessing data segments -- two
little-known privileged machine instructions called

  MFDS (Move From Data Segment)

  MTDS (Move To Data Segment)

Their  principle  of  operation  is simple -- they  move data from the
stack  to a data segment or from a data segment to the stack. Thus, if
you  want  to read location 3 of data  segment 55, you'd issue an MFDS
instruction indicating that you want to move 1 word from location 3 of
data  segment 55 to some buffer in  your stack. It's simple, and since
you  don't do an EXCHANGEDB, you can  keep using all of your variables
with no problems.

Now,  let's  be  a  bit  more  specific:  just  how does  one tell the
instruction  where  to move from and where  to move to? Well, the best
place  where instructions can take input  parameters is from the stack
--  not  just  from your stack data segment,  but from the topmost few
locations  on  the  stack,  just  below  the  S register.  You can put
parameters onto the stack using the SPL construct

  TOS:=parameter value;

and then issue the instruction by entering

  ASSEMBLE (MFDS 4);

    or

  ASSEMBLE (MTDS 4);

The  "4" in the ASSEMBLE statement  is the so-called "stack decrement"
--  it indicates how many of  the parameters passed to the instruction
should  be  removed from the stack once  the instruction is done. Each
instruction  takes  4  parameters  (which we'll talk  about soon), and
there's  no reason to leave any of them laying around on the stack, so
we tell the instruction to get rid of all 4 of them.

Now, each instruction must take as parameters:

  The address of the buffer you want to move from or move to. Usually,
  this  is  "@BUFFER",  where BUFFER is the  name of the buffer array.
  This  must be a word address; this usually means that if you specify
  "@BUFFER",  BUFFER must not be a byte array, but should rather be an
  integer, logical, or double array.

  The data segment number you want to move to or move from.

  The offset in the data segment at which you want to start the move.

  The length (in words) of the data to be moved.

Note  that  if  you specify an incorrect  data segment number, invalid
offset,  invalid buffer address, or invalid length, HP will reward you
with a system failure...

The  order  of  the parameters is NOT  the same for both instructions.
Rather,  for  each  instruction  you  should  put  onto the  stack the
parameters  that describe "where to move to", then the parameters that
describe  "where  to move from", and then  the length. In other words,
the MFDS calling sequence would be

  TOS:=@BUFFER;    << move to >>
  TOS:=DSEG'NUMBER;  << move from >>
  TOS:=OFFSET'IN'DSEG;  << move from >>
  TOS:=LENGTH;
  ASSEMBLE (MFDS 4);

and the MTDS calling sequence would be

  TOS:=DSEG'NUMBER;  << move to >>
  TOS:=OFFSET'IN'DSEG;  << move to >>
  TOS:=@BUFFER;  << move from >>
  TOS:=LENGTH;
  ASSEMBLE (MTDS 4);

Also, don't forget that the instructions must be executed
when in privileged mode.

So,  let's  try a real-live application. Say  that you want to write a
program  that  prints  out the session limit  -- the maximum number of
sessions  that can be running at one  time. Let's see how you could go
about doing it.

There's  an old joke about a Chinese  recipe for broiled rabbit -- the
recipe  starts with "first, catch the  rabbit". We must first find out
where  the  session  limit  is  stored.  Well,  the  session  limit is
logically  job  information  ("job"  in this context  pertains to both
batch jobs and online sessions), so it would most reasonably be in the
"JOB INFORMATION" chapter of the System Tables manual -- Chapter 8. In
fact,  on  page  8-???  is the layout of a  table called the JMAT (Job
MAster  Table), and lo and behold, there in word 8 (MPE IV) or in word
10  (MPE V), is the session limit.  So far, so good. And, what's more,
on  that  very  page (or barring that, in  chapter 2) it says that the
JMAT   is   data   segment   number  (they  probably  say  "DST  Entry
assignments") 25.

So,  the  session limit is word 8 (MPE IV)  or word 10 (MPE V) of data
segment  number  25. We know that it's one  word long, so we can write
the following program:

  $CONTROL NOSOURCE, USLINIT
  << :PREP me with ;CAP=PM >>
  BEGIN
  INTRINSIC
    GETPRIVMODE,
    GETUSERMODE,
    QUIT;
  INTEGER
    SESSION'LIMIT;

  GETPRIVMODE;
  TOS:=@SESSION'LIMIT;  << Move to the SESSION'LIMIT variable >>
  TOS:=25;  << Move from data segment 25 >>
  TOS:=8;  << MPE IV; in MPE V, use 10 >>
  TOS:=1;  << Move 1 word >>
  ASSEMBLE (MFDS 4);  << Move From Data Segment >>
  GETUSERMODE;
  QUIT (SESSION'LIMIT);
  END.

When  you run this program, it'll get the session limit, and call QUIT
with the specified parameter. I decided to call QUIT because QUIT will
print  the parameter and it's easier than calling PRINT and ASCII (I'm
actually used to using my own SPL output package, which I described in
the  "Winning at MPE" column of the DEC 1983 through MAR 1984 Interact
magazines; it makes numeric formatting much easier).

Now,  you know for sure whether or  not this program works because you
can  do  a  :SHOWJOB  and see the real  session limit. However, if you
tried  to  move something from a data  segment and wasn't sure whether
your  program was working right or  not, you could use privileged mode
DEBUG  to  check it out. The command  you'd use is "DDA" (Display DAta
segment),  and  you'd  enter "DDA  dsegnumber+offset,length". Thus, to
check out the above program, you can type:

  :DEBUG

  *DEBUG* PRIV.xx.xx
  ?DDA#25+#8 <<or #10 for MPE V>>,1,I
  DA31+10     +xxxxx
  ?E

The   "xxx"s   stand  for  the  values  that  depend  on  your  system
configuration  --  the  "+xxxxx"  that  comes out on  the same line as
"DA31+10"  (or "DA31+12" in MPE V systems) is your session limit. Note
that  DEBUG  echoes  "DA31+10"  --  31 is the  octal representation of
decimal 25, and 10 is the octal for decimal 8.

So  that's  really  all there is to it  -- figure out the data segment
number  to which to move from/to (which  is either a constant given in
the  System  Tables  manual  or  a variable kept  in some other system
table)  and  the address within the data  segment to move from/to, and
then  issue  the  appropriate  MFDS  or MTDS  instruction. It's really
rather  simple,  and it's a shame that  HP doesn't explain it anywhere
except a remote corner of the Machine Instruction Set Manual (and even
there, not too well). It would have made sense for HP to describe this
--  in  fact,  describe  everything mentioned in this  paper -- in the
System  Tables  Manual or even a  more widely distributed document. Oh
well, I guess that's just not the "HP way".

However, there is one gigantic problem with the above approach -- even
if  you're just doing MFDSs, passing an incorrect parameter will crash
the  system. Even if you write a  procedure that does the "TOS:="s and
the  ASSEMBLEs, thus avoiding typographical errors, you're still going
to  have  many  bugs in your program, and  having the system crash for
each  one  of  them is not a very  appealing prospect (in fact, a very
appalling prospect).

This  is especially a problem if you're not accessing permanent system
tables  (like  the  JMAT) but rather more  ephemeral tables like other
process's  stacks.  You  could  easily get the  data segment number of
another  process's  stack,  and  by  the time you try  to read it, the
process (and the data segment) might be gone.

The  solution  is  to  write procedures that  get passed the MFDS/MTDS
parameters,  check  them  to ensure validity, and  only do the MFDS or
MTDS  if  the  parameters are OK. That's what  I'm going to talk about
now, because writing code that uses MFDS and MTDS without having these
safeguards  (at  least  while  debugging)  is, in  my opinion, grossly
impractical.

What  kinds of errors can you have  in your choice of parameters? Well
one  is that you're moving data from  or into the wrong place. There's
nothing  any automatic checking can do  about this -- that's a logical
error  that  you'll  have  to  find  yourself. I only  hope that these
logical errors will happen to you mostly on MFDSs and not on MTDSs.

There are several errors, though, that are automatically detectable:

  Negative  length,  data  segment number, or  offset. I'm not certain
  about  this  (and  I'm not going to risk  a system failure to try to
  find  out),  but maybe a negative  length would mean "right-to-left"
  movement  (like it does in the MOVE  statement -- see the section on
  the MOVE statement in the SPL Reference Manual). However, since even
  if  this  were  so, it wouldn't be a  very useful option, a negative
  length,  data segment number, or offset are pretty certainly errors.
  Note  that  negative  buffer  address  is NOT an  error -- remember,
  buffers  in  the  DL-to-DB area will  have negative addresses. Also,
  data segment number 0 is an error.

  Invalid  data segment number. Not all data segment numbers are valid
  --  they  can  be  up to 1023 in MPE IV  and even more in MPE V, and
  there  aren't  always  going  to  be that many  real, allocated data
  segments.  How could we check for this? Well, we'd have to look this
  up  in  another  system  table,  the Data Segment  Table (DST); it's
  described  in  System  Tables Manual Chapter 2,  and we'll talk more
  about it later.

  Buffer  address out of bounds. This  means that the starting address
  of  the buffer is less than the  DL register (i.e. the buffer starts
  in  the PCBX or even lower, outside of your stack in somebody else's
  chunk   of   memory)   or   the   ending   address   of  the  buffer
  (@BUFFER+LENGTH-1) is greater than the S register. Remember, this is
  privileged mode -- no bounds violation checking!

  Data  segment  offset  out of bounds. This  means that the offset is
  less  than  zero  (already  mentioned  above) or  offset+length-1 is
  greater  than the current length of  the data segment. The length of
  the data segment is also stored in the DST.

So, our code would look something like:

  $CONTROL NOSOURCE, SEGMENT=DSEG'IO, SUBPROGRAM, USLINIT
  << Caller must be :PREPped with ;CAP=PM >>
  BEGIN
  DEFINE
    TURNOFFTRAPS =
      BEGIN
      PUSH (STATUS);
      TOS.(2:1):=0;
      SET (STATUS);
      END #;

  LOGICAL PROCEDURE DSEG'CHECKPARMS
    (DSEG'NUMBER, OFFSET, BUFFER, LENGTH);
  VALUE
    DSEG'NUMBER,
    OFFSET,
    LENGTH;
  INTEGER
    DSEG'NUMBER,
    OFFSET,
    LENGTH;
  ARRAY
    BUFFER;
  BEGIN
  << Returns TRUE if all parameters OK. >>
  INTRINSIC
    GETPRIVMODE;
  INTEGER POINTER
    DST = 2;  << I'll talk more about this later >>
  INTEGER POINTER
    DL;

  << First, find out the address of DL -- the lowest valid stack
     address -- for use later on. >>
  PUSH (DL);  << Push the value of the DL register onto the stack >>
  @DL:=TOS;  << Pop it into our own pointer called "DL" >>

  << Now, get privileged mode for subsequent uses of "DST", the
     system table pointer we defined above (more about this later).
     Privileged mode should NOT be relinquished by this procedure
     (again, more about this later). >>
  TURNOFFTRAPS;
  GETPRIVMODE;

  << Now, check all the possible conditions >>
  IF
     << Is any of the parameters negative (or DST=0)? >>
     DSEG'NUMBER<=0 OR OFFSET<0 OR LENGTH<0
       OR
     << Is DSEG'NUMBER greater than the maximum data segment #? >>
     DSEG'NUMBER>=DST(0)
       OR
     << Is DSEG'NUMBER invalid (indicated by having the length
        in its DST entry be 0)? >>
     4*DST(4*DSEG'NUMBER).(3:13)=0
       OR
     << Is the last data segment address to be moved
        (OFFSET+LENGTH-1) greater than the last valid data segment
        address (which is (data segment length - 1))? >>
     OFFSET+LENGTH-1>DST(4*DSEG'NUMBER).(3:13)*4-1
       OR
     << Is the starting address of the buffer below DL? >>
     @BUFFER<@DL
       OR
     << Is the ending address of the buffer (@BUFFER(LENGTH-1)) above
        the greatest possible address, which is just one word below
        the first parameter to this procedure? >>
     @BUFFER(LENGTH-1)>@DSEG'NUMBER-1
     THEN
    DSEG'CHECKPARMS:=FALSE
  ELSE
    DSEG'CHECKPARMS:=TRUE;
  END;  << the procedure exit gets you back to user mode >>

  PROCEDURE DSEGREAD
    (DSEG'NUMBER, OFFSET, BUFFER, LENGTH);
  VALUE
    DSEG'NUMBER,
    OFFSET,
    LENGTH;
  INTEGER
    DSEG'NUMBER,
    OFFSET,
    LENGTH;
  ARRAY
    BUFFER;
  BEGIN
  << Reads LENGTH words from offset OFFSET of data segment #
     DSEG'NUMBER into the array BUFFER.
     Calls QUIT if any parameter is invalid. >>
  INTRINSIC
    GETPRIVMODE,
    QUIT;
  IF NOT DSEG'CHECKPARMS (DSEG'NUMBER, OFFSET, BUFFER, LENGTH) THEN
    QUIT (1717)
  ELSE
    BEGIN
    TOS:=@BUFFER;
    TOS:=DSEG'NUMBER;
    TOS:=OFFSET;
    TOS:=LENGTH;
    TURNOFFTRAPS;
    GETPRIVMODE;
    ASSEMBLE (MFDS 4);
    END;
  END;  << the procedure exit gets you back to user mode >>

  PROCEDURE DSEGWRITE
    (DSEG'NUMBER, OFFSET, BUFFER, LENGTH);
  VALUE
    DSEG'NUMBER,
    OFFSET,
    LENGTH;
  INTEGER
    DSEG'NUMBER,
    OFFSET,
    LENGTH;
  ARRAY
    BUFFER;
  BEGIN
  << Writes LENGTH words from the array BUFFER to offset OFFSET of
     data segment # DSEG'NUMBER.
     Calls QUIT if any parameter is invalid. >>
  INTRINSIC
    GETPRIVMODE,
    QUIT;
  IF NOT DSEG'CHECKPARMS (DSEG'NUMBER, OFFSET, BUFFER, LENGTH) THEN
    QUIT (1718)
  ELSE
    BEGIN
    TOS:=DSEG'NUMBER;
    TOS:=OFFSET;
    TOS:=@BUFFER;
    TOS:=LENGTH;
    TURNOFFTRAPS;
    GETPRIVMODE;
    ASSEMBLE (MTDS 4);
    END;
  END;  << the procedure exit gets you back to user mode >>
  END.

I'll  be the first to admit -- that's a lot of code. But with it, I am
able  to  confidently  test  all  my programs that  read data segments
(those  that  write  other  data segments can,  of course, cause other
nasty  problems) on a production computer during working hours with no
fear  at  all.  In fact, I don't recall  a single system failure in my
past  year of development, in which  I've been writing and maintaining
many  programs like VESOFT's MPEX, LOGOFF, and others, all of which do
heavy  system tables work. When I did the conversion of these programs
from  MPE  IV  to  MPE  V,  the  first thing I did  was to ensure that
DSEGREAD  works. Once that was done,  despite the fact that there were
bugs  in  the  programs that it took several  hours to iron out, there
were no system failures.

Now, a couple of comments about the above code are in order:

  *  First,  note  that  the  procedures  abort whenever  an incorrect
    parameter is passed -- why? Well, an incorrect parameter is almost
    guaranteed  to be caused by a  program bug (by definition). What's
    the use of going on, doing other things, when you know that due to
    a  problem  bug, your program couldn't read  or write data that it
    may be relying on? You could, of course, have the procedure return
    a  logical flag, but then you'd have  to check the flag every time
    you call it and probably wind up calling QUIT anyway. A good idea,
    however,  is  to  somehow get the procedure  to indicate the place
    they  aborted and the cause of  their abort -- negative parameter,
    invalid data segment number, bad offset, etc.

  *  Second,  note  that all three procedures  get privileged mode AND
    NEVER  GET  USER  MODE!  This  is because whenever  a procedure is
    exited, the mode is always reset to the mode of the caller; so, we
    don't need to get user mode explicitly. What's more, if we try to,
    it  could  actually  hurt  because  it will cause  an abort if the
    calling  procedure itself is privileged.  MPE does not permit code
    executing  in  user  mode to exit to  code executing in privileged
    mode.  So,  getting  user mode explicitly  is both unnecessary and
    undesirable.

  * Third, what's this "TURNOFFTRAPS" nonsense? Well, once upon a time
    (a  long,  long time ago, in a galaxy  far, far away but really in
    Cupertino),   I  was  having  problems  with  my  privileged  code
    aborting.  Well,  a  friendly HP person suggested  that I turn off
    arithmetic  traps (things like INTEGER OVERFLOW and others) before
    going  into  privileged  mode,  and  lo and behold!  all was well.
    Apparently, some privileged operations require arithmetic traps to
    be  off (although why I'll never know), so to be on the safe side,
    I  always  turn  them off before going  into privileged mode. Like
    privileged  mode,  an exit from a  procedure resets the setting of
    the arithmetic traps to what it was in the calling procedure. Note
    that some people (including Stan Sieler, an ex-HPer now at Allegro
    Consultants, who's likely to know about these things) believe that
    you  only  need  to TURNOFFTRAPS before  calling a system internal
    procedure (ATTACHIO, EXCHANGEDB, GETSIR, etc.) and not when you're
    just  executing  a privileged instruction  (MFDS, LST, etc.). They
    may be right -- try it both ways and see.

  *  Finally,  let  me just caution you that  the above checks are NOT
    foolproof.  Not  only will they allow  you to write incorrect data
    into  data segments -- they'll make  certain that the data segment
    number's  correct but there's no way  they could check the data --
    but  also, if you read or write a segment that is still around but
    is  being  deleted (or contracted), the  procedure might check the
    segment  while it's still around, see that all's well, and then do
    the  MFDS/MTDS after the segment is gone. In this case, the system
    will  indeed  crash. I have NEVER gotten  this to happen, but it's
    possible  --  the only way to completely  avoid it is to make sure
    that there are no bugs in your programs.  Good luck...


USING MFDS/MTDS TO ACCESS THE PCBX AREA

When  I was talking about accessing  the DL-negative area, I mentioned
that  there  was a way of doing it that  I liked better than the one I
presented. Well, here it is.

Remember, your stack is a data segment like any other segment. You can
access  it using MFDS just as easily  (or as difficultly) as, say, the
PCB,  or  somebody  else's stack. All you have  to do is find its data
segment  number, which is not very hard, since it is in your process's
PCB  (that's  Process  Control  Block  -- a very  useful system table)
entry.  Then,  to read the PXGLOB, just  do a DSEGREAD from your stack
segment,  location  0,  length  8  (MPE IV) or 12  (MPE V). To get the
PXFIXED,  just do a DSEGREAD from  your stack segment, with a starting
location  of 8 (MPE IV) or 12 (MPE V) -- just above the PXGLOB. To get
the PXFILE, you have to get the PXGLOB, get the address of DL relative
to  the start of the data segment (that's PXGLOB cell 0), and then get
the PXFILE address from DL-3.

So, just write the following procedure:

  INTEGER PROCEDURE MYSTACK;
  BEGIN
  << Returns the data segment number of the process's stack >>
  INTRINSIC
    GETPRIVMODE;
  INTEGER POINTER   << see below for more info on this construct >>
    PCB = 3;

    << SL procedure that returns the process's PIN >>
    INTEGER PROCEDURE MYPIN;  OPTION EXTERNAL;

  TURNOFFTRAPS;
  GETPRIVMODE;
  MYSTACK:=
    << MPE IV: >>  PCB (16*MYPIN+3).(1:10);
    << MPE V: >>   PCB (21*MYPIN+3).(2:14);
  END;

Note the use of the MYPIN procedure -- that's how you get your Process
Identification  Number  (PIN),  which  is also the  number of your PCB
entry.

Now,  you  can  just  call  DSEGREAD, pass to  it MYSTACK, and presto!
you're reading your own stack.

"Now,  Eugene," you must be saying, "why would you want do a damn fool
thing  like that? There you have the  DL negative area, easy as pie to
access  using pointers, and you insist  on using an entirely different
strategy.  Why bother?" Well, the answer is really quite simple -- I'm
lazy.  I  don't  want  to  have  to  write  and  remember two  sets of
procedures  --  DSEGREAD/DSEGWRITE and  PCBXREAD/PCBXWRITE. I'd rather
have  one  set  that I would use to read  either a data segment or the
PCBX,  and  one way of looking at  things -- everything's just another
data  segment,  including  the  PCBX.  System  tables  are complicated
things, and any bit of conceptual simplification helps.


AN EXAMPLE -- GETTING YOUR OWN SESSION NAME

The  perfect  example  of  using  MFDSs (or actually,  DSEGREAD -- you
should  always call DSEGREAD, and never  have an MFDS or MTDS anywhere
else  in  your  code) is getting your own  job/session name. This is a
useful piece of information that no HP-supplied intrinsic gives you --
you  can  use  it  to  identify  users  (e.g. there's  one user called
CLERK.PAYROLL,  with session names JOE, SUSAN,  etc. -- a technique we
support  and encourage with our VESOFT SECURITY/3000 product), provide
accounting information, and whatever else.

First,  we  have  to  find  out where it is.  It comes under the broad
category  of "JOB INFORMATION", and if you look into the System Tables
Manual,  Chapter  8, you'll find it as  a field of the Job Information
Table  (also known as the JIT). But,  the JIT isn't like other tables,
like  the PCB, DST, or JMAT,  which have constant data segment numbers
--  each session has its own JIT. Well,  it turns out (and this is NOT
easy to find out) that the data segment number of the JIT is stored in
the PXGLOB. So, what you'd do is:

  *  Get  the  PXGLOB  (using  either  of  the methods  shown above of
    accessing your PCBX).

  * With the JIT data segment number from the PXGLOB, get the JIT.

  * Extract the session name from the JIT.

To see the actual program, look at program 2 in Appendix 1.



ACCESSING MEMORY-RESIDENT SYSTEM TABLES

As  if EXCHANGEDB and MFDS/MTDS  weren't confusing enough, there's yet
another way of accessing some system tables.

Certain  system  tables (e.g. the PCB,  DST, CST [Code Segment Table],
etc.)  are  memory-resident -- they are always  in main memory and are
never  swapped  out  to  disc. These tables can  be accessed using two
special  machine instructions called LST  (Load from System Table) and
SST  (Store into System Table). This would  be a moot point if not for
the fact that SPL has an feature that uses these instructions to allow
you  to  easily access memory-resident system  tables. (Note that this
feature  has  only  been  documented in the FEB  84 version of the SPL
reference  manual, p. 2-13, 7-18, and  7-19; all prior versions of the
reference manual do not mention it.)

In  SPL,  if you say "INTEGER POINTER  name = number;", all subsequent
references  to  "name(index)"  will  access  the index'th  word of the
memory-resident  table  indicated  by  "number". Thus,  if you declare
"INTEGER  POINTER TAB'PCB = 3;",  saying "I:=2+TAB'PCB(1);" will set I
to  2  plus  the  value  of  word  1  of  the  PCB.  Or,  if  you  say
"TAB'PCB(SIZE'PCB*PIN+7):=24;",    it    will    move    24   to   the
SIZE'PCB*PIN+7th word of the PCB.

This  means  that  to access these tables, you  need not do an MFDS or
MTDS  -- which are somewhat slower and also more cumbersome to call --
but  rather just treat the table as if it were an array that you could
index just like an ordinary array.

Several things must be noted about these constructs:

  *  MEMORY-RESIDENT TABLE IDENTIFIERS (the numbers that you put after
    the  "="  in  an INTEGER POINTER declaration)  ARE NOT THE SAME AS
    DATA  SEGMENT  NUMBERS!  In the case of  the PCB, the data segment
    number  and the memory-resident table  identifier happen to be the
    same  (3). For other tables, this may  not be the case. The way to
    determine  a  table's memory-resident table  identifier is to look
    into Chapter 1 of the System Tables Manual where the System Global
    (SYSGLOB)  area  is  described.  If  the Nth word  is indicated as
    containing  the address of (or pointer  to) a system table, then N
    is that table's identifying number.

  *  Memory-resident  tables can only be  accessed in privileged mode.
    That  means  that whenever you want to  use a memory table pointer
    (declared  using  the "INTEGER POINTER  name = number" construct),
    you must be in privileged mode.

  *  Memory-resident table pointers can only be used when indexed, and
    then only in expressions or in assignment statements. They can not
    be  used in MOVE or SCAN statements, as by-reference parameters in
    procedure  calls, or without an index. If you want to move several
    words  from a system table, use  either an MFDS or memory-resident
    table pointers and a FOR loop.

  *  I haven't the foggiest notion of what would happen if you specify
    an  invalid index when using a memory-resident table pointer (i.e.
    a  negative  index  or  one  that is greater than  the size of the
    table). I'd guess that if you pass an index greater than the table
    size,  it  would just get you data  from whatever happens to be in
    memory  at the system table base + the index, but if you specify a
    negative  index or an index that would cause the effective address
    (the  system  table base + the index)  to be outside of the memory
    bank  in  which  the  system  table is located,  be prepared for a
    system failure.

  *  Remember that relatively few system tables are accessible in this
    way.

Personally, I do not often use this method of system table access. For
one,  as  I said before, I like  to consistently use one approach, and
not  confuse  myself  with  many  different  ones  --  my  favorite is
MFDS/MTDS.  Furthermore,  since  I have allocated  a special array for
each  system table, and have DEFINEs that access that array, I usually
end  up having to copy an entire table entry anyway (not just a single
word);  however,  if you use the  alternative approach described above
(i.e. having DEFINEs specify only the index into the entry without the
array name), you wouldn't have to move the entire table entry.

Sometimes,  I  do  use memory-resident table  pointers, primarily when
speed  is of the essence -- I've been told that LST and SST are faster
than MFDS and MTDS. It's mostly a matter of personal preference -- use
whichever approach you find most appropriate.

An  example  of  this  approach could be  the following procedure that
determines whether it's running on MPE IV or MPE V:

  LOGICAL PROCEDURE MPE'V;
  BEGIN
  << result := TRUE if MPE V, FALSE if MPE IV >>
    INTRINSIC GETPRIVMODE;
    INTEGER POINTER TAB'PCB = 3;
    DEFINE PCB'ENTRY'LEN = TAB'PCB (1) #;
    EQUATE MPE'V'ENTRY'LEN = %25;
    TURNOFFTRAPS;
    GETPRIVMODE;
    IF PCB'ENTRY'LEN=MPE'V'ENTRY'LEN THEN
      MPE'V:=TRUE
    ELSE
      MPE'V:=FALSE;
  END;

Note  that the PCB table entry size changed  from %20 in MPE IV to %25
in MPE V. Since the entry size is stored (in both MPE IV and MPE V) in
word 1 of the PCB table, we can just look at that word and see if it's
%25 or not.

This  procedure is exceptionally useful for writing programs that work
on  either  MPE  IV  or  MPE V, or at least  abort if they're run on a
version  of  MPE  other  than  the one they were  written for. If your
program  is written for MPE IV, it's  much better to abort with a nice
error message when run on MPE V rather than crashing the system.

Note  however  that if you wanted to,  you could perform the same task
using MFDS.



ABSOLUTE MEMORY LOCATIONS

Some data is stored not in data segment-relative locations, but rather
absolute  memory  addresses. For instance, there  is an area in memory
called  "SYSGLOB"  (which  stands  for SYStem  GLOBal), which contains
certain  interesting  pieces of information,  like the current console
device,  the global allow mask, and  lots of other goodies. It happens
to  be  stored  at a fixed address --  it goes from locations %1000 to
%1377  in  memory  bank  0,  and can be  accessed using the "ABSOLUTE"
construct of SPL. For instance to determine the system console logical
device  number (which is stored in location %74 of the SYSGLOB), you'd
do something like:

  TURNOFFTRAPS;
  GETPRIVMODE;
  CONSOLE'LDEV:=ABSOLUTE (%1074);
  GETUSERMODE;
  TURNONTRAPS;

Simple  --  think of "ABSOLUTE" as an  integer array whose 0th word is
memory  address 0 of bank 0. To store a value into this location, just
say  "ABSOLUTE (%1074) := NEW'CONSOLE'LDEV;". Note however that unlike
a  normal  array,  you  can  only  read and  write individual absolute
locations;  you can't do a MOVE or  SCAN with an absolute address, nor
can  you  pass  it by reference to a  procedure. In this respect, it's
like the system table pointers discussed above.

To  confirm  your  results, or to access  absolute locations without a
program,  you can use privileged  mode DEBUG's "DA" (Display Absolute)
command:

  :DEBUG

  *DEBUG* PRIV.xx.xx
  ?DA1074,I
  A1074   +00020
  ?E

Also note that DEBUG has a "DSY" (Display SYstem global) command, with
"DSY  n"  being equivalent to "DA 1000+n" --  it just gets you the nth
word of SYSGLOB.

One  word  of  caution:  ABSOLUTE  can  only be used  to access memory
locations  with addresses 0 to 65535  -- those memory locations in the
so-called  "memory  bank  0".  HP  has recently been  on a campaign to
reduce the amount of data that must be stored in bank 0 -- tables like
the  PCB, CST, DST, and others that used to be always in bank 0 in MPE
IV are no longer always in bank 0 in MPE V. The upshot of this is that
since the only things that can be accessed using ABSOLUTE are the ones
that  are  guaranteed  to be stored in bank  0, you should avoid using
ABSOLUTE  whenever a non-bank 0 dependent access method (e.g. MFDS) is
available.



DISC RESIDENT SYSTEM TABLES, PART I -- FILE LABELS

Unlike   process   information,  data  segment  information,  and  job
information,  which are stored in memory, file information -- the size
of  a file, its file code, lockword, location on disc, etc. -- can not
be  stored  in  memory  because  it has to  stay around through system
failures.

Most  of  the  important file information, including  all of the stuff
that  all modes of :LISTF show, is  stored on disc in "file labels". A
file's  file  label is pointed to by  its directory entry and occupies
one  sector (128 words) on disc. Since it's stored in disc rather than
in  memory,  accessing  it  is  rather  different from  accessing data
segments.

In  order  to access -- read or write  -- a file's file label you must
first  find out the logical device number  (LDEV) of the disc on which
the file label resides and the sector number of the file label on that
disc.  This is by no means a trivial  task, and in many instances is a
lot  more complicated than actually accessing  the file label once you
have  this. Logical device numbers and  sector addresses are stored in
various  system  tables, in a variety  of formats (which we'll discuss
later).  However,  for  the duration of  this discussion, we'll assume
that you are trying to read or write the file label of a file that you
have  opened;  that way, you can figure  out the file label address by
calling FGETINFO (or FFILEINFO mode 19).

Now  you  have the file label address  -- a doubleword, containing the
LDEV  in  the 8 most significant bits and  the sector number in the 24
least  significant bits (a format we'll call  type L). All you need to
do is to perform the actual I/O.

The fundamental file label I/O procedure is, not surprisingly, FLABIO:

  INTEGER PROCEDURE FLABIO (LDEV, ADDRESS, CMD, FLAB);
  VALUE LDEV, ADDRESS, CMD;
  INTEGER LDEV, CMD;
  DOUBLE ADDRESS;
  INTEGER ARRAY FLAB;
  OPTION EXTERNAL, UNCALLABLE;  << must be called from PM >>

Its  operation  is quite simple -- you pass  to it the LDEV and sector
address,  CMD=0 to read or CMD=1 to write, and the 128-word array into
which  the file label is to be read or from which it is to be written.
The  result returned is 0 if all is  OK, 1 if there was a "soft error"
doing  the I/O (I think that this means that the check sum in the file
label  being  read is wrong), and 2 if  there was a "hard error" doing
the I/O (I think that this means a physical I/O error). Simple enough,
once you have the file label's address.

So, we can write the following procedure:

  << Read the file label of the file opened as FNUM into the array
     FLAB; return true if all OK, FALSE if failed. >>
  LOGICAL PROCEDURE FREADFILELABEL (FNUM, FLAB);
  VALUE FNUM;
  INTEGER FNUM;
  ARRAY FLAB;
  BEGIN
  INTRINSIC
    FGETINFO,
    GETPRIVMODE;
  INTEGER LDEV;
  DOUBLE ADDRESS;
  INTEGER ARRAY ADDRESS'I(*)=ADDRESS;

    INTEGER PROCEDURE FLABIO (LDEV, ADDRESS, CMD, FLAB);
    VALUE LDEV, ADDRESS, CMD;
    INTEGER LDEV, CMD;
    DOUBLE ADDRESS;
    INTEGER ARRAY FLAB;
    OPTION EXTERNAL, UNCALLABLE;

  << To be really safe, you should have code here that checks to make
     sure that reading 128 words into FLAB won't cause a bounds
     violation.  For more info on this, see below in the discussion
     of disc address validity checking. >>
  FFILEINFO (FNUM, 19, ADDRESS);
  IF <> OR ADDRESS=0 THEN
    << either file not opened or not on disc (address=0) >>
    FREADFILELABEL:=FALSE
  ELSE
    BEGIN
    LDEV:=ADDRESS'I(0).(0:8);  << high-order 8 bits >>
    ADDRESS'I(0).(0:8):=0;  << ADDRESS now is sector address >>
    TURNOFFTRAPS;  << See above (MFDS) to learn about this >>
    GETPRIVMODE;
    IF FLABIO (LDEV, ADDRESS, 0 << read >>, FLAB) <> 0 THEN
      FREADFILELABEL:=FALSE
    ELSE
      FREADFILELABEL:=TRUE;
    << no GETUSERMODE -- also see above to learn about this >>
    TURNONTRAPS;
    END;
  END;

Using  this procedure, you can now read the file label of any file you
have  opened,  and  find  out  a lot of  information that FGETINFO and
FFILEINFO  won't give you -- things  like the file creation date, last
access  date, last modification date, the file's extent map, lockword,
and  other useful things. A similar  procedure can be written to write
out  an  open file's file label -- just  be sure that you don't use to
change  things that are also kept  in memory-resident file tables that
exist for open files (like the FCB, ACB, etc. -- see the System Tables
Manual).

Similarly,  say  that  you  want  to  read the file  label of $OLDPASS
*without*  opening  it.  Why  would you want to?  Well, in the case of
$OLDPASS  you  usually  wouldn't (except if you  want to save time) --
however, you might want to do this for other files that you can't open
so  easily,  like  spool  files  or other people's  temporary files or
$OLDPASSes.

The  first thing you'd have to do is  to find out where the file label
address for $OLDPASS is located. When you can recite this from memory,
you  are truly a superior system programmer -- mere mortals would have
to  scan  through the System Tables Manual,  make an educated guess or
two,  and  find it in the Job  Information Table (chapter 8), words 36
and  37. Looks simple enough -- there  is your file label address; all
you  have  to  do is extract the LDEV  from the 8 high-order bits, the
sector address from the remainder, and call FLABIO. Right? Wrong.

The  fundamental means of doing the I/O  are still the same -- you get
the  LDEV,  get the sector address, and  call FLABIO. However, what is
stored  in the high-order 8 bits of words  36 and 37 of the JIT is NOT
(!!!)  the logical device number of  the disc. The $OLDPASS address in
the JIT is what I call the "type V address" (as opposed to the "type L
address" described earlier) -- its high-order 8 bits are the so-called
"volume  table  index"  of  the  disc  on which the  sector address is
contained.

So,  you  have  a  problem.  You are trying to  get the logical device
number  of  the  disc on which the file  label is located and the file
label's  sector address. You have the file label's sector address, but
instead of the LDEV, you have a volume table index (VTABX).

What  you need to do is to convert the VTABX into LDEV, and to do this
you use a system internal procedure called LUN:

INTEGER PROCEDURE LUN (VTABX, MVTABX);
VALUE VTABX,
      MVTABX;
INTEGER VTABX,
        MVTABX;
OPTION EXTERNAL, UNCALLABLE;

What  LUN  (which I suspect stands  for "Logical Unit Number", another
name for LDEV) does is take a VTABX and a MVTABX (Mounted Volume Table
Index)  and return the LDEV which they  describe. Where do you get the
MVTABX?  Well,  in  this case, it is  also (fortunately) stored in the
JIT, in word 57.

So, to read $OLDPASS's file label, you'd do the following:

  *  First,  read  the  JIT  into  an array (you  can use the DSEGREAD
    procedure we described earlier to do this).

  *  Then,  take  the  $OLDPASS type V address  (words 36 and 37), and
    separate  out the VTABX (high-order 8  bits) and the sector number
    (low-order 24 bits).

  *  If  $OLDPASS's  address  is 0, GO NO FURTHER  -- it means that no
    $OLDPASS file exists.

  *  Call LUN, passing to it the VTABX and the MVTABX (from word 57 of
    the JIT).

  *  Make a prayer that you did everything right ("Oh Lord, watch over
    this humble program and prevent it from crashing the system").

  *  Call  FLABIO,  passing  to  it the LDEV you  got from LUN and the
    sector  address you got from the low-order 24 bits of words 36 and
    37 of the JIT.

  *  I  don't  suppose  I  need  to tell you  to experiment with CMD=0
    (read), not CMD=1 (write)...

Voila!  All  you  ever wanted to know about  FLABIOing in ten pages or
less...

Incidentally, you're probably wondering what all this VTABX and MVTABX
nonsense  is  about.  Well,  it  was  implemented  to  support Private
Volumes,  in  which  the logical device number of  a disc is not known
until  the disc is actually mounted -- thus, the directory on the disc
would  have  to  contain  not  LDEVs,  but VTABXs. If  you expect your
program  to  NEVER  need to be run on  Private Volumes, you can forget
about  MVTABXs,  and  just call LUN with an  MVTABX of 0. However, you
still have to call LUN, because even if your system never even smelled
a private volume, the VTABXs need not correspond to the LDEVs.

If  you  intend to use FLABIO often  in your programs, I would suggest
that you write the following procedures:

  *  FLABREAD  and  FLABWRITE,  which take a type  L (LDEV and sector)
    address,  and  either  read  into  or write  from a user-specified
    array.  For  consistency's sake, they should  return a file system
    error number (e.g. FLABIO error 1 would map into file system error
    108 [INVALID FILE LABEL]; error 2 would map into file system error
    47 [I/O ERROR ON FILE LABEL]). Also, since passing an out-of-range
    LDEV  or  sector  to  FLABIO  will  cause  a system  failure, this
    procedure  can check the address using some tricks I'll talk about
    presently.

  *  V'TO'L'ADDRESS,  which takes a type  V (VTABX and sector) address
    and  a MVTABX, and returns the type L address. This would call LUN
    to convert the VTABX and MVTABX into the LDEV, and would allow you
    to easily read or write a file label given a type V address:

      FLABREAD (V'TO'L'ADDRESS (V'ADDRESS, MVTABX), FLAB);

  * FNUM'TO'L'ADDRESS, which takes the file number of an open file and
    returns  the  type L address. Simply  calls FFILEINFO or FGETINFO.
    Again, makes reading/writing file labels given an FNUM easier:

      FLABREAD (FNUM'TO'L'ADDRESS (FNUM), FLAB);

  * Finally, FILE'TO'L'ADDRESS, which takes a filename and returns the
    type  L address. The simplest way to do this is to FOPEN the file,
    call  FNUM'TO'L'ADDRESS, and FCLOSE the file. A far more difficult
    approach,  which  however  is  faster and doesn't  care about file
    security,  lockwords, or whether the file is already opened, is to
    get  the file label address directly from the directory. For this,
    you'd  have  to  call  the  DIRECFIND procedure to  get the sector
    address  from  the  directory,  call  DIRECFIND  again to  get the
    directory  entry  for the group in which  the file resides (to get
    the  group's MVTABX), and then  call V'TO'L'ADDRESS to convert the
    type  V  address  that  is  stored  in  the directory to  a type L
    address.  For  the beginning, stick to  the simple approach -- the
    directory  is probably worth a whole  paper dedicated to it alone,
    and will not be elaborated further in this document.

The  algorithm  of  checking  a  type  L  address for  validity is not
trivial,  but still well worth implementing  (unless you want to get a
reputation as "Mike 'System Failure' Johnson"):

  * Call the CHECKDISC procedure:

    PROCEDURE CHECKDISC (LDEV, STATUS);
    VALUE LDEV;
    INTEGER LDEV;
    LOGICAL STATUS;
    OPTION EXTERNAL, UNCALLABLE;

    Pass to it the LDEV, and make sure that the low-order 3 bits (bits
    .(13:3))  of STATUS are all 0 -- if  bit 15 is set, this means the
    LDEV  is  out  of  range;  if  bit  14  is  set,  the LDEV  is not
    configured;  if bit 13 is set, the LDEV  is not a disc. If none of
    these bits is set, you know you have a good LDEV.

  * Call the DISCSIZE procedure:

    DOUBLE PROCEDURE DISCSIZE (LDEV);
    VALUE LDEV;
    INTEGER LDEV;
    OPTION EXTERNAL, UNCALLABLE;

    Pass  to  it  the  LDEV, and make sure  that the sector address is
    greater  than  or  equal  to  0 and less  than the number DISCSIZE
    returned to you (which is the number of sectors on the device).

  *  If  you're a general-purpose procedure,  check the buffer address
    passed  to you to make sure that it's within bounds, i.e. that its
    start is at DL or above, and that its 127th word is at Q or below.
    Remember  that bounds violations are not checked for in privileged
    mode,  and trying to write to an out-of-bounds address would cause
    whatever  is at that address (i.e.  in someone else's space) to be
    over-written.

  *  If either check failed, abort --  you've got a bad address, which
    if used in an FLABIO call would crash the system.

If you want to check the results of your programs, or dabble with file
labels  without writing a program, there are several utilities you can
use:

  *  For  some  specific file label  retrieval and modification tasks,
    VESOFT's  own  MPEX,  which  allows  you to easily  look at files'
    creator  ids, access/modify/creation dates,  etc., and modify many
    file attributes (like creator id).

  *  HP's  DISKED2, which allows you to  read and write arbitrary disc
    locations. If you intend to modify file labels, make sure that you
    are either using the ">FILE" command of DISKED2 to get to the file
    label's  sector (rather than just a  >DISC and >MODIFY) or set the
    file label's checksum (word 34) to 0. If you use >DISC and >MODIFY
    but  do not set the checksum to 0, the file system will not update
    the checksum and will think that the file label is corrupted.

  *  Privileged mode DEBUG, which has  a "DV" command (Display Virtual
    memory,  a misnomer -- really  displays an arbitrary disc sector).
    Note  that the syntax of this  command, especially when the sector
    address is more than 32768, is non-trivial; see the DEBUG Manual.

  *  FLUTIL3, a contributed program which allows you to read and write
    the  file label. It's easier to use than DISKED2, and is also safe
    for modifying file labels.

Finally,  a  caveat  to  the  user who knows  about and uses ATTACHIO:
resist  the temptation to do file label I/Os using ATTACHIO instead of
FLABIO.  On reads, calling FLABIO is better because it checks the file
label's  check  sum,  letting  you  know  if  the  file  label appears
corrupted.  On writes, it is IMPERATIVE  that you call FLABIO, because
otherwise  the check sum will not be updated, and next time the system
wants to read the file label, it will think that it's corrupted.

As  a  final  present  to  all  you file label  hunters out there, the
following table should tell you how to get file addresses:

  *  Permanent  files  --  FOPEN the file  and call FGETINFO/FFILEINFO
    (type  L address); if you want to  be fancy, call DIRECFIND to get
    the file's directory entry (type V address).

  *  Your  session's  temporary files --  FOPEN/FGET(FILE)INFO (type L
    address), or get type V address from your JDT (described in System
    Tables  Manual Chapter 8; pointed to  by the PXGLOB, which is also
    described in Chapter 8).

  *  Your session's $OLDPASS -- FOPEN/FGET(FILE)INFO (type L address),
    or  get  type V address from your  JDT (described in System Tables
    Manual  Chapter  8;  pointed  to  by  the  PXGLOB,  which  is also
    described in Chapter 8).

  * Other sessions' temporary files and $OLDPASSes -- type V addresses
    stored in each session's JIT or JDT.

  *  Spool  files  --  IDD or ODD system  tables (System Tables Manual
    Chapter 14); type L.

Good luck, and happy hunting.



DISC RESIDENT SYSTEM TABLES, PART II -- USING ATTACHIO

Other  information  besides  file  labels  is also stored  on discs --
things like disc labels, defective track tables, the contents of files
themselves,  and  so on. To get at them,  you have to use a privileged
system internal procedure called ATTACHIO.

ATTACHIO  is  *the* primitive I/O procedure. You  can use it to do any
arbitrary  operation  against  any arbitrary device,  from reading and
writing discs to reading, writing, and doing various control functions
to  non-disc devices. All file system  I/O functions eventually end up
as ATTACHIO calls.

The calling sequence for ATTACHIO is:

DOUBLE PROCEDURE ATTACHIO (LDEV,QMISC,DSTX,ADDR,FUNC,CNT,P1,P2,FLAG);
VALUE LDEV, QMISC, DSTX, ADDR, FUNC, CNT, P1, P2, FLAG;
INTEGER LDEV, QMISC, DSTX, ADDR, FUNC, CNT, P1, P2, FLAG;
OPTION EXTERNAL, UNCALLABLE;

This  is  quite  a  mouthful  (just what *is*  a QMISC?). Fortunately,
however, for discs these parameters are rather simple:

  *  LDEV: This is self-explanatory. I'm sure I need not tell you that
    passing an incorrect LDEV will cause a system failure (#206, to be
    precise).

  * QMISC: 0.  QMISC could have a lot of different values for non-disc
    devices, but for disc devices 0 will do quite well.

  *  DSTX: Technically, the data segment number of the segment to/from
    which  the  read/write  is to take place.  0 means your stack, and
    this is the value you'd use most often.

  *  ADDR:  The  address  (within the data  segment indicated by DSTX)
    to/from which the read/write is to take place. When DSTX = 0 (i.e.
    your  stack), this is the ordinary word address of the array which
    you're  using for your I/O. Don't forget  to have room for as much
    as  you  want  to  read/write  in the array,  since like all other
    privileged  internal  procedures,  this one doesn't  do ANY bounds
    checking. Also note that if you're reading into a stack array, you
    should   be   sure   to  pass  the  ADDRESS  of  the  array  (i.e.
    "@arrayname"). "ATTACHIO (1,0,0,@FOO,0,10,ADDR'HI,ADDR'LO,1)" will
    read  into the array FOO; if you  omit the "@" before the FOO, you
    will read into the stack address pointed to by the 0th word of FOO
    (which is probably not what you want).

  * FUNC: Curiously enough, the function. 0 means read, 1 means write.
    Try something else.  I dare you.

  * CNT: If positive, this is the number of words to read or write; if
    negative,  the  absolute  value of this is  the number of bytes to
    read  or write. Thus, 10 means 10  words; -20 means the same thing
    (20 bytes).

  *  P1:  The high order word of the  sector number for the I/O. Don't
    forget  that  all  reads  and  all  writes must start  at a sector
    boundary (although CNT need not be a multiple of the sector size).

  *  P2: The low order word of the sector number. If you have a double
    word address D, you can say
      INTEGER D0=D, D1=D+1;
    D0  will  now  refer  to  the  high-order word of D  and D1 to the
    low-order word of D. Or you can calculate the high-order word of D
    on  the fly by saying "INTEGER(D&DLSR(16))" and the low-order word
    by saying "INTEGER(D)".

  *  FLAG: 1. This means "blocked,  wait until request is complete" --
    the  I/O  system's equivalent of ordinary  file I/O (as opposed to
    no-wait I/O).

  * Function returns a double-word:

    Word  0  bits  8:5:  qualifying  status for the  I/O; this further
    describes  the  result  of the operation.  The general status (see
    below) is usually all you need to find out what happened.

    Word 0 bits 13:3: general status of the I/O:
     0: Pending.  I don't think this should ever happen with
         flags = 1.
     1: Normal.
     2: End of file.  I can't see how this could happen on disc
         (remember, ATTACHIO knows nothing about disc files).
     3: Unusual condition.  The qualifying status probably describes
         this better -- try figuring it out.
     4: Irrecoverable error.  Again, look at the qualifying status
         if you can figure it out.


    Word   1:   number   of   words  (positive)  or  bytes  (negative)
    transferred.

Incredibly,  that's all there is to it.  Once you've got an LDEV and a
sector  address,  just  plug them into the  right parameters, and call
away!  Of course, remember to enter privileged mode and turn off traps
(just  as you would with FLABIO  or an MFDS/MTDS). Also, remember that
many disc addresses (the type V addresses I mentioned in the ACCESSING
FILE  LABELS  CHAPTER)  contain not an LDEV,  but rather a VTABX which
you'll  have  to  convert  into  an LDEV. But, once  you get all these
preliminaries  out  of  the  way, calling ATTACHIO is  just as easy as
calling FLABIO.

For  instance,  say that we want to  determine the number of defective
tracks on a given LDEV.

First,  as  always,  we have to find out  where this number is stored.
System  tables Chapter 3 contains a  lot of useful information on disc
format,  including  the fact that sector 1  of every disc contains the
disc's  defective  track table, and word 0  of the sector contains the
actual number of defective tracks.

Thus, our program would look something like:

INTEGER PROCEDURE NUM'DEF'TRACKS (LDEV);
VALUE LDEV;
INTEGER LDEV;
BEGIN
  INTRINSIC GETPRIVMODE;
  EQUATE SIZE'DEF'TRK'TAB = 128;
  ARRAY DEF'TRK'TAB (0:SIZE'DEF'TRK'TAB-1);
  DEFINE TAB'NUM'DEF'TRACKS   = DEF'TRK'TAB (0) #;
  INTEGER DISC'STATUS;
  DEFINE LDEV'IS'DISC = (DISC'STATUS.(13:3) = 0) #;
  EQUATE FUNC'READ = 0;  << ATTACHIO read function >>
  EQUATE DISC'IO'FLAGS = 1;  << ATTACHIO flags for a disc I/O >>

    PROCEDURE CHECKDISC (LDEV, STATUS);
    VALUE LDEV;
    INTEGER LDEV;
    LOGICAL STATUS;
    OPTION EXTERNAL, UNCALLABLE;

    DOUBLE PROCEDURE ATTACHIO
      (LDEV, QMISC, DSTX, ADDR, FUNC, CNT, P1, P2, FLAG);
    VALUE LDEV, QMISC, DSTX, ADDR, FUNC, CNT, P1, P2, FLAG;
    INTEGER LDEV, QMISC, DSTX, ADDR, FUNC, CNT, P1, P2, FLAG;
    OPTION EXTERNAL, UNCALLABLE;

  TURNOFFTRAPS;   << See MFDS chapter for a discussion of this >>
  GETPRIVMODE;
  CHECKDISC (LDEV, DISC'STATUS);
  IF NOT LDEV'IS'DISC THEN
    NUM'DEF'TRACKS:=-1  << error, not a valid sic >>
  ELSE
    BEGIN
    ATTACHIO (LDEV, <<qmisc>> 0,
              << Amazingly dreadful things will happen if we leave
                 off the "@" in "@DEF'TRK'TAB >>
              <<dstx>> 0, <<address>> @DEF'TRK'TAB,
              FUNC'READ, <<cnt>> SIZE'DEF'TRK'TAB,
              <<sector address:>> 0, 1,
              DISC'IO'FLAGS);
    NUM'DEF'TRACKS:=TAB'NUM'DEF'TRACKS;
    END;
END;

That's  all. Relatively simple, and because of the CHECKDISC call that
ensures  the  LDEV  is in range, configured, and  a disc, no danger of
system failure.

Of  course, this particular ATTACHIO calling sequence is only relevant
to  discs;  terminals,  printers, tape drives,  and other devices have
their  own parameters for QMISC, FUNC, P1, P2, and FLAGS, which I will
not  explain  here  for lack of space and  because they really are not
relevant to system table access.



SIRS, AND WHAT HAVE THEY DONE TO GET SUCH DEFERENCE?


Whenever  you  are accessing (reading or  writing) a system table that
somebody  else  might  be  modifying, you have  to beware of something
being changed out from under you.

For  instance,  say  that you're reading the  ODD -- the Output Device
Directory,  a rather odd name for  the table that contains information
on  all the output spool files in the system. The ODD is structured as
several linked lists of entries, one for each spooled device or device
class.  The  way  that  you'd  read  it is that  you'd read one entry,
process  it, get the address of the next entry from the current entry,
get  the  next  entry, process it, and so  on. However, say that while
you're processing this entry, the next entry in the list gets deleted.
Then,  when  you  try to get the next  entry from the location that is
pointed to by the current entry, you'll get garbage.

Even worse, say that you read the entry, process it, make some changes
to the copy of the entry you keep in your stack, and then try to write
it  back out. Then, if the entry  is deleted between the time you read
it  and write it back out, you stand the risk of over-writing whatever
new  entry  might  have  been  put  there,  probably  with  disastrous
consequences.

This  is  by no means a new problem  -- it arises in file and database
systems  all  the  time. The general solution to  this is some kind of
locking mechanism, and that is precisely what SIRs are for.

SIR  stands  for System Internal Resource. You  may lock it by calling
the  privileged  system  internal  procedure GETSIR and  release it by
calling the privileged system internal procedure RELSIR. Since all (in
theory)  processes that modify a system table must lock its SIR before
starting the modification, if you lock the SIR you are guaranteed that
no other process will modify the table until you unlock it.

Most  system  tables  that  are  usually  modifiable by  more than one
process at a time (including things like the PCB, JMAT, ODD, etc., but
excluding  JITs  and JDTs, because these  are session-local tables and
are  thus  very  rarely  concurrently  accessed)  have a  SIR. The SIR
assignments are listed in Chapter 5 of the System Tables Manual.

Before  using  GETSIR  and  RELSIR,  you have to  declare them in your
program as follows:

INTEGER PROCEDURE GETSIR (SIR'NUMBER);
VALUE SIR'NUMBER;
INTEGER SIR'NUMBER;
OPTION EXTERNAL, UNCALLABLE;

PROCEDURE RELSIR (SIR'NUMBER, GETSIR'RESULT);
VALUE SIR'NUMBER, GETSIR'RESULT;
INTEGER SIR'NUMBER, GETSIR'RESULT;
OPTION EXTERNAL, UNCALLABLE;

When  you want to get a SIR, you figure out its SIR number and pass it
to  GETSIR, saving the result returned  by GETSIR. Then, to release it
you  call RELSIR, passing to it the SIR number and the result returned
by  GETSIR (this is very important!). For instance, say you want to go
through  the  ODD,  which  is a linked list  of entries, without being
afraid  that somebody might be in the  middle of adding or deleting an
entry, causing you to encounter a bad link. What you'd do is:

  *  Find out the ODD SIR number from the System Tables Manual Chapter
    5 (it's 4).

  *  Lock  the  ODD  SIR by calling GETSIR,  saving the result in some
    variable  --  have  a special variable,  reserved for this purpose
    (say,  ODD'GETSIR'RESULT),  that  you  are  certain  will  not  be
    accidentally  changed  --  I  and  J  are  definitely  out  of the
    question.

  *  Do whatever you want to do  with the ODD, remembering that if you
    abort  for  any reason before you unlock  the SIR, THE SYSTEM WILL
    CRASH.  Also, be sure that neither you nor any procedure you might
    call  while  you  have  the SIR tries to  get a SIR whose priority
    number (see the discussion below) is lower than the ODD's.

  *  When  you're done, call RELSIR, passing  to it the ODD SIR number
    and the value that GETSIR returned.

  * Breathe a sigh of relief that you haven't crashed the system.

Similar  procedures  are used for locking  other SIRs -- however, note
that  if you have more than one SIR  locked at the same time, you must
unlock  them in the REVERSE ORDER of locking. Also note that you won't
get  any problems if you try to lock  a SIR twice -- say, you lock it,
and another procedure you call locks it again; just remember to unlock
it  twice,  too (in the case both  you and another procedure locks it,
all  that is necessary is that both the other procedure and you unlock
it, again in the reverse order that you two locked it in).

Watch  out, though -- when you're dabbling  with SIRs, there are a lot
of possible pitfalls involved:

  *  Locking  a  SIR  is  not  just  a nice thing  to do that'll avoid
    problems  for you. If you modify a table without first locking its
    SIR, you get everybody else in deep trouble, much like if you were
    modifying  a shared file without first FLOCKing it. Unfortunately,
    unlike  KSAM files and IMAGE databases  (but like MPE flat files),
    no  checking is done to ensure that you've gotten the right SIR --
    it's  your responsibility, and if you don't live up to it, may God
    have mercy on your system.

  *  Like other resources, it's very easy to get into deadlock trouble
    when  you're getting more than one SIR. Say that you get SIR A and
    then  try  to  get  SIR B. SIR B is  locked by another process, so
    you're   suspended  until  the  other  process  unlocks  the  SIR.
    Meanwhile,  the  other  process  tries to get  SIR A, and suspends
    waiting  for  you  to unlock it! Both of  you are waiting for each
    other  to release a SIR, and you'll stay that way until the system
    is  rebooted.  What's worse, anyone else  who tries to lock either
    SIR  will  be  suspended,  too, causing an  ever-growing number of
    suspended problems, and making the system come to a grinding halt.
    To  avoid this, there is a  certain very fixed and unalterable SIR
    locking  sequence that you MUST obey. It is described in Chapter 5
    of  the  MPE  V  System  Tables Manual, where each  SIR is given a
    priority  number,  and  you  must never lock  a SIR whose priority
    number  is  lower  than that of one  you already have locked. Note
    that  a  similar  table  in Chapter 5 of  the MPE IV System Tables
    Manual  is  quite  incomplete  --  try  to  get the  MPE V locking
    sequence  (which, to the best of my knowledge, is the same as that
    for  MPE  IV,  although it is described  much better). When you're
    debugging  SIR code that you're afraid might deadlock, it's a good
    idea  to  keep  an  OPT  or  a  SOO process running  on some other
    terminal.  OPT  and some versions of  SOO have commands that allow
    you  to see who is holding what SIRs -- this can help you find the
    cause of the deadlock.

  * Not only must you watch out to make sure that you lock all SIRs in
    the  right  order, you must also be  certain that all your callers
    and  called procedures do too. Thus, if  you want to lock the FILE
    SIR   (which   must   never  be  locked  before  the  FMAVT  [File
    Multi-Access  Vector Table] SIR), you must be sure that you do not
    call  any  procedure that tries to lock  the FMAVT SIR, since that
    would  be  a  SIR locking sequence  violation. Since virtually all
    file  system intrinsics can under some  conditions try to lock the
    FMAVT SIR, you must either lock neither the FILE SIR nor the FMAVT
    SIR  or both the FILE SIR and  the FMAVT SIR before calling a file
    system  intrinsic. That goes for all  other procedures you call --
    you  better  know what SIRs they try  to lock, and you better make
    sure  that your SIR locking  sequences are appropriate. Similarly,
    if  a procedure that you write uses SIRs, you have to be sure that
    all your callers realize this and make sure that they either don't
    have  any SIRs locked when they call you, or they are certain that
    your and their locking sequences mesh well.

  * If your process terminates itself for any reason (TERMINATE, QUIT,
    STACK  OVERFLOW, whatever) while you have  a SIR locked, you get a
    System  Failure 314. You best be  VERY careful. Note that you need
    only  be  afraid  of your process terminating  itself -- while you
    hold  a  SIR,  you  can't  be  killed  from  outside  (say,  by an
    :ABORTJOB);  if someone tries, you'll keep on processing until you
    release your last SIR, and then you'll die.

  * Always disable break before locking a SIR. This is not because you
    can  be  :ABORTed  from break -- if  someone tries, you'll keep on
    going  until  you release your SIRs, and  only then will the abort
    take  hold. Rather, if you let a user hit break while you've got a
    SIR,  the  user might try to execute  an MPE command that tries to
    get the same SIR, and you'll get a deadlock -- the command waiting
    for  the  SIR  (which  is  held by your  program) and your program
    waiting for MPE to :RESUME it (which it can't because it's waiting
    for a SIR). Deadlocks work in mysterious ways.

  * Never do any terminal I/O while you have a SIR locked. In fact, do
    not  perform  any task that you suspect  might take a long time to
    finish  --  like issuing a request for  a tape, reading a possibly
    empty  message  file,  etc. The reason for this  is simple -- if a
    process  that has a SIR suspends, everybody else who wants the SIR
    will  suspend, too, thus effectively stopping the system until the
    SIR  holder  is  re-activated. The last thing  you want is for the
    system to come to a grinding halt because someone went on a coffee
    break while a SIR-holding program was expecting input from him. As
    I said, this is also relevant for terminal I/O because a control-S
    on the terminal will suspend the writing program until a control-Q
    is hit.

  *  Always be sure that the GETSIR result you pass to a RELSIR is the
    result  returned  by  the  GETSIR  that  got  the  particular  SIR
    involved. If not, three guesses as to what happens...

  *  Always  be  sure  that  you  unlock SIRs in  the inverse order of
    locking them, under penalty of you-know-what.

This  is an array of warnings  that should make the bravest programmer
cringe.  Unfortunately,  if you want to modify  a system table or even
read  system tables that you fear might change out from under you, you
have to lock the SIR. So, how can you do this without fearing a system
failure?

  *  Try  to avoid locking SIRs whenever  possible. In MPEX, I used to
    lock  the FMAVT SIR and the FILE SIR (the two SIRs you should lock
    when doing certain operations on files -- always lock them in that
    order)  when I modify a file's  file label. Recently, I've changed
    it so that I wouldn't modify the file label without first FOPENing
    the  file exclusively -- that way, I'm guaranteed that no-one else
    (except possibly :STORE/:RESTORE) is dabbling with the file, and I
    no  longer need to lock the SIR. This avoids risk of deadlocks and
    system failures.

  *  When you need to get a SIR, release it as soon as possible. Every
    statement  between  a  GETSIR  and  a  RELSIR  is  just  one  more
    opportunity for a disastrous (system-crashing) abort to occur.

  * Write three procedures, SIR'GET, SIR'RELEASE, and SIR'RELEASE'ALL.

    -  SIR'GET  should take a SIR number,  turn off break (see above),
      lock the SIR, and save the SIR number and the GETSIR result in a
      global  array. This global array  should be shared between these
      three  procedures,  and  should actually be  treated as a stack,
      with  new entries being added to  the end, and old entries being
      deleted (by SIR'RELEASE) in a last-in, first-out fashion.

    -  SIR'RELEASE  should take a SIR number,  make sure that it's the
      most  recently  gotten  SIR,  get  the  GETSIR  result  from the
      last-added entry in the global array, and call RELSIR.

    -  SIR'RELEASE'ALL shouldn't take any  parameters, but should only
      release all the SIRs mentioned in the global array maintained by
      SIR'GET  and  SIR'RELEASE. It should be  called by any procedure
      that  wants to terminate or abort  in any fashion. For instance,
      the  DSEGREAD  procedure that I talked  about above does a MFDS,
      checking  its parameters for validity first, and aborting if the
      parameters  are invalid (to avoid a system failure). If you ever
      call   it   while   you  have  a  SIR  locked,  put  a  call  to
      SIR'RELEASE'ALL into it right before it aborts. That way, if you
      have  a bug and DSEGREAD aborts,  it'll release all the SIRs you
      have locked before aborting, thus preventing a system failure.

    I  use  these procedures exclusively,  almost never calling GETSIR
    and  RELSIR  outside  them, and my MPEX  (or any of VESOFT's other
    privileged  mode-using  programs) has never  crashed the system in
    production by aborting while holding a SIR (in fact, they've never
    crashed  a production system, period).  Sometimes, we get a letter
    containing  a PSCREEN of an abort caused by an MPEX bug (yes, even
    MPEX  has  bugs) with the message  "DSEGREAD passed bad parameter,
    SIRs  released", and I feel kind of proud -- sure, MPEX may have a
    bug,  but even though it was trying to read a non-existent segment
    while  it  had  a SIR locked, it just  aborted with a nice message
    instead of crashing the system.

The  number  one  cause of user program-caused  system failures is not
carelessness,  but  laziness  --  the  user  didn't spend  the time to
develop  some simple utility procedures  that would prevent minor bugs
or typos from causing system failures.

A  final  aside  on the topic of  SIRs -- sometimes (fortunately, very
rarely) you may want to ensure that NOBODY else on the system is doing
anything.  Essentially,  you want to turn  off interrupts to make sure
that  until you turn them back on, you and you alone will use the CPU.
This  is kind of a "super-SIR" --  you don't just lock some table, you
lock the entire system.

I  don't like this, and I've never felt  the need to do this. For one,
you  can  crash  the  system  this  way  easy  as  pie --  from what I
understand,  any interrupt, including one caused  by a request on your
part  to  access  a data segment that  isn't currently in memory would
cause a system failure. Furthermore, this isn't as good as a SIR in at
least  one  way  -- when you lock, say, the  JMAT SIR, you can be sure
that you will wait until anybody who locked the SIR to modify the JMAT
releases  it; that way, when you get  it, you know that nobody else is
dabbling  with the table. This is not so for turning off interrupts --
the  process which is modifying the table  you want to modify may have
just  been  swapped  out,  and  the  table  might be  in a temporarily
incosistent  state.  Finally,  when you turn  off interrupts, you also
turn  off  clock  interrupts, and the system  clock is essentially not
running until you turn interrupts back on.

As  I  said,  I  personally  have  never needed to  do this, and don't
foresee  myself doing it in the future. However, if you feel up to it,
there   are   two   machine   instructions   that   do  this  --  PSDB
(PSeudo-DisaBle) and PSEB (PSeudo-EnaBle).

PSDB  disables  process dispatching, thus assuring  that you'll be the
only  one to run until you do  a PSEB. Unfortunately, this may (or may
not  --  I'm  not sure) mean that the system  will crash if you try to
access  a data segment that is not in  memory, or do any other kind of
disc  I/O.  This  means  that  about the only thing  you can safely do
between  a PSDB and PSEB is to  access system tables that you know are
always memory-resident (e.g. CST, DST, PCB, etc.).

If  you  have any luck doing this, let me  know -- I'd like to see how
this should really be done.



SYSTEM TABLES AND SPL PROGRAMMING STYLE

Everybody  and his brother have  their own opinions about programming.
The  last  thing that I want to do  is to enter this controversial and
generally thankless field. However, in my experience with using system
tables in SPL, I discovered certain useful programming guidelines that
I want to mention in passing.

The  major  problem in working effectively  with system tables is that
virtually  nobody can remember just what the  5th bit of the 53rd word
of  an  entry  in data segment number  22 contains. Furthermore, it is
rather  cumbersome  to  always  flip through the  System Tables manual
whenever  you're  writing or reading programs.  Comments help some for
program  readability, but you'd still have to look at the manual while
writing, and you also stand the risk of incorrect comments.

What  one  really  wants  is  record structures (like  in PASCAL). You
declare  a data type PCB'ENTRY'TYPE, declare  a data structure of this
type  called  PCB'ENTRY, read the entry  into PCB'ENTRY, and then just
refer    to    the   PCB   entry   fields   as   PCB'ENTRY.FATHER'PIN,
PCB'ENTRY.PRIORITY,  PCB'ENTRY.CS'QUEUE, etc. If  you have another PCB
entry  you  want  to simultaneously work on,  just put it into another
data     structure     --     ANOTHER'PCB'ENTRY     --     and     use
ANOTHER'PCB'ENTRY.FATHER'PIN, etc.

Unfortunately,  SPL  does  not have these  kinds of record structures.
However, they can be cleverly emulated.

What I do is that I declare an array called PCB, and I set up a number
of DEFINEs -- PCB'FATHER'PIN, PCB'PRIORITY, PCB'CS'QUEUE, etc. -- that
refer  to  the  appropriate fields of the  array. Then, when the array
contains  a PCB entry, these DEFINEs can  refer (on either the left or
right  side  of an assignment statement)  to the appropriate fields of
the  entry  stored in the array. Note  that these DEFINEs can refer to
individual bits of the array as well as to entire words. For instance,
one such definition of some of the PCB entries might look like:

  << Definitions of some fields of MPE IV PCB entries. >>
  EQUATE SIZE'PCB = 16;
  INTEGER ARRAY PCB(0:SIZE'PCB-1);
  DEFINE PCB'FATHER      = PCB( 5).( 0: 8)  #,
         PCB'PRIORITY    = PCB(13).( 8: 8)  #,
         PCB'CS'QUEUE    = PCB(13).( 2: 1)  #;

Note  that  I  also have an equate for the  PCB size; I'd also have an
equate  for  the  PCB  data  segment  number, the  PCB System Internal
Resource  (SIR)  number  (if  it had one), equates  or defines for all
interesting  PCB  constants (e.g. the possible  values of the "process
type"  field),  etc.  What's more, I'd put  this all into one $INCLUDE
file  so  that  it  will be kept in one  place and will thus be easily
modifiable.

Other  tables  are  a  bit  more  difficult. The  JIT (Job Information
Table),  for instance, contains not just  word (integer) and bit field
values,  but  also  character arrays and double  integers. For that, I
equivalence  (using  the "BYTE/DOUBLE ARRAY  xxx(*)=yyy") a byte array
and a double array to JIT, the main array (which is an integer array).
Thus, the file looks something like:

  EQUATE SIZE'JIT=61;
  INTEGER ARRAY JIT(0:SIZE'JIT-1);
  BYTE    ARRAY JIT'B(*)=JIT;
  DOUBLE  ARRAY JIT'D(*)=JIT;
  DEFINE JIT'JOB'ID     = JIT  ( 9)           #,
         JIT'MAIN'PIN   = JIT  (10).  ( 8: 8) #,
         JIT'GROUP'SEC  = JIT'D( 7)           #,  << Word 14 >>
         JIT'ACCT'NAME  = JIT'B(32)           #;  << Word 16 >>

A  very few tables contain double integers  that are not aligned on an
even  word  boundary  (e.g.  word  14), but are rather  on an odd word
boundary (e.g. word 21), and thus can't easily be accessed as elements
of  a  double array equivalenced to the  main integer array. For them,
you have to equivalence another double array to the 1st (as opposed to
0th,  the default) element of the  main array, and then reference them
as elements of this array. For example,

  EQUATE SIZE'DIR'GROUP=41;
  INTEGER ARRAY DIR'GROUP(0:SIZE'DIR'GROUP-1);
  BYTE    ARRAY DIR'GROUP'B(*)=DIR'GROUP;
  DOUBLE  ARRAY DIR'GROUP'D(*)=DIR'GROUP;
  DOUBLE  ARRAY DIR'GROUP'D'1(*)=DIR'GROUP(1);
  DEFINE DG'CPU'COUNT   = DIR'GROUP'D'1(6)     #;  << Word 13 >>

Another  advantage  of this scheme is that  it makes it much easier to
change  your  programs  to  work on a new release  of MPE in which the
format  of  a  table  has  been changed -- sometimes  you only have to
change   the   $INCLUDE   files  and  recompile.  Also,  it  makes  it
surprisingly easy to have the same source code work with two different
MPE  versions  (e.g. MPE IV and MPE  V) -- just declare a compile-time
switch, say, X5 that is ON when you're compiling the program to run on
MPE  V  and  OFF when you're compiling to run  on MPE IV. Then, in all
your  $INCLUDE files that describe tables that are different in MPE IV
and  MPE V, just check this switch and depending on its value, declare
either  the  MPE  IV  or  MPE  V  layouts.  For  more  information  on
compile-time switches, see the SPL Reference Manual. An example is the
PCB:

  $IF X5=OFF  << MPE IV >>
  EQUATE SIZE'PCB=16;
  INTEGER ARRAY PCB(0:SIZE'PCB-1);
  ...
  DEFINE PCB'STACK'DST    = PCB( 3).( 1:10)  #;
  ...
  $IF X5=ON   << MPE V >>
  EQUATE SIZE'PCB=21;
  INTEGER ARRAY PCB(0:SIZE'PCB-1);
  ...
  DEFINE PCB'STACK'DST    = PCB( 3).( 2:14)  #;
  ...
  $IF

The  major problem of this method is  that it requires the entry to be
stored  in a certain fixed place (say, the array PCB). If you have two
entries  that  you want to manipulate at  the same time, you're out of
luck;  if  you  have an entire array of  entries that you want to look
through,  you have to step through the array, moving each entry to the
array  PCB  before  processing the entry. Also,  if you want to access
system   tables   using   SPL  memory-resident  system  table  support
constructs  (see below), you have to move  each word of the table into
the array.

One  solution to this problem is to  have the DEFINEs contain only the
word  number of the element in the table. For instance, for the JIT we
might have:

  EQUATE SIZE'JIT=61;
  DEFINE JIT'JOB'ID      = 9 #,
         JIT'ACCT'SEC    = 12 #,
         JIT'ACCT'NAME   = 32 #,    << Byte field! >>
         JIT'ALLOW'1     = 40 #;

Then, to get the job id, we'd read the JIT entry into anywhere we want
to    (say,    the    array    MY'JIT'ENTRY),   and   access   it   as
MY'JIT'ENTRY(JIT'JOB'ID).  This will work not only for fields that are
whole words, but also for bit fields:

  DEFINE JIT'MAX'PRI     = 10).(0:8 #,
         JIT'MAIN'PIN    = 10).(8:8 #;

This   way,  MY'JIT'ENTRY(JIT'MAIN'PIN)  would  map  to  MY'JIT'ENTRY(
10).(0:8  ) -- the right value.

Unfortunately, one of the drawbacks of this method is that if you want
to  retrieve a byte field or a double field, you can't just specify it
by    name    --    you    have   to   explicitly   retrieve   it   as
MY'JIT'ENTRY'B(JIT'ACCT'NAME).     If    you    forget    and    enter
MY'JIT'ENTRY(JIT'ACCT'NAME), you'll get a very wrong result.

Also,  this method requires more typing --  you have to enter both the
field designator (e.g. JIT'ACCT'NAME) and the array neme.

Both  methods are acceptable approaches,  each with its own advantages
and  disadvantages.  Use  whichever you prefer, or  even your own, but
keep in mind the following:

  * Use DEFINEs and EQUATEs (for field names, entry size, data segment
    numbers,  SIR  numbers,  possible field values,  etc.). If you use
    "magic  numbers"  (e.g. bits 12:3 of word  35) in your code, it'll
    only make it harder to write, read, and maintain.

  *  Put  DEFINEs and EQUATEs into an  $INCLUDE file. That way, if you
    need  to change something (because you made an error in putting it
    in  in  the  first place or because it  has changed in the new MPE
    release), you'll only have to change it in one place.



AND NOW, FOR SOMETHING COMPLETELY DIFFERENT...

Until  now, I have been concentrating  mostly on describing how system
tables  can  be  accessed. To do this,  I've introduced some important
tables  like the PCB and JMAT, but have skimped on description of what
other system tables may exist and what they may contain.

Unfortunately,  the  System  Tables  Manual  does  not  really explain
anything except table formats. It'll tell you what the ODD looks like,
but won't tell you thing one about how it fits into the overall system
structure  and  what you need to know  to access it. Once I've whetted
your  appetite  by  telling  you  how to access  system tables, I feel
obligated  to present my concept of  how all these tables fit together
and where you should go to get a particular piece of data.

As  I  said  before,  system  tables  are  just  the places  where the
operating  system "keeps its stuff" --  where it stores information on
all  the  objects  it must manage. A convenient  way to look at system
tables is in terms of what objects they describe.

Don't  feel  nervous  if you don't understand  the purpose or even the
contents  of  some  of  the  tables I mention below.  Some of them are
really arcane, and may not be worth much to you anyway. If you're just
starting  to  learn the innards of the  system, you might want to skip
reading  about  everything  except  the  simpler  job-,  process-, and
file-oriented system tables.


JOB-ORIENTED SYSTEM TABLES

Almost  all  the  work  done on an HP3000 is  done on behalf of a job
submitted by the user. Note that whenever I say "job", I mean either a
batch  job  or  on-line session, since they  are rather similar to the
system.  From  the  system's  point of view, a  job is a collection of
processes.  The Command Interpreter (CI) of a job is one such process.
Any  processes you create, either using  the :RUN command or using the
CREATE or CREATEPROCESS intrinsics, also belong to that job.

Every  job has an entry for it in the Job MAster Table (JMAT). This is
a  very  simple  table;  it has a header  entry, which contains system
global  information (like the number of jobs/sessions, the job/session
limit,  etc.), and one entry for each job on the system. Each of these
entries  contains some (but not all)  information about the job -- its
job  name,  user, account, group, terminal  number (for sessions), the
Process  Identification  Number  of  its  main  process, and  so on. A
:SHOWJOB  takes  virtually  all of the information  it prints from the
JMAT   --   just  think  of  the  JMAT  as  a  :SHOWJOB  stored  in  a
machine-readable form. Since there's only one JMAT, it occupies a data
segment with a fixed data segment number. If you want to know the data
segment  number  or the page of the  System Tables Manual on which the
JMAT is described, just look at Appendix 2 of this paper.

A  job's  JMAT  entry,  however, does not  contain all the information
about  the  job.  Much  of the remaining information  -- CPU time, the
capabilities of the user running the job, :ALLOW mask, etc. -- is kept
in a table called the Job Information Table (JIT). There's one JIT for
each  session,  and  it's  pointed  to  by  the PXGLOB  area (which we
mentioned  earlier)  of the stack of each  process that belongs to the
job. To get to your JIT, you'd first find its data segment number from
your  PXGLOB,  and  then use an MFDS to  read it. Just as the :SHOWJOB
command  reads the JMAT, the WHO intrinsic  looks at your JIT; all the
information  it gives you --  capabilities, user, account, group, home
group,  local  attributes,  etc. -- all come from  the JIT. The JIT is
even  easier  to  work  with  than  the JMAT, being  just a big record
structure  with  a  lot  of fields (each of  which is at least briefly
mentioned  in  the  System  Tables  Manual).  Note  that  there's some
duplication  of  data  (user  name,  account  name, group  name, etc.)
between the JMAT and the JIT. HP does some damnedest things.

If you've been paying particularly close attention, you'll find that I
lied.  A job is more than just  a collection of processes -- there are
also  some  job-local entities, such as  :FILE equations and temporary
files,  that  have to be maintained for each  job. These are kept in a
Job  Directory  Table  (JDT), of which there  is one per session (also
pointed to by the PXGLOBs of the processes belonging to that session).
This table actually contains a header and five sub-tables: one for the
job's  temporary files, one for the :FILE equations, one for the JCWs,
one  for  the  job-local  data  segments,  and  one for  the :CLINE (a
datacomm  command) equations. The header  contains the pointer to each
of  the  sub-tables, and each sub-table is  in turn is a collection of
variable-length  entries.  Thus, to find  a particular :FILE equation,
you'd  get  the  JDT's  data segment number from  your PXGLOB, get the
JDT-relative  index  of the :FILE equation  table from the JDT header,
and  then  go through the :FILE equation  table entries until you find
one with the name you're looking for. Not the most entertaining job in
the world, but not too difficult, either.

Finally,  for completeness' sake I  must mention two other, relatively
unimportant, tables -- the Job CUtoff Table (JCUT) and the Job Process
CouNt  Table (JPCNT). The JCUT  contains information used for aborting
all  jobs that have a CPU time limit. I haven't the foggiest notion of
what the JPCNT is actually useful for, except for a little-used (if at
all)  device  called  Job  SIRs.  This is one table  you can afford to
ignore.

Thus,  to summarize, the structure of those system tables that pertain
to jobs is roughly like the following:

  JMAT (a single data segment), which contains for each job
    The job number, job name, user, account, group, terminal
      number, etc.
    The Process Identification Number (PIN) of the job's main
      (CI) process

  One PXGLOB per process belonging to the job, which points to
    One JIT per job, which contains
      The job number, job name, user, account, group, home group,
        CPU time, :ALLOW mask, etc.
    One JDT per job, which contains information on the job's
      Temporary files (JTFD)
      :FILE equations (JFEQ)
      Data segments (JDSD)
      JCWs (JJCW)
      :CLINE equations (JLEQ)
    The JMAT entry referring to the job

  JCUT (stand-alone table), which contains
    Information on all jobs that have a CPU time limit

  JPCNT (stand-alone table), which contains
    A bunch of bits the reason for which I could never fathom


PROCESS-ORIENTED SYSTEM TABLES

Rather  more  important than the concept of a  job is the concept of a
process.

A  process  is a single instance of a  program running. It has its own
data  space  and code (the code may  be shared among several processes
that  are  running  the same program). All work  done on the system is
done  on  behalf of a process, some of  which -- the user processes --
belong to jobs, and others -- the system processes -- do not.

The  master table that describes all  processes is the Process Control
Block  table (PCB). It, like the JMAT, contains a header entry and one
entry  for each process. The process entry contains useful information
like  a  process' priority, the data segment  number of its stack, its
family relationships (father, son, brother), wait flags (is it waiting
for   a  SIR,  a  RIN,  etc.),  and  more.  A  process'  PIN  (Process
Identification Number) is nothing more than the number of the process'
entry in the table. If you have a PIN, you can just multiply it by the
PCB  entry size, and do an MFDS of the entry from that location in the
PCB (which has a fixed data segment number).

Also like the JMAT, the PCB does not contain all the information worth
knowing  about  a particular process. Much  very useful information --
what files are open by the process, what its register values are, what
traps  it has enabled, where its JIT and JDT are, and so on -- is kept
in  the  so-called  PCB  eXtension  (PCBX),  which  is  stored  in the
DL-negative area of the process' stack. The PCBX consists of

  *  The  PXGLOB, which is mostly the same  for all processes in a job
    and  contains  global information like the  job's JIT and JDT data
    segment numbers, the device on which the job is running, the index
    of the process' JMAT entry, etc.

  *  The  PXFIXED,  which contains a variety  of useful data, like the
    values  of  the process' DB and S  registers (they have to be kept
    somewhere  when  the  process gets swapped  out), the addresses of
    whatever control-Y and other trap routines are set, the job number
    of  the  job  the process is running, and  so on. Like the PXGLOB,
    this is just one big record structure.

  *  The  PXFILE, which describes the files  the process has open. Its
    structure  is  so  complicated  that it deserves  a separate paper
    (which  may  or  may not be  forthcoming). Fortunately, the System
    Tables Manual is somewhat more lucid than usual with regard to the
    structure  of the PXFILE (which isn't  saying much), and you might
    be able to figure things out by reading it. But don't bet on it.

This  is  about  all there is to the  structure of the process tables.
Note  the  recurring  concept  of  a  master table  that contains some
information  on all processes or jobs, and a table for each process or
job  that  contains  more  detailed  information about  the particular
process  or job. You'll see this again  when we get to logical devices
and the I/O tables.

Finally,  two  other,  less  useful,  tables.  The  Process-Job  cross
REFerence  table  (PJXREF),  whose  format IS NOT  GIVEN IN THE SYSTEM
TABLES  MANUAL,  contains, for every process,  its job number. The job
number  of  process  PIN  is  in  the  PINth  word of  this table. The
Process-Process  COMmunication  table (PPCOM)  contains information on
any mail the process might have in its mailbox.

Thus, to summarize, the process table structure looks like:

  PCB (a single data segment), which contains for each process
    The process' priority, wait flags, etc.
    The PINs of the process' father, son, and brother
    The data segment number of the process' stack

  PCBX (stored in the process' stack's DL-negative area), containing
    PXGLOB, which contains
      The data segment of the process' JIT and JDT
      The index of the process' JMAT entry
      Some other information (like the process' terminal)
    PXFIXED, which contains
      The job number, saved registers, trap addresses, etc.
    PXFILE, which contains
      Information on all the files used by the process

  PPCOM (a single data segment), which contains for each process
    Information on any mail message it might have in its mailbox

  PJXREF (a single data segment), which contains for each process
    Its job number; the job # of process PIN is stored in the
      PINth word of the table


DEVICE-ORIENTED SYSTEM TABLES

Many  system tables describe devices --  discs, terminals, etc. Of all
the  parts  of MPE, the I/O system  is probably the most intricate and
most confusing portion.

Devices  are  referred  to  by  their Logical  DEVice numbers (LDEVs).
Although  this  means  there's  such a thing as  a physical device, us
software  people  don't  care  about  it. In fact,  I'll use the terms
device, logical device, and LDEV interchangeably (since everybody else
does, anyway).

Every  device  has an entry in the  Logical Device Table (LDT) and the
Logical-Physical  Device  Table (LPDT), both of  which are single data
segments  with fixed data segment numbers. The LDT contains the device
type,  record  with,  main  pin  of owning process  (for terminals and
tapes),  etc.  Note  that  the  second half of  the LDT's data segment
contains  the so-called LDT eXtension (LDTX), which contains some more
useful information. Like the LDT, the LDTX has one entry per LDEV. The
start  address of the LDTX can be  deduced from finding out the number
of  configuring  LDT  entries  from  the  LDT  header  entry  and then
calculating  the  index of the first word  that isn't used by the LDT.
The  LPDT  contains some other stuff,  like the device subtype, state,
some  more flags, and the  address (SYSGLOB-relative!) of the device's
Device Information Table (DIT).

The  DIT contains various information about  the device that is highly
specific  to the particular device type.  The System Tables Manual has
50-odd  pages  describing  the  various  DIT  formats. It  is not very
entertaining reading, and will never compete with a well-written, say,
telephone book.

All device classes in the system are listed, along with the LDEVs they
represent,  in  the  Device  Class  Table  (DCT),  which  contains one
variable-length entry per device class. To find out, say, what devices
the  class  TAPE  contains, just traverse the  table until you find an
entry  with  a  name  of  TAPE. Like the LDT,  the DCT shares its data
segment   with   another  (less  useful)  table  called  the  Terminal
Descriptor Table (TDT).

All  the I/Os that are currently pending in the system have entries in
the I/O Queue (IOQ, for non-disc devices) and Disc Request Queue (DRQ,
for  disc devices) tables. The last thing  I want to do is expend more
energy describing these truly complicated tables that most people will
never use anyway.

Finally,  some more useless tables: the Interrupt Linkage Table (ILT),
Driver  Linkage  Table (DLT), System  BUFfers (SBUF), Terminal BUFfers
(TBUF),  and  the  Interrupt Control Stack  (ICS). Definitely not your
bread-and-butter  programming  stuff.  If  you don't  know about them,
you're none the worse for it.

Finally, a recap of the I/O System tables:

  LDT (a single data segment), which contains for each device
    Device type, record size, main owner pin (for non-discs), etc.

  LPDT (a single data segment), which contains for each device
    Device sub-type, device status, various other flags, etc.
    The address of the device's DIT

  DCT (a single data segment), which contains for each device class
    The LDEVs that are in the class
    The type of class (discs, serial I/O devices, etc.)

  DIT (a chunk of memory for each LDEV, pointed to by a
       SYSGLOB-relative address), which contains a lot of
       device-dependent data

  IOQ, DRQ, SBUF, TBUF, DLT, ILT, ICS, LDTX, and TDT,
    which you'll have to look up in the System Tables Manual
    if you really want to be a big hit at cocktail parties


FILE-ORIENTED SYSTEM TABLES

The entity that everybody works most with is the file.

Every  file in the system has a File  LABel (FLAB) kept in a sector on
disc. Although this is not a data segment, it can properly be called a
system table, since it contains system-maintained data. The file label
is  a  veritable treasure-trove of  information, containing everything
you'd  ever want to know about a  closed file. All modes of :LISTF get
their information straight from the file label.

Getting  to  a  file's file label, however,  isn't very easy. The disc
address of a file label may be stored:

  * for permanent files, in the system directory

  * for job temporary files, in the job's JDT

  *  for  spool  files, in the Output  Device Directory (ODD) or Input
    Device Directory (IDD)

  *  for files that were FOPENed as  new files and not yet FCLOSEd, in
    the File Control Block (FCB) belonging to that file

For  open files, the matter is  somewhat more complicated. In addition
to  the  permanent, relatively unchanging information  that is kept in
the FLAB for a closed file, the system must also keep track of an open
file's  current record pointer, current block number, buffers, etc. As
I  said  before,  the topic of open file  tables is such a complex one
that  it deserves a paper of its own. All I'll do here is give a brief
structural description:

  FLAB (stored on disc, one per file) contains
    The file name, code, size, extent map, etc. -- everything a
      :LISTF could give you

  The  system  directory  contains  entries  that  point  to  FLABs of
    permanent files

  Each job's JDT contains entries that point to FLABs of job temporary
    files

  The spool file directories IDD and ODD contain entries that point to
    FLABs of spool files

  Each process' Active File Table (AFT), stored in the process' PXFILE
    area,  briefly  describes  all the files opened  by the process (a
    file number is an index into this table) and points to each file's
    LACB and PACB

  For  each  open  of  a file, a Physical  Access Control Block (PACB)
    describes  the  current  state  of the file  -- the current record
    number,  the buffers used for buffered  file access, the number of
    physical and logical I/Os that have occurred, and so on. For files
    opened  MULTI,  or  GMULTI,  there's  only  one such  PACB for all
    openers; for all other open files, there's one PACB per opener.

  For each open of a file that is being accessed using MULTI or GMULTI
    access,  there's  one LACB per opener;  for files not opened using
    MULTI or GMULTI there are no LACBs

  For  each  opened  file, there's an  FCB, which contains information
    common  to  all  readers of the file (mostly  a rehash of the file
    label,  kept  in memory for faster access).  The FCB is pointed to
    both by all the file's PACBs and by the file's file label.

  FMAVT  (File Multi-Access Vector Table)  contains information on all
    files opened with MULTI or GMULTI access


SEGMENT-ORIENTED SYSTEM TABLES

Segments  are  broken  up  into  two  very  distinct  categories: data
segments and code segments.

Data  segments are described in the  Data Segment Table (DST); there's
one  entry for each data segment, and it contains information like the
data segment length, memory address, flags, etc. That's all there's to
it,  a  welcome  change  from the complicated  structures in place for
files, devices, and even jobs and processes.

No  such luck with code segments, though. The Code Segment Table (CST)
contains  only  the  entries  --  also listing  the segments' lengths,
addresses,  and  other  flags -- referring to  all currently loaded SL
segments  and  the  program segments of the  process that is currently
running.

Say  that  you  want to find out about  the code segments of a process
given its PIN. What you want to do is to get to the entries of the CST
eXtension  (CSTX) table -- which is stored in a single data segment --
that correspond to the process' code segments.

Your  first step would be to get  the "CSTX block map index", known as
CSTXEIX, of the process from the process' PCB. Note that this value is
listed in the PCB table layout as BLKINX and PBX.

This  is  an  index into a table called  the CSTBLK, also known as the
CSTXMAP.  The CSTXEIXth entry in the CSTBLK contains one thing and one
thing  only:  the  address  of  the  first  CSTX entry  describing the
segments of the process.

However,  this  is  NOT  a  CSTX-relative address. This  is not even a
SYSGLOB-relative address. An absolute address, you say? No.

This address is relative to the base of the DST, an entirely different
data  segment altogether! Since you can't  very well MFDS CSTX entries
from  the  DST, you have to subtract  the contents of SYSGLOB location
%33 from the value, and then you have the CSTX-relative index. SYSGLOB
%33 (absolute %1033) is the DST-relative address of the first entry of
the CSTX.

Once  you're  done with all this -- you  got the CSTXEIX from the PCB,
you got the DST-relative address from the CSTBLK, and you converted it
to  a  CSTX-relative-address -- you can now  get the CSTX entries. The
first  entry, the one pointed to by  the address you worked so hard to
get,  tells  you how many code segments  the process' has and how many
users  are  sharing  it.  Subsequent  entries  describe  each  of  the
program's segments.

In summary, the segment-oriented tables look like this:

  DST, one entry per data segment containing
    The length of the data segment
    The memory address of the start of the data segment
    Miscellaneous flags

  CST, one entry per SL code segment and each code segment of the
    currently-executing program containing
    The length of the code segment
    The memory address of the start of the code segment
    Miscellaneous flags

  CSTX, one entry per loaded program (not process! all processes
    running the same program share code segments) containing
    The number of segments in the program
    The number of processes running the program

  Also in the CSTX, one entry per segment of each loaded program,
    formatted like a CST entry

  CSTBLK (also known as CSTXMAP), one entry per program containing
    A DST(!)-relative pointer to the first CSTX entry belonging
      to the program


MISCELLANEOUS SYSTEM TABLES -- SPOOLING

The  information on all spool files in the system is kept in the Input
Device  Directory  (IDD)  for input spool files  and the Output Device
Directory  (ODD) for output spool files. Both of these are independent
data segments, and the formats of all their entries are identical. XDD
is a term used to generically talk about either the IDD or the ODD; an
XDD entry is an entry that could be from the IDD or the ODD.

An XDD has three kinds of entries:

  *  One header entry, describing the table -- it contains things like
    the  size of the table and the next input/output spool file number
    to be allocated.

  *  One device header entry for each device in the system. Each entry
    contains the device number, device outfence (if any), and the head
    and  tail  addresses  of  the  linked  list of  spool file entries
    belonging to that device. The 0th device header entry contains the
    head  and  tail  addresses  of  the linked list  of all spool file
    entries belonging not to a device, but to a device class.

  *  One  spool  file  entry for each existing  spool file. Each entry
    contains  the  name of the spool file,  the user, account, and job
    name  that it belongs to, the disc address of its file label, etc.
    SPOOK's  >SHOW command (especially  >SHOW;@) essentially print the
    contents of these entries.

This  is  a  comparatively  easy table to  navigate. MPEX's spool-file
manipulation   routines   --   %PURGE:SPOOL,   %LISTF:SPOOL,  etc.  --
essentially  traverse  the  ODD, picking up spool  file file labels as
they  go  along.  Note  that  the  addresses  in  the linked  list are
table-relative  addresses of the start of the next spool file entry; 0
indicates no next entry.


MISCELLANEOUS SYSTEM TABLES -- OTHERS

There are several other relatively useful system tables.

The  LST  (Loader  Segment  Table) contains information  on all loaded
program  files  and  SL files. MPEX's  %LISTF,ACCESS goes through this
table to find out who may have a particular file loaded.

The  SIR (System Internal Resource)  table contains information on all
SIRs  that are currently locked. If you  see the system hanging up and
you're  afraid  that  it's  because  someone isn't releasing  a SIR or
you're  getting a SIR deadlock, you can go into privileged mode :DEBUG
and have a look at this table.

The RIN (Resource Identification Number) table contains information on
all RINs that are currently locked. MPEX's %LISTF,ACCESS looks at this
to find out who is locking a file or waiting to lock the file.

The   RIT  (Reply  Information  Table)  contains  information  on  all
outstanding replies.

There  are  other tables, having to  do with logging, private volumes,
message  files,  :WELCOME messages, you name  it. I won't talk further
about  them  simply  because  of space limitations.  Also, most of the
stand-alone  tables are relatively simply structured, and you can find
out  how  to  get  to  them  by  reading the System  Tables Manual and
conducting some experiments with privileged mode :DEBUG.

Remember  that  there  is  a brief description  of every (well, almost
every)  table in the system and a "pointer" to its entry in the System
Tables Manual in Appendix 2 of this paper.



CONCLUSION


System tables are not the incomprehensible monsters that they may seem
to  be  at  first  glance.  Once  you  master  the  various techniques
described  in  this  paper  and get a  reasonable familiarity with the
actual  contents  of  the  tables, you should,  with relatively little
effort  and  worry, be able to access  system tables just as easily as
you would, say, a file or a database.

Good luck and HAPPY HACKING.



ACKNOWLEDGEMENTS


I  would  like to thank the following  people for reviewing and making
many useful comments on this paper:

  Steve Cooper, Allegro Consultants
  Robert Green, ROBELLE Consulting Ltd.
  David Greer, ROBELLE Consulting Ltd.
  Jack Howard, Consultant, Los Angeles
  Stan Sieler, Allegro Consultants
  Jim Squires, HP Fullerton
  Vladimir Volokh, VESOFT, Inc.

and  of  course everybody -- too numerous  to mention -- who taught me
all  this  stuff. May your programs never  fail and your systems never
crash.


APPENDIX 1 -- PROGRAM 1

<< Don't forget to :PREP me with ;CAP=PM! >>
<< Works on MPE IV and MPE V. >>
$CONTROL MAIN=PROGRAM'1, NOSOURCE, USLINIT
<< This program:
     Prints out word 0 of the user's capability matrix;
     Prints out the job or session's job/session number;
     Gives the user SM capability and does a LISTUSER
       before (which should fail) and after.
>>
BEGIN
INTRINSIC
  ASCII,
  COMMAND,
  GETPRIVMODE,
  GETUSERMODE,
  PRINT;
INTEGER POINTER
  DL;
INTEGER
  CAPS,
  DUMMY,
  ERROR,
  JS'NUMBER,
  LENGTH;
ARRAY
  BUFFER'L(0:127);
BYTE ARRAY
  BUFFER'(*)=BUFFER'L;
DEFINE
  INDEX'PXGLOB = DL(-1) #,
  INDEX'PXFIXED = DL(-2) #;

<< Get the DL pointer >>
PUSH (DL);
@DL:=TOS;

<< Get and print word 0 of the capability matrix >>
GETPRIVMODE;
CAPS:=DL(-INDEX'PXGLOB+2);
GETUSERMODE;
MOVE BUFFER':="Capability word 0 = %";
PRINT (BUFFER'L, -21, %320);
ASCII (CAPS, 8, BUFFER');
PRINT (BUFFER'L, -6, 0);

<< Get and print the session number >>
GETPRIVMODE;
JS'NUMBER:=DL(-INDEX'PXFIXED+19);
GETUSERMODE;
IF JS'NUMBER.(0:2)=1 THEN
  << JS'NUMBER.(0:2) indicates session (1) or job (2) >>
  MOVE BUFFER':="#S"
ELSE
  MOVE BUFFER':="#J";
PRINT (BUFFER'L, -2, %320);
LENGTH:=ASCII (JS'NUMBER.(2:14), 10, BUFFER');
PRINT (BUFFER'L, -LENGTH, 0);

<< First, show that LISTUSER MANAGER.SYS fails without SM >>
MOVE BUFFER':="Trying LISTUSER MANAGER.SYS before getting SM";
PRINT (BUFFER'L, -45, 0);
MOVE BUFFER':=("LISTUSER MANAGER.SYS", %15);
COMMAND (BUFFER', ERROR, DUMMY);
IF ERROR<>0 THEN
  BEGIN
  MOVE BUFFER':="CI Error # ";
  PRINT (BUFFER'L, -11, %320);
  LENGTH:=ASCII (ERROR, 10, BUFFER');
  PRINT (BUFFER'L, -LENGTH, 0);
  END;

<< Now, get SM capability >>
GETPRIVMODE;
DL(-INDEX'PXGLOB+2).(0:1)  << SM bit >>  := 1;
GETUSERMODE;

<< Now, show that LISTUSER MANAGER.SYS succeeds >>
MOVE BUFFER':="Trying LISTUSER MANAGER.SYS after getting SM";
PRINT (BUFFER'L, -44, 0);
MOVE BUFFER':=("LISTUSER MANAGER.SYS", %15);
COMMAND (BUFFER', ERROR, DUMMY);
IF ERROR<>0 THEN
  BEGIN
  MOVE BUFFER':="CI Error # ";
  PRINT (BUFFER'L, -11, %320);
  LENGTH:=ASCII (ERROR, 10, BUFFER');
  PRINT (BUFFER'L, -LENGTH, 0);
  END;
END.


APPENDIX 1 -- PROGRAM 2

<< Don't forget to :PREP me with ;CAP=PM! >>
<< Works on MPE IV; changes needed for MPE V are indicated. >>
<< This program assumes the existence of the DSEGREAD and MYSTACK
   procedures described in the text. >>
$CONTROL MAIN=PROGRAM'2, NOSOURCE, USLINIT
BEGIN
<< This program:
     Prints out the job/session name of the current job/session
     (or blanks if no job/session name.)
>>
INTRINSIC
  PRINT;

EQUATE
  SIZE'PXGLOB = 8;  << 12 for MPE V >>
DEFINE
  PXGLOB'JIT'DSEG   = PXGLOB(6).(6:10) #;  << JIT data segment # >>
                     << PXGLOB(11) for MPE V >>
ARRAY
  PXGLOB(0:SIZE'PXGLOB-1);  << Array for holding the PXGLOB data >>
EQUATE
  OFFSET'PXGLOB = 0;  << Offset of the PXGLOB in the stack dseg >>

EQUATE
  SIZE'JIT = 61;  << 67 for MPE V >>
DEFINE
  JIT'JS'NAME    = JIT(44) #;  << MPE IV or MPE V >>
ARRAY


DSEGREAD (MYSTACK, OFFSET'PXGLOB, PXGLOB, SIZE'PXGLOB);
DSEGREAD (PXGLOB'JIT'DSEG, 0, JIT, SIZE'JIT);
PRINT (JIT'JS'NAME, -8, 0);
END.


APPENDIX 2 -- SUMMARY OF SYSTEM TABLES

System tables are listed alphabetically by their initials. The chapter
numbers  of  the  tables  in the System Tables  Manual are given. Page
numbers are not given because they may vary from release to release of
each manual -- they shouldn't be too hard to find in any case.

All  tables  are  stored  in  memory  (not  on disc)  unless otherwise
specified.


Table    Chapter   Table describes or contains
-----    -------   ---------------------------
AFT      6  [3]    One/process; part of PXFILE
ASSOC    15        All :ASSOCIATE commands
BKPNT    17        :DEBUG breakpoints
CBT      6  [3]    Contains file system control blocks
CILOG    None      :(CMD) USER.ACCT logons
CST      2 *       Loaded SL and current program's code segments
CSTAB    23  [2]   Contains communications system info
CSTBLK   2 *       Contains pointers into the CSTX
CSTX     2         All code segments in the system
DCT      13        Device classes
DIT      13  [3]   One/device; contains device-specific info
DLT      13        Device drivers
DRQ      13 *      Queued disc I/O request
DST      2 *       Data segments
FCB      6  [3]    One/file opener; file system info
FLAB     6  [3]    One/file, stored on disc; file parameters
FMAVT    6         Files opened with MULTI/GMULTI access
ICS      13        Contains stack of interrupt-handling process
IDD      14        Input spool files
ILT      13        Contains interrupt info
IOQ      13 *      Queued I/O requests
JCUT     8 *       Jobs that have a CPU limit
JDSD     8  [3]    One/job; job data segment info, part of JDT
JDT      8  [3]    One/job; job's :FILE eqns, temp files, etc.
JFEQ     8  [3]    One/job; job :FILE equation info, part of JDT
JIT      8  [3]    One/job; detailed job info
JJCW     8  [3]    One/job; job JCW info, part of JDT
JLEQ     8  [3]    One/job; job :CLINE equation info, part of JDT
JMAT     8         Jobs
JPCNT    8 *       Contains arcane info about jobs
JTFD     8  [3]    One/job; job temp file info, part of JDT
LACB     6  [3]    One/MULTI or GMULTI file opener; file system info
LDT      13        LDEVs
LDTX     13        LDEVs
LIDTAB   17        Logging identifiers
LOGBUFF  17        Contains logging buffers
LOGTAB   17        Logging
LPDT     13 *      LDEVs; points to DITs
LST      11        All loaded program and SL files
MEASINFO 17 *      Measurement info
MVTAB    12        Mounted private volumes
ODD      14        Output spool files
PACB     6  [3]    One/file opener; file system info
PCB      7 *       Processes
PCBX     7  [3]    One/process; contains PXGLOB, PXFIXED, PXFILE
PJXREF   None      Maps PINs into job numbers
PPCOM    7         MAIL messages that have been sent but not gotten
PVUSER   12        Users of private volumes
PXFILE   6  [3]    One/process; contains info on process' files
PXFIXED  7  [3]    One/process; contains miscellaneous process info
PXGLOB   7  [3]    One/process; points to JIT and JDT
RIN      5         RINs (including file locks)
RIT      15        Outstanding :REPLYs
SBUF     13 *      Contains buffers used for some system I/O's
SIR      5 *       Locked SIRs
SRT      2 *       Special memory management requests
SWAPTAB  2 *       Contains segment swapping info
SYSGLOB  1         Contains miscellaneous system info
SYSJDT   8         Contains the JDT used by system processes
SYSJIT   8         Contains the JIT used by system processes
TBUF     13 *      Contains buffers used for terminal I/O's
TDT      13  [1]   Special configurations for terminals
TRL      17 *      Requests for system timer (PAUSEs, timeouts, etc.)
UCRQ     8         All requests to the process controller process
VDD      17        Mounted labelled tapes
VDSMTAB  3 *       Virtual memory on discs
VTAB     3         Discs
XDD      14        Generic name for either the IDD or ODD


* - Indicates a system table that is accessible using the SPL "INTEGER
    POINTER  table  = number;" construct. For  the system table number
    (not necessarily equal to the data segment number), see the layout
    of  SYSGLOB  in  chapter  1.  If  SYSGLOB  cell N  is indicated as
    containing the base of a table, then N is the table's number.

[1] - Indicates a table that exists in MPE V but not in MPE IV.

[2]  - Indicates a table documented in the MPE V manual but not in the
      MPE IV manual.

[3]  - Indicates that there is more  than one of these tables, one for
      each  one of a number of objects.  For instance, a JIT, of which
      there's one per job, and an FLAB, of which there's one per file.
      All tables not so marked are unique.

Go to Adager's index of technical papers