QDOS Internals

Anything you never knew you wanted to know about the Sinclair QL.

User Tools

Site Tools


Job Details

What is a job ?

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.

The Job Table.

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.

Job Ids.

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' :

  • Find the base of the QDOS job table. This lives at the address SV_JBBAS(A6) and continues up to, but not including SV_JBTOP(A6) in memory.
  • Take the low word of the job id and multiply it by 4 to get the offset from SV_JBBAS(A6) to the entry for this job.
  • Read the long word from the table at the address given by SV_JBBAS(A6) plus your calculated offset. This is the address of JB_LEN in the jobs header.
  • Add $68 to this address to get the first byte of code in the actual job.
  • In the above, A6 is assumed to hold the base address of the system variables. This used to be always $28000 but can and probably will move around.
  • Easy stuff this ;-)

Job Headers

In memory, a job looks like this :

Job headerJob codeJob 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 :

$00JB_LENLongJob area size. Code + data + header.
$04JB_STARTLongAbsolute address of the start of the code. (JB_END)
$08JB_OWNERLongJob id of this job's parent job.
$0CJB_HOLDLongAddress which is to be cleared when this job is removed from the system. Zero if not required.
$10JB_TABWordJob tag = Location in QDOS job table for this job.
$12JB_PRIORByteThis job's priority.
$13JB_PRINCBytePriority increment. Zero if job is inactive.
$14JB_STATWordJob status :
0 Possibly active.
+ve Delay until job's reactivation.
-1 Job is suspended.
-2 Job is waiting for another to finish.
$16JB_RELA6ByteBit 7 is set after a trap #4. To signal that some parameters are relative to A6 and not absolute.
$17JB_WFLAGByteBit 7 set if another job is waiting for this one to finish.
$18JB_TRAPVLongPointer to this jobs RAM exception table or zero if not used. See MT_TRAPV (Trap #1).
$20JB_D0LongStorage for D0.
$24JB_D1LongStorage for D1.
$28JB_D2LongStorage for D2.
$2CJB_D3LongStorage for D3.
$30JB_D4LongStorage for D4.
$34JB_D5LongStorage for D5.
$38JB_D6LongStorage for D6.
$3CJB_D7LongStorage for D7.
$40JB_A0LongStorage for A0.
$44JB_A1LongStorage for A1.
$48JB_A2LongStorage for A2.
$4CJB_A3LongStorage for A3.
$50JB_A4LongStorage for A4.
$54JB_A5LongStorage for A5.
$58JB_A6LongStorage for A6.
$5CJB_A7LongStorage for A7.
$60JB_SRWordStorage for job's status register.
$62JB_PCLongStorage for the job's program counter.
$66JB_NUWordNot used.
$68JB_END Op-code This is where your job's code starts.

Job Stack

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.

Inactive Jobs

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.

Suspended Jobs

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.

Killing Jobs

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 ...

Writing Jobs

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.

qdosmsq/jobs/jobs.txt · Last modified: 2020/11/16 19:44 by superuser