$35
Process Creation and Process Management in C
Overview
Homework specifications
In this second homework, you will use C to implement a rudimentary interactive shell similar to that of bash. The focus of this assignment is on process creation, process management, and inter-process communication (IPC) via fork(), wait(), waitpid(), pipe(), etc.
As with Homework 1, continue to use calloc(), realloc(), and free() to properly and efficiently manage your memory usage. Consider using valgrind to verify that there are no memory leaks. We will continue to test for this on Submitty.
To properly implement your shell, create an infinite loop that repeatedly prompts the user to enter a command, parses the given command, locates the command executable, then executes the command (if found).
To execute the given command, a child process is created via fork(), with the child process then calling execv() to execute the command. In the meanwhile, the parent process calls waitpid() to suspend its execution and wait for the child process to terminate. This is called foreground processing. (And note that you must use these specific system calls.)
If instead the command is to be run with the parent process not waiting for the child process to complete its execution, then your shell will instead use background processing, which is achieved by using the & symbol (and explained in more detail on page 4).
Locating the command executable
Before executing a command entered by the user, the command executable must be found using the list of possible paths specified by an assignment-specific environment variable called $MYPATH. Do not use $PATH for this assignment (since $PATH is used for bash).
Similar to $PATH, this new $MYPATH environment variable consists of a series of paths delimited by the : character. And if $MYPATH is not set, use /bin:. as the default, meaning commands will be searched for first in the /bin directory, then the . (i.e., current) directory. By default, the $MYPATH variable is not set, so for testing, set and unset this variable manually in the bash shell before running your shell. Here’s how:
bash$ export MYPATH=/usr/local/bin:/usr/bin:/bin:.
bash$ echo $MYPATH MYPATH=/usr/local/bin:/usr/bin:/bin:.
bash$ unset MYPATH
To obtain $MYPATH (or any environment variable, e.g., $HOME) from within your program, use the getenv() function. Do not use setenv().
Executing the command
Searching left-to-right in $MYPATH, if the requested command is found in one of the specified directories, your program runs the executable in a child process via the fork() and execv() system calls. Note that you must use execv().
Further, in the parent process, you must use lstat() to determine whether the requested command exists (e.g., does /bin/ls exist?) and whether it is executable. See the man page and the directories.c example.
Commands are line-based, as in bash. Therefore, each command may optionally have any number of arguments (i.e., argv[1], argv[2], etc.). You can assume that each command read from the user will not exceed 1024 characters. Further, you can assume that each argument will not exceed 64 characters, but all memory must be dynamically allocated.
You may also assume that command-line arguments do not contain spaces. In other words, do not worry about parsing out quoted strings in your argument list, as in:
bash$ cat a.txt b.txt "some weird file.txt" d.txt
Special shell commands
Not all commands entered into the shell actually result in a call to fork(). For the cd command, if your shell did execute the command via fork(), your shell’s current working directory would not change! Therefore, you must use the chdir() system call in the parent to handle this special case. Further, if the cd command has no arguments, then you should use the $HOME environment variable as the target directory.
As for wildcards and special characters, you do not need to support *, ?, and [] in your shell, though note that these are typically expanded by the shell before calling fork() and execv().
Finally, to exit your shell, the user enters exit. When this occurs, your shell must output bye and terminate.
Required output
The command prompt in the shell must show the current working directory followed by the '$' prompt character and one space. To obtain the current working directory for the running process, use the getcwd() function. And use fgets() to read in a command from the user.
Required output is shown below, with sample input also shown. As per usual, you must match the given output format exactly as shown.
/cs/goldsd/s19/os/assignments/hw2$ cocoapuffs
ERROR: command "cocoapuffs" not found
/cs/goldsd/s19/os/assignments/hw2$ ls
annoying.c a.out code hw2.aux hw2.log hw2.out hw2.pdf hw2.tex /cs/goldsd/s19/os/assignments/hw2$ ls -l
total 156
drwxrwx--x 3 goldsd goldsd 4096 May 27 15:08 . drwxrwx--x 5 goldsd goldsd 51 May 27 11:59 ..
-rw-rw----+ 1 goldsd goldsd 197 May 27 15:08 annoying.c -rwxrwx--x 1 goldsd goldsd 8344 May 27 15:08 a.out
drwxrwx--x 2 goldsd goldsd 6 May 27 15:04 code
-rw-rw---- 1 goldsd goldsd 662 May 27 15:04 hw2.aux -rw-rw---- 1 goldsd goldsd 20507 May 27 15:04 hw2.log
-rw-rw---- 1 goldsd goldsd 0 May 27 15:04 hw2.out
-rw-rw---- 1 goldsd goldsd 97289 May 27 15:04 hw2.pdf -rw-rw----+ 1 goldsd goldsd 9886 May 27 15:07 hw2.tex /cs/goldsd/s19/os/assignments/hw2$ cat annoying.c
/* annoying.c */
#include <stdio.h> #include <stdlib.h> #include <unistd.h>
int main()
{
while ( 1 )
{
printf( "Hey, get back to work!\n" );
sleep( 3 );
}
return EXIT_SUCCESS;
}
/cs/goldsd/s19/os/assignments/hw2$ exit bye
Foreground and background processing
Normally, a shell will execute the given command via a child process, with the parent calling either wait() or waitpid() to wait for the child process to complete its command and terminate. The child process essentially calls execv() to execute the given command with the arguments allocated dynamically.
Your shell must be able to execute a process in the background if the user includes an ampersand (i.e., '&') at the end of a command. In this case, when the child process is created, the parent does not wait for the child to terminate before prompting the user for the next command.
For a background process, the parent must report that the child process has been created.
The parent must also report when the child process does terminate (if it does). When you detect that the background process has terminated (i.e., before you display the prompt), display the child process ID and its exit status, as in:
[process 9335 terminated with exit status 0]
Or if an abnormal termination occurred, display the following:
[process 9335 terminated abnormally]
Note that the & symbol can only be included at the end of the command line; otherwise, this is a user error. Also note that the output may be interleaved with background processes, so do not expect to always match the example output exactly line for line.
/cs/goldsd/s19/os/assignments/hw2$ ls
annoying.c a.out code hw2.aux hw2.log hw2.out hw2.pdf hw2.tex /cs/goldsd/s19/os/assignments/hw2$ a.out &
[running background process "a.out"]
Hey, get back to work!
/cs/goldsd/s19/os/assignments/hw2$ Hey, get back to work!
Hey, get back to work! Hey, get back to work! /cs/goldsd/s19/os/assignments/hw2$ ls
annoying.c a.out code hw2.aux hw2.log hw2.out hw2.pdf hw2.tex /cs/goldsd/s19/os/assignments/hw2$ Hey, get back to work!
Hey, get back to work! Hey, get back to work! /cs/goldsd/s19/os/assignments/hw2$ exit
bye
Hey, get back to work!
Hey, get back to work!
For the above example, you will need to use kill in the bash shell to terminate the background process since it will continue to execute after your shell terminates. And note that for this assignment, you are required to use waitpid() for both foreground and background processes.
Do not do anything extra to kill any child processes, in particular when the user exits your shell. And to properly “catch” background processes when they terminate, mimic the behavior of bash by checking for terminated background processes immediately before you display the prompt to the user. Do not use a signal handler for this.
IPC via pipes
Finally, add support for a pipe between two processes; you need only support one pipe per command line. Two processes (i.e., A and B) may be connected via a pipe such that the output on stdout from process A is the input on stdin to process B.
A pipe is indicated via the | symbol. To create a pipe, use the pipe() system call. Further, the two processes A and B must have your shell process as their parent process.
/cs/goldsd/s19/os/assignments/hw2$ ps -ef | grep goldsd
root
23553
1414
0
15:00 ?
00:00:00
sshd: goldsd [priv]
goldsd
23556
1
0
15:00 ?
00:00:00
/lib/systemd/systemd --user
goldsd
23558
23556
0
15:00 ?
00:00:00
(sd-pam)
goldsd
23714
23553
0
15:00 ?
00:00:00
sshd: goldsd@pts/0
goldsd
23715
23714
0
15:00 pts/0
00:00:00
-bash
goldsd
23716
23715
0
15:00 pts/0
00:00:00 myshell
root
23729
1414
0
15:01 ?
00:00:00
sshd: goldsd [priv]
goldsd
23813
23729
0
15:01 ?
00:00:00
sshd: goldsd@notty
goldsd
23814
23813
0
15:01 ?
00:00:00
/usr/lib/openssh/sftp-server
goldsd
24615
23716
0
15:15 pts/0
00:00:00 ps -ef
goldsd
24616
23716
0
15:15 pts/0
00:00:00 grep goldsd
/cs/goldsd/s19/os/assignments/hw2$ ls -1
annoying.c
a.out
code
hw2.aux hw2.log hw2.out hw2.pdf hw2.tex /cs/goldsd/s19/os/assignments/hw2$ ls -1 | wc -l
8
/cs/goldsd/s19/os/assignments/hw2$ exit
bye
Pipes and background processes
Note that a pair of piped processes can be run in the background if the user specifies an ampersand at the end of the line. When run in the background, both processes are background processes. And when these processes terminate, show both processes.
Here is an example with 12117 and 12118 as the process IDs of the two background processes:
/cs/goldsd/s19/os/assignments/hw2$ ls -1 | wc -l 8
/cs/goldsd/s19/os/assignments/hw2$ ls -1 | wc -l & [running background process "ls"]
[running background process "wc"]
/cs/goldsd/s19/os/assignments/hw2$
8
[process 12117 terminated with exit status 0] [process 12118 terminated with exit status 0] /cs/goldsd/s19/os/assignments/hw2$ exit
bye
As noted previously, output may be interleaved with background processes, so do not expect to always match the example output exactly line for line.
Relinquishing allocated resources
Be sure that all processes (i.e., the parent shell process and all child processes) properly deallocate memory via free(), close all opened file descriptors, etc.
Error handling
If improper command-line arguments are given, report an error message to stderr and abort
further program execution. In general, if an error is encountered, display a meaningful error
message on stderr by using either perror() or fprintf(), then abort further program execution.
Error messages must be one line only and use the following format:
ERROR: <error-text-here>
Note that you should not abort your shell program if the user enters an invalid command (e.g., command not found) or if the child process reports an error.
Submission instructions
To submit your assignment (and also perform final testing of your code), please use Submitty, the homework submission server.
Note that this assignment will be available on Submitty a minimum of three days before the due date. Please do not ask when Submitty will be available, as you should first perform adequate testing on your own Ubuntu platform.
That said, to make sure that your program does execute properly everywhere, including Submitty, use the techniques below.
First, as discussed in class, use the DEBUG_MODE technique to make sure you do not submit any debugging code. Here is an example:
#ifdef DEBUG_MODE
printf( "the value of x is %d\n", x ); printf( "the value of q is %d\n", q );
printf( "why is my program crashing here?!" ); printf( "aaaaaaaaaaaaagggggggghhhh!" );
#endif
And to compile this code in “debug” mode, use the -D flag as follows:
bash$ gcc -Wall -Werror -D DEBUG_MODE hw2.c
Second, as discussed in class, output to standard output (stdout) is buffered. To disable buffered output for grading on Submitty, use setvbuf() as follows:
setvbuf( stdout, NULL, _IONBF, 0 );
You would not generally do this in practice, as this can substantially slow down your program, but to ensure good results on Submitty, this is a good technique to use.