CS111 Lecture 5 Scribe Notes - Winter 2012

by Elliot Daniel and Allison Le for a lecture by Professor Paul Eggert on January 25, 2012.

Orthogonality

Overview

In operating systems, we often find it desirable to keep various axes independent of each other. For instance, in a given OS API, we may have the following commands, separated into two groups: The two areas represent independent axes because functions from one area don't affect the other. Their independence creates a kind of encapsulation which enhances stability and security, against both malicious users and careless programmers.

Other axes, like signals, should also be considered for orthogonality.


FORK()

Calling
p = fork()
results in two processes, the parent proccess and the child (new) process. For the most part, the child is a direct clone of the parent, except:

Each parent/child process has a process descriptor.


getpid() versus getppid()

Both functions return different values depending upon whether they are called in the parent or child process.


exec() Variants

Primary:

Alternately:

Any one of the exec() family of functions starts a new process, ending execution of the current process at the point it is called. exec()s have no return value unless they fail, as the process they are called from no longer exists once they complete. exec()s do the following:


crt0()

The C runtime initialization routine. Because fork() is the only way to create a new process, the first process must somehow exist.

Upon starting:


fork() versus exec()

fork() and exec() have fully orthogonal (complementary) domains. Example shell scripts:
#!/bin/sh
exec date
No fork(), immediate exec();
#!/bin/sh
date
echo hello
fork()s, runs date in child process, then returns to parent to run echo.


printdate()

_Noreturn void error (void);                // Solves every problem
// possible; never returns.
void printdate (void) {
        pid_t p = fork();
        switch (p) {
                case -1:
                        error();
                case 0:
                        execvp(“/usr/bin/date”, (char* [2]){“date”, 0});
// int execvp(char const *,
// char * const *)
                        error();
// If execvp() succeeds this
// point is never reached.
                default:
                        int status;
                        if (waitpid(p, &status, 0) < 0)
                                error();
                        if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
                                error();
        }
}


date.c

int main (void) {
        clock_gettime(CLOCK_REALTIME);
                .
                .
                .
        localtime( . . . );
                .
                .
                .
        if (printf(“%4d-%2d-%2d”,  Y, M, D ) > 0)
                return 0;
        else
                return 1;
}


crt0.c

int argcMAGIC;
char **argvMAGIC;
void MAGIC (void) {
        _exit(main(argcMAGIC, argvMAGIC));
}


exit() versus _exit()


logdate()

void logdate (void) {
                // similar starting code to printdate();
// insert this code before printdate()’s body
        int fd = open(“log”, O_WRONLY | O_APPEND);
        if (fd < 0)
                error();
// close(1);         /* covered by dup2() */
        dup2(fd, 1);
        if (fd != 1)
                error();
}


posix_spawnvp()

int posix_spawnvp (pid_t *restric pid, char const *restrict file,
		 posix_spawnfile_action_t const *file_acts,
		 posix_spawn_attr_t const *restrict attrp,
		 char *const *restrict argv,
		 char *const *restrict eavp); 
		 
// restrict promises no aliasing; all pointers point to distinct objects
..Needless to say, this is a giant mess, but a valid approach nonetheless.


Infinite loops with exec() and fork()

Example 1:

int main (void) {
	execlp(“/usr/bin/date”, . . . );  // infinite loop
	}

Example 2:

while (fork() > 0)  // infinite loop
	continue;

Example 3:

while (fork() >= 0)  // “fork bomb”, number of processes        
	continue;  // increases exponentially


Access to devices and files

Issues and considerations:

Device categories:

Network Storage

Device operations:

Network
	open()
	close()
	read()
	write()
Storage
	lseek()
	pread()		// lseek() followed
	pwrite()	// by operation


Big Idea of UNIX


Perhaps the #1, overarching, Fundamental Theorem of UNIX is that all actions are accomplished by way of a relatively small number of system calls, all of which operate on file descriptors only.

File descriptors

What can go wrong?

Example 1:

while (open(“/”, O_RDONLY) >= 0)
        continue;
// Descriptor table and/or RAM can quickly become exhausted.

Example 2:

int fd = open(“file”, O_RDWR | O_CREAT, 0666);
unlink(“file”);
write(fd, . . . );
read(fd, . . . );
// Nameless files that are inaccessible except through fd but can’t be deleted.

Example 3:

char name[sizeof(“/tmp/sort”) + (sizeof(pid_t) * CHAR_BIT + 2) * 3];
sprintf(name, “/tmp/sort%d”, getpid());
open(name, O_RDWR | O_CREAT, 0600);
unlink(name);
// Potential obliteration of actual files named “/tmp/sort#” where # is a number.


Race Conditions

Two processes running in parallel access the same location and stomp on each others’ data, potentially causing undefined behavior or worse.

Example 1:

if (stat(“/tmp/f”, &st) != 0)
	open(“/tmp/f”, . . . );
else
	error();
// Between stat() and open(), another process can open(),
// making this operation seem safer than it actually is.

Example 2:

int i;
// Assume this is initialized somewhere

f = new_file_name(i);
while (stat(f, &st) == 0)
	f = new_file_name(++i);
	
open(f, O_RDWR | O_CREAT, 0600);

// Alternatively, open(f, O_RDWR | O_CREAT | O_EXCL, 0600); 
// removes the need for stat. If f exists, fail.

Example 3:

while(fd = open(f, O_RDWR | O_CREAT | O_EXCL, 0600) < 0)
	f = new_file_name(f);	// Messy, but no more race condition