Run Better
This assignment involves making three changes the the posted
runner
example. You should be able to solve it in whatever environment you used for
the first assignment: Unix, Linux (including VMs or WSL), or Mac should work
fine. Start by grabbing the program and compiling it on your platform.
To get started, download this starting code.
Unpack it into its own directory in the Unixy environment where asst1 can
compile. Go there on the command line, and say make to build
two executable files, the original runner and lbrun which we'll talk about
later. (You may need to say instead gmake on Mac, but I don't think
it will matter.) If you want to build the files using an IDE, that's fine,
but it's your problem to make it work. You always have the option
use it to edit, and build from the command line.
Then, make the three improvements.
Error Reporting
Improve the reporting of errors by the program.
- Unix system calls generally return negative values (or sometimes NULL)
to indicate failure. You should capture these, report the errors, and
generally exit. Some very useful tools are errno
and strerror.
For instance, the existing fork call should become something like this:
pid_t kidpid = fork();
if(kidpid < 0) {
cerr << "Fork failed: " << strerror(errno) << endl;
exit(10);
}
This will whine and exit if the call fails. Afterwards, test the variable
kidpid to see if you are the child or parent. (Don't call fork again).
You can use a similar pattern to improve the existing wait call,
and use strerror() to report the reason for an exec failure.
The wait call
can also return the reason why the child process terminated. It does
this through its parameter. So your wait call will look something like
int status;
kidpid = wait(&status);
Use the return value to tell if the wait succeeded. If it did, then
status will tell you what happened to the child process.
Read the wait documentation
for details. If the process was signaled, that means it crashed. If it
exited, that means it didn't, though it also reports a status code to say
if something went wrong. Zero is normal termination, nonzero is an error.
Determine if the process exited or crashed. If exited,
say if it exited normally or gave a code and print the code.
For crashed, extract the signal number which gives the cause, and use
the strsignal utility to get a nice message for printing it.
The wait manual documents several tests that you
can apply to the status after wait returns to see what
happened. If instance, say if(WIFEXITED(status) to see if the
process exited, rather than crashed. If so, the
WEXITSTATUS(status) will tell you code it exited with.
extrunner$ ./runner
Your command? /bin/ls -l missing.txt
ls: cannot access 'missing.txt': No such file or directory
Abnormal exit code 2
extrunner$ ./runner
Your command? /bin/ls -l runner.cpp
-rw-rw-r--. 1 bennet bennet 1302 Sep 19 14:57 runner.cpp
Normal exit
extrunner$ ./runner
Your command? boomer stk
I'm going down!!!!!
Killed: Segmentation fault
Parse An Input Line
The current
runner asks separately for a command and a parameter, and
can have only one. A command interpreter generally takes a line of input
(a command) and breaks it up into pieces to provide the parameters for
the command. You should do this. To simplify the task, you are given
a class LineBreaker2. (Yes, there is an older version.) This object
takes a C++ string and breaks it up into words and stores them as plain
C strings in its peculiar way. There are three files, the header,
linebreaker2.h, the implementation,
linebreaker2.cpp, and
a test driver lbrun.cpp.
Read the comments in
linebreaker2.h to see what the methods do,
look at lbrun.cpp to see them used. Run
lbrun and follow your nose to see some execution examples.
The
purpose of this class is to help you modify the runner so it acts more
like a command interpreter. Let your
program ask for and read a single line, which is a command to
run, then break it into separate parameters and run it.
tom@fedora:~/courses/cs422/asst/extrunner$ ./runner
Your command? /bin/ls -l runner.cpp lbrun
-rwxr-xr-x. 1 tom tom 182320 Sep 21 20:48 lbrun
-rw-r--r--. 1 tom tom 1302 Sep 21 14:05 runner.cpp
Normal exit
When you exec the program, you should use the
execv form of exec. This takes two parameters, the full
file name to execute, and an array of strings which are the parameters for
the execution. This is an array of plain C strings, rather than C++ ones.
By design, LineBreaker2 provides exactly what is needed. Subscript
at 0 for the file name, and send the whole C array as the second parameter.
This effectively sends the file name twice, but that is what exec
expects.
Providing Input
The
third change is to provide a file for the child process to write in
place of the console. This a Unix (or perhaps originally Multics) invention
which makes command use much more flexible. If the output of a program is
long, you can direct the shell to save it in a file instead of printing it,
so you can examine the file later. The feature is implemented on Unix and
friends, and also Windows using the
>, but, just to be different,
we'll use a colon instead.
extrunner$ ./runner
Your command? /bin/ls jobs.txt
ls: cannot access 'jobs.txt': No such file or directory
Abnormal exit code 2
extrunner$ ./runner
Your command? /usr/bin/ps -e
PID TTY TIME CMD
1 ? 00:00:24 systemd
2 ? 00:00:00 kthreadd
3 ? 00:00:00 pool_workqueue_release
4 ? 00:00:00 kworker/R-rcu_gp
... many lines omitted ...
155842 ? 00:00:00 kworker/u16:4-ext4-rsv-conversion
155843 ? 00:00:00 kworker/u16:5-ext4-rsv-conversion
155875 pts/5 00:00:00 runner
155877 pts/5 00:00:00 ps
Normal exit
extrunner$ ./runner
Your command? /usr/bin/ps -e :jobs.txt
Normal exit
extrunner$ head -5 jobs.txt
PID TTY TIME CMD
1 ? 00:00:24 systemd
2 ? 00:00:00 kthreadd
3 ? 00:00:00 pool_workqueue_release
4 ? 00:00:00 kworker/R-rcu_gp
extrunner$
So, the first time I run the process listing command, it types many lines
to the console. The second time, I specified an output file. The runner
program arranged that output from ps will go into that file, which I can
then examine more carefully with a text editor or other tools. So:
- After entering and parsing the command, see if the last string on the line
starts with a colon.
- If it does, remove the file name from the parameter list, then take
the (former) last parameter and remove the colon to extract the file name.
Assuming your LineBreaker2 object is called lb, I recommend this
procedure:
char *fn = lb.pop_back();
fn++;
This uses the fact that we are using the plain C representation of a string
as a pointer to the first character. (The actual
storage space is allocated inside
the LineBreaker2 object.) Then, simply incrementing the pointer
makes it point to the second character, omitting the first from the string.
If the last word on the line was an input file,
perform kernel file open. This is neither the plain C fopen call,
nor the C++ ifstream::open. It looks like this:
int fd = open(fn, O_WRONLY|O_CREAT|O_TRUNC, 0644);
You will also need to
#include fcntl.h.
The returned value
fd is called a file descriptor, and refers to
the record of the created stream within the PCB for the process.
The open may fail, so if fd is negative, print an error and exit.
The second parameter specifies write only, create the file if it does
not already exist, and discard any existing contents if it does. This is
what most any higher-level open would send for you. The third parameter
limits the final permissions of the file, subject to the setting for your
login, which is undoubtedly some default no one ever asked you about.
Be careful with this. When the above open call runs with the name of
an existing file, the file will be clobbered without asking. Watch out
while testing.
Open the file before running fork.
In the child portion of your code, after testing the fork return but
before the exec, change the
process output stream to be the file you opened.
The required incantation is
dup2(fd, 1);
close(fd);
The numbers fd and 1 refer to open file streams. Traditionally, they index a
table in the process control block. The number 1 is the standard output
stream, fd is the number assigned to the file when you opened earlier.
The first call, makes the #1 entry an alias for this file, and the close
removes the old entry which we no longer need.
Make sure not to do any of this if no input file was specified; don't
break the simpler case.
- Since this change applies to the child process created by fork, and
the program you run with exec runs inside this process, the child will
run with its output changed to the file you opened.
- On the parent side, when fork() returns nonzero, close the file
descriptor. The parent will not be using it.
Enough improvements. Test these to make sure they work.
When your program works correctly and looks nice,
submit
it here.