Task Creation

Two routines, DoFork and NewKernelTask, in the file newtask.c handle the creation of tasks. NewKernelTask creates a task running as part of the kernel, with the appropriate privileges. DoFork duplicates the current task (this implements the fork system call) and returns the pid of the new task to the routine that caaled it, and 0 to the new routine. (Because fork duplicates the current task it, in effect, returns twice - once in the original task, once in the new one. It returns different values so that the tasks know which one they are. The normal thing that happens is that the new task will immediately call DoExec to load a new program from disk, whereas the original task will just continue to do other things.)

NewKernelTask is the easier one to understand. First interrupts are suspended; we don't want anything to interfere whilst we are creating the task. The stack pointer is set to point to a temporary stack which is filled with values which would be expected after an interrupt has been called (the new process will actually be activated by a task switch which is activated by an interrupt, so we must simulate that situation for the new task). This will include the start address of the main routine for the kernel task. The waiting field of the task structure is set to 0 to indicate that the task is ready to run. A new Page Directory is created for the task by the call to VCreatePageDir and this is stored in the cr3 field; also the ds field is set. A few other fields in the task structure are also initialized, the others will have zero values. Having filled in these necessary fields in the task structure (the remainder, except for firstfreemem, which will be set shortly, are unimportant at the moment; these will be filled in next time the task is switched from - they will be important then) the task is nearly ready to run.

Note that I said that some values are put on the new task's stack (and we also set up some values in its data segment to initialize the heap). This sounds straightforward enough, but there is a slight problem that isn't immediately apparent - the entries in our Page Table point to the pages for the current task, not the new one. There are different ways that we could deal with this (accessing the Page Table of the new task comes to mind), but the simple remedy that I hae adopted is to have pointers to those values in the Page Table for the current task. When we called VCreatePageDir earlier in this routine it set up entries TempUserStack and TempUserData in the current Page Table which point to the appropriate pages for the new task. Thus a write to an address in TempUserStack, for example, is also a write to UserStack in the new task.

Now the task has a task structure, a memory map, and code and data. The only thing left to do is to set the field firstfreemem in the task structure, and to link the task structure into the linked list of runnable tasks by a call to LinkTask. Now we re-enable interrupts and the task is ready to take its turn at the next task switch. (You can appreciate why we didn't want any interrupts - and hence, potentially, task switches - whilst we were setting the task up. It wouldn't have been pretty if the task were activated whilst it was still being set up!)

DoFork is a fairly simple function. It duplicates the task structure and Page Table of the current task (creating new pages for the task specific parts of memory - this is all handled in VCreatePageDir), creates the standard FCBs (more details in Filesystem) and links the new task into the queues. As we want the new task to start at this point a routine SaveRegisters is called to save the registers at this point to the new task structure. The routine now returns the appropriate value and the forked task is ready to run.

DoExec is also fairly simple in principle; it takes parameters specifying the filename and the complete command line (including arguments). All executables are assumed to be in the /BIN directory, so a full path to the file is constructed and the file opened. Assuming the open didn't fail (in which case the routine now terminates and retuns an error code) we can proceed to read the code from the disk file with a series of ReadFile calls. The first of these calls merely skips past a 4-character header; I really ought to check this reads "IJ64" to ensure that this is a valid program file. Next the lengths of the code and data are read. Now we release the memory pages and Page Table Entries for code, data, and stacks and create new pages and entries. (This might seem pointless, but it may be that the calling program occupied more pages than the new one. It might be better to retain the first pages in each category - something to look at - to save a little time.) The code and data themselves are now read into their respective memory pages and the file is closed. Next we parse the environment string to provide the array of pointers to strings for argv[] and to set the value of argc. A little inline assembler is called here to move these values into rsi and rdi, respectively, which is where the main(int argc, char **argv) routine of the user program expects to find them.

The new code is all set to run now, so we return. But how does the new code know what address to start at? That's easy; DoExec is called from the execve system call, so we return to the code in syscalls.s. The syscall instruction saves the address it was called from in register rcx and sysretq will return to whatever value is stored in that register. For most system calls we save this register on the stack and pop it back again just before the return (indeed we do that for the execve system call when the return value from DoExec is non-zero), but in this case we just place the value $UserCode in rcx; this is the address that all user programs start at.

As well as creating tasks we want to be able to terminate them; this is accomplished by the function KillTask in newtask.c, which kills the current task. This is fairly straightforward - first the task is moved from the runnable tasks queue to the dead tasks one, and removed from the all tasks queue. Next memory that has been allocated to the task (e.g. by the filesystem for FCBs) is released. We also need to mark all physical pages allocated to the task as free; this is actually handled by dummyTask, which will also free up the Process Table entry. This can be done because the elements of the PMap[] array record which task each page belongs to. It's probably safer to do this in a separate task, to avoid the possibility of memory being reallocated whilst it is still in use. One other thing is done by KillTask; if the field parentPort in its process table entry is non-zero it means that the process that created the task wants to be notified, via a message sent to this port, that the task has ended.