;	SCCSID = @(#)dosasync.asm	6.3 91/09/30
;      SCCSID = @(#)dosasync.asm	6.3 91/09/30
;
;  This file contains the code for DOSREADASYNC and DOSWRITEASYNC.
;  Both are implemented in ring three code.  The basic algorithm
;  is simple:
;
;	Caller				New Thread
;
;	Copy arguments to data area
;	Get a stack for new thread
;	Create new thread
;	Return				Copy arguments from data area
;					Do I/0
;					Return stack
;					Commit suicide
;
;  There is no magic here.  There is nothing that the caller could not
;  do itself.  These calls are more of a convience to help people.
;
;  Changes:
;    11/9/94 - James Summers - Correct the MMPH hooks so the Pre hooks
;			       store the Post hook's major/minor in a valid
;			       place.  The Pre hooks were moved so the Post
;			       major/minor can be pushed on the stack.
;
;  Thread Creation:
;
;	Every call we get we create and destory a thread.  The first
;	time we do this it will be very slow.  Subsequent times will
;	be much quicker.  One thought was not to destroy threads at
;	the end of the call.  The next async call would then be quicker.
;	We decided not to do this for three reasons.  First a thread takes
;	a lot of resources.  Second, due to the way thread work, the second
;	creation will be a lot quicker.  Third, if the caller is really
;	concerned about preformance it will can create it own threads and
;	not use the async calls.
;
;	If we cannot create a thread we use the caller's to proform
;	the I/O.  In this case we have synchronous reads and write.
;
;
;  Stack Management
;
;	Each thread we create needs a stack.  To get this stack we
;	allocate it using DOSALLOCSEG.	When the thread is done
;	we do not free the segment.  It was felt forcing an alloc
;	on ever call was too much overhead.  The first call will
;	be slow because of the slow create thread.  The second and
;	subsequent calls to create thread will be fast.  We did not
;	want to slow this down allocating the stack.  If the alloc
;	fails we do the synchronous I/O like when the create thread fails.
;
;	Free stacks are kept in a queue.  Before allocating a stack
;	we check if there are any free ones.  If not, we allocate one
;	upto a set maximum.  After that we do synchronous I/O.
;
;  Major Data structure:
;
;	We uses an instance data segment for each process calling
;	the async calls.  The segment contains a data area for
;	holding the call arguments.  This data area is protected
;	with the "argSem" semaphore.  The segment also contains
;	the free stack queue and the associated pointer.  This is
;	protected by the "datsem" semaphore.
;

	.286p
	page	,132
	TITLE	AsyncIO - Asynchronous Read/Write


CODE	 SEGMENT						; DCR 704
CODE	 ENDS

DATA	 SEGMENT						; DCR 704
DATA	 ENDS

.xlist
include basemaca.inc
include infoseg.inc
include dc1segs.inc						; DCR 704
include dc1mmpha.inc

	.LFCOND 			; Show false conditionals
	.SALL				; Supress macro expansion
.list

ASSUME	CS:CODE

	.lall

	EXTRN	DOSALLOCSEG:FAR
	EXTRN	DOSCREATETHREAD:FAR
	EXTRN	DOSSEMCLEAR:FAR
	EXTRN	DOSEXIT:FAR
	EXTRN	DOSSEMREQUEST:FAR
	EXTRN	DOSSEMWAIT:FAR
	EXTRN	DOSREAD:FAR
	EXTRN	DOSSLEEP:FAR
	EXTRN	DOSWRITE:FAR
	EXTRN	DOSSEMSET:FAR


DATA	  SEGMENT


argSem	DD	0		; semaphore protects arglist

arglist EQU	$
a_brad	DD	0		; "bytes read" address
a_blen	DW	0		; buffer length
a_badr	DD	0		; buffer address
a_radr	DD	0		; return code addr
a_sadr	DD	0		; ramsem address
a_hand	DW	0		; file handle
ARGLEN	=	$-arglist


;
; Stack constants and data structures
;
; Each Asynchronous operation needs a stack and a thread.  As calls come
; in we allocate a stack then create a thread.	Before killing the
; thread we save away the stack.  The selectors for the stacks are
; kept in a queue structure.  Elements are added to the rear of the
; stack and removed from the front end to the stack.  The structure
; and algorithms for stack manipulation come from Knuth Vol 1, page 241
; Equations 6a and 7a.	I have tired to keep the same terminalogy as
; Knuth.
;
; Here is a picture of the queue with 5 selectors in it (E - empty
; element, S - selector in queue):
;
;		      Front - remove items	   Rear - add items
;			      here			  here
;
;		      fStk			   rStk
;		       |			     |
;		       V			     V
;	-------------------------------------------------------
;	|     |     |	  |	|     |     |	  |	|     |
;	|  E  |  E  |  E  |  S	|  S  |  S  |  S  |  S	|  E  |
;	|     |     |	  |	|     |     |	  |	|     |
;	-------------------------------------------------------
;
; fStk points to the to an empty space.  rStk points to the last
; element in queue.  The pointer advance to the right then wrap.
; The queue is empty when fStk == rStk.  The array for the queue
; has MAXSTK + 1 elements but can only hold MAXSTK selector.  This
; simplifies the queue manipulation algorithms.  The queue is
; manipulated by GetStk and RtnStk ONLY.  numStk holds the number
; of stacks we have allocated.	When a stack is given to a thread
; it is removed from the queue.  The number of allocated stacks
; will not, generally, equal the number stack in the queue.
;
;	BUGBUG - IBM's KBD/VIO calls use WAY too much stack space.  We use
;	a ridiculously large number to appease the stack gods.

MAXSTK	EQU	8		; maximum number of stack we will allocate
STKSIZ	EQU	2048		; 2 Kbytes of stack space

datsem	DD	0		; semaphore to protect async stack structures

fStk	DW	stkSrt		; pointer to front element of queue
rStk	DW	stkSrt		; pointer to rear element of queue


stkSrt	DW	MAXSTK DUP (0)	; the queue of stack selectors
stkEnd	DW	0		; queue has one more space than max elements

numStk	DB	0		; number of stack we have allocated

;    Here is some sleazy magic.  Each thread created needs to return it's
;    stack and call DOSEXIT.  The call to DOSEXIT requires a stack.
;    What the thread does is return the stack to the queue then
;    switch to the tkstack below.  From this stack it does the DOSEXIT.
;    Since DOSEXIT is a kernel call we never push anything on this
;    stack.  The call gate copies the arguments to the kernel stack.
;    thstack is already set up with the arguments to DOSEXIT - 0,0.
;

tkstack DW	3 dup (0)			; special thread kill stack


;    The following data item is used for Performance Builds (MMIOPH). Since
;    ReadAsync and WriteAsync shared a common exit point, they also share a
;    common post hook. Thus the pre hook will store in the following data
;    item, the major/minor code to be used by the post hook.

;ifdef MMIOPH			removed on 11/9/94 by JHS
;mmphcode  DW	 0		removed on 11/9/94 by JHS
;endif				removed on 11/9/94 by JHS

DATA	  ENDS


CODE	    SEGMENT

;*	Segment # of instance data segment
;
;	There is a separate segment for each process using this library
;	package; but each has the same segment descriptor.
;
DSinst	DW	DATA



;**	DOSREADASYNC - Process Asynchronous Read
;
;	DOSREADASYNC processes the DOSREADASYNC system call.  This
;	call is not implemented in the kernel code (i.e., resident
;	ring 0 code) but is a dynlink library routine.	It creates
;	a thread and uses it to execute a synchronous DosRead call.
;	The parent thread meanwhile is released to the caller.
;
;	If a thread or stack is not available then DOSREADASYNC performs
;	synchronously.
;
;	ENTRY	PUSH  WORD   FileHandle   ; File handle
;		PUSH@ DWORD  RamSemaphore ; Address of Ram semaphore
;		PUSH@ WORD   ReturnCode   ; Address of I/O error RC
;		PUSH@ OTHER  BufferArea   ; Address of user buffer
;		PUSH  WORD   BufferLength ; Buffer length
;		PUSH@ WORD   BytesRead	  ; Bytes read
;		CALL  DOSREADASYNC
;	EXIT	returns "immediately"
;	USES	AX, Flags
;
PUBLIC DOSREADASYNC
DOSREADASYNC PROC FAR
ALIGN 4

;***DT	preDOSREADASYNC - DosReadAsync pre-invocation trace point
;
;    TRACE MINOR = <RAS_DC1_PRE_DOSREADASYNC>,
;	   TP	 = .DOSREADASYNC,
;	   TYPE  = (PRE,API),
;	   GROUP = FS,
;	   DESC  = "(OS) DosReadAsync Pre-Invocation",
;	   MEM	 = (<R3SP_ARGS>+<C_PARM4>+2,D,4),
;	   MEM	 = (<R3SP_ARGS>+<C_PARM5>,D,2),
;	   FMT	 = "Sem at = <FMT_C_P16>  Handle = <FMT_C_WORD>"

; (JHS) MMPHPreDOSReadAsync	; Performance Hook (300Ch)

	push	ds		; save caller's registers
	push	es
	pusha

			; The hook was moved to here on 11/9/94 by JHS
	MMPHPreDOSReadAsync	; Performance Hook (300Ch)

	sub	bp,bp		; flag a read function
	jmp	short dwa1	; join common code




;**	DOSWRITEASYNC - Process Asynchronous Write
;
;	DOSWRITEASYNC processes the DOSWRITEASYNC system call.	This
;	call is not implemented in the kernel code (i.e., resident
;	ring 0 code) but is a dynlink library routine.	It creates
;	a thread and uses it to execute a synchronous DosWrite call.
;	The parent thread meanwhile is released to the caller.
;
;	If a thread or stack is not available then DOSWRITEASYNC performs
;	synchronously.
;
;	ENTRY	PUSH  WORD   FileHandle   ; File handle
;		PUSH@ DWORD  RamSemaphore ; Address of Ram semaphore
;		PUSH@ WORD   ReturnCode   ; Address of I/O error RC
;		PUSH@ OTHER  BufferArea   ; Address of user buffer
;		PUSH  WORD   BufferLength ; Buffer length
;		PUSH@ WORD   BytesWritten ; Bytes written
;		CALL  DOSWRITEASYNC
;	EXIT	returns "immediately"
;	USES	AX, Flags
;
PUBLIC DOSWRITEASYNC
DOSWRITEASYNC PROC FAR
ALIGN 4

;***DT	preDOSWRITEASYNC - DosWriteAsync pre-invocation trace point
;
;    TRACE MINOR = <RAS_DC1_PRE_DOSWRITEASYNC>,
;	   TP	 = .DOSWRITEASYNC,
;	   TYPE  = (PRE,API),
;	   GROUP = FS,
;	   DESC  = "(OS) DosWriteAsync Pre-Invocation",
;	   MEM	 = (<R3SP_ARGS>+<C_PARM4>+2,D,4),
;	   MEM	 = (<R3SP_ARGS>+<C_PARM5>,D,2),
;	   FMT	 = "Sem at = <FMT_C_P16>  Handle = <FMT_C_WORD>"

; (JHS) MMPHPreDOSWriteAsync	; Performance Hook (3013h)

	push	ds		; save caller's registers
	push	es
	pusha

			; The hook was moved to here on 11/9/94 by JHS
	MMPHPreDOSWriteAsync	; Performance Hook (3013h)

	mov	bp,cs		; flag write operation

;**	DOSREADASYNC and DOSWRITEASYNC are identical except for the actual
;**	Dos call.  They share common code from here on out.
;**
;**	(BP) = 0 if read
;**	    != 0 if write

dwa1:
	mov	DS,DSinst	; BUGBUG?? Hand waving done here??
	ASSUME	DS:DATA
	push	ds
	pop	es

;	first claim the argument buffer area

	push	DS
	push	offset data:argSem
	push	-1		; wait forever
	push	-1
	call	DOSSEMREQUEST	; DOSSEMREQUEST (argSem, -1)
	mov	ax,ds
	push	ss
	pop	ds		; address ss for movsw
	ASSUME	DS:nothing

;	copy the arguments into the argument structure

	mov	si,sp
ifdef MMIOPH
; Since I've added a word to the stack, I've gotta adjust this pointer. (JHS)
	add	si,16+4+4+2	; (ss:si) = address of arg list (JHS)
else
	add	si,16+4+4	; (ss:si) = address of arg list
endif
	mov	cx,arglen/2
	cld
	mov	di,OFFSET data:arglist
	rep movsw		; get all the arguments from the parent
	mov	ds,ax
	mov	di,bp		; return type of call to di
	ASSUME	DS:DATA
	call	CreaThread	; allocate a thread
	jnc	dwa9		; Child was created. Let it handle I/O.

;
;	We're the parent thread if we can't get more threads. Do the
;	work synchronously.
;
	CALL	DoIORequest	; Perform the I/O request

;	we're the parent thread.  Restore registers and return

dwa9:	MMPHPostDOSReadWriteAsync	; Performance Hooks (308C & 3093)

	popa
	pop	es
	pop	ds
	xor	ax,ax

	ret	arglen

DOSWRITEASYNC ENDP
DOSREADASYNC ENDP


;*****	DoIORequest - Perform the I/O
;
;	DoIORequest performs the READ or WRITE operation.
;
;	ENTRY	DI = 0 if READ, non-zero if WRITE
;

DoIORequest	PROC
ALIGN 4

	ASSUME	ds:DATA
	mov	bp,di		; Get function code
	push	a_hand		; push arguments for Dos Fcn
	push	WORD PTR a_badr+2
	push	WORD PTR a_badr
	push	a_blen
	push	WORD PTR a_brad+2
	push	WORD PTR a_brad

;	we've pushed all the args for the DosFcn... we need to load
;	the remaining Dos?????Async args into registers so that we can
;	free up the arglist area before we do Dos Fcn.	If we don't
;	then we'll serialize all async I/O functions...

	mov	cx,WORD PTR a_sadr
	mov	dx,WORD PTR a_sadr+2	; (DX:CX) = semaphore address
	les	di,a_radr	; (ES:DI) = return code address
	push	ds
	push	offset data:argSem
	call	DOSSEMCLEAR	; clear the arglist semaphore
	and	bp,bp		; check the function subcode
	jz	dwa5
	call	DOSWRITE	; do the write function

;***DT	postDOSWRITEASYNC - DosWriteAsync post-invocation trace point
;
;    TRACE MINOR = <RAS_DC1_POST_DOSWRITEASYNC>,
;	   TP	 = .postDOSWRITEASYNC,
;	   TYPE  = (POST,API),
;	   GROUP = FS,
;	   DESC  = "(OS) DosWriteAsync Post-Invocation",
;	   MEM	 = (RSS+SP+(0xfff4),I,2),	/* Use stack garbage ! */
;	   REGS  = (AX),
;	   FMT	 = "Bytes Written = <FMT_C_WORD>  Return Code = <FMT_WORD>"

	PUBLIC	postDOSWRITEASYNC
postDOSWRITEASYNC:			; RAS post-invocation trace

	jmp	short dwa6

dwa5:
	call	DOSREAD

;***DT	postDOSREADASYNC - DosReadAsync post-invocation trace point
;
;    TRACE MINOR = <RAS_DC1_POST_DOSREADASYNC>,
;	   TP	 = .postDOSREADASYNC,
;	   TYPE  = (POST,API),
;	   GROUP = FS,
;	   DESC  = "(OS) DosReadAsync Post-Invocation",
;	   MEM	 = (RSS+SP+(0xfff4),I,2),	/* Use stack garbage ! */
;	   REGS  = (AX),
;	   FMT	 = "Bytes Read = <FMT_C_WORD>  Return Code = <FMT_WORD>"

	PUBLIC	postDOSREADASYNC
postDOSREADASYNC:			; RAS post-invocation trace

dwa6:
;	MMIOPH Performance Hook Removed
;	Maj/Min = (if bp==0), 308C else 3093.
;	Data = word at 16:16 address at SS:[SP+12], ax (bytes read, rc)
;	Note: Old code was wrong here:	[a_brad] is invalid after
;	DOSSEMCLEAR(data:argSem)

	stosw			; store the result code
	push	dx
	push	cx		; push address of ram sem
	call	DOSSEMCLEAR	; wake up the client
	RET
DoIORequest	ENDP


;**	CreaThread - Allocate a stack and create a thread
;
;	CreaThread is called to create a child thread to do the
;	asynchronous work.  It first finds a free stack and then
;	creates a thread.
;
;	Is the following paragraph relevant anymore??
;
;	NOTE that the child thread is executing on a very small stack -
;	only STKSIZ bytes.  The caller must count its stack requirements
;	very carefully to make sure that the stack size is not exceeded.
;	Signals can make an asynchronous demand upon stack size, but
;	since the child thread is not thread #1, unless the child thread
;	invokes a trap signal (FPE, Address Violation, etc.) it will
;	not receive any signals.
;
;	ENTRY	(DS) addresses DATA
;	EXIT	'C' set if error
;		'C' clear if OK
;	USES	AX, BX, SI, DS, ES, Flags
;
CreaThread PROC NEAR		; must be NEAR cause of thread returns
ALIGN 4
	ASSUME	ds:DATA

;	Claim the DATA	   semaphore

	call	ldatsem 	; get the stack semaphore

;	see if there's a stack we can use.

	call	GetStk
	jc	gthr8		; could not get stack - do synchronous I/O

;	We have a stack.  Allocate our child thread
;
;	DatSem locked
;	(AX) = selector of stack
;	(SI) = contents of stkhead (and non-zero)
;
;	We have a free stack, but its possible that although the stack
;	was in the free queue that its not truely vacated.  We'll loop
;	here until the stack is vacated.  This is a rare situation since
;	used stacks go on the end, so we'd have to be out of or just about
;	out of stacks for this to come up.  See ThreadProc for more info.
;

	mov	es,ax
gthr5:	test	BYTE PTR ES:[0],0FFh		; check use flag
	jz	gthr6		; it's truly free
	push	0		; it's not free
	push	0
	call	DOSSLEEP	; yield a slice
	jmp	gthr5

;	Stack is ours.	Its off the queue and certified unused

gthr6:	call	udatsem 	; unlock datsem lock
	push	cs
	push	offset CODE    :ThreadProc	; Address of routine to call by thread
	mov	ax,sp
	push	ss
	push	ax		; ThreadIDWord is on stack
	push	es
	push	STKSIZ-2	; push address of child thread stack
; BUGBUG
; The following instruction is necessary because DOSCREATETHREAD does not
; pass on the parent's BP to the child process!!!
	mov	di,bp		; DI=0 if READ, non-zero if WRITE
	call	DOSCREATETHREAD
	and	ax,ax
	jz	gthr10
;
;	the child thread failed for some reason.  Put the stack back on
;	the free chain and return to caller
;
	call	ldatsem 	; lock datsem lock
	mov	ax,es		; put stack selector in ax
	call	RtnStk		; return stack
gthr8:	call	udatsem 	; unlock datsem lock
	stc
gthr10:
	RET

CreaThread ENDP



;**	ThreadProc - Do the I/O, deallocate the stack, and destroy the thread.
;
;	The newly created thread jumps to ThreadProc.  It must
;	do the I/O.  Then it returns the stack and commits suicide.
;
;	ENTRY	When the thread is created in jumps to here.
;		DS addresses datsem
;	EXIT	doesn't return
;	USES	N.A.
;
ThreadProc PROC FAR
ALIGN 4

	CALL	DoIORequest	; Perform the I/O request

;	Claim the queue manipulation semaphore

	call	ldatsem 	; lock datsem lock

;	put our stack in the free queue.  Nobody can start using it yet
;	cause we've still got the segment in use
;
;	This is tricky because we have to unlock the semaphore AFTER
;	we've put our stack on the free queue, but we'll still be using it.
;	What we do is return the stack to the end of the free queue, so no
;	body will probably be wanting to use it, and we set a flag in the lowest
;	byte of the stack to indicate that its still in use, even if on
;	the free list.	We clear this flag immediately before we exit the
;	thread.

	mov	ax, ss			; put selector where RtnStk expects
	call	RtnStk			; return stack

	mov	BYTE PTR SS:[0],0FFh	; mark stack still in use
	call	udatsem 		; unlock datsem lock

;	We've marked the stack available and we're ready to clear our
;	special holdoff flag.  We can't clear that and then do the
;	"call DosExit" since that last call (and its arguments) will be
;	still using the stack!	What we do is move to a special stack
;	invented just for the purpose of calling DosExit.  There isn't
;	a mutual exclusion semaphore on that stack, so its possible that
;	multiple threads are using it.	 Since they're all executing just
;	this code and they're all using the same arguments won't be any
;	damage if two or more threads should be pushing on this stack
;	simultaneoulsy.

	mov	BYTE PTR SS:[0],0	; we are done with stack

;	Change to the special tkstack. We need a stack to do the
;	DOSEXIT call.  DOSEXIT is a kernel call.  The call copies
;	the stack arguments in the call gate.  The ring 3 stack is
;	really read only.  Therefore we can share the stack for
;	all the threads.  Arguments are already pushed on tkstack.

	mov	ax,ds
	mov	ss,ax			; move to special private stack
	mov	sp,OFFSET data:tkstack	; set to special thread kill stack
	call	DOSEXIT

ThreadProc ENDP


;**	RtnStk - Return stack to free queue
;
;	RtnStk puts a stack onto the end of the free queue.
;
;	ENTRY	(AX) = selector of stack
;		DS = DATA
;		Must own datsem
;	EXIT	none
;	USES	SI, Flags
;
;	NOTE:	There is a two line consistancy check commented out.
;		The check makes sure the queue is over full.  We do
;		not need this check because GetStk will not allocate
;		enough stack to fill the stack.
;

RtnStk	PROC	NEAR
ALIGN 4
	ASSUME ds:DATA

	mov	si, rStk
	cmp	si, OFFSET data:stkEnd
	jb	rs2
	mov	si, (OFFSET data:stkSrt) - 2 ; wrap to beginning of queue
rs2:	add	si, 2			; point to next element
;	cmp	si, fStk		; consistancy check
;	je	???			; jump to internal error
	mov	[si],ax 		; put selector in queue
	mov	rStk,si 		; update rear pointer
	ret


RtnStk	ENDP


;**	GetStk - get a stack
;
;	GetStk get a selector for a stack off the queue.  If the
;	queue is empty we allocate a stack.  If the allocation
;	fails or we have already allocated MAXSTK stacks we
;	return an error.  The operation will be done synchronously.
;
;	ENTRY	DS = DATA
;		must own datsem
;	EXIT	Carry clear
;			(AX) = Selector of stack
;		Carry set
;			Unable to get new stack
;	USES	AX, SI, Flags
;
GetStk	PROC	NEAR
ALIGN 4
	ASSUME ds:DATA

	mov	si,fStk 		; (si) = front of queue
	cmp	si,rStk 		; is queue empty?
	je	gs4			; yes, alloc another stack
	cmp	si,OFFSET data:stkEnd
	jb	gs2
	mov	si,(OFFSET data:stkSrt) - 2
gs2:	add	si,2			; point to next element
	mov	ax,[si] 		; get selector
	mov	fStk, si		; update front pointer
	jmp	short gs8		; carry is clear

;	No selectors in the queue.  If we have allocated more than
;	the maximum return an error.  Otherwise allocate another stack.

gs4:	cmp	BYTE PTR numStk, MAXSTK ; allocated maximum?
	jae	gs6			; yes, return error
	push	ax			; return argument
	mov	ax,sp
	push	STKSIZ			; size of block to allocate
	push	ss			; pointer to return argument
	push	ax
	push	0			; not shared selector
	call	DOSALLOCSEG
	or	ax,ax			; error?
	pop	ax			; get selector
	jnz	gs6			; error
	inc	BYTE PTR numStk 	; record stack allocated.
	mov	es,ax
	mov	BYTE PTR ES:[0],0h	; new stack, clear use flag
	jmp	short gs8		; carry is clear

gs6:	stc
gs8:	ret

GetStk	ENDP


;**	ldatsem - lock datsem semaphore
;
;	ENTRY	DS addresses the data segment
;	EXIT	none
;	USES	ax, flags
;
;	NOTE:	We assume the DOSSEMREQUEST will not return error.
;		Is this safe??

ldatsem PROC	NEAR
ALIGN 4
	push	DS
	push	offset data:datsem	; set datsem
	push	-1			; infinite timeout
	push	-1
	call	DOSSEMREQUEST		; DOSSEMREQUEST (datsem, -1)
	ret

ldatsem ENDP


;**	udatsem - unlock datsem semaphore
;
;	ENTRY	DS addresses the data segment
;	EXIT	none
;	USES	ax, flags
;
;	NOTE:	We assume the DOSSEMCLEAR will not return error.
;		Is this safe??

udatsem PROC	NEAR
ALIGN 4
	push	DS
	push	offset data:datsem
	call	DOSSEMCLEAR	; release lock on data segment
	ret

udatsem ENDP

CODE	    ENDS

END
