Complete rewrite of 14 exec_os articles from stubs to comprehensive deep-dive technical references with architecture diagrams, pitfalls, and best practices. New: multitasking.md (scheduler, IPC, memory safety, real-world scenarios) Enriched: exec_base, tasks_processes, library_system, library_vectors, interrupts, exceptions_traps, memory_management, message_ports, signals, semaphores, io_requests, lists_nodes, resident_modules Updated indexes: 06_exec_os/README.md, root README.md
14 KiB
Tasks and Processes — Structures, States, Creation, Scheduling
Overview
AmigaOS uses preemptive priority-based scheduling with round-robin time-sharing among equal-priority tasks. Tasks are the fundamental unit of execution; Processes are Tasks with an additional DOS environment (message port, CLI context, segment list, filesystem handles). The scheduler runs at each vertical blank interrupt (50/60 Hz) and after any Signal() or Wait() call.
For the full multitasking deep-dive — scheduler algorithm, context switch costs, IPC strategies, and memory safety — see Multitasking.
Task vs Process
graph TB
subgraph "struct Process"
subgraph "struct Task (embedded)"
NODE["Node<br/>ln_Name, ln_Pri, ln_Type"]
SIGS["Signal Fields<br/>tc_SigAlloc, tc_SigWait,<br/>tc_SigRecvd"]
STACK["Stack Bounds<br/>tc_SPLower, tc_SPUpper,<br/>tc_SPReg"]
EXCEPT["Exception<br/>tc_ExceptCode,<br/>tc_ExceptData"]
end
PORT["pr_MsgPort<br/>(built-in message port)"]
DOS["DOS Context<br/>pr_CurrentDir, pr_CIS, pr_COS,<br/>pr_CLI, pr_Arguments"]
SEG["pr_SegList<br/>(loaded code segments)"]
end
style NODE fill:#e8f4fd,stroke:#2196f3,color:#333
style DOS fill:#e8f5e9,stroke:#4caf50,color:#333
| Capability | Task | Process |
|---|---|---|
| Scheduling | ✅ | ✅ |
| Signals | ✅ | ✅ |
| Message Ports | Manual setup | ✅ Built-in pr_MsgPort |
| DOS I/O (Open/Read/Write) | ❌ | ✅ |
| CLI environment | ❌ | ✅ (if started from Shell) |
| Current directory | ❌ | ✅ pr_CurrentDir |
| stdin/stdout/stderr | ❌ | ✅ pr_CIS/pr_COS/pr_CES |
| Local variables | ❌ | ✅ pr_LocalVars |
Rule of thumb: Use
CreateNewProcTags()for everything. Use rawAddTask()only for bare-metal interrupt-level code or when you explicitly don't need DOS.
struct Task
/* exec/tasks.h — NDK39 */
struct Task {
struct Node tc_Node; /* ln_Type=NT_TASK or NT_PROCESS */
/* ln_Pri = scheduling priority (-128 to +127) */
/* ln_Name = task name string */
UBYTE tc_Flags; /* TF_LAUNCH, TF_SWITCH, TF_EXCEPT */
UBYTE tc_State; /* TS_RUN, TS_READY, TS_WAIT, TS_EXCEPT */
BYTE tc_IDNestCnt; /* interrupt disable nesting (-1 = enabled) */
BYTE tc_TDNestCnt; /* task disable (Forbid) nesting (-1 = enabled) */
ULONG tc_SigAlloc; /* allocated signal bits mask */
ULONG tc_SigWait; /* signals this task is waiting for */
ULONG tc_SigRecvd; /* signals received but not yet consumed */
ULONG tc_SigExcept; /* signals that trigger tc_ExceptCode */
UWORD tc_TrapAlloc; /* allocated trap vectors */
UWORD tc_TrapAble; /* enabled trap vectors */
APTR tc_ExceptData; /* data pointer passed to exception handler */
APTR tc_ExceptCode; /* exception handler function */
APTR tc_TrapData; /* data pointer passed to trap handler */
APTR tc_TrapCode; /* trap handler function */
APTR tc_SPReg; /* saved stack pointer (when not running) */
APTR tc_SPLower; /* lowest valid stack address */
APTR tc_SPUpper; /* highest valid stack address + 2 */
void (*tc_Switch)(); /* called when task is switched OUT */
void (*tc_Launch)(); /* called when task is switched IN */
struct List tc_MemEntry; /* list of memory entries to free on RemTask */
APTR tc_UserData; /* application-private data pointer */
};
Key Field Reference
| Field | Description |
|---|---|
tc_Node.ln_Pri |
Scheduling priority (−128 to +127). Higher = more CPU time |
tc_State |
Current state: TS_RUN, TS_READY, TS_WAIT, TS_EXCEPT, TS_REMOVED |
tc_IDNestCnt |
Interrupt disable nesting counter. −1 = interrupts enabled |
tc_TDNestCnt |
Task disable nesting counter. −1 = task switching enabled |
tc_SigAlloc |
Bitmask of allocated signal bits (1 = allocated) |
tc_SigWait |
Bitmask of signals this task will wake for (set by Wait()) |
tc_SigRecvd |
Bitmask of signals received (set by Signal(), cleared by Wait()) |
tc_SPReg |
Saved SP when task is not running — points to saved context on stack |
tc_SPLower / tc_SPUpper |
Stack bounds — exec fills these with guard patterns for stack overflow detection |
tc_MemEntry |
List of MemEntry structures — automatically freed by RemTask() |
tc_Switch / tc_Launch |
Optional callbacks on context switch — used by FPU context save/restore |
struct Process (extends Task)
/* dos/dosextens.h — NDK39 */
struct Process {
struct Task pr_Task; /* embedded Task — MUST be first field */
struct MsgPort pr_MsgPort; /* built-in message port for DOS packets */
UWORD pr_Pad;
BPTR pr_SegList; /* segment list (loaded code) */
LONG pr_StackSize; /* stack size in bytes */
APTR pr_GlobVec; /* BCPL global vector (legacy) */
LONG pr_TaskNum; /* CLI task number (0 = Workbench) */
BPTR pr_StackBase; /* base of stack (BPTR) */
LONG pr_Result2; /* secondary result (IoErr()) */
BPTR pr_CurrentDir; /* current directory lock */
BPTR pr_CIS; /* current input stream (stdin) */
BPTR pr_COS; /* current output stream (stdout) */
APTR pr_ConsoleTask; /* console handler task */
APTR pr_FileSystemTask; /* filesystem handler task */
BPTR pr_CLI; /* pointer to CommandLineInterface */
APTR pr_ReturnAddr; /* return address for exit */
APTR pr_PktWait; /* custom packet wait function */
APTR pr_WindowPtr; /* window for error requesters (or −1 to suppress) */
BPTR pr_HomeDir; /* home directory of program */
LONG pr_Flags; /* PR_FREESEGLIST, PR_FREEARGS, etc. */
void (*pr_ExitCode)(); /* exit handler */
LONG pr_ExitData; /* data for exit handler */
UBYTE *pr_Arguments; /* argument string */
struct MinList pr_LocalVars; /* local shell environment variables */
ULONG pr_ShellPrivate; /* shell private data */
BPTR pr_CES; /* current error stream (stderr) — V39+ */
};
Important Process Fields
| Field | Description |
|---|---|
pr_MsgPort |
Built-in message port — used for DOS packet communication |
pr_CurrentDir |
Lock on current directory — inherited from parent |
pr_CIS / pr_COS / pr_CES |
File handles for stdin, stdout, stderr |
pr_CLI |
Non-NULL if started from CLI/Shell, NULL if from Workbench |
pr_WindowPtr |
Window for error requesters. Set to −1 to suppress "Please insert volume" dialogs |
pr_Arguments |
Raw argument string (not parsed — use ReadArgs()) |
pr_Result2 |
Secondary error code — retrieved by IoErr() |
pr_ExitCode |
Called when process exits — cleanup handler |
Task States
| State | Constant | Value | Meaning |
|---|---|---|---|
| Invalid | TS_INVALID |
0 | Not a valid task |
| Added | TS_ADDED |
1 | Just added, not yet scheduled |
| Running | TS_RUN |
2 | Currently executing (exactly one task) |
| Ready | TS_READY |
3 | On SysBase→TaskReady, waiting for CPU |
| Waiting | TS_WAIT |
4 | Blocked on Wait() — on SysBase→TaskWait |
| Exception | TS_EXCEPT |
5 | Handling a task-level exception |
| Removed | TS_REMOVED |
6 | Removed from scheduling |
State Machine
stateDiagram-v2
[*] --> ADDED : AddTask() / CreateNewProc()
ADDED --> READY : Exec enqueues by priority
READY --> RUN : Scheduler picks highest priority
RUN --> READY : Quantum expired / higher-priority task wakes
RUN --> WAIT : Wait(signal_mask)
WAIT --> READY : Signal() delivers matching signal
RUN --> EXCEPT : Exception signal received
EXCEPT --> RUN : Exception handler returns
RUN --> REMOVED : RemTask() / task function returns
REMOVED --> [*]
Scheduling: Priority-Based Round Robin
The scheduler (exec.library internal) picks the highest-priority task from SysBase→TaskReady:
| Priority Range | Typical Use |
|---|---|
| +127 | (Unused — would starve everything) |
| +20 | input.device handler |
| +10 | trackdisk.device, filesystem handlers |
| +5 | Real-time applications (audio players) |
| 0 | Normal applications |
| −1 | Background workers (file copy, indexing) |
| −128 | Idle task |
/* Change priority of current task */
BYTE oldPri = SetTaskPri(FindTask(NULL), 5); /* LVO -300 */
/* Returns old priority */
Warning
: Priority > 20 will starve the input handler. Priority > 10 will starve filesystem tasks. Choose wisely.
Creating Tasks
Raw Task (exec level)
#define STACKSIZE 4096
/* Allocate task structure and stack together */
struct Task *task = AllocMem(sizeof(struct Task), MEMF_PUBLIC | MEMF_CLEAR);
APTR stack = AllocMem(STACKSIZE, MEMF_ANY);
task->tc_Node.ln_Type = NT_TASK;
task->tc_Node.ln_Name = "MyTask";
task->tc_Node.ln_Pri = 0;
task->tc_SPLower = stack;
task->tc_SPUpper = (APTR)((ULONG)stack + STACKSIZE);
task->tc_SPReg = task->tc_SPUpper; /* Stack grows downward */
/* AddTask(task, initialPC, finalPC) */
AddTask(task, MyTaskEntry, NULL); /* LVO -282 */
/* NULL finalPC = exec's default task cleanup */
Caution: Raw
AddTask()tasks cannot call DOS functions (Open, Read, Write, Printf). They have nopr_MsgPort, no current directory, no stdin/stdout. UseCreateNewProcTags()for anything that needs I/O.
Process (DOS level — preferred)
struct Process *proc = CreateNewProcTags(
NP_Entry, MyProcEntry, /* entry function */
NP_Name, "MyProcess", /* process name */
NP_StackSize, 8192, /* stack size in bytes */
NP_Priority, 0, /* scheduling priority */
NP_CurrentDir, DupLock(GetProgramDir()), /* inherit directory */
NP_Input, NULL, /* or a file handle for stdin */
NP_Output, NULL, /* or a file handle for stdout */
NP_CloseInput, FALSE, /* don't close stdin on exit */
NP_CloseOutput,FALSE, /* don't close stdout on exit */
TAG_DONE
);
if (!proc) { /* creation failed */ }
Task Cleanup: tc_MemEntry
Memory added to tc_MemEntry is automatically freed when the task is removed:
/* Add stack + task struct to auto-cleanup list */
struct MemList *ml = AllocMem(sizeof(struct MemList) + sizeof(struct MemEntry),
MEMF_PUBLIC | MEMF_CLEAR);
ml->ml_NumEntries = 2;
ml->ml_ME[0].me_Un.meu_Addr = task;
ml->ml_ME[0].me_Length = sizeof(struct Task);
ml->ml_ME[1].me_Un.meu_Addr = stack;
ml->ml_ME[1].me_Length = STACKSIZE;
AddHead(&task->tc_MemEntry, &ml->ml_Node);
/* Now RemTask() will free both task struct and stack */
Task Identity
/* Get current task */
struct Task *me = FindTask(NULL); /* LVO -294 — NULL = current */
Printf("Running as: %s (pri %ld)\n", me->tc_Node.ln_Name, me->tc_Node.ln_Pri);
/* Find another task by name */
Forbid();
struct Task *other = FindTask("TargetApp"); /* Returns NULL if not found */
Permit();
/* Check if current task is a Process */
if (me->tc_Node.ln_Type == NT_PROCESS)
{
struct Process *pr = (struct Process *)me;
/* Safe to use pr_CLI, pr_MsgPort, pr_CurrentDir, etc. */
}
Removing Tasks
/* Remove current task (task commits suicide): */
RemTask(NULL); /* LVO -288 — NULL = remove self */
/* Never returns — task is destroyed */
/* Remove another task (dangerous!): */
Forbid();
struct Task *victim = FindTask("OtherTask");
if (victim) RemTask(victim);
Permit();
Caution:
RemTask()on another task does NOT:
- Close its open files
- Free its message ports
- Reply to pending messages
- Close its libraries
This leaks resources permanently. Use
Signal()+ cooperative shutdown instead.
Pitfalls
1. Calling DOS from a Raw Task
/* BUG — raw Task has no DOS environment */
void __saveds MyTaskFunc(void)
{
BPTR fh = Open("RAM:test", MODE_NEWFILE); /* CRASH — no pr_FileSystemTask */
}
2. Stack Overflow
/* BUG — recursive function exhausts 4 KB stack */
void MyTask(void)
{
char buffer[2048]; /* Half the stack gone in one frame */
ProcessData(buffer);
MyTask(); /* Stack overflow → corrupts next task's memory */
}
The system does NOT catch stack overflows — memory just gets silently corrupted. Use StackSwap() for deep recursion.
3. RemTask Without Cleanup
/* BUG — resources leaked */
void MyProcess(void)
{
struct Library *base = OpenLibrary("mylib.library", 0);
struct MsgPort *port = CreateMsgPort();
/* ... */
RemTask(NULL); /* Library not closed, port not freed */
}
4. Caching Task Pointers
/* BUG — task may have exited */
struct Task *t = FindTask("Worker");
/* ... some time passes ... */
Signal(t, mask); /* t may be freed memory */
Best Practices
- Use
CreateNewProcTags()— rawAddTask()is for kernel-level code only - Use
tc_MemEntryfor automatic cleanup of task-allocated memory - Always check
FindTask()return — tasks can exit at any time - Use cooperative shutdown — send a signal, let the task clean up and exit itself
- Set
pr_WindowPtr = -1to suppress "Please insert volume" dialogs in background tasks - Size stacks generously — 8192+ bytes for processes, 4096+ for tasks
- Use
StackSwap()if you need temporary deep stack for recursive algorithms - Never
RemTask()another task in production code — it leaks everything
References
- NDK39:
exec/tasks.h,dos/dosextens.h,dos/dostags.h - ADCD 2.1:
AddTask,RemTask,FindTask,SetTaskPri,CreateNewProcTags,StackSwap - See also: Multitasking — scheduler algorithm, context switch, IPC strategies
- See also: Signals — tc_SigAlloc, tc_SigRecvd, tc_SigWait
- Amiga ROM Kernel Reference Manual: Exec — tasks and scheduling chapter