Process Creation and Structure

An AmigaDOS process is an exec task extended with what dos.library needs: a message port for talking to handlers, a current directory, standard input/output/error streams, and the secondary result code returned by IoErr(). Most dos.library functions must be called from a process because they read and update these fields. This article covers struct Process field by field, every NP_* tag of CreateNewProc() with its real default and ownership rules, the legacy CreateProc(), process termination, and the accessor functions (several new in V47).

The rule of thumb throughout: use an accessor function whenever one exists (Input(), Cli(), GetCurrentDir(), IoErr() …). Only pr_WindowPtr and pr_TaskNum have no accessor and are meant to be touched directly.

struct Process

From dos/dosextens.h:

struct Process {
    struct Task    pr_Task;
    struct MsgPort pr_MsgPort;
    WORD    pr_Pad;
    BPTR    pr_SegList;
    LONG    pr_StackSize;
    APTR    pr_GlobVec;
    LONG    pr_TaskNum;
    BPTR    pr_StackBase;
    LONG    pr_Result2;
    BPTR    pr_CurrentDir;
    BPTR    pr_CIS;
    BPTR    pr_COS;
    APTR    pr_ConsoleTask;
    APTR    pr_FileSystemTask;
    BPTR    pr_CLI;
    APTR    pr_ReturnAddr;
    APTR    pr_PktWait;
    APTR    pr_WindowPtr;
    /* V36 and later */
    BPTR    pr_HomeDir;
    LONG    pr_Flags;
    void    (*pr_ExitCode)();
    LONG    pr_ExitData;
    STRPTR  pr_Arguments;
    struct MinList pr_LocalVars;
    ULONG   pr_ShellPrivate;
    BPTR    pr_CES;
};
Field Meaning Access
pr_Task exec task; tc_Node.ln_Type is NT_PROCESS (not NT_TASK) —
pr_MsgPort Port used by dos.library to exchange packets with handlers —
pr_Pad Alignment only —
pr_SegList BCPL segment array (not a seglist), see below leave alone
pr_StackSize Stack size in bytes (multiple of 4) — of the shell, not of a command it runs use tc_SPUpper - tc_SPLower
pr_GlobVec BCPL Global Vector; for C/assembler processes the shared system vector leave alone
pr_TaskNum Shell task number as printed by Status; 0 for Workbench programs, handlers and processes made by CreateNewProc()/CreateProc() read directly; FindCliProc()
pr_StackBase BPTR to the low end of the stack (start of the BCPL stack); initialized but unused, and wrong for commands leave alone
pr_Result2 Secondary result code IoErr(), SetIoErr()
pr_CurrentDir Current directory lock; ZERO = root of pr_FileSystemTask GetCurrentDir(), CurrentDir()
pr_CIS Standard input; may be ZERO Input(), SelectInput()
pr_COS Standard output; may be ZERO Output(), SelectOutput()
pr_ConsoleTask MsgPort of the console handler for * and CONSOLE: GetConsoleTask(), SetConsoleTask()
pr_FileSystemTask MsgPort of the file system used for the ZERO lock (boot volume by default) GetFileSysTask(), SetFileSysTask()
pr_CLI BPTR to struct CommandLineInterface, ZERO if not a shell Cli() (no setter: do not modify)
pr_ReturnAddr BCPL stack frame used by Exit() leave alone
pr_PktWait Optional function called to wait for a packet reply see below
pr_WindowPtr Window for error requesters; NULL = default public screen, -1 = no requesters read/write directly
pr_HomeDir Lock on the directory the program was loaded from (PROGDIR:) GetProgramDir(), SetProgramDir()
pr_Flags PRF_* cleanup flags, system use only do not touch
pr_ExitCode Function called first during process shutdown set via NP_ExitCode
pr_ExitData Argument for pr_ExitCode set via NP_ExitData
pr_Arguments Command-line arguments (without command name), or NULL GetArgStr(), SetArgStr()
pr_LocalVars Local shell variables and aliases; no semaphore, owner process only GetVar()/SetVar() family
pr_ShellPrivate Reserved for the Shell do not touch
pr_CES Standard error stream; ZERO means "use pr_COS". Present since V36 but only used from V47 ErrorOutput(), SelectError()

Notes on individual fields

  • Stack size on the stack. Before starting the process, dos.library pushes the stack size, so ((ULONG *)pr_Task.tc_SPUpper)[-1] holds the stack size in bytes. Some binaries, notably those built with Aztec (Manx) C, depend on this.
  • Trap handler. dos.library installs its own tc_TrapCode, which shows the "Software Failure" requester (suspend = Wait(0) forever; otherwise exec's Alert()). Up to V45 it misread 68K exception stack frames and left garbage on the stack before entering the ROM debugger.
  • Real stack bounds. pr_StackSize and pr_StackBase describe the shell's stack when a command runs in a shell. tc_SPLower/tc_SPUpper are always correct: StackSwap() and RunCommand() update them.
  • ZERO streams are not NIL:. Workbench-started programs get pr_CIS, pr_COS and pr_CES all ZERO, and no console task. Reading from or writing to a ZERO handle crashes. Older documentation claiming they receive NIL: handles is wrong.
  • ZERO home directory. If pr_HomeDir is ZERO (resident or built-in commands), resolving PROGDIR: makes GetDeviceProc() ask for a volume called PROGDIR — most likely a defect. GetProgramDir() returning ZERO means "no home directory", not "boot volume root". Never unlock pr_HomeDir; dos.library (or whoever loaded the program) does.
  • pr_WindowPtr in shell commands. Commands share their process with the shell and every other command run in it. A command that changes pr_WindowPtr must save the old value and restore it before exiting, otherwise the next error requester may reference a closed window and crash. Requesters raised by the file system itself (e.g. an unreadable block) are not redirected by this field.
  • pr_Arguments vs. ReadArgs(). The arguments are also in register a0 (length in d0) at startup and in the buffer of the pr_CIS handle. ReadArgs() reads the input buffer, not pr_Arguments, so SetArgStr() never changes what ReadArgs() sees.
/* Shell command that suppresses DOS requesters while it runs */
struct Process *me = (struct Process *)FindTask(NULL);
APTR oldwin = me->pr_WindowPtr;

me->pr_WindowPtr = (APTR)-1L;
/* ... work that might otherwise show "Please insert volume" ... */
me->pr_WindowPtr = oldwin;            /* always restore before returning */
/* Stack size of the running program, correct under Shell and Workbench */
struct Task *me = FindTask(NULL);
ULONG stack = (ULONG)me->tc_SPUpper - (ULONG)me->tc_SPLower;

pr_PktWait

If non-NULL, dos.library calls this function instead of waiting on pr_MsgPort whenever it waits for a packet reply:

struct Message *(*pr_PktWait)(void);   /* result in d0 */

It receives no arguments (find the process with FindTask(NULL)), must block until a message arrives, and must never return NULL. dos.library extracts the packet from the returned message. See DOS Packets.

Process flags (pr_Flags)

System use only — listed here to explain the cleanup logic:

Flag Value Cleanup action at exit Set by
PRF_FREESEGLIST 2.0+ 1 UnLoadSeg() of pr_SegList[3] NP_FreeSeglist
PRF_FREECURRDIR 2.0+ 2 UnLock() pr_CurrentDir any non-ZERO initial current directory
PRF_FREECLI 2.0+ 4 Free the CLI structure and everything it references NP_Cli
PRF_CLOSEINPUT 2.0+ 8 Close() pr_CIS NP_CloseInput
PRF_CLOSEOUTPUT 2.0+ 16 Close() pr_COS NP_CloseOutput
PRF_FREEARGS 2.0+ 32 Free pr_Arguments NP_Arguments
PRF_CLOSEERROR 3.2+ 64 Close() pr_CES (V47) NP_CloseError

The only way to avoid PRF_FREECURRDIR is to pass a ZERO lock in NP_CurrentDir.

pr_ExitCode

Called as the first step of process shutdown:

/* SAS/C register syntax */
LONG __asm ExitFunc(register __d0 LONG rc,        /* value in d0 at final RTS */
                    register __d1 LONG exitdata); /* pr_ExitData */

The return value is meant to replace the process result, but it is currently useless: the shutdown code goes straight into exec RemTask(), which discards all registers.

The BCPL legacy fields

pr_SegList points to an array whose entry 0 is a count and whose other entries are BPTRs to segment lists: entries 1 and 2 hold the BCPL klib and blib system segments, entry 3 the program's own seglist, and entry 4 the shell's seglist when the process is a shell (the shell moves itself from 3 to 4 to make room for commands). The count is 3 for BCPL programs (handlers with GLOBVEC 0 or -3) and 2 for C/assembler programs, so the binder ignores entry 3 for them. pr_GlobVec and pr_ReturnAddr belong to the same mechanism. See BCPL Runtime Binder; this layout reflects V47 and may change.

Creating a process: CreateNewProc()

struct Process *CreateNewProc(CONST struct TagItem *tags);          /* V36 */
struct Process *CreateNewProcTagList(CONST struct TagItem *tags);   /* same entry */
struct Process *CreateNewProcTags(ULONG tag1type, ...);             /* varargs stub */

Callable from a plain task as well. On success the process is already running (depending on its priority). On failure it returns NULL and does not set IoErr() consistently.

Tags (dos/dostags.h)

Tag Type Default Ownership / notes
NP_Seglist 2.0+ BPTR — Seglist to run, entered at the first byte of the first segment. One of NP_Seglist/NP_Entry is required
NP_FreeSeglist 2.0+ BOOL FALSE UnLoadSeg() the seglist at exit. Never freed if creation fails. Must be absent or FALSE with NP_Entry
NP_Entry 2.0+ APTR — Absolute entry address; mutually exclusive with NP_Seglist
NP_Input 2.0+ BPTR Open("NIL:") Becomes pr_CIS. On failure the caller still owns a handle passed here; the default NIL: handle is cleaned up
NP_CloseInput 2.0+ BOOL TRUE Closes whatever is in pr_CIS at exit
NP_Output 2.0+ BPTR Open("NIL:") Becomes pr_COS; same ownership rule as NP_Input
NP_CloseOutput 2.0+ BOOL TRUE Closes whatever is in pr_COS at exit
NP_Error 2.0+ BPTR ZERO Becomes pr_CES (ZERO = fall back to pr_COS). Not implemented before V47
NP_CloseError 2.0+ BOOL FALSE V47; closes whatever is in pr_CES at exit. Different default for compatibility
NP_CurrentDir 2.0+ BPTR DupLock() of caller's directory (ZERO from a task) Absorbed: released at exit, and also released if creation fails
NP_StackSize 2.0+ LONG 4000 Bytes
NP_Name 2.0+ STRPTR "New Process" Copied; the caller keeps the original
NP_Priority 2.0+ LONG 0 −128…127; useful range 0…19
NP_ConsoleTask 2.0+ struct MsgPort * caller's console (NULL from a task) Handler for * and CONSOLE:
NP_WindowPtr 2.0+ struct Window * see below Becomes pr_WindowPtr
NP_HomeDir 2.0+ BPTR DupLock() of caller's pr_HomeDir (ZERO from a task) Absorbed: released at exit even if it is the caller's own lock (pass a DupLock()), and released if creation fails
NP_CopyVars 2.0+ BOOL TRUE (per dostags.h) Deep copy of the caller's local variables and aliases; nothing is copied from a task
NP_Cli 2.0+ BOOL FALSE Create a CommandLineInterface for the new process (pr_CLI)
NP_Path 2.0+ BPTR copy of caller's path (if caller has a CLI) Only with NP_Cli. Referenced, not copied: owned by the new process on success, still the caller's on failure
NP_CommandName 2.0+ STRPTR caller's command name, else empty Only with NP_Cli; copied
NP_Arguments 2.0+ STRPTR none Copied into pr_Arguments, a0/d0, and the pr_CIS buffer. NP_Input must then not be ZERO
NP_ExitCode 2.0+ function none Installed in pr_ExitCode
NP_ExitData 2.0+ LONG — Passed to NP_ExitCode in d1
NP_NotifyOnDeath, NP_Synchronous — — Defined but ignored

Note: the NDK 3.2 autodoc and dos/dostags.h state that NP_Priority and NP_WindowPtr default to the same value as the parent.

NP_WindowPtr default. Without the tag, the caller's window pointer is copied only if it is NULL or -1; any real window pointer becomes NULL in the child (and it is NULL from a task). This keeps the child from outliving the parent's window. To share the caller's window, pass the tag explicitly — and keep the window open as long as the child runs.

Not a tag: pr_FileSystemTask is copied from the caller, or taken from dos.library's default file system when the caller is a task.

NP_Cli details. Prompt, path and command name are copied from the caller's CLI if there is one, otherwise defaults are used. cli_CurrentInput and cli_StandardInput are set to the new pr_CIS, cli_StandardOutput to pr_COS. Startup code should leave these streams alone: some commands (e.g. Execute) compare the process streams with those in the CLI structure to detect redirection. To run shell commands, System() is usually the better tool.

Note: the NDK 3.2 dos/dostags.h states that NP_Arguments was not operational until V37 (random registers in V36).

What is released on failure

CreateNewProc() is inconsistent here. If it returns NULL:

  • Released by dos: NP_CurrentDir and NP_HomeDir locks (and any default NIL: handles, the copied name/arguments and an implicitly copied path).
  • Still yours: the seglist (regardless of NP_FreeSeglist), handles passed in NP_Input/NP_Output/NP_Error, and an explicit NP_Path list.

Note: the NDK 3.2 autodoc states that on a NULL return you must free items passed via tags, such as NP_CurrentDir. Doing so would unlock the lock twice.

Example: run a loaded program as a background process

#include <exec/types.h>
#include <dos/dos.h>
#include <dos/dostags.h>
#include <proto/dos.h>

BOOL LaunchBackground(STRPTR file)
{
    struct Process *pr;
    BPTR seg, out;

    if (!(seg = LoadSeg(file)))
        return FALSE;

    out = Open("CON:0/0/480/120/Child/AUTO/CLOSE/WAIT", MODE_NEWFILE);
    if (!out) {
        UnLoadSeg(seg);
        return FALSE;
    }

    pr = CreateNewProcTags(NP_Seglist,     (ULONG)seg,
                           NP_FreeSeglist, TRUE,
                           NP_Output,      (ULONG)out,
                           NP_CloseOutput, TRUE,
                           NP_StackSize,   8192,
                           NP_Name,        (ULONG)"Child",
                           TAG_DONE);
    if (!pr) {
        /* seglist and NP_Output stay ours on failure; the default
           current dir / home dir locks were already released by dos */
        Close(out);
        UnLoadSeg(seg);
        return FALSE;
    }
    return TRUE;    /* child frees seg and closes out when it exits */
}

On V47, ScanStackToken(seg, 8192) gives the stack size honouring the program's $STACK: cookie.

Starting Workbench or Shell programs

CreateNewProc() alone does not provide the environment such programs expect:

  • Workbench programs also need a WBStartup message (workbench/startup.h) sent to the new process's port. It can be built and sent by hand; the WBLoad program added with AmigaOS version 45 does exactly that.
  • Shell commands should be run with RunCommand() (overlays the calling process, which must already have a shell environment) or System() (creates a shell and runs the command line in it).

CreateProc() (legacy)

struct MsgPort *CreateProc(CONST_STRPTR name, LONG pri, BPTR segList, LONG stackSize);
  • Returns the new process's pr_MsgPort, not a struct Process *. Subtract sizeof(struct Task) to get the process.
  • Name is copied; pr_ConsoleTask, pr_WindowPtr and pr_FileSystemTask are copied from the caller (NULL/0 and dos.library's default file system from a task).
  • Input, output and error handles are ZERO; current and home directory are ZERO; no CLI, no arguments, no variables.
  • The seglist is never freed, and there is no protocol telling you when it is safe to free it. Prefer CreateNewProc() with NP_FreeSeglist unless V34 compatibility is required.
  • Callable from a task since V36; in V34 and below that crashes.
  • Returns NULL on failure without setting IoErr() reliably.

A seglist that will never be passed to UnLoadSeg() can be faked in assembler:

        CNOP    0,4             ; long-word align
        DC.L    16              ; segment "length" (faked)
        DC.L    0               ; BPTR to next segment: none
        ; ... start of code ...

Pass MKBADDR() of the address of the DC.L 0 link word as the seglist.

Terminating a process

There is no function to kill another process. A process ends when its code returns from main() or executes its final RTS; the AmigaDOS shutdown code then calls pr_ExitCode, releases what the PRF_* flags say, and removes the task.

Method Verdict
return from main() / final RTS Correct
C library exit() Correct for C programs: releases ANSI-C resources too
Exit(rc) Avoid. BCPL legacy: unwinds the BCPL stack frame in pr_ReturnAddr and returns to the process shutdown code or to the shell (RunCommand() cleanup). Releases only what AmigaDOS allocated at startup, nothing of the program's or its C startup code. rc is ignored for processes
exec RemTask() Never: skips even the AmigaDOS cleanup
VOID Exit(LONG returnCode);

atexit() in ANSI C or NP_ExitCode are the clean ways to hook shutdown.

Stack size

NP_StackSize defaults to 4000 bytes. V34 and below reserved 1500 bytes at the bottom of the 68K stack for the BCPL environment of every called function, so DOS-using programs needed a larger stack, while programs not using dos.library could get away with about 2000 bytes. The default stack can only be raised by the program's $STACK: cookie (see Hunk Version and Stack Cookies).

Accessor functions

These act on the calling process and do not touch IoErr() unless stated.

Function Since Field Notes
BPTR Input(void) all pr_CIS ZERO if none; do not Close() it
BPTR SelectInput(BPTR fh) V36 pr_CIS Returns old handle; restore before exit
BPTR Output(void) all pr_COS ZERO if none
BPTR SelectOutput(BPTR fh) V36 pr_COS Restore before exit
ErrorOutput(void) V47 pr_CES Returns pr_CES, else pr_COS, else ZERO — the stderr equivalent
SelectError(BPTR fh) V47 pr_CES Restore before exit
BPTR GetCurrentDir(void) V47 pr_CurrentDir Does not change the directory
BPTR CurrentDir(BPTR lock) all pr_CurrentDir The new lock now belongs to the process; restore the original lock (not a copy) before a shell command exits
LONG IoErr(void) / LONG SetIoErr(LONG) all / V36 pr_Result2 SetIoErr(0) before calls that do not always set it
struct MsgPort *SetConsoleTask(struct MsgPort *) V36 pr_ConsoleTask Takes a port, not a process; revert before returning to the shell
struct MsgPort *SetFileSysTask(struct MsgPort *) V36 pr_FileSystemTask Should match SYS:; changing it makes path resolution inconsistent between processes
BPTR GetProgramDir(void) V36 pr_HomeDir ZERO = no home directory; never unlock
BPTR SetProgramDir(BPTR lock) V36 pr_HomeDir Restore the original before exit
STRPTR GetArgStr(void) V36 pr_Arguments NULL for Workbench programs; excludes the command name
STRPTR SetArgStr(STRPTR) V36 pr_Arguments Does not affect ReadArgs()
struct CommandLineInterface *Cli(void) V36 pr_CLI No setter

Note: the NDK 3.2 clib/dos_protos.h declares ErrorOutput() and SelectError() as returning LONG.

DoPkt() also works from a plain task, but then cannot set IoErr(); CreateNewProc() and (since V36) CreateProc() are task-callable too. AddPart(), FilePart() and PathPart() only manipulate strings and need no process.

See Also


Sources: Thomas Richter, Amiga ROM Kernel Reference Manual: AmigaDOS (2024, v1.27), ch. 10 (pp. 140–161); NDK 3.2 R4 dos/dosextens.h, dos/dostags.h, clib/dos_protos.h, Autodocs/dos.doc Updated: 2026-11-13