Lecture 9 - More Unix Systems Programming, Synchronization


Agenda

Announcements

  • CS Colloquium tomorrow: I'll be speaking.
  • Unless...it's Mountain Day.
  • More Unix Systems Programming

    Low-level File Operations

    You may (or may not) be familiar with the C standard file I/O routines defined in stdio.h, such as fopen(), fscanf(), fprintf(), and fclose(). These provide relatively "high-level" access to files in that you deal with data types rather than a low-level stream of bytes.

    Underneath the stdio functions, you will find those low-level operations: open(), close(), read(), write(). Tanenbaum describes these breifly in Section 1.6.2, and the man pages describe them in great detail.

    Here's an example that uses them: [everyother.c]

    Pipes

    Processes may wish to send data streams to each other. Unix pipes are one way to achieve this. You've almost certainly used Unix pipes at the command line. You can also use them in programs.

    An unnamed pipe can be created using the

    int pipe(int fd[]);
    

    system call. fd is an array of two int values. These are file descriptors, very similar to the file descriptors used for file I/O using open(), read(), and write().

    fd[0] is the "read end" and fd[1] is the "write end". 0 return means success. -1 means failure.

    An example of communication between two processes, a parent and its child created by fork(), communicating via an unnamed pipe.

    #include <stdio.h>
    
    char *text="Let's send this through a pipe";
    
    int main() {
      int fd[2], size;
      char message[100];
    
      pipe(fd);  /* create the pipe */
      if (fork() == 0) { /* child */
        close(fd[0]);
        write(fd[1], text, strlen(text)+1);  /* include NULL */
        close(fd[1]);
      } else { /* parent */
        close(fd[1]);
        size=read(fd[0], message, 100); /* read up to 100 bytes */
        close(fd[0]);
        printf("Got %d bytes: <%s>\n", size, message);
      }
      return 0;
    }
    

    This required the shared values of fd. This is fine when you create your pipe just before a fork(), but what if we have two processes already in existence that wish to communicate through a pipe?

    We can create a named pipe with mkfifo (command or system call).

    Here is the same example, using a named pipe.

    #include <stdio.h>
    #include <fcntl.h>
    
    char *text="Let's send this through a named pipe";
    
    int main() {
      int fd, size;
      char message[100];
    
      if (fork() == 0) { /* child */
        fd=open("testpipe", O_WRONLY);
        write(fd, text, strlen(text)+1);  /* include NULL */
        close(fd);
      } else { /* parent */
        fd=open("testpipe", O_RDONLY);
        size=read(fd, message, 100); /* read up to 100 bytes */
        close(fd);
        printf("Got %d bytes: <%s>\n", size, message);
      }
      return 0;
    }
    

    The two examples above are available: [pipe1.c] [pipe2.c]

    Error checking/reporting

    Most Unix system calls may fail for a variety of reasons. You should always check the return value of system calls that may fail. The reason for a failure in the errno variable. A list of errors can be found in intro(2).

    The system calls perror(3) and strerror(3) allow you to print out (hopefully) meaningful error messages when you detect a failed system call.

    Example: [perror-ex.c]

    More Synchronization

    Readers-Writers

    We have a database and a number of processes that need access to it. We would like to maximize concurrency.

  • There are multiple "reader processes" and multiple "writer processes"
  • Readers see what's there, but don't change anything. Like a person on a travel web site seeing what flights have seats available
  • Writers change the database. The act of making the actual reservation
  • It's bad to have a writer in with any other writers or readers - may sell the same seat to a number of people (airline, sporting event, etc) Remember counter++ and counter-!
  • multiple readers are safe, and in fact we want to allow as much concurrent access to readers as we can. Don't want to keep potential customers waiting.
  • A possible solution:

    Note that the semaphore mutex protects readcount and is shared among readers only.

    Semaphore wrt is indicates whether it is safe for a writer, or the first reader, to enter.

    Danger: a reader may wait(wrt) while inside mutual exclusion of mutex. Is this OK?

    This is a reader-preference solution. Writers can starve! This might not be good if the readers are "browsing customers" but the writers are "paying customers!"

    Sleeping Barber

    We develop a solution somewhat different than, but functionally equivalent to the one given in Tanenbaum, Section 2.4.3.

    Other Synchronization Primitives

    Monitors

    Semaphores are error-prone (oops, did I say wait? I meant signal!). You might never release a mutex, might run into unexpected orderings that lead to deadlock.

    Monitors are a high-level language constuct intended to avoid some of these problems. A monitor is an abstract data type with shared variables and methods, similar to a C++ or Java class.

    monitor example_mon {
      shared variables;
    
      procedure P1(...) {
        ...
      }
    
      procedure P2(...) {
        ...
      }
    
      ...
    
      initialization/constructor;
    }
    

    A monitor has a special property that at most one process can be actively executing in its methods at any time. Mutual exclusion everywhere inside the monitor!

    But if only one process can be in, and a process needs to wait, no other processes can get in. So an additional feature of monitors is the condition variable. This is a shared variable that has semaphore-like operations wait() and signal(). When a process in a monitor has to wait on a condition variable, other processes are allowed in.

    But when happens on a signal? If we just wake up the waiting process and let the signalling process continue, we have violated our monitor's rules and have two active processes in the monitor. Two possibilities:

  • Force the signaler to leave immediately
  • Force the signaler to wait until the awakened waiter leaves
  • There are waiting queues associated with each condition variable, and with the entire monitor for processes outside waiting for initial entry.

    See Tanenbaum example of Producer-Consumer with a monitor.

    Note that monitors are a language construct. They can be implemented using OS-provided functionality such as semaphores.

    Also note the similarity between these monitors and Java classes and methods that use the synchronized keyword.