[comp.sys.ibm.pc] Disk I/O in Resident MS-DOS Program

lbr@holos0.UUCP (Len Reed) (02/17/89)

I have an MS-DOS terminate-and-stay resident program that must be able
to open/close/read/write hard disk files.  It appears that the "file
handles" move on me.  That is, if this routine opens a file, as the
foreground processes spawn and die, I lose my handles since they
really belong to the foreground processes.

FCB I/O appears to work better, but I'm not crazy about using it since
it is primitive.  My opens and closes have to be in the current directory,
which means I have to perform the following to do an open or close:
	1. Lock the foreground out of DOS.  (I have a mechanism in place to
	   stall DOS calls.)
	2. Save the current directory.  (MS-DOS has one directory per disk,
	   not--as in UNIX--one per process.)
	3. Go to the proper directory.
	4. Perform the open/close.
	5. Go back to the original directory.
	6. Unlock DOS.

This scenerio seems unnecessary for read/writes.  I can do them regardless
of my current directory.

Is there a better way to do this?  What I'm doing seems to work; am I
missing something that will cause it not to work?

BTW--the application is a communications package that sits in the background
and steals time to transfer/receive files to/from a mainframe.

Thanks for any help.
-- 
    -    Len Reed

dixon@sagittarius.steinmetz (walt dixon) (02/17/89)

Writing programs which run in the background under MS-DOS is a kludge
at best.  The existance of the print.com proves that it is possible for
one to construct these applications.  Print relies on some undocumented
DOS services.  The bottom line is that you must have a thurough understanding
of DOS internals before attempting to write a TSR which needs to make
any int 21h requests.  I would strongly recommend reading chapter 4 of
the MS-DOS Developers Guide 2nd Edition (Howard Sams,  1988).  Please note,
although I am the author of this chapter,  I get no royalties;  I'm just
citing a known good reference.

I'll briefly describe the problem that you are experiencing.  MS-DOS
stores the address of the Job File Table (JFT) in the program segment
prefix.  (In most cases the JFT itself is also in the PSP,  but there
is no guarantee).  The file handle is an index into the JFT.  JFT
entries are System File Numbers (SFNs) which are indicies into the System
File Table (SFT).  Each SFT entry maintains file position and instructs
DOS how to find the correct device driver.  When you make a handle i/o
request,  DOS uses the current PSP to find the correct SFT entry.  If
your program is in the foreground,  DOS will not have any trouble.  If
a TSR asnychronously decides that it wants to make a handle request,
it must ask DOS to change the current PSP. (current PSP is a global variable
maintained by MS-DOS).  (Int 21H AH=50, BX=PSP changes the current PSP.
This function is undocumented).  There is a certain amount of houskeeping
one must do before changing the PSP including recording the previous PSP and
establishing your own break handler.  You should set up a critical error
handler before performing any io.

Now another observation.  If you are writting a communications application,
you cannot always rely on trapping int 21h requests.  Your internal buffer
may fill up while then foreground program is compute bound or waiting for
console I/O.  You should be using int 28H and a timer interrupt as well.
You must be particularly careful of the timer interrupt.

Walt Dixon		{arpa:		dixon@ge-crd.arpa	}
			{us mail:	ge crd			}
			{		po box 8		}
			{		schenectady,  ny 12345	}
			{voice:		518-387-5798		}

dean@sun.soe.clarkson.edu (Dean Swan) (02/21/89)

From article <1872@holos0.UUCP>, by lbr@holos0.UUCP (Len Reed):
> I have an MS-DOS terminate-and-stay resident program that must be able
> to open/close/read/write hard disk files.  It appears that the "file
> handles" move on me.  That is, if this routine opens a file, as the
> foreground processes spawn and die, I lose my handles since they
> really belong to the foreground processes.
> 
> BTW--the application is a communications package that sits in the background
> and steals time to transfer/receive files to/from a mainframe.


I have written more TSR's that do DOS I/O than I'd like to admit.  Anyway,
your problem is VERY SIMPLE to solve.  Ok, DOS I/O from TSR's lesson #1:

   1) When doing DOS I/O from TSR's you have to make sure that your routine
      didn't get called while the foreground process was doing any I/O of
      it's own.  MS-DOS is not re-entrant, and neither is the BIOS, so you have
      to hook up to a lot more interrupts than you might think just to be
      able to safely do DOS I/O.

   2) INTERRUPTS TO LINK TO:

      int 1Ch		timer interrupt - use it to activate your program
      int 13h		BIOS disk handler
                        You basically have to link a piece of code in that
                        sets a BIOS_busy flag, then calls the bios function,
                        then clears your flag.  This flag is then used by
                        your ISR tied to the timer interrupt to tell if its
                        safe to call the BIOS disk handler.  If your flag is
                        set when the timer ISR is called it isn't safe.
      int 28h		This is the DOS special interrupt.  It gets called
                        only when nothing else is going on.  Whenever an
                        ISR linked to this interrupt is called it is always
                        safe to do DOS I/O  (and BIOS I/O too.)
      int 24h           DOS critical error handler - hook up to this so
                        you can process DOS errors.
      int 1Bh           break - so you don't get nuked by break interrupts

      If you want to do a "Hot Key" thing then you also have to grab int09h.

   3) Always check the DOS Busy flag when you enter an ISR that's going to
      do I/O.  Int 21h, funtion 34h returns a far pointer to the busy flag
      in ES:BX.  The flag is a byte value and will be 0 if DOS isn't busy.
      If this flag is non-zero then just return and wait for the next time
      you get called. 

   4) Always reset the 8259, or it will get tempermental and refuse to
      generate any more interrupts.  This is easy.  Just do an

                    MOV		AL,20h
                    OUT         20h, AL

      or equivalent code at the beginning of your timer ISR and everything
      will be ok.

   5) If your OPEN a file handle in an ISR,

          BE SURE TO CLOSE THE HANDLE BEFORE PASSING CONTROL BACK TO
          THE FOREGROUND PROCESS.

      Basically, never leave a file open when you leave your ISR.  If you
      were writing to a file and need to add more later, re-open it and 
      append to it, instead of trying to leave it open.

      If you're reading from a file, just remember the current position in
      the file, close the file, and exit your ISR.  The next time your ISR
      gets called, simply re-open the file and seek to the position you
      remembered from last time and pick up where you left off.

   6) ONE LAST THING:
      Keep a busy flag for each of your own ISR's because an interrupt can 
      ocurr while you are servicing one and re-call your ISR.  Then check
      the flag whenever you enter your own ISR's. If its set then do and IRET
      right away.  This prevents recursive ISR calls, which can make a
      debugger's worst nightmare, not to mention lock up your machine.

  Well, I guess that's it.  I write all my TSR's in Turbo C, and use assembly
  where appropriate.  There's not much point to doing everything in assembly
  anymore, so why make life difficult.  The only thing you lose is that in
  C, the installation code remains resident, but it's usually only a few
  hundred bytes, so who cares.  I'd be happy to talk with anybody on this
  subject, as well as any of a million other subjects (which I'll refrain
  from listing here).  Send E-mail or Snail-Mail, or if you really need to,
  call me at the office.

  Dean Swan
  Korex Software Corporation
  Building 3, Unit 5 Seacomm Plaza
  PO Box 688
  Potsdam, NY 13676
  (315) 265-1203

  dean@sun.soe.clarkson.edu

dixon@sagittarius.steinmetz (walt dixon) (02/21/89)

From article <2506@sun.soe.clarkson.edu>, by dean@sun.soe.clarkson.edu
(Dean Swan)
>From article <1872@holos0.UUCP>, by lbr@holos0.UUCP (Len Reed):
>> I have an MS-DOS terminate-and-stay resident program that must be able
>> to open/close/read/write hard disk files.

...(remainder of quoted message deleted)

>I have written more TSR's that do DOS I/O than I'd like to admit.  Anyway,
>your problem is VERY SIMPLE to solve.  Ok, DOS I/O from TSR's lesson #1:
>
>   1) When doing DOS I/O from TSR's you have to make sure that your routine
>      didn't get called while the foreground process was doing any I/O of
>      it's own.  MS-DOS is not re-entrant, and neither is the BIOS, so you have
>      to hook up to a lot more interrupts than you might think just to be
>      able to safely do DOS I/O.

This is mostly true.  Portions of MS-DOS were retrofitted to provide limited
reentrancy.  The two main sources of MS-DOS (IBMDOS) non-reentrancy are stack
switching within the int 21h dispatcher and the use of static variables.  The
int 21h dispatcher works with 3 stacks -- auxilliary,  user,  and disk.  A very
limited number of requests (ah = 33h,50h,51h,62h) are serviced with no stack
switch.  Other requests with ah > 0ch are  finally serviced on the disk stack.
Requests corresponding to ah=1 to 0ch are serviced on the user stack. A request
on the user stack can be preempted by an int 21h request ah > 0ch.  DOS provides
the int 28h hook so a tsr can tell if it is safe to issue an int 21h request.
There is also limited reentrency to support critical error processing.


>   2) INTERRUPTS TO LINK TO:
>
>      int 1Ch           timer interrupt - use it to activate your program
>      int 13h           BIOS disk handler
>                        You basically have to link a piece of code in that
>                        sets a BIOS_busy flag, then calls the bios function,
...
Add int 25h and int 26h to this list.  The int 25h and int 26h routines switch
stacks and touch DOS global variables.  These opereations occur with interrupts
disabled.  So long as the device driver does not mess with interrupts,  you
can safely ignore these interrupts.  I would not recommend taking this chance.
PRINT.COM,  which should be regarded as a model TSR hooks int 25h and 26h as well
as int 13h.

 >     int 28h           This is the DOS special interrupt.  It gets called
 >                       only when nothing else is going on.  Whenever an
 >                       ISR linked to this interrupt is called it is always
 >                       safe to do DOS I/O  (and BIOS I/O too.)

 Only certain io operations are safe.  DOS executes an int 28h instruction
 when time consuming operations (eg keyboard poll) are pending on the user
 i/o stack.  At this time it is safe to issue int 21h requests which are
 serviced on the disk stack (ah > 0ch) or don't do any stack switching.
 
>	  int 24h           DOS critical error handler - hook up to this so
>                        you can process DOS errors.
An absolute necessity.

>      int 1Bh           break - so you don't get nuked by break interrupts

Probably not a bad idea,  but grabbing int 23h (as PRINT.COM does) is sufficient.
The int 9h ISR issues an int 1bh when the break key is pressed.  It is standard
pactice for the console driver do set up an int 1bH ISR.  The console driver in
turn executes an int 23h.

>      If you want to do a "Hot Key" thing then you also have to grab int09h.

>   3) Always check the DOS Busy flag when you enter an ISR that's going to
>      do I/O.  Int 21h, funtion 34h returns a far pointer to the busy flag
>      in ES:BX.  The flag is a byte value and will be 0 if DOS isn't busy.
>      If this flag is non-zero then just return and wait for the next time
>      you get called.

You must check both the DOS busy flag *AND* the critical error flag.  In some
versions of DOS,  the critical error flag is the byte before the busy flag.
When DOS detects a critical error,  it increments the critical error flag and
decrements the busy flag.  Checking the busy flag from within an int 28h ISR
defeats the purpose of this hook.  The busy flag is always set when DOS issues
an int 28h.  DOS will not issue an int 28h during critical error processing.

>   4) Always reset the 8259, or it will get tempermental and refuse to
>      generate any more interrupts.  This is easy.  Just do an
>
>                    MOV         AL,20h
>                    OUT         20h, AL
>
>      or equivalent code at the beginning of your timer ISR and everything
>      will be ok.

This issues an non-specific EOI to the 8259.  The DOS timer ISR (int 8h) does
its internal processing (update time of day,  etc) and then executes an int 1ch.
The int 08h ISR does not send its EOI to the 8259 until *AFTER* the int 1ch ISR
executes.  Since the timer ISR is the highest priority ISR,  no interrupts will
be reconginzed until the int 08h interrupt is dismissed (by sending an EOI).

>   5) If your OPEN a file handle in an ISR,
>
>          BE SURE TO CLOSE THE HANDLE BEFORE PASSING CONTROL BACK TO
>          THE FOREGROUND PROCESS.
>
>      Basically, never leave a file open when you leave your ISR.  If you
>      were writing to a file and need to add more later, re-open it and
... (Text Deleted)
Unless you switch PSPs (undocumented int 21h ah=50h),  you are messing with
the PSP of the foreground program.  There is no guarantee that there will
be space in the JFT to open a file.  There is also a small risk that some
foreground program will mess with the file.  The correct procedure for doing
background i/o is
    1.  Determine if it is safe to run (check busy and critical error flags as
        appropriate).  Make sure int 13h, 25h, 26h not in progress.
	1a. You may want to look at the 8250 UART to see if an interrupt is pending.
	    (PRINT.COM does this).  If you block service of UART interrupts for too
		long,  you could loose characters.
	2.  Record the current PSP (int 21h ah=51h or 62h).
	3.  *RECORD THE CURRENT DTA* (DOS uses the DTA in non-intuative ways). 
	4.  Set up your own critical break and critical error handlers. The default
	    action is to terminate the *CURRENT* program.  Before altering any
		variables,  you must protect your TSR against termination.  It is also
		poor form to let an error caused by your TSR terminate another program.
	5.  Change the current PSP to that of your TSR (int 21h ah=50h)
    6.  *CHANGE THE DTA* to a local value.
	7.  *SWITCH TO YOUR OWN STACK*.  There is no guarantee of stack avaibalility.
    8.  Undo these actions when your TSR is done.
	
... (Remainder of article deleted)

NB you probably don't want to do anything time consuming from within an int 28h
ISR.  There are no technical reasons for this caveat,  but the user may notice
substantial response degredation if you are not careful.

If you are implementing a hot key,  you may need to save and restore screen contents
at appropriate times.  You should be careful about preserving adapter mode.  Some
adapters (MDA/CGA) are very touchy.  Loading the wrong value in a key register
(scan rate) can fry your hardware.

If you must deal with a character device,  you may want to record the address
of its driver and pass request headers directly to the device.  PRINT.COM uses
this technique to avoid reentrancy problems and utlize a write until busy request
which DOS does not know how to handle.

You can find out more about writing TSRs in Chapter 4 of the MS DOS Developer's
Guide,  2nd Edition.  (Howard Sams,  1988) I am the author of that chapter.  I don't
get any royalties;  I'm just citing a good reference.

Sorry about the length of this reply.  I felt that the technical issues raised in
the quoted article needed further clarification.

Walt Dixon			{ARPA:		dixon@ge-crd.arpa		}
					{US MAIL:	ge crd					}
					{			po box 8				}
					{			schenectady,  NY 12345	}
					{VOICE:		518-387-5798			}
					
					
Standard disclaimer's apply.

vlruo02@dutrun.UUCP (Ge van Geldorp) (03/01/89)

In article <13211@steinmetz.ge.com> dixon@sagittarius.steinmetz.ge.com (walt dixon) writes:
>... The
>int 21h dispatcher works with 3 stacks -- auxilliary,  user,  and disk.  A very
>limited number of requests (ah = 33h,50h,51h,62h) are serviced with no stack
>switch.  Other requests with ah > 0ch are  finally serviced on the disk stack.
>Requests corresponding to ah=1 to 0ch are serviced on the user stack. A request
>on the user stack can be preempted by an int 21h request ah > 0ch.

Question: when is the auxilliary stack used?

>PRINT.COM,  which should be regarded as a model TSR hooks int 25h and 26h as
>well as int 13h.

Are you sure about this? The 25h and 26h interrupt vectors remain
unchanged when I run PRINT.COM (using PC-DOS 3.30). 


Ge van Geldorp.