Small Utilities with Raw System Calls
In this part, you may not use any higher-level C-library functions in your code (i.e., no printf, malloc, etc.)–only system calls.
Tip
Because you may only use system calls, don't worry about complex error handling, just have the program exit with a non-zero exit status if an error occurs (and possibly a super-generic error message written with write to STDERR_FILENO if you're feeling fancy).
Tip
If you need to debug your code, you can use gdb (not os161-gdb since your code is running directly on the machine, not inside the sys161 machine simulator).
Reading #1
- Skim the UNIX manual pages for the following system calls so that you have a sense of how they can be used (you'll probably want to refer to them in the next part):
openclosereadwrite
-
Read the UNIX manual page for the system program
tee. Perform some experiments withteeto see how it works. You may want to try the following command:echo 'Hello, world!' | tee hello.outYou should find that the string
Hello, world!is written to the filehello.outand also to the standard output.
Tip
When reading system call manual pages, you may feel a bit overwhelmed by the amount of detail (much of which is extraneous for basic usage). As an alternative to reading the Linux manual pages for system calls, you may instead check out the OS/161 pages for the same system calls. These are included with the OS/161 distribution from Homework 1, but you can also view them on-line here.
Writing mytee
In mytee.c, write your own version of tee, called mytee, that performs the basic functionality of tee (i.e., it does not need to take option switches, just a single output filename as its argument). You may only use the following system calls: open, close, read, and write.
Tip
Standard input, standard output, and standard error are represented by the file descriptors 0, 1, and 2, respectively. There are convenience macros called STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO that are defined in <unistd.h> that you can use to make your code more readable.
Although it is mentioned in the manual pages, the read and write system calls have some subtle gotchas. In particular, you should not assume that you will always get the number of bytes you asked for when you call the read system call. If you ask to read, say, 1024 bytes, you may get fewer than that if
- There are fewer than 1024 bytes left in the file, or
- The file is a terminal, and the line the user types is shorter than 1024 bytes.
- The operating system just feels like giving you fewer bytes because it's a Tuesday.
It will give you exactly zero bytes if you're at the end of the file. You should check the return value of read to see how many bytes you actually got and act accordingly.
Likewise, technically write is allowed to write fewer bytes than you asked it to. You should check the return value of write to see how many bytes were actually written and write the rest later. (These days it almost always writes all the bytes you asked it to, but you should still check.)
Reading #2
Skim the UNIX manual pages for the following system calls:
dupexecve_exit
Writing harness
In harness.c, write a program called harness that executes another program with standard input set up to read from the file test.in and its standard output and standard error set up to write to the file test.out. For example, if test.in contained ten words, you should be able to use harness to run the word-count program, /usr/bin/wc, with the option to count words as follows:
./harness /usr/bin/wc -w
You should find the result, 10, in test.out.
You may only use the following system calls: open, close, dup, and execve. You do not need to use fork or waitpid.
Wait a moment, it would be so much easier to use
dup2for this!
The Linux manual page for
dupnotes “The new file descriptor number is guaranteed to be the lowest-numbered file descriptor that was unused in the calling process.” which tells you enough for you to figure out how to usedupto accomplish the task. (Hint: Close some things before you calldup.)
Tip
This program needs test.in to exist. You can create it with the following command:
echo 'This file has five words.' > test.in
Or by creating it in Visual Studio Code or another text editor.
Reading #3
Skim the UNIX manual pages for the following system calls:
forkwaitpidkill
Writing not
In not.c, write a program, not, such that not ./foo x y runs ./foo x y, and then
- Exits with status 1 if the program it ran exits with status 0
- Exits with status 0 if the program it ran has non-zero exit status
If the program that not runs dies due to a signal, the not program should terminate with the same signal.
You may only use the following system calls: fork, execve, waitpid, kill, and getpid.
Writing compete
In compete.c, write a program, compete, where compete starts ./foo x y running concurrently. For example, compete 10 /bin/echo hello would start 10 copies of echo hello running concurrently, and then waits until they all finish before exiting.
Exit with an error if
The exit code from compete should be the number of programs that failed (i.e., exited with a non-zero status or died due to a signal), so if everything runs successfully, compete should exit with status 0.
You may use any of the system calls you've used so far in this assignment.
Ordinarily, you would use a function like strtol or atoi to convert the integer argument int, but in this part of the assignment we're only allowed to use system calls, not the C standard library. Here's a simple function that converts a string to an integer that you can use (or you can write your own):
/* Convert a string to a positive integer
* - the string must contain only digits
* - returns -1 if the string is empty or contains non-digits
* [Attribution: This code was provided by Prof. Melissa.]
*/
int
str2int(const char *s)
{
int result = 0;
const char *cp;
// Null pointer or empty string
if (s == NULL || *s == '\0') {
return -1;
}
for (cp = s; *cp != '\0'; cp++) {
if (*cp < '0' || *cp > '9') {
return -1;
}
result = result * 10 + (*cp - '0');
}
return result;
}
Investigation Using compete
Using compete and a suitable test program to run, determine the UNIX policy for handling concurrent access to the same file descriptor from different processes. (You may be able to find this information in the Unix manual pages, too, but you should, nevertheless, verify these claims empirically.) Add your conclusions to the file fd-policy.txt. You may also want to see if you obtain different results for file descriptors opened with O_APPEND.
Some useful Unix commands for your investigation are
seq: This command prints a sequence of numbers. For example,seq 1 1000prints the numbers from 1 to 1000, performing 1000 distinct writes to standard output (the full path to this command is/usr/bin/seq).- Unfortunately, when not writing to a terminal,
seqbuffers its output, so it's not a good tool for this investigation.
- Unfortunately, when not writing to a terminal,
- Instead of
seq, you can use/usr/bin/perl -le '$| = 1; print "$$: $_" foreach (1..1000)'to print 1000 lines to standard output, each with the process ID and a number. wc: This command counts the number of lines, words, and characters in its input. For example,wc -l myfile.txtcounts the number of lines inmyfile.txt.less myfile.txt: This command allows you to view the contents of a file one screen at a time. If the file has non-printable characters,lesswill display them as^@,^A, etc.- Your
harnessprogram from earlier (i.e., runcompeteinsideharnessto run multiple copies of a program concurrently, writing to the sametest.outfile).
(FWIW, on Linux the results may not be all that surprising. You may want to focus on things that don't seem to ever happen, but you thought might have been possible.)
Reading #4
Skim the UNIX manual pages for the sbrk system call.
Aside…
On most modern systems, sbrk is no longer an explicit system call, but is instead emulated by library code. We will imagine it to be a system call. (It's documented in section 2 of the manual!)
Also, sbrk may round up the amount of memory it allocates to a multiple of the system's page size. This possibility is not a problem for this assignment, but it is something to be aware of.
Writing reverse
In reverse.c, write a program, reverse, that reads in standard input and then writes it out to standard output with its lines in reverse order (the content of the lines is not reversed). The input can be of arbitrary length, so you must allocate memory dynamically, but you may do so using only with the sbrk system call (with read and write).
You may assume that each line ends with a newline character (a file where the last line does not end with a newline can be an annoying edge case). You can also assume that while the input size is unknown, it will be reasonable (i.e., it will fit in memory).
You will have to work out how to store the input in memory. How you do so is up to you, but your method should match some definition of “sane”.
Tip
Here's a possible strategy for reverse:
- Read the whole file into memory, allocating memory as you go with
sbrk. - Either
- Start at the end and find the beginning of each line, and print it out (which will involve lots of calls to
write), or, - Use an in-place reversing algorithm to reverse the whole buffer in memory and then print it out in one go. You probably need two reversing steps, one to reverse the whole buffer, and one to unreverse each line.
- Start at the end and find the beginning of each line, and print it out (which will involve lots of calls to
(When logged in, completion status appears here.)
Tip
You can tell whether something is a system call or a library function by looking at the manual page for the function. If it is a system call, it will be in section 2 of the manual; if it is a library function, it will be in section 3. You can restrict the
mancommand to looking in a specific section by providing the desired section before the name (e.g.,man 2 openorman 3 printf).