**************************************************************************** * * * Pascal Multi Process Unit written by Dieter Pawelczak * * * * Instruction Manual * * * * (c) 1995,1996 by Dieter Pawelczak, Fasanenweg 41, 85540 Haar, Germany * * * * Release Process Unit Version 3.1 * * * **************************************************************************** 1. Introduction 2. The create_process function and the tasklist 3. Delay_task, kill_task, kill_process procedure 4. Set_Delay, Task_priority, End_priority, Wait_Dos, OK_Dos 5. Multiprogramming and the use of signals, the run_multitasking flag 6. Multiprogramming with exec_exe and create_exe 7. Multiprogramming and process communication, the use of stop_process and continue_process, send_message and read_message 8. The Break Function 9. The Task Timer : Set_Thread_Time A. Appendix - The Pascal sources B. Appendix - The units C. Appendix - What the unit isn't able to do N. Important Notes The process unit release Version 3.1 added the function set_thread_time. With this function you can set the task switch timer to (55ms div x), with x is an integer value between 1 and 65535. And finally, all source codes are available for the PROCESS unit. A short instroduction, of how the unit works can be found in SOURCE.DOC...! ** *** ** ** ** 1. Introduction **** My intention for this unit was to create a multitasking unit for Turbo-Pascal, which is easy to use. Some commands to split tasks, to create new tasks, to kill, delay, stop and continue tasks. More than one task is for example very useful for games: Every enemy, the score counter, etc. could get its own task. During coding this unit more and more problems occured, so I was forced to write more than a short document about this unit. But all together the number of commands is still countable and the use, with only a few exceptions, quite easy; depending of course on what you want to do! I wrote some very easy examples and some more complicated examples. The SWITCH2 demonstration program is one of the more difficult examples, but after reading this manual you should as well be ready to understand the program complete. Although most of the examples are quite simple and the use of the unit is simple as well, I want to advise you to read the manual before coding your own programs, because you get some instruction of what you shouldn't do or what you can't do with the process unit To begin with an easy example: You want a program with different tasks. The first task displays an ASCII textfile character by character on the screen. The second task displays the percentage of bytes read from the file in a separate window. The third task only waits for the ESC key to be pressed... example file PRODEMO1.PAS { **************************************************************** } { * Program: prodemo1 - example how easy it is to work with * } { * the process unit * } { * * } { * * } { * Process (c) 1996 by Dieter Pawelczak * } { * * } { **************************************************************** } uses process,dutils,crt; var filename:string; ff:text; totalbytes,bytesread:longint; mainend:boolean; pid1,pid2,pid3:integer; procedure readtask; { This task will read from an ascii textfile } var i:word; key:byte; ch:char; begin assign(ff,filename);reset(ff); repeat read(ff,ch); delay(1);inc(bytesread); write(ch); until eof(ff); close(ff); kill_task(pid1); { End of Task 1 } end; procedure percentagetask; { shows percentage of bytes read } var r1,r2:real; i,wper:word; begin cwin2(40,1,77,4,48+15); { create colour window - attributes 48+15 } print(50,1,filename); { print filename } repeat r1:=bytesread; r2:=100.0; if totalbytes<>0 then r2:=r1 / totalbytes * 100; wper:=trunc(r2) div 3; for i:=0 to wper do print(42+i,2,chr(177)); { print scrollbar } print(52,3,twodecs(r2)+'%'); { print percentage } delay_task(1); until 1=0; { endless loop } end; procedure keytask; var key:char; color:byte; begin color:=0; repeat color:=(color+1) mod 14+1; cprint(1,25,'Press ESC to exit..',color); if keypressed then key:=readkey; delay_task(0); until key=#27; mainend:=true; kill_task(pid3); end; begin filename:='process.doc'; if paramstr(1)<>'' then filename:=paramstr(1); if not fileexist(filename) then begin writeLn('File not found:',filename); halt(0); end; totalbytes:=filelength(filename); bytesread:=0; pid1:=create_process(@readtask,20000,1,'READ'); pid2:=create_process(@percentagetask,8000,0,'PERCENT'); pid3:=create_process(@keytask,8000,0,'PERCENT'); clrscr; twin1(1,4,80,24); window(2,5,79,23); Init_multitasking; repeat delay_task(0); { Main task does nothing ... } until mainend; done_multitasking; clear_tasks; { Alway at program end! } window(1,1,80,25); gotoxy(1,25); end. This simple example shows how easy parallel tasks can be created. The procedures readtask and percentagetask aren't called explicitly. They are declared as a parallel process by the create_process function. When the Init_Multitasking procedure is called, the main program task is split in four different tasks: - the main task will continue in the repeat until loop, - the readtask will open the file and display it, - the percentagetask will display the percentage of bytes read. - the keytask will display the 'Press ESC' - message in different colours and waits for the ESC key. When the main task reaches the Done_Multitasking procedure every task except the main task will be 'killed'. I should better say stopped, because you could initialise the multitasking again with Init_Multitasking, but only under the condition that this is the only program running multitasking at the moment. We will see later, that more than one program can use the process unit. The procedure clear_tasks removes the tasks from the tasklist. This procedure is necessary, because if you call the same program twice from DOS, the old tasks will appear again and will certainly lead to disfunctions of the program. *** ****** ** ** ** ** 2. The create_process function and the tasklist ****** The create_process function is called with the following parameters: pid:=create_process(procedure,stacksize,priority:word,taskid:string); -procedure is a procedure pointer, e.g. @task1, @task2 etc -stacksize the size of stack for the procedure with a minimum of 2048 bytes and a maximum of 65533 bytes. -with priority the frequency of calls is established. Priority '0' means a maximum of calls. Priority '1' means every other procedure with priority '0' will be called once more than this task. Priority '18' means every other task with priority 0 will be called 18 times more. -the taskid is a short identification for the task. The first two letters are stored in the tasklist. -the return parameter is integer. The value is below zero, if the function call hasn't been successful. + Return of -1 if the stacksize is too small + Return of -2 if there is too less memory available to allocate the stack on the heap. If the function call has been successful the return value is the pid, the Process IDentity. The pid is a value from 0 to 50 and indicates the process number. In this release the create_process procedure doesn't check the numbers of tasks! You should control that your programs create less than 50 tasks. The tasklist is an array which contains for every task several flags, the stackpointer, the stacksegment, the number of calls and the priority. Every task created by the create_process function gets an entry in the tasklist. The procedure task_list displays all entries of the list and can be used as a debugging tool. In case of an error the task_list procedure will be called, so you can locate the error. Look at the following example: uses process; var pid1,pid2,pid3:integer; procedure task1; begin kill_task(pid1); end; procedure task2; begin repeat until 1=0; end; begin pid1:=create_process(@task1,8000,0,'T1'); pid2:=create_process(@task2,8000,2,'T2'); pid3:=create_process(@task2,8000,3,'T3'); task_list; clear_tasks; end. The programs output should be the following: ================================================================ TASKLIST PID MPID STATUS WAITSTATE STACKSIZE STACKUSE CALLS DOS ID ================================================================ 0 1 WAIT 0: 0 8000 0.52 % 0 . T1 1 1 WAIT 2: 2 8000 0.52 % 0 . T2 2 1 WAIT 3: 3 8000 0.52 % 0 . T3 The first entry in the tasklist is the pid. This is the location of the task in the tasklist. The next value is the mpid, the so called Mother Process IDentification. The process unit can be used by more than one program. If a program for example executes another program, so both programs can create new procedures in the same tasklist. The procedures of the first program got the mpid=1, the procedures of the second program the mpid=2. The mpid is necessary, because both programs can kill processes of the other program, but they can not free memory that was allocated by the other program. The next item is the task's status. A task can have the following states: not exist - this task hasn't been created yet(won't be displayed) wait - this task is waiting to be called run - this is the current running task killed - this task has been killed and won't be called further stopped - the same effect as killed, but the task can be continued (see Chapter 7 for more details) The waitstate and the priority are nearly the same. The first value is the waitcounter which is decremented by every task switch. The second the task priority. We imagine the last example running: - The counter of the first task has already reached zero, the task will be called immediately. The wait counter of the other tasks will be decremented: PID MPID STATUS WAITSTATE STACKSIZE STACKUSE CALLS DOS ID ================================================================ 0 1 RUN 0: 0 8000 0.52 % 1 . T1 1 1 WAIT 1: 2 8000 0.52 % 0 . T2 2 1 WAIT 2: 3 8000 0.52 % 0 . T3 - the first task is running and - remember the example - kills itself, so with the next task switch every counter will be decremented and the second task will be called: PID MPID STATUS WAITSTATE STACKSIZE STACKUSE CALLS DOS ID ================================================================ 0 1 KILLED 0: 0 8000 0.52 % 1 . T1 1 1 RUN 0: 2 8000 0.52 % 1 . T2 2 1 WAIT 1: 3 8000 0.52 % 0 . T3 - with the next task switch the counters are decremented and the counter of task 2 (T3) reaches zero. The counter of task 1 (T2) is set to the task priority PID MPID STATUS WAITSTATE STACKSIZE STACKUSE CALLS DOS ID ================================================================ 0 1 KILLED 0: 0 8000 0.52 % 1 . T1 1 1 WAIT 2: 2 8000 0.52 % 1 . T2 2 1 RUN 0: 3 8000 0.52 % 1 . T3 - and so on.. The stacksize is arranged by the create_process function call. The stackuse compares the current position of the stackpointer and the stacksize in percent. In the example we see that before the task was called the stack is already in use. The explanation is easy: All registers of the task will be stored on stack. So before the first call the registers are initialised with useful values on the stack(DS for example with the value of the data segment). The next item contains the information of how often the task was called. The task switch is connected with the hardware interrupt 08h and therefore is created 18.2 times a second. The DOS flag may be marked as . for false and X for true. Any marked task must wait until the INDOS flag is not set. To avoid multiple non reentrant DOS calls, you can mark a task before the execution of the dos call. The last entry in the tasklist is a short ID of the task in ASCII codes. The first two letters of the taskid (given by the create_process function) will be stored in the tasklist. +==================================================================+ | What you need to consider by using the create_process function ? | +==================================================================+ | - every task should end with the kill_task(pid:word) procedure or (*) | | stay in an endless loop. If any task runs over the end; command in | | Pascal, or even worse if you create a function as a process and | | the function is going to return something, this task runs into | | nowhere, because the task never had any entry point! | | - you should avoid any DOS function calls, (int 21h) because DOS calls | | aren't re-entrant. This means DOS is used that only one DOS command | | will be called at the same time, so DOS only reserves one stack for | | these functions. If you use two DOS calls at the same time the | | DOS stack will be overwritten and the return address unknown! | | How to use DOS calls in a multitasking program will be explained in | | | Chapter 4. | | - as debug aid use the task_list procedure to check what's going on ! | (*) The Version 2.1 allows tasks to end without kill_task, if they are declared as far procedures. The first task in our example could as well be written: procedure task1; far; begin { This task will kill itself - if it is a far procedure } end; *** ****** ** **** ** ****** 3. Delay_task, kill_task, kill_process procedure *** The following examples show the work and advantage of the delay_task procedure. These examples are the files PRODEMO2.PAS and PRODEMO3.PAS { **************************************************************** } { * Program: prodemo2 - shows the use of delay_task * } { * together with prodemo3 * } { * * } { * * } { * Process (c) 1996 by Dieter Pawelczak * } { * * } { **************************************************************** } uses process2,crt,dutils,dos; var pid1,pid2:integer; counter:longint; mainend:Boolean; var sec,min,hour,sec100:word; totaltime:longint; (***) procedure task1; var i:byte; begin writeLn('Press ESC to exit!'); repeat i:=0; if keypressed then i:=ord(readkey); until i=27; mainend:=true; kill_task(pid1); end; procedure timetask; var sec,min,hour,sec100:word; currenttime:longint; begin set_delay(pid2,5); repeat gettime(hour,min,sec,sec100); currenttime:=hour*3600+min*60+sec; print(70,1,'TIME:'+decs(currenttime-totaltime,2)); if currenttime-totaltime>=5 then mainend:=true; { counts 5 seconds and stops the program } until 1=0; { endless loop } end; begin clrscr;writeLn; pid1:=create_process(@task1,8000,0,'T1'); pid2:=create_process(@timetask,20000,0,'TI'); gettime(hour,min,sec,sec100); totaltime:=hour*3600+min*60+sec; Init_Multitasking; repeat inc(counter); print(1,1,'Counting:'+decs(counter,8)+' '); until mainend; Done_Multitasking; writeLn; writeLn('counted until:',counter); writeLn; task_list; clear_tasks; end. This program does nothing else than counting five seconds long. At the end the program displays the result of the counting. What I want to demonstrate is the use of the delay_task procedure. So we have a closer look at task1 (***): - This task wait until we press the [ESC] key. With every call to the task we run through the loop many times (for about 55ms, which is the time a task is running). Even if no key was pressed it is improbable that during the next 55ms any key is struck. So the processor is working on useless stuff all the time and can not go on with the more important things (here e.g. the counting). - We can create a more effective code if we do not waste processors time. So at the end of the loop we enter the delay_task() procedure and switch the tasks by ourselves. This is the example file PRODEMO3.PAS which is counting faster because of the task switch operation: (***) procedure task1; { The better solution! } var i:byte; begin writeLn('Press ESC to exit!'); repeat i:=0; if keypressed then i:=ord(readkey); delay_task(0); { forces task switch } until i=27; mainend:=true; kill_task(pid1); end; The delay_task(waitcounts:word) procedure changes the waitcounter. If the task was created with the priority 10 and you delay the task with delay_task(1) the tasks priority will be changed to 1 in this instance. What happens if we use delay_task( 0 )? Theoratically the task wouldn't switch at all, because the task itself would have the highest priority at the moment. So this makes no sense at all. For this reason, delay_task(0) simply forces a task switch with no effect on the priority. | - a task priority will be changed by delay_task(n) for n>=1 | | - to force a simple task_switch with no effect on the priority use | | delay_task(0) | The kill_task and the kill_process procedures have the same effect: The task will be marked as killed and therefore not called any more. The kill_task procedure will also switch the task. If any process reaches the kill_task procedure the process will be marked as killed and the task will be switched! You can kill other processes by using the kill_process(pid:word) procedure but you can not kill the own task with it. The task would indicate the kill flag in the tasklist but go on running! | - to kill a different process use kill_process(pid:word) | | - to kill the running task or to end a task use kill_task(pid:word) | Look at the following example: uses process; var mainpid:word; begin Init_Multitasking; mainpid:=get_current_pid; { To get the current task pid } kill_task(mainpid); Done_Multitasking; clear_tasks; end. This is of course horrible! You kill the only task that exists! You can try this example of course, because the process unit creates an error if every task was killed or no task exists. The same example using the kill_process procedure will rarely create an error, because the program is finished before the task is switched, before the program recognises that the task was killed. uses process; var mainpid:word; begin Init_Multitasking; mainpid:=get_current_pid; kill_process(mainpid); Done_Multitasking; clear_tasks; end. You can check whether a process was killed or not with the functions: iskilled(pid:word):boolean; and isnotkilled(pid:word):boolean; If you create new tasks the pid number can be one of earlier killed tasks, so to be sure you killed a specific task you should check its identity as well: get_taskid(pid:word):String; For example to see if the keytask has been killed: pid:=create_task(@keytask,10000,0,'KT'); ... if iskilled(pid) or (get_taskid(pid)<>'KT') then write('Attention, the keytask had been killed! '); ** ** ** ** ****** ** ** 4. Set_Delay, Task_priority, End_priority ** Wait_Dos, Ok_Dos The following example shows the use of the three functions. - Set_Delay(pid,waitcounts:word) can be used to alter the task priority. the wait counter will now be initialised with the new value waitcounts. - The Task_priority(pid,waitcounts:word) procedure can be used to delay the next task switch. This is useful if you want to make a dos function call and be sure that no other task will run at the moment. If you use Task_priority(get_current_pid,18) the next task switch is delayed for 1 second. - A Task_priority must always be followed by the End_priority(pid:word) command. The End_priority procedure restores the waitcounts of all tasks as they were before the Task_priority call. This example is PRODEMO4.PAS: uses process,dutils,dos,crt; var mainend:boolean; pid1,pid2,pid3:integer; procedure calendar; var i,j,mo,ye,ii,b1:word; kj:integer; as:String; begin cwin2(40,1,79,12,14+16); set_delay(pid1,17); { change the task_priority } repeat getdate(ye,mo,i,j); ii:=getweekday(0,mo,ye); cprint(44,2,' Su. Mo. Tu. We. Th. Fr. Sa. ',30); cprint(44,3,'=============================',30); cprint(55,4,monthstr[mo]+' '+decs(ye,4),30); for i:=0 to 5 do print(41,5+i,' '); for i:=0 to monthlen[mo]-1 do begin cprint(45+((ii+i) mod 7)*4,(ii+i) div 7+5,decs(i+1,2),30); end; as:=time; cprint(41,11,' Date: '+Date(True)+' Time: '+as,30); delay_task(0); until 1=0; end; procedure task2; begin repeat gotoxy(1,13); task_priority(pid2,10); { We want to have the task_list of that moment, } { so we don't want the function call to be interrupted } task_list; end_priority(pid2); { The priority ends here! } delay(100); until 1=0; end; procedure task3; begin repeat if strpressed and altpressed then mainend:=true; print(1,25,'PRESS [STR] and [ALT] to exit!'); delay_task(1); until 1=0; end; begin clrscr; pid1:=create_process(seg(calendar),ofs(calendar),10000,0,'CA'); pid2:=create_process(seg(task2),ofs(task2),10000,0,'TL'); pid3:=create_process(seg(task3),ofs(task3),10000,0,'KE'); mainend:=false; init_multitasking; repeat delay(100);delay_task(1); until mainend; done_multitasking; clear_tasks; gotoxy(1,25); end. The calendar process is created with a priority of 0. When the colour window is drawn by the cwin function the process priority is changed with the Set_Delay procedure. The time is changing every second, so the procedure should only be called every second. The task2 process wants to display the current tasklist. But the tasklist is not correct, if the task is switched during the output of the list. We use the task_priority procedure to give all priority to task2. After displaying the tasklist the procedure end_priority restores the wait counts of the other tasks. Of course we can see in the tasklist that the wait counts were set to 10. We can, of course use the task_priority procedure to allow dos calls. If the dos call is within a priority period, no other tasks are allowed. Remember, the priority of 18 means that for one second no other tasks can run. But as long as only one dos call is executed, there is no need to stop other tasks. For this reason we have two functions: - Wait_Dos; to check if any dos call is running at the moment, and under the circumstances to wait until the dos call had been finished. If dos is active (the INDOS flag set to true), the task will be marked as DOS-task (see task_list) and suspended until the indos flag is false again. If there is no dos call at the moment, the task gains a short priority over all other tasks (otherwise, the task may be switched before the dos call is made and another task is able to make a dos call at the same moment) - Ok_Dos; disables the DOS mark of the task. To make a dos call we simply create a block around the call with wait_dos and ok_dos. The next example shows two dos calls at the "same" moment: This example is PRODEMO5.PAS: uses process,dutils,dos,crt; var mainend:boolean; pid1,pid2,pid3:integer; procedure calendar; var i,j,mo,ye,ii,b1:word; kj:integer; begin cwin2(40,1,79,12,14+16); set_delay(pid1,17); repeat wait_dos; getdate(ye,mo,i,j); { DOS CALL ! } ok_dos; ii:=getweekday(0,mo,ye); cprint(44,2,' Su. Mo. Tu. We. Th. Fr. Sa. ',30); cprint(44,3,'=============================',30); cprint(55,4,monthstr[mo]+' '+decs(ye,4),30); for i:=0 to 5 do print(41,5+i,' '); for i:=0 to monthlen[mo]-1 do begin cprint(45+((ii+i) mod 7)*4,(ii+i) div 7+5,decs(i+1,2),30); end; wait_dos; cprint(41,11,' Date: '+Date(True)+' Time: '+time,30); { DOS CALLS ! } ok_dos; delay_task(0); until 1=0; end; procedure task2; begin repeat wait_dos; print(70,25,time); { DOS CALL ! } ok_dos; delay_task(1); until 1=0; end; procedure task3; begin repeat if strpressed and altpressed then mainend:=true; print(1,25,'PRESS [STR] and [ALT] to exit!'); delay(30); until 1=0; end; begin clrscr; pid1:=create_process(@calendar,10000,0,'CA'); pid2:=create_process(@task2,10000,0,'TL'); pid3:=create_process(@task3,10000,0,'KE'); mainend:=false; init_multitasking; repeat delay(100);delay_task(10); until mainend; done_multitasking; clear_tasks; gotoxy(1,25); end. | - when using dos calls (for example functions of the dos unit, file | | handling, etc.) use wait_dos to check if any dos call is running | | at the moment. | | - always end a block started with wait_dos with the ok_dos function, | | otherwise the task is marked as DOS task. | | - use Set_Delay to alter the tasks priority. This procedure has no | | effect if you use delay_task to create a local task_priority. | The following example is a multiple copy procedure, which allows to copy several files parallely: procedure copyfile(Source,Dest:String); var FROMF, TOF:FILE; Numread,Numwrite:WORD; BUF:array[0..8191] of CHAR; begin if (dest='') then dest:=copy(source,1,pos('.',source))+'bak'; wait_dos; if filecount>5 then begin while filecount>5 do begin wait_dos; ok_dos; delay_task(0); end; end; inc(filecount); wait_dos; assign(FRomF,Source); Reset(FROMF,1); ok_dos; wait_dos; if filecount>5 then begin while filecount>5 do begin wait_dos;ok_dos; delay_task(0);end; end; wait_dos; assign(TOF,Dest); Rewrite(ToF,1); ok_dos; writeln('Copy file:'+source+' to '+dest); repeat wait_dos; Blockread(FromF,Buf,SizeOf(Buf),Numread); ok_dos; delay_task(0); { Here to allow multiple copy } wait_dos; Blockwrite(TOF,Buf,Numread,Numwrite); ok_dos; until (Numread=0) or (Numwrite<>Numread) or (Numread<>SizeOF(BUF)); wait_dos; Close(Fromf); ok_dos; wait_dos; Close(Tof); ok_dos; writeLn('Transfer to '+Source+' complete.'); dec(filecount); end; The global variable filecount is necessary to avoid too many open files. You can call the procedure from different tasks for example: procedure task1; begin copyfile('demo.pas','A:demo.pas'); kill_task(get_current_pid): end; procedure task2; begin copyfile('demo2.pas','A:demo2.pas'); kill_task(get_current_pid): end; ... procedure taskN; begin copyfile('demoN.pas','A:demoN.pas'); kill_task(get_current_pid): end; ... pid1:=create_process(@task1,14000,0,'C1'); pid2:=create_process(@task2,14000,0,'C2'); etc. An example of a multiple copy function is the file PCOPY.PAS. This example allows to enter several file names, which will be copied to disk A:. During the file copy, you can enter new file names to copy, or list the directory. ****** ** ** ***** ** ** 5. Multiprogramming and the use of signals, the ***** run_multitasking flag In connection with the mpid parameter it was said, the process unit could run on different programs. You can execute another program within your program that creates parallel procedures as well. You can use the exec-procedure from the dos unit to start another program in your own. But the exec procedure has two disadvantages: | - The exec procedure of the dos unit creates a new program environ- | | ment. You can kill the action of the program but not the environment!| | If your program terminates before the other program has terminated | | the system might hang itself. | | - You can only create one exec call in your program. A second call to | | the exec procedure - and the system is down! | For the first problem a solution is found with the signal sending: A process can send a signal to another signal. For interprocess communication, every process can send a signal to another process: - send_signal(pid:word;signal:integer); - function get_signal:integer; - function wait_signal:integer; - clear_signal; The first task can send a signal to the second task for example with the following command: send_signal(pid2,-1); The second task (pid2) receives the signal via get_signal and can do some special action because of the signal. After the signal was read the signal should be cleared with clear_signal, so that another signal can be received. The function wait_signal has the same function than get_signal, but as long as the signal is equal to zero, it executes a task switch. You can use wait_signal to synchronize different tasks. You can use the signals to terminate a program started with exec. The examples SIGNAL.PAS and STEST.PAS show how the communication works: {$A+,B-,D+,E+,F-,G-,I+,L+,N-,O-,P-,Q-,R-,S-,T-,V+,X+,Y+} {$M 16384,100000,150000} { SIGNAL.PAS } uses process,dos,crt; var pid1,pid2:integer; mainend:Boolean; startdir:string; procedure task1; { Process to exec STEST.EXE } begin textcolor(14); writeLn('SIGNAL.EXE: calling: Signal Test! - executing STEST.EXE'); exec(startdir+'\STEST.EXE',''); kill_task(pid1); end; procedure task2; { Process to send the Signal } begin textcolor(14); task_list; textcolor(14); writeLn('Press ENTER to KILL STEST.EXE!'); readln; writeLn('Sending Signal!'); send_signal(pid1,-1); delay_task(5); textcolor(14); task_list; textcolor(14); writeLn('Press ENTER to leave SIGNAL.EXE'); readln; mainend:=true; kill_task(pid2); end; begin getdir(0,Startdir); textcolor(14); writeLn('SIGNAL - demonstration by D. Pawelczak'); pid1:=create_process(@task1,40000,0,'EXEC'); pid2:=create_process(@task2,10000,10,'DUMMY'); Init_Multitasking; repeat delay_task(1);delay(20);{ The Main Task does nothing but waiting...} until mainend; done_multitasking; textcolor(14); writeLn('Exit SIGNAL.EXE!'); clear_tasks; textcolor(7); end. The program that will be executed, displays its messages in another colour: uses process,crt; begin textcolor(13); writeLn('STEST.EXE: Signal Test: waiting for signal!'); if not run_multitasking then begin writeLn('No mother process to send signal!'); halt(0); end; task_list; Init_Multitasking; wait_signal; done_multitasking; textcolor(13); writeLn('Caught Signal'); clear_tasks; textcolor(13); writeLn('EXIT!'); textcolor(7); end. The function run_multitasking checks if already any process unit is running in the multi process mode. The STEST.EXE program needs the sending of a signal for termination. With no other multitasking process this signal could never be sent. If you design programs to be executed as a parallel process, you should use the function run_multitasking to check if this program was really executed by a multitasking program or by the dos shell! If the program establishes as well any parallel tasks you should use in any case the clear_tasks procedure to be sure that no task of the program remains in the memory. A program executed by the exec procedure will free all its memory at its termination. The procedures Init_Multitasking and Done_Multitasking have little effect if the multitasking is already running, but you should use them in any way. The function Done_Multitasking is necessary to restore the pascal exit procedure. | - Warning! If you create any process by create_process and the multi- | | tasking is already running, this process will be called even before | | the use of Init_Multitasking! | ***** ** ** ****** ** ** ** ** 6. Multiprogramming with exec_exe and create_exe ***** The main disadvantage of the dos unit's exec procedure is that only one other program can be executed parallel to the main program. For this reason the functions exec_exe and create_exe are implemented. Whereas the exec_exe function has nearly the same effect as the exec procedure the create_exe function is similar to the create_process function. You use the exec_exe function with the following parameters: function EXEC_EXE(Filename,parameter_line:String;addr:pointer):integer; - The filename is the name of the exefile including the extension '.exe' - The parameter_line are the parameters for the program - The address pointer is the memory location for the program - the function returns -1 if the execution failed. | - Warning! The function does not check the exe file! if you try to read | | and execute a text document, for example, this would kill your system! | The function does not create a new program environment. You can leave your main program while tasks of the executed program are still running. But what happens when the executed program end? While working and debugging under Turbo Pascal the exit of the parallel running program leads automatically to the end of the main program. This is of course because Turbo-Pascal does not really terminate the program, but uses its own exit procedure. If your program is run under dos, the program jumps back to the dos shell, so the dos shell is now a parallel process and should be killed as soon as possible! But using signals, you can of course watch when any executed program terminates. Windows instead using again its own exit procedure and terminates all process, if one process calls the exit procedure. The best thing would be to design the programs you want to load and execute, with the help of signals and an endless loop in such way, that only the main program can terminate all processes. You can, for example, end such an program by the following sequence: Done_Multitasking; clear_tasks; if run_multitasking then kill_task(get_current_pid); If you executed this program by another multitasking program, the run_multitasking flag is still set to true and therefore the last running task of the program will be killed. The function create_exe loads an exe program as a new process. As return value you get -1 if file not found or the pid of that process. function create_EXE(Filename,Param_line:String;addr:pointer; priority:word;Taskid:string):integer; - The first three parameters are the same as the parameters of the exec_exe function. - the next parameters are the task priority and the task identification (see create_process function) The same exit problems occur as there are with the exec_exe function. Again you must allocate the memory first before loading the file. The allocation may look like: var p:pointer ... getmem(p,64000); create_exe('HALLO.EXE','Two parameters',p,0,'EX'); The demo program PRODEMO6.PAS explains the use of exec_exe, the following example PRODEMO7.PAS explains the use of the create_exe function. Both programs differ only in the use of these two functions. The program executes STARS.EXE and KALEND.EXE. STARS.EXE draws stars (*) randomly all over the screen. For creating the colours at random, a new process is established. The KALEND.EXE displays every second a calendar and the time. In the main program you can enter some commands: - cls to clear screen - ps to see the tasklist - signal to send signals (both programs answer the signals!) - kill x to kill any of the processes - exit to kill all processes and leave the program - help to see the list of commands. uses process,dos,crt,dutils; var pid4,pid1,pid2,pid3,pid5:integer; mainend:boolean; mainpid:word; startdir:string; procedure paralleltask5; begin task_priority(pid5,3); writeLn('PRODEMO 7 - demonstration '); writeLn('Three tasks are running '); writeLn('-the input shell'); writeLn('-the STARS.EXE'); writeLn('-and KALEND.EXE'); writeLn; writeLn('Use these commands;'); writeLn('exit - to leave'); writeLn('kill 1 - to kill KALEND.EXE '); writeLn('kill 2 - to kill STARS.EXE '); writeLn('ps - to show the task status '); writeLn('signal - to send signals'); writeLn('cls - to clear screen'); writeLn('help - to view this'); end_priority(pid5); kill_task(pid5); end; procedure paralleltask; var s:string; hpid:word; begin delay_task(48); repeat write('->'); readln(s); if pos('kill',s)<>0 then begin hpid:=ord(s[6])-48; if hpid=get_current_pid then writeLn('you cannot kill input task! How do you wanna leave the program?') else if process_exist(hpid) then kill_process(hpid) else writeLn('No such process!'); end; if s='help' then pid5:=create_process(seg(paralleltask5),ofs(paralleltask5),4096,0,'HELP'); if s='ps' then task_list; if s='signal' then begin send_signal(pid2,-1); send_signal(pid3,-1); end; if s='cls' then clrscr; if s='end' then mainend:=true; if s='exit' then begin writeLn('Exit - killing process'); kill_process(pid3); kill_process(pid2); if iskilled(mainpid) then begin writeLn('Main task killed...'); writeLn('Killing last open task!'); kill_task(pid1); end; mainend:=true; end; until 1=0; mainend:=true; end; var p:pointer; begin getdir(0,startdir); pid1:=create_process(@paralleltask,8096,0,'DOS'); getmem(p,64000); pid2:=create_EXE(startdir+'\KALEND.EXE','',p,1,'KA'); getmem(p,64000); pid3:=create_EXE(startdir+'\STARS.EXE','',p,8,'ST'); if (pid2<0) or (pid3<0) then begin writeLn('The programs KALEND.EXE and STARS.EXE should be in the same directory!'); halt(0); end; pid5:=create_process(@paralleltask5),8096,3,'HELP'); clrscr; Init_Multitasking; delay_task(8); mainpid:=get_current_pid; repeat delay_task(18);delay(1); until mainend; Done_Multitasking; clear_tasks; writeLn('Exit [',K^.taskcount:2,'] PRODEMO7 '); end. The main problem using create_exe or exec_exe is the memory. It is always difficult to say how much memory a program might need. The program needs memory for the code, data, stack and heap. The allocated memory space should be big enough for code, data and stack. Your main program should not take all the heap, because there would be no heap for the parallel process. You should therefore use always as less heap as you need. You can check this with the memavail function. Another problem is: Turbo Pascal programs make some dos function calls in the beginning (allocating memory etc.). You should avoid to let two programs crash in the beginning. This means If you start two programs with create_exe, the second program should have a lower priority. When the programs are running, you can alter the priorities with set_delay! To avoid problems with dos functions: Use wait_dos and ok_dos in every program. Avoid the use of create_exe and exec_exe with programs you don't know! Most programs use a lot of re-entrant dos functions and you do not have control on these function calls! ******* ** ** ** ** ** 7. Multiprogramming and process communication, ** the use of stop_process and continue_process, send_message and read_message Up to our last demo all processes print their output on the same screen. To control more than one process you can use the procedures stop_process and continue_process. If you 'stop' a task the task is in a state similar to the killed state and won't be called anymore. In the opposite of a killed task, the stopped task can be continued with the continue_process procedure. stop_process(pid:word); continue_process(pid:word); You can not stop the task that is just running. This is similar to the kill_process procedure. The task would go on running and would be stopped with the next task switch. The next example is a bit more complex. The program SWITCH.PAS starts two other programs, the KALEND.EXE and the PACMAN.EXE. PACMAN.EXE is a simple pacman demo for the text screen, using eleven(!) processes. The KALEND.EXE draws about every second the calendar and time on the screen. The PACMAN.EXE uses the whole screen for its output. The best solution is a task switch function. You can switch between two different programs: The pacman game and the KALEND.EXE with a simple input shell. The input shell allows you to show the tasklist and kill certain tasks. This can be useful for example to eliminate your enemies in the game (kill W2,W3,W4 processes!). To avoid that PACMAN.EXE terminates and starts the dos shell if you press escape, SWITCH.EXE and PACMAN.EXE communicate with the message-string. The message-string is a string variable (max. 255 char long), that can be read and written by every program using the process unit. Two procedures and a function is defined to handle the message communication: - send_message(Message:String); - clear_message; - read_message:string; Every task had its own signal. The message string is global for all processes. If more than two processes communicate by the message string you should only send a message if the message is cleared ('') and after reading the message your process should clear the message! The SWITCH2.PAS example is far more complex. You have more commands in the shell, so you can restart pacman and kill the pacman program from the shell. This example should demonstrate the process control of the main program by using signals and messages. But now the SWITCH.PAS example: {$A+,B-,D+,E+,F-,G-,I+,L+,N-,O-,P-,Q-,R-,S-,T-,V+,X+,Y+} {$M 16384,160000,160000} {no more heap than needed!} uses process,dos,crt,dutils; var pid4,pid1,pid2,pid3:integer; mainend:boolean; mainpid:word; startdir:string; switchvar:word; type screentyp=array[0..3999] of byte; { to save the screen! } var scr:^screentyp; { switch to pid1,pid2,pid4 or to pid3,pid4 and all the rest! } procedure switch; var i:byte; scr1,scr2:screentyp; begin switchvar:=0; scr1:=scr^; stop_process(pid1); stop_process(pid2); repeat if read_message='PACMAN:ends' then { Before exit pacman sends } begin { this message! } kill_process(pid3); print(1,25,'TASK KILLED use ALT to switch to shell!'); clear_message; end; print(66,1,'[ALT] to switch'); if altpressed then begin task_priority(pid4,150); if switchvar=0 then begin scr2:=scr^; cwin2(28,8,52,10,64+15); for i:=0 to 50 do if (i<>pid1) and (i<>pid2) and (i<>pid4) and (i<>mainpid) then stop_process(i); print(35,8,'Switch Tasks!'); print(32,9,' Switch To Shell ! '); repeat until not altpressed; scr^:=scr1; continue_process(pid1); continue_process(pid2); switchvar:=1; end else begin scr1:=scr^; cwin2(28,8,52,10,64+15); stop_process(pid1); stop_process(pid2); print(35,8,'Switch Tasks!'); print(32,9,' Switch To PACMAN! '); repeat until not altpressed; scr^:=scr2; for i:=0 to 50 do if (i<>pid1) and (i<>pid2) then continue_process(i); switchvar:=0; end; end_priority(pid4); end; delay_task(1); until 1=0; end; procedure paralleltask; var s:string; hpid:word; begin delay_task(48); writeLn('Enter exit to quit'); writeLn('Commands:ps, kill, cls'); repeat write('->'); readln(s); if pos('kill',s)<>0 then begin hpid:=ord(s[6])-48;if length(s)>6 then if (s[7]>='0') and (s[7]<='9') then hpid:=hpid*10+ord(s[7])-48; if hpid=get_current_pid then writeLn('you cannot kill input task! How do you wanna leave the program?') else if process_exist(hpid) then kill_process(hpid) else writeLn('No such process!'); end; if s='ps' then task_list; if s='signal' then begin send_signal(pid2,-1); send_signal(pid3,-1); end; if s='cls' then clrscr; if s='end' then mainend:=true; if s='exit' then begin writeLn('Exit - killing process'); kill_process(pid3); kill_process(pid2); if iskilled(mainpid) then begin writeLn('Main task killed...'); writeLn('Killing last open task!'); kill_task(pid1); end; mainend:=true; end; until 1=0; mainend:=true; end; var p:pointer; begin getdir(0,startdir); scr:=ptr(scrtyp,0); pid1:=create_process(@paralleltask,8096,1,'DOS'); getmem(p,64000); pid2:=create_EXE(startdir+'\KALEND.EXE','',p,1,'KA'); getmem(p,64000); pid3:=create_EXE(startdir+'\PACMAN.EXE','',p,5,'ST'); pid4:=create_process(@switch,20096,4,'SW'); if (pid2<0) or (pid3<0) then begin writeLn('The programs KALEND.EXE and PACMAN.EXE should be in the same directory!'); halt(0); end; clrscr; Init_Multitasking; mainpid:=get_current_pid; repeat delay_task(18);delay(1); until mainend; Done_Multitasking; clear_tasks; writeLn('Exit [',K^.taskcount:2,'] SWITCH '); end. You can check whether a process was stopped not with the functions: isstopped(pid:word):boolean; and isnotstopped(pid:word):boolean; ***** ** ** ** ** ***** ** ** ** ** 8. The Breakflag ***** This extension is available since unit Version 2.1. Users of older versions of the process unit may already have experienced what happens in a multiprogramming environment (for example switch.pas) when you press the C-Break key. What do we expect to happen? Either your program ignores the break function or the program terminates to DOS. But what happened was far from what we had expected. If you pressed the C-Break key, for example running pacman under the switch demo? The pacman program would have received the C-Break and this program would have tried to return to DOS immediately. All other tasks would go on running, because the clear_task or done_multitasking function would never be called. For this reason, I implemented a new 1bh Interrupt, which controls the C-Break key. If you press the C-Break key in any task, all tasks will be killed and cleared immediately, and the program will terminate to DOS via the mainexit procedure (from the mother process (mpid=0)). To avoid the Break function I declared a Boolean breakflag. If the breakflag is set to true (default) the break function is enabled. Otherwise the C-Break key will be ignored. The new 1bh Interrupt checks as well the shift function. If you press [CRTL][SHIFT][Break] you get the current task list, before the program terminates. ***** ** ** ** ** ****** ** ** 9. The Task Timer : Set_Thread_Time ***** This extension is available since unit Version 3.1. We always assumed, that the time between a task switch is about 55ms. This is not always useful. We can, of course create more task switches in a cooperative way - by using the delay_task procedure. But sometimes, we need a faster task switch, and for this reason, we can alter the time between a task switch to (55ms div x) with x:=1..65535. The set_thread_time procedure sets a global task switch time. Note, that in a multiprogramming environment, every program is able to alter the task switch time. This might, of course, change the performace of the other programs. procedure set_thread_time(typ:word); Some task switch times for values of typ: typ = 0,1 : thread time = 55 ms typ = 2 : thread time = 27.5 ms typ = 3 : thread time = 18.3 ms typ = 4 : thread time = 13.8 ms typ = 5 : thread time = 11 ms typ = 6 : thread time = 9.2 ms typ = 7 : thread time = 8 ms typ = 8 : thread time = 6.8 ms typ = 9 : thread time = 6.1 ms typ = 10 : thread time = 5.5 ms typ = 11 : thread time = 5 ms typ = 14 : thread time = 3.9 ms typ = 18 : thread time = 3.0 ms typ = 27 : thread time = 2.0 ms typ = 55 : thread time = 1.0 ms typ = 110 : thread time = 0.5 ms typ = 550 : thread time = 0.1 ms The new exit procedure of the process unit resets the thread time, as well as CRTL+Break, or Done_Multitasking executed form the mother process. ** ** ** ** ** ********* ** ** APPENDIX A - The Pascal sources: ** ** DEMO.PAS : executes several tasks parallely - simple demonstration PRODEMO1.PAS : reads an ascii text file as one task and displays the number of bytes read in percent as another task, a simple example of the use of the process unit. PRODEMO2.PAS : Shows the effectiveness of the use of the delay_task function PRODEMO3.PAS : Both demos count and of course PRODEMO3.PAS is faster! PRODEMO4.PAS : example for the task_priority, end_priority and the set_delay procedures, this demo display the tasklist PRODEMO5.PAS : uses dos calls in two different tasks. The re-entrant problem is solved with the task_priority procedure PRODEMO6.PAS : demonstration for the exec_exe function: the program executes two other programs parallel. PRODEMO7.PAS : demonstration for the create_exe function: the program executes two other programs parallel. SWITCH.PAS : more process communication with read_message. This program allows to switch between the pacman game and a process shell. SWITCH2.PAS : demonstrates the controlling of more than one process. You can kill and restart the pacman game from a process shell. SIGNAL.PAS : example of the dos unit's exec procedure and the use of signals. STEST.PAS : program executed as parallel process by SIGNAL.EXE STARS.PAS : program executed by PRODEMO6.EXE, PRODEMO7.EXE KALEND.PAS : program executed by PRODEMO6.EXE, PRODEMO7.EXE, SWITCH.EXE and SWITCH2.EXE PACMAN.PAS : simple pacman game to demonstrate the us of parallel tasks, run as dos program and executed by SWITCH.EXE and SWITCH2.EXE PCOPY.PAS : an emultaion of the DOS COPY command implemented as a parallel task: this example allows to copy several files parallely to disk. ******* ** ** ******* ** ** ** ** APPENDIX B - The pascal units ******* PROCESS.PAS : The multi process unit source code DUTILS.PAS : An utility unit, which provides direct screen access, etc. ****** ** ** ** ** ** ** APPENDIX C - What the unit isn't able to do ****** - As it was said in the Chapter 6, about the create_exe and exec_exe function, these functions don't create any dos environment. This of course saves memory (which is really rare!), but the use of the pascal functions getenv(), envstr(), envcount() doesn't create useful results. The paramstr(0) variable can not be initialised correctly, too. - The task switch doesn't save the cursor position. That's why I nearly almost use the print or cprint procedure of the dutils unit. Both procedures directly write into the video ram at the segment variable SCRTYP (dutils unit). ** ** *** ** **** ** ** ** ** ** **** ** *** Important Notes ** ** This is the third release of the process unit. The unit is especially designed for Turbo Pascal, Borland. Inc. Programs must be compiled for DOS, no protected mode and of course no windows! The unit uses only 16 Bit code, the EAX,EBX,ECX,EDX,EDI,ESI,FS and GS registers are not saved by the task switch. The programs had been tested under DOS, WINDOWS 3.11 and Windows 95. The following list shows frequently done errors that crashes the system: - In connection with the create_process function: + A task didn't end with kill_task or wasn't placed in an endless loop + The stacksize of the task had been too small. Pay attention, every local variable uses the stack, some routines of the process unit and of the dutils unit need over 3 Kbytes Stack (e.g. dutils.copyfile() ). This error can hardly be traced, because the use of task_list displays only the stack use at that moment and not the maximum of stack use You can solve the problem by giving always enough stack (16Kbytes e.g.) + You mix up the pids variable and kill, stop, delay the wrong task - With Multiprogramming, exec_exe, create_exe: + The heap size for the executed programs is too small, + Executed programs call the exit-procedure and exit to dos + You free the memory of an executed program while its tasks are still running + You try to run .com files with the exec_exe / create_exe procedure + The startup of the executed program use DOS calls you can't give task_priority, these DOS calls collide with another DOS call. Problems with the BIOS: + The pascal procedure crt.keypressed and/or crt.readkey may get mixed with the hardware interrupt 9 (keyboard). This may leed to a short or permanent disfunction of the keyboard. A typical sign for a this scene is the ignoring of a key (entering for example 'quit' and the program simply gets 'qut' ...). You might get a permanent disfunction if you press Crtl-Break, while crt.keypressed and/or crt.readkey collides with the hardware interrupt. Problems with WINDOWS 3.11: + If you are running more than one DOS-SHELL, some times multitasking programs cause trouble with other DOS Applications. Problems with Windows 95: + You should give your programs 640 KBytes main memory. The automatically memory allocation by Windows 95 sometimes fails. + You should execute your programs in a DOS-screen and not in a window. The process unit was coded at low level. As DOS was not build for multitasking, a bug in your program usually brings your system down. With Windows 95 you can check some errors and avoid the reboot process. Usually no errors should occur using the EMM386 driver Version 4.0. +========================================================================+ | You are using this code at your own risk. In any case you should read | | the manual before writing your own code. You should avoid direct | | hard disc access in your code, because a system break down during | | a hard disc writing process can crash your hard disc! | +========================================================================+ For this reason never use the EMM driver together with the Smartdriver's write behind cache! This software is public domain. You can use the code without admission. For commercial use you should add a note in the program's information. Dieter Pawelczak, July 1996, Sep 1996, Nov 1996, Jan 1998 email: et_514@eikon.e-technik.tu-muenchen.de dieterp@bigfoot.fe www: http://www.eikon.e-technik.tu-muenchen.de/~et_514/ http://www.geocities.com/SiliconValley/Bay/9159/