Assignment 03: Trust the Process (Part 2)

Due Sunday, Feb 9, before midnight

The goals for this assignment are:

  • Working with stdout, stdin, and files

  • Understanding redirection and pipes at the command line

  • Working with additional system calls: pipe, dup2, chdir, pwd, perror

  • C practice/review

We will use the same starter code as last week: here

1. Better Shell

This question is from this lab.

In the file, shell.c, extend the features of your shell from Assignment 02.

Requirements:

Your shell should satisfy the requirements of Assignment 02. Additionally, your shell should support the following features, outlined in the sections below.

  • Change the current working directory if the users types cd

  • Support pipes

  • Support file redirection

  • Error reporting

  • Cleanup

1.1. Change the working directory

If the user enters the cd command, change the current working directory. Your shell should change the working directory to the specified path using chdir(). You can execute the pwd command to print the current working directory (to test if cd is working).

1.2. Piping

Your shell should support piping the output (standard out) from one command to the input (standard in) of another. A pipe is an inter-process communication (IPC) mechanism for two Unix processes running on the same machine. It’s a one-way communication channel between the two processes (one process always writes to one end of the pipe, the other process always reads from the other end of the pipe). The interface to a pipe is like a temporary file with two open ends — writes to one end by one process can be read from the other end by the another process. However, unlike a regular file, a read from a pipe results in the removal of data that was written by the other process.

The pipe() system call will create a pipe when given an array of two integers. It creates two file descriptors, one for the read end of the pipe (array element 0), the other for the write end of the pipe (array element 1).

Child processes inherit file descriptors from their parent, so you’ll want to create a pipe before calling fork() so that any newly-created processes will get a copy of the pipe when they’re forked. After the fork, a child can set up its file descriptors before calling execv() by using dup2() to associate the read or write end of the pipe with the appropriate file descriptor.

Placing a pipe between two commands causes the output of the first to feed into the input of the second. For example:

ls | sort

Says to take the output from ls and rather than printing it, instead send it as input to sort, which will sort it and then print it.

cat file.txt | grep blah

Says to take the output from cat (the contents of file.txt) and send it to the input of grep, which searches for all lines containing the string "blah". This is a silly way to run grep, since grep can read files on its own without cat, but it works well for testing pipes.

You are not required to support multiple pipes in a single command line, although I would encourage you to think about how you might make that happen (it’s not that different).

1.2.1. Creating a pipe

Calling the pipe() system call creates two new file descriptors for a process: a read-end and a write-end. Any data written to the write-end will be available for reading from the read-end. When all of the FDs representing the write-end of a pipe are closed, any attempt to read from the pipe will return end-of-file (EOF) to the reader, signifying that no more data is coming.

When working with pipes in your shell, it’s important to close any parts of the pipe that you aren’t using. For example, if a child process is planning to write into a pipe, it should close the read-end, since the read-end isn’t useful to it. Similarly, a pipe reader should close the write-end.

Your parent shell will also need to close both ends after it has forked all of the child processes that need access to a shared pipe. If you don’t close all the write ends of a pipe, your shell will hang because the pipe’s reader will never get an EOF!

1.3. I/O redirection

Your shell should support I/O redirection whereby files replace a process’s standard in (0), standard out (1), or standard error (2) file descriptor streams. The user can ask to replace standard in by supplying "< filename". For example:

grep blah < input_file.txt 1> output_file.txt

Says to use the file input_file.txt as file descriptor 0 rather than reading from terminal input and the file output_file.txt as file descriptor 1 rather than printing to standard out.

For output streams, the user must specify which I/O stream(s) to replace:

1> filename redirects the standard out stream to a file.

2> filename redirects the standard error stream to a file.

The user may also choose to redirect both streams, either to separate files or to the same one. To implement this functionality, most of the complexity is in the parser, since you’ll need to recognize that an I/O redirect is happening and treat the next token as the file name rather than an ARGV token. After parsing is complete and you’ve created a child process, you can use open() and dup2() to place the file at the appropriate file descriptor.

When opening files for writing, pay attention to the flags and modes you pass to open(), since you’ll need to create files with usable access permissions that don’t already exist. See the Tips section below.

Note that most shells will assume you mean "1>" if you just say ">", but you are not required to do that. For output streams, you may assume there will be an explicit 1 or 2 preceding the ">". You may also assume that the user won’t give you nonsense numbers (e.g., 3>).

1.4. Error reporting

Your shell should report errors using perror() and continue executing whenever possible. If the main shell process (parent) makes a system call that fails (e.g., signal, fork, pipe) it’s fine to report the error and terminate, since you can’t really recover from those. For minor errors though (e.g., exec fails because the user’s command is invalid) a child process should terminate, but the shell should continue.

Always always always check the return value of any system call you make!

1.5. Cleanup

Your shell should reap all child processes when they terminate, close all file descriptors it’s no longer using, and get a clean bill of health from valgrind: no invalid memory accesses, uninitialized variables, or memory leaks.

Submit your work to Github

Add and check in your program using git and then push your changes to Github. Run the following command inside your shell-USERNAME directory.

$ cd shell-USERNAME
$ git add .
$ git commit -m "Descriptive message"
$ git push

Run git status to check the result of the previous git command. Check the Github website to make sure that your program uploaded correctly.

2. Grading Rubric

Assignment rubrics

Grades are out of 4 points.

  • (4 points) Shell

    • (0.3 points) style and header comment

    • (0.3 points) cd

    • (0.7 points) pipes

    • (0.7 points) redirection

    • (2.0 points) Polish. No memory errors.

When using readline(), valgrind will report that some of your memory is still reachable. This memory is not leaked. It hasn’t been cleaned up yet from `readline()’s internal memory. For example, output such as the following is ok
==3504== HEAP SUMMARY:
==3504==     in use at exit: 204,143 bytes in 221 blocks
==3504==   total heap usage: 453 allocs, 232 frees, 244,790 bytes allocated
==3504==
==3504== LEAK SUMMARY:
==3504==    definitely lost: 0 bytes in 0 blocks
==3504==    indirectly lost: 0 bytes in 0 blocks
==3504==      possibly lost: 0 bytes in 0 blocks
==3504==    still reachable: 204,143 bytes in 221 blocks
==3504==         suppressed: 0 bytes in 0 blocks

Code rubrics

For full credit, your C programs must be feature-complete, robust (e.g. run without memory errors or crashing) and have good style.

  • Some credit lost for missing features or bugs, depending on severity of error

  • -12.5% for style errors. See the class coding style here.

  • -50% for memory errors

  • -100% for failure to checkin work to Github

  • -100% for failure to compile on linux using make