Assignment 5, Solutions

Part of the homework for 22C:112, Spring 2012
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

  1. Background: In Unix-derived systems, files may be marked as "close on exec", but if they are not marked as such, they remain open when one program uses execve() (or any other flavor of exec) to launch another program. This is how access to standard input and standard output are passed to new programs.

    When a program uses fork(), the parent and child share the same open files, but when the child used open() or close(), this has no effect on the parent.

    Kernel files in Unix are opened by open(), closed by close() and read and written by read() and write(). The standard stream model of languages like C and Java is implemented by user-level library code. At the kernel interface level, open() returns an integer that is used as the file handle, and the other operations on files take this integer as a parameter in order to know what file to operate on. stdin (standard input) uses the integer file handle 0, stdout (standard output) uses the integer file handle 1, and stderr (standard error) uses the integer file handle 2. When you open a new file, by definition, the integer used as a file handle will be the lowest integer not currently associated with an open file. If the system permits you to have at most 32 open files, the file numbers will always be in the range 0 to 31, as a result.

    Note, for further information on these kernel calls, use man 2 xxx (the 2 prevents the man command from telling you about shell commands with the same name).

    a) Why does close(1);open(f); reliably reset stdout to file f. (0.6 points)

    The man page for open() says it uses the lowest-numbered file descriptor not currently open for the process. Since the file descriptors for standard input, standard output, and standard error are 0, 1 and 2, these are always open. So, if you close one of these, and then immediately do an open command, you will open the descriptor you just closed, since it will be the lowest-numbered available file descriptor.

    b) In the C (and C++) stream model, the analogs of open() and close() are fopen() and fclose(), but the designers of the stream abstraction added freopen(). Is there something about the stream abstraction that forced this (really sensible) addition, or was it added just for aesthetic reasons. (0.6 points)

    The stream abstraction does not include any ordering relationship between different logical streams, so re-opening a stream must be a primitive operation.

  2. Background: In a bounded buffer, enqueue and dequeue can be coded as follows:
    enqueue(ch){
       tail = (tail + 1) % bufsize;
       buffer[tail] = ch;
    }
    dequeue(){
       head = (head + 1) % bufsize;
       return buffer[head];
    }
    

    a) With this buffer implementation when head and tail are equal, either the buffer is full, or it is empty. Briefly give two alternative ways to distinguish the full and empty conditions, and discuss their relative merits. One or both solution may require adding code to the enqueue() and dequeue() routines, but you need not write out the code if you can describe it compactly.. (0.6 points)

    First, consider maintaining an extra variable, for example, to count the number of items in the queue. Increment it on enqueue, decrement it on dequeue. It will be zero when the queue is empty, and equal to the queue capacity when the queue is full.

    Second, consider forbidding a completely full queue. That is, declare the queue to be full when (tail+1)%bufsize is equal to head This reduces the capacity of the queueue by one.

    Note that, if integers are 32 bits and characters are 8 bits, the second solution is more memory efficient.

    b) Consider the limiting case where the buffer size is exactly one. Give concise code for enqueue(), dequeue() full(), and empty() in this case. The total code required is very small. (0.6 points)

    enqueue(ch){
       count = 1;
       buffer = ch;
    }
    dequeue(){
       count = 0;
       return buffer;
    }
    full(){
       return (count == 1);
    }
    empty(){
       return (count == 0);
    }
    

  3. Background: If we take an object oriented perspective on I/O, the natural thing to do is define a class "stream" with methods "put" and "get", and then define a subclass for each type of stream device, a keyboard stream, a mouse stream, an asynch comm port stream, a parallel port stream, and so on. Some of these devices don't fit the stream model very well. The comm port, for example, needs methods added for changing the baud rate and data format.

    The Unix system, which is decidedly non object-oriented, handles this problem with some very odd extra I/O kernel calls such as ioctl() and fcntl().

    Background: How should these device specific operations be handled in a purely object-oriented world? (0.6 points)

    In an object oriented world, subclasses of a parent class must implement all of the methods of the parent class, but they may also implement methods that are specific to that subclass. So, all subclasses of the generic stream class must implement "put" and "get", but the asynchronous terminal subclass might also have the method "setbaudrate" and random access files might also have the method "seek".