CS 111 Lecture 5 Scribe Notes - Winter 2012

by Elison Chen, Steven La & Benjamin Lin for a lecture by Professor Paul Eggert on January 25, 2012

Table of Contents

  1. Orthogonality
  2. fork()
    1. Differences between child and parent processes
    2. Example of a fork()
  3. Exec
    1. Example of an Exec
  4. File Descriptors
    1. Access to Devices and Files
    2. Example of a fork() With File Descriptors
    3. What could go wrong?
    4. Race Conditions

Orthogonality

Orthogonality

Orthogonality is a property in computer systems design that increases the compactness of a system. The concept of orthogonality is that when you change one component of a system it neither creates nor propagates side effects to other components of the same system.

The goals of orthogonality is to create a simple yet complete system that is combinable. It should be simple in that making changes to one component should not force a change on another component. It should be a complete system in that it should have all the necessary components to have it run. Lastly, it should be combinable in that the user should be able to call any component and have it work in conjunction with another component, but have the components themselves independent.

To imagine OS orthogonality, visualize an xyz graph, where the x-axis represents the files API, the y-axis represents the processes API, and the z-axis represents the computer's memory. Changing any of these will not effect the other, because these axis' are perpendicular to one another.

Roughly, an Operating Systems API should have these independent feature sets, e.g. open can be called, then immediately waitpid:

Process File
fork open
waitpid read
exec write
exit close

fork()

The fork() system call creates a clone of the parent process, the clone is called a child.

pid_t fork(void);
// returns 0 if the fork succeeded and is now running in the child process
// returns -1 if the fork failed
// returns a value > 0 if the fork succeeded and is now running in the parent process
Differences between child and parent process
Example of a fork()
void printdate(void) {
	pid_t p = fork();
	//If for had an error
	if(p < 0)
		error();
	//In child process
	if(p == 0) {
		char* args[2] = {"date", NULL};
		execvp("/usr/bin/date", args);
		error();
	}
//Back in the parent process int status; //Wait for children to finish if(waitpid(p, &status, 0) < 0) error(); }

Exec

execlp(const char *file, const char *arg);

The exec family replaces the current process with a new process. Execlp is similar to fork() as it clones all the same things that fork() clones. However, the exec family destroys all the things that fork() keeps. This includes all the stack data and the instruction pointer. Unlike fork(), exec does not have a parent-child process – rather, the new process simply completely replaces the old process.

An example of the file and arg parameters, and execution of execlp:

execvp
Example of an Exec
void printDate (void) {
	pid_t p = fork();
	switch(p){
	case -1: error();		//assuming we have a function error() handling errors
	case 0:
		execvp(“/usr/bin/date”, ((char *)[2]){“date”, NULL});
		//if execvp fails, it will keep going in the process
		error();		//so errors, since execvp failed
		
		//default – this is unnecessary since we have to be parent 
		//process to be at this point and not hit error()
	}
	int status;
	if (waitpid(p, &status, 0) < 0) 
	error();	//produces error if child returns -1
	if (WIFEXITED(status) || WEXITSTATUS(status) != 0)
		error;
}

File Descriptors

The kernel contains a data structure called a file descriptor table, which contains the information for all open files. A file descriptor is one index of this data structure which contains the details of an open file. Each process has a process descriptor which then contains a file descriptor table. When an application wants to access a file, it does so through a system call. This will tell the kernel to access the file on behalf of the application. This protects the system because the application will not directly modify the file descriptor, instead it will have to go through the kernel.

file descriptors

Access to Devices and Files

In Operating Systems, there are frequently times that user input or device input is needed, such as the hard drive or the mouse/keyboard. In this case since devices tend to be slow as compared to CPU <-> RAM, system call overheads are usually recommended for the sake of robustness. However, there are many different types of devices out there.

Device Categories Stream Storage
Examples Network, mouse, keyboard Hard drives, SSD, etc.
Differences Spontaneous data generation, infinite size Request/response, random access, finite size

In order to achieve this, we use system calls

open close read write lseek
Stream
Storage
Example of a fork() With File Descriptors

void logdate(void) {
	pid_t p = fork();
	switch(p){
	//if error in fork
	case -1:
		errors();
	case 0:
		//open file descriptor to the “log” file
		int fd = open(“log”, O_WRONLY|O_APPEND);
		if (fd < 0)
			error();
		if (fd != 1)
		{
			//set the stdoutput to fd
			if (dup2(fd, 1) < 0)
				error();
			//close fd
			close(fd);
		}		
		//do the rest of log date function
	default:
	}
}

Notice the comment //Open the file here!. In order to achieve this, we must utilize file descriptors and other system call functions, such as open/close. Below are both the syntax for open/close and example code:

int open(const char *path, int oflag, ... );
Where path is a string of the filename, and oflag is the flags associated with open.
int close (int fd);
Where fd is the file descriptor in which one is trying to close.

int fd = open(“log”, O_WRONLY|O_APPEND);
if (fd < 0)
	error();
if (fd != 1)
{
	//set the stdoutput to fd
	if (dup2(fd, 1) < 0)
		error();
	//close fd
	close(fd);
}

What can go wrong?

For each kernel, there is only a finite amount of file descriptors available to be used. Thus once all file descriptors are used, all the file descriptors are exhausted. After a file descriptor is no longer in use, it must be closed with the close(int fd) function.

Another potential problem is disconnection of file descriptor and the file itself. An example is shown below:

while (open (“/”, O_RDONLY) >= 0)
	continue;
int fd = open(“file”, O_WRONLY|O_CREAT, 0666);
unlink(“file”);
write(fd, …);	//doing I/O to a nameless file
read(fd, …);	//doing I/O to a nameless file

Race Conditions

A race condition is a situation in which two processes access the same “location”. This leads to problems as the two competes to see which one finishes the task earlier, which usually implies the other’s failure (but we want both to succeed!)