Phase 2 -- Process Control and System Calls
Operating Systems
Spring, 1999
The second phase of your operating system project introduces basic (non-preemptive) process control and a system call interface. Your system will create several processes and oversee their execution. Each process will have its own TSS to facilitate transfer of control between processes. This phase also introduces the code needed to implement system calls; that is, procedure calls from user programs to the operating system. This will be done by using a kernel task (operating as a coroutine) which receives and processes system call requests from users. Each system call triggers a task switch to the kernel task for processing.
System call interface
To create the mechanism by which the kernel task can be called on by user processes, we need to do the following:
1. Kernel task. This is in fact an extension of your main program. A TSS for the kernel has already been set up by the startup code (in boot16.c). After creating the first user process and yielding control to it, the kernel task enters a loop in which it accepts and processes system calls. It will get control by means of a task gate whenever a user makes a system call. After processing the call, the kernel performs the "iretd" instruction (return from interrupt) to return control to the calling user task. The basic logic of this loop is as follows:
while (1){
Locate the calling task's TSS;
Get the function number and arguments from the calling task's TSS; (The
function number will be in the EAX register. Arguments (1 word each) will
be in [EBP+8], [EBP+12], [EBP+16], etc...)
Call the worker routine corresponding to the requested function; (Use a
switch statement to divide control.)
Put the return code in the calling task's TSS; Put the TSS selector of
the task to return to in the BackLink field of the kernel's TSS. (This
allows the kernel to return control to a task different than the one which
called it.)
Perform the iretd instruction;
}
Note: It is the kernel task's responsibility to verify that incoming parameters are valid.
2. All of our system calls will use INT 49. In order to cause the INT 49 instruction to cause a task switch to the kernel task, your system initialization (your main function) must create a task gate which references the kernel task, and insert it in the interrupt descriptor table (slot 49; that is, IDT[49]). The gate should contain:
a. Its selector field is the selector of the kernel TSS (SelKernel).
b. Its type is "32-bit task gate".
c. Its DPL is user level (i.e., ring 3). This allows it to be called by
a user program.
d. It is "present" and a "system" descriptor.
Implementing Process Control
In this phase we are implementing basic system calls for process control, using the UNIX model. These are the fork, wait, and exit routines.
Data Structures:
Ready queue. The Ready queue is a queue of processes which are awaiting execution on the CPU. It can be implemented as a singly-linked list of the PCBs of the ready processes.
PCB. Add a "next" field as the first entry in the PCB, so that PCB's may be linked together. Add four additional data fields to each PCB: this process's exit code, the number of children of this process, the pid of this process's parent, and a queue of terminated children ("zombies"). Allow for two new process states: 2 = waiting for a child to terminate, 3 = terminated.
Code:
Modifications to the initialization routine (main program):
Modifications to ProcessCreate:
Assign values to the new PCB fields described above. When the PCB has been initialized, insert it in the ready queue.
Queue ADT
Implement a Queue as a singly-linked list of nodes. Include basic access routines such as QInit, QEmpty, QLength, QInsert, and QDelete. Put the (application-independent) code for the queue ADT in separate files called queue.h and queue.c. Make the "next" field in a queue node the first field in the node. This will make it possible to use the queue routines for a variety of block types, including PCBs.
dispatch()
Dispatch is responsible for choosing the process to run and then transferring control to that process. with a yield_ operation. Processes will be dispatched in FIFO order. Dispatch performs the following steps:
getpid and getppid
These are two simple system calls which simply get the pid and ppid fields from the running PCB and pass them back to the caller.
fork -- create a child process
The fork function is called by a process (the "parent") to create a new process (the "child") which is a logical copy of itself. All processes in the system, except for the first one, are created by fork. To create the child process, fork allocates and initializes a new PCB, TSS, and LDT for the child. The value returned to the parent is the PIN of the child; the child receives a return code of 0. The action of fork is similar to that of the ProcessCreate function from the previous assignment, except that most of the contents of the PCB is copied from the parent process. In more detail, fork should do the following:
yield -- yield control of the cpu to another process
A process can call yield in order to voluntarily give up the CPU; that is, cause a task switch to a different process. The kernel task accomplishes this by calling the dispatcher.
exit_(int exitcode) -- exit from a process.
The exit_() routine marks a process for removal from the system. It also must perform some interactions with its parent and child processes.
int wait_(int*status) -- wait for termination of a child process
Wait allows a parent process to wait for termination of one of its children. There are three cases:
cleanup -- clean up after a terminated process
Cleanup may be called by either exit or wait to remove the resources from a terminated process. Cleanup should:
Notes: