A job is a piece of code (or even just data) created in the Transient Program Area of the QDOS memory map. It exists and runs (if it is executable) as a separate task and is under the control of the job scheduler. It will have a job Id and an entry in the Jobs table.
Jobs can be independent or owned by other jobs, and may have child jobs of their own. Independent jobs are actually owned by SuperBasic which is the 'Mother of all jobs' and has an id of zero.
Jobs are what allows multi-tasking in QDOS, well, the scheduler is really, but each individual job gives the impression that multi-tasking is taking place.
In QDOS, there is a table which lives in memory and holds the absolute address in memory of all current jobs. This is not a pointer to the start address of the job's code, but to the start address of the job's header (see below) which is situated $68 bytes below the start of the job's code.
Each job in the system has a unique identifier. SuperBasic is the first job created after bootup and has the unique id of zero. This job cannot be removed from the system.
A job id is a long word (4 bytes) in which the high word is a unique tag word and the low word is an index that holds the position of the job's location in the job table.
As spaces in the job table are reused, the tag is used to make sure that the job id is always unique. This avoids the (rare) possibility that a job id may be stored by a parent job for some communication purposes. The child job dies or is removed accidentally. In the meantime, another job uses the child's slot in the job table. Now the parent would be talking to the wrong job !
The tag word simply increments each time a new job is created in the system.
So, if all you have is a job's id, finding where its code lives in memory is 'simple' :
In memory, a job looks like this :
|Job header||Job code||Job data & stack space|
The job code and data sizes are specified when the job is created and cannot be changed. The header is always $68 bytes and looks like this :
|$00||JB_LEN||Long||Job area size. Code + data + header.|
|$04||JB_START||Long||Absolute address of the start of the code. (JB_END)|
|$08||JB_OWNER||Long||Job id of this job's parent job.|
|$0C||JB_HOLD||Long||Address which is to be cleared when this job is removed from the system. Zero if not required.|
|$10||JB_TAB||Word||Job tag = Location in QDOS job table for this job.|
|$12||JB_PRIOR||Byte||This job's priority.|
|$13||JB_PRINC||Byte||Priority increment. Zero if job is inactive.|
|$14||JB_STAT||Word||Job status :|
|0 Possibly active.|
|+ve Delay until job's reactivation.|
|-1 Job is suspended.|
|-2 Job is waiting for another to finish.|
|$16||JB_RELA6||Byte||Bit 7 is set after a trap #4. To signal that some parameters are relative to A6 and not absolute.|
|$17||JB_WFLAG||Byte||Bit 7 set if another job is waiting for this one to finish.|
|$18||JB_TRAPV||Long||Pointer to this jobs RAM exception table or zero if not used. See MT_TRAPV (Trap #1).|
|$20||JB_D0||Long||Storage for D0.|
|$24||JB_D1||Long||Storage for D1.|
|$28||JB_D2||Long||Storage for D2.|
|$2C||JB_D3||Long||Storage for D3.|
|$30||JB_D4||Long||Storage for D4.|
|$34||JB_D5||Long||Storage for D5.|
|$38||JB_D6||Long||Storage for D6.|
|$3C||JB_D7||Long||Storage for D7.|
|$40||JB_A0||Long||Storage for A0.|
|$44||JB_A1||Long||Storage for A1.|
|$48||JB_A2||Long||Storage for A2.|
|$4C||JB_A3||Long||Storage for A3.|
|$50||JB_A4||Long||Storage for A4.|
|$54||JB_A5||Long||Storage for A5.|
|$58||JB_A6||Long||Storage for A6.|
|$5C||JB_A7||Long||Storage for A7.|
|$60||JB_SR||Word||Storage for job's status register.|
|$62||JB_PC||Long||Storage for the job's program counter.|
|$68||JB_END||Op-code This is where your job's code starts.|
Originally, the QDOS documentation specified the format of any extra data that appears on the stack when a job is first created. At that time, there was no way to actually get the extra data onto the job's stack (from SuperBasic at least). Since then, Toolkit 2, Turbo Toolkit and all the rest, have managed to make it easy to pass command strings and opened channels to the new job. These are passed on the new job's stack as follows :
The stack grows downwards into the data area. A7 holds the address of the top of the stack. If the long word at (A7) is zero, then no information is on the stack.
If the word at (A7) is not zero then this is a counter of the number of opened channels that have been passed to this job. This word is followed by a long word for each channel - the channel id - for the input channel, the output channel and the 'reporting' channel (in that order). If only one channel has been opened, this is the input channel and if only two channels have been opened, these are the input & output.
Above this list of channel ids, we have a word holding the length of a command string placed on the stack. And above this word, there are the bytes of the command string.
|Bytes of command string - padded to an even length|
|Length of command string (WORD) or Zero|
|Channel ID - report channel (LONG)||May not be present.|
|Channel ID - output channel (LONG)||May not be present.|
|Channel ID - input channel (LONG)||May not be present.|
|Number of channel ids on stack (WORD) or zero ←(A7)|
The stack pointer for the job, A7, points at the word holding the number of opened channels.
So if your job discovers that there are open channels for it to use on the stack, it should use them !
All jobs that can be activated are given a priority when they are activated. The priority for jobs that are EXEC/EXEC_W'd is 32 which is the same as the priority of SuperBasic.
In the original QDOS, priorities ranged from 0 to 127 and had to be positive. Then Minerva came along and allowed negative priorities as well, so now priorities range from -128 to +127.
If a job is given, or has, a priority of zero, then that job is deemed to be inactive and will never be executed (again). Priorities are relative, the bigger the priority, the bigger the slice of time it has to execute, but it is not proportional - a job with a priority of 10 doesn't get 10 times less time than a job with a priority of 100, for example.
An inactive job is simply a job whose priority is zet to zero. JB_PRINC is zero in the job's header. As long as this is the case, then the job will not be given a time slice by the scheduler.
Inactive jobs can be used to hold data for other jobs, but if so, must never be activated.
A suspended job is active, its header field, JB_PRINC, is not zero, but JB_STAT is a positive number. This is the amount of time (in fiftieths of a second, sixtieths if you live in the USA) until the job will be activated again.
Suspending a job is a good way of being 'friendly' to the rest of the machine as it allows the time slices that would have been allocated to that job to be utilised in other jobs - hopefully helping the response times of the system as a whole.
For example, a job that watches the keyboard for a specific key combination, say for a screen grabber program, need not be run at all times. Once per half second or so should be enough. This sort of job will suspend itself for 25 cycles (30 in the States) or half a second each time through its 'scanning' loop.
When a job has finished, it must call the operating system to kill itself. Killing a job releases all memory areas, channels, open files and any other resources it obtained while running. While there is no real need for the developer to manually reclaim these it is tidy to do so. In the event of a job error, for example, letting the system clean up is probably the best idea.
The code for a job to kill itself is:
job_end move.l d0,d3 Copy error code from D0.L to D3.L for exit to SuperBasic moveq #mt_frjob,d0 Job is about to die moveq #-1,d1 And it is this job trap #1 RIP (there is not return)
The above assumes that on entry to job_end, D0.L holds either zero (no error) or an error code. I have also seen programs where the developer doesn't quite trust mt_frjob to work correctly, so they (and I) write code like this instead:
job_end move.l d0,d3 Copy error code from D0.L to D3.L for exit to SuperBasic moveq #mt_frjob,d0 Job is about to die moveq #-1,d1 And it is this job trap #1 RIP (there is not return) bra.s job_end Should never get here, but ...
QDOS expects that a job's code be in a specific format. When writing a job, you must adhere to this format, which is :
start bra.s JobCode dc.l 0 dc.w $4AFB dc.w size of job name dc.b Bytes of job name JobCode Do stuff here !
The address 'start' is actually JB_END. The job flag is the word $4AFB and this must be located at JB_END + 6, the word defining the size of the job name is at JB_END + 8 and the job's name starts at JB_END + $0A.
At the end of a job, you don't just RTS back to SuperBasic, you have to remove the job, see Killing Jobs above for details.