Lecture 5 - 4/14/08: Orthogonality, Processes, and Race Conditions

Class: CS111
Professor: Paul Eggert
Scribes: Matthew Pham, John Gang, Edward Chang

:Orthogonality:

Orthogonal Axes

We want our operating system's features to be choosable independently - the choice of one feature shouldn't affect other choices.

Think of each feature as an axis. Changing one axis should not change any of the other axes. The axes are orthogonal to each other.

By designing an orthogonal system, we reduce the propagation of effects.

Process Creation and Destruction:.

Process creation and destruction involves several orthogonal functions:

  • pid_t fork(void);
    creates a child process, which is a copy of the calling (parent) process
  • int execvp(char const* file, char* const *argv);
    executes file with arguments argv.
  • pid_t waitpid(pit_t pid, int* status, int flags);
    returns -1 on fail, pid on success
    • pid_t pid: the process id
    • int* status: pointer to child's exit status
    • int flags: options; a popular option is WNOHANG, which just reports the currently exited children

Here's an example that uses these functions orthogonally. It is an application that prints out the time and date, using an external "date" program, located at /bin/date.

void printdate(void) {
	pid_t p = fork();
	if (p < 0) error(); // exercise for reader; assumption is it does not return
	if (!p) {
		char const *args[] = {"/usr/bin/date", NULL};
		execvp(*args,args); // maybe /bin/date or /usr/bin/date
		error();
	}
	int status;
	if (waitpid(p,&status,0) < 0) error();
	if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) error();
}

There are two library functions that a program can use to report its exit status:

  • void exit(int); // cleanup, then exit, flush buffers, library function
  • void _exit(int); // just get me out of here, low level exit, no cleanup, syscall

SIDENOTE: When the main program returns, the OS automatically calls exit on it.

int main(void){
	...
	return 3;
}

The OS does this: exit(main());
Also, the OS will only call exit(main()) on the top-level main, so a recursive function will not exit prematurely.

Pipes:.

A pipe is a buffer through which one process can communicate to another.

The pipe function:

int pipe(int fd[2])

This function creates a pipe. It returns zero if successful, and -1 on failure. It places the file descriptors for the read and write ends of the pipe into the argument. This is equivalent to int pipe(int *fd).

The write end goes into the pipe, the read end comes out

fd[0] is the file descriptor for the read end.
fd[1] is the file descriptor for the write end.

Pipes are bounded buffers:

  • The read end waits if there is nothing to be read or in the pipe.
  • The write end waits if the buffer is full.

What can go wrong with pipes?

  • Write to a pipe that only you can read from, then you wait forever
  • Create a pipe and never use the file descriptor, then you get a memory leak
  • Write to a pipe with no reader:
    1. Your process gets SIGPIPE(it dies)
    2. (if you are ignoring signals) write fails, returns -1, and sets errno to ESPIPE.
  • The "dancing pipes" problem:
    Take a look at the shell command below:
    $ obliviouswriter | less
    
    If we ignore signals using method 2 above, obliviouswriter will write to a broken pipe, and an infinite loop may be created depending on how obliviouswriter handles the error.

Back to Orthogonality:.

  • There are some programs that call fork but not exec: (Apache clones itself).
  • Likewise, there are some that call exec but not fork: (nohup gcc foo.c).
    (Run some code to ignore the SIGHUP signal, then exec gcc)

Common patterns of uses in orthogonal interfaces:
Orthogonality often generates interfaces in nice pieces, but some common patterns become a pain to use.

One example: "I just want to run a program" vs. fork/execvp/exit/waipid/pipe/close/read/write

Solutions:

  1. Create a library function implemented atop orthogonal primitives, int status = system ("/bin/date")
  2. Add a syscall to capture a common pattern:
    For example, the following function spawns a child process:
    int posix_spawnp(pid_t *restrict pid
    	const char *restrict file; // Things to do between fork and exec
    	const posix_spawn_file_actions_t *file_acts // see above comment
    	const posix_spawn_attr_t *restrict attrp
    	char* const *restrict argv
    	char* const *restrict envp);
    
    This function is for programs that need to run quickly on both Windows and UNIX.

    However, this is non-orthogonal! posix_spawnp is dependant on both fork and exec.

SIDENOTE: The "restrict" keyword:
pid_t buf;
posix_spawnp(&buf,...,&buf);
If we say a pointer is restricted, we are telling the compiler that no other pointers point to that memory block. This allows for compiler optimization.

Orthogonality and Files:.

The following is a list of file-access primitives:

  • open
  • read
  • write
  • close
  • lseek

How do we create a file?

Long ago, some smart people created creat.

The creat function:

int creat(const char *path, mode_t mode)

EXAMPLE:
fd = creat("/tmp/newfile",0666);

The file descriptor fd is what we use for read-write access. "/tmp/newfile" refers to the name of the file that we want to create. "0666" refers to the permissions (in octal) of the file. In binary, the permissions come out to 000110110110.

creat is a standard function in UNIX v7, POSIX, and Linux. Note: if the file already exists, creat truncates it (it discards all data in that file, setting the size to zero).

SIDENOTE: UNIX permissions

In UNIX, each file is assigned an owner. Permissions are split into three groups: owner, group, and other. The owner user is given owner permissions. Users in the same group as the owner are allowed group permissions. Users outside the owner's group are allowed other permissions. Permissions allow users to read, write, or execute a file. The table below shows the permissions of the file that we just created:

Owner Group Other
rwx rwx rwx
110 110 110

As we can see, the owner,group, and other users can all read and write this file, but they cannot execute it.

However, there is a problem with creat: it is not orthogonal!

Why isn't it orthogonal?

  • It bundles together creation and truncation
  • We now have two syscalls interpreting filenames and allocating fds

The other syscall that interprets filenames and allocates fds is open. The creat function is now deprecated.

Nowadays, we just use open, with flags.

The open function:

int open(const char *path, int oflag, ... )

EXAMPLE:
open("/tmp/newfile",0666, O_RDWR | O_CREAT | O_TRUNC);

This call is equivalent to creat("/tmp/newfile", 0666). Let's take a look at the flags.

O_RDWR
specifies the kind of access that the file descriptor that returned from open. In this case, it can read and write.

O_CREAT
tells open to create the file if it does not exist.

O_TRUNC
tells open to truncate the file if it already exists.

NOTE: If you use 0 instead of 0666, there are will not be any permissions at all. However, the file descriptor that is returned to our process will still have read-write access, provided that the O_RDWR flag is included.

SIDENOTE: the "umask"

If we do:

$ echo foo > bar
$ ls -l bar

We get:

-rw-r--r-- ... bar

But the shell says:

open(...,0666,...)

What happened to our permissions?

Blame the umask, a per process mask that applies to each "open" that creates a file. You may want to use the umask for security reasons.

EXAMPLE:

$ umask 022

This will set the umask to 022. If open creates a file with permissions 0666 , those permissions will go through the umask (0666 &~022) to become 0644. This means that we're not letting group members or others have write permissions to any files that we create.

:Race Conditions:

Suppose we have two processes accessing same file; they both want to create a temp file/use/remove it.
We now have a problem: a race condition. A race condition usually happens when two or more processes try to access the same thing at the same time. It can lead to some very unexpected results.

Let's say process 1 creates the file and starts storing data in it. Process 2, which is running at the same time, tries to create the file. It sees that the file already exists, so it truncates the file. If process 1 wants to read back some of the data that it wrote to the file, it is out of luck.

It usually works, but if you call the processes at same time (or close), you get a collision.

Possible solutions:

  • Remove O_TRUNC: DOESN’T WORK, the other process can still write data to the file
  • Remove O_CREAT: DOESN’T WORK, one of the processes will truncate the file
  • Use 0 permissions: this works, but we are misusing permissions
  • Use different file names (/tmp/newfile.37968): this sort of works, but we need one that’s more robust; it only works if we have one temp file per project. It does not work with devices, and it is harder to do with more than two processes
  • Create a new primitive exists("tmp/new/file): DOESN’T WORK, there is still a race condition for exists. A process could call exists on a nonexistent file, then another process could create a file right after that.