The assignment introduces background process, file redirection,
pipe, and programming with POSIX threads
(pthreads), including mutual exclusion with pthreads.
Experience with some more system calls and C
library functions will also be done to hone your programming skills.
Reading of man pages will also be a must to do well.
Try sample code to experiment more.
./a.out&
", "amd1|cmd2
",
"cmd >>outfile
", "cmd>> outfile
", etc.
When a parent process does not (get the chance to) do a
wait(2)
/waitpid(2)
call
for its children, the children will become zombie processes
(marked by <defunct> in Linux or Z) when they exit or die
involuntarily (e.g., killed). To prevent
this for your backgrounded processes (jobs), you need to do a
nonblocking waitpid(2)
call `at some point'
(by using the WNOHANG option). See the man page for options to
use to reap an entry in a nonblocking fashion.
It is suggested that you do this BEFORE
printing the next prompt.
If the shell has a zombie child, the call
will reap the process entry,
otherwise no harm (waiting) is done either. Check out sample
zombie code that deliberately creates
a zombie process. Also check out mysh_bg.c
,
slp_exit.c
, and slp_killed.c
in
sample code
to "play with" background processes and zombies.
(1) mysh_bg.c: a simple shell that can "background" a child process (by running an external command with &). Compiled into executable mysh_bg as follows. $ gcc mysh_bg.c -o mysh_bg (2) slp_exit.c: a program that prints a message every second for 7 seconds. Compiled into executable (external command) slp_exit as follows. $ gcc slp_exit.c -o slp_exit (3) slp_killed.c: a program that never terminates. It is to be killed via the kill command. Compiled into executable (external command) slp_killed as follows. $ gcc slp_killed.c -o slp_killed [A] Run mysh_bg with slp_exit Inside mysh_bg, run external command slp_exit in a bachground process as follows. % ./slp_exit & While slp_exit is running and printing, pressing RETURN key will print a new prompt %. When slp_exit exits, a SIGCHLD signal is sent to mysh_bg that catches the signal and prints a message. [B] Run mysh_bg with slp_killed Inside mysh_bg, run external command slp_killed in a bachground process as follows. % ./slp_killed & (a) When slp_killed is running in the background, pressing RETURN key prints a new prompt %. This is the same as [A]. (b) Since slp_killed never ends, it has to be "kill"ed after running for a few seconds. When that happens, slp_killed becomes a zombie.The issue now is how to "get rid of" a zombie. Zombie is defined as a process that has exited/died but has not yet been "wait"ed by its parent (the shell, in our case). Technically, the issue is when to call
waitpid()
for a background process.
Extra Credit (10 points): Implement the 'fg' built-in command to bring a backgrounded job into the foreground. With no arguments, a default backgrounded job will be chosen. It should also take an argument to specify which backgrounded job to bring to the foreground. Add your own test to show this works and clearly document if you implement this. Your shell should work just like when a job isn't backgrounded when a job is brought into the foreground. Refer to Sections 9.8 and 9.9 of Stevens and Rago's APUE book.
To do the file redirection requires that you understand file
descriptors well (a child process inherits file descriptors
from its parent and open file descriptors are left open across
exec()
[Stevens and Rago' APUE, p. 252]).
You will need to close and reopen file descriptors
0, 1, or 2. For this, you will need to use open()
,
close()
and dup()
system calls.
(Refer to Section 3.12 of Stevens and Rago's APUE book
for dectails of dup()
.)
Please use dup(2)
instead of dup2(2)
.
The new file descriptor returned by dup(2)
is guaranteed
to be the lowest-numbered available file descriptor.
This does not require a lot of code, just the right calls in the right
places. To get the normal function of stdin, stdout and stderr back,
opening of /dev/tty
will be required. Be sure to use the
right options for open()
in each situation (i.e.,
read/write, truncate/append). Read man pages!
To help out in testing both stdoue and stderr, there is a small program test-1+2.c. Compile it as execuatble test-1+2 and use it in test runs. Sample code to redirect stdout to a file:
fid = open(filename, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP); close(1); dup(fid); close(fid);To redirect stdout back to the terminal, replace the
open()
with
fid = open("/dev/tty", O_WRONLY);and repeat the other 3 system calls.
Additionally, add the noclobber built-in command which
will affect how these redirection operators handle file creation.
All the noclobber command should do is to "switch" the value of the noclobber
tcsh shell variable
(to prevent accidental overwriting of existing files) from 0 to 1 or
from 1 to 0 and print out the new value of it, as follows.
[cisc361:/usa/cshen/361/PA_4/SHELL 737] ./mysh >> noclobber Executing built-in [noclobber] 1 >> noclobber Executing built-in [noclobber] 0 >> noclobber Executing built-in [noclobber] 1 >> exit Executing built-in [exit] [cisc361:/usa/cshen/361/PA_4/SHELL 739]The
noclobber
shell variable should initially default to 0
and cause your shell to act the same way as csh/tcsh does with respect
to the noclobber
shell variable as shown below.
That is when it is 0, > and >& will overwrite
existing files and >> and >>& will create the file if
it doesn't exist. When noclobber
is 1, your shell should
refuse to overwrite existing files (for > and >&), refuse to create a (new) file for
appending (for >> and >>&), and print the same error messages csh/tcsh do in those
situations as follows. Note that in csh/tcsh, noclobber
initially defaults to "set" (i.e., value 1).[cisc361:/usa/cshen/361/PA_4/SHELL 761] tcsh unknown OS cisc361[31] [~/361/PA_4/SHELL/]> ls file* file_1 cisc361[32] [~/361/PA_4/SHELL/]> more file_1 hello cisc361[33] [~/361/PA_4/SHELL/]> echo world > file_1 file_1: File exists. cisc361[34] [~/361/PA_4/SHELL/]> echo world > file_2 cisc361[35] [~/361/PA_4/SHELL/]> echo hello >> file_2 cisc361[36] [~/361/PA_4/SHELL/]> more file_2 world hello cisc361[37] [~/361/PA_4/SHELL/]> echo kitty >> file_3 file_3: No such file or directory. cisc361[38] [~/361/PA_4/SHELL/]> unset noclobber cisc361[39] [~/361/PA_4/SHELL/]> more file_1 hello cisc361[40] [~/361/PA_4/SHELL/]> echo good job > file_1 cisc361[41] [~/361/PA_4/SHELL/]> more file_1 good job cisc361[42] [~/361/PA_4/SHELL/]> ls file* file_1 file_2 cisc361[43] [~/361/PA_4/SHELL/]> echo good luck >> file_3 cisc361[44] [~/361/PA_4/SHELL/]> more file_3 good luck cisc361[45] [~/361/PA_4/SHELL/]> echo bye bye >> file_1 cisc361[46] [~/361/PA_4/SHELL/]> more file_1 good job bye bye cisc361[47] [~/361/PA_4/SHELL/]> exit exit [cisc361:/usa/cshen/361/PA_4/SHELL 762]In light of the actual outputs from csh/tcsh, your shell would behave as follows.
>> ls file_* Executing [/bin/ls] file_1 file_2 >> noclobber Executing built-in [noclobber] 1 >> echo world > file_1 Executing built-in [echo] file_1: File exists. >> echo world >> file_4 Executing built-in [echo] file_4: No such file or directory.
[cisc361.acad.ece.udel.edu:/usa/cshen 132] tcsh cisc361[29] [~/]> which echo echo: shell built-in command. cisc361[30] [~/]> echo hello world | wc 1 2 12 cisc361[31] [~/]>
cisc361[34] [~/361/2018/apue.3e/intro/]> cisc361[34] [~/361/2018/apue.3e/intro/]> ls X X: No such file or directory cisc361[35] [~/361/2018/apue.3e/intro/]> ls X | wc X: No such file or directory 0 0 0 cisc361[36] [~/361/2018/apue.3e/intro/]> ls X |& wc 1 6 29 cisc361[37] [~/361/2018/apue.3e/intro/]>
To implement this, you will need to use pipe(2)
.
To implement pipes, file descriptor redirection will be
required again by using close()
and dup()
.
(Refer to Section 3.12 of Stevens and Rago's APUE book
for dectails of dup()
.)
However you will be getting open file descriptors by using pipe(2)
instead of open()
. This task is a bit trickier than the
previous, but again does not take much code. It will take some trials
and thinking to get right. You will probably need to move your command
running code to a new function which takes info about the style of the
pipe (if any) and which side of the pipe this command will be on. The
left side of the pipe needs to not cause the parent to do a blocking
wait, while the right side does.
Once the parent process (your shell) has created a pipe and
forked the two child processes that will write and read from the pipe,
make sure the parent (your shell) explicitly closes the file
descriptors for its read and write access to the pipe. If you fail
to do this, the (right) child
reading from the pipe will never terminate. This is because the child
reading from the pipe will never get the END-OF-FILE (end-of-pipe)
condition (and hence never terminate) as long as one process (in this
case your shell, by mistake) has an open write file descriptor
for the pipe. The overall result will be a deadlock - your shell
waiting for a child to terminate and the child waiting for the shell to
close the pipe, which was inadvertently left open.
Check out mysh_pipe.c
in
sample code to see how
pipe works in the simplest shell possible.
(Review
Pipes
and Section 9.9 of Stevens and Rago's APUE book for dectails.)
This part will also require that your command line processing
puts the arguments into two separate argument vectors. And do not use popen()
;
you will get no credit.
watchuser
built-in command.
This command takes as its first argument a username to keep track
of when the user logs on, similar to the watch
shell variable in tcsh
(review tcsh shell variables
under Enhanced security in tcsh: Monitor
everyone who's using the system.).
This command also takes a
second optional argument of "off" to stop watching a user.
The first time
watchuser
is ran, a (new) thread should be
created/started via pthread_create(3)
, which
runs a while (TRUE)
loop with a sleep(3)
call for 20 seconds in it to track logins of users.
Only one watchuser
thread should ever be running.
The thread should get the list of users from a
global linked list
which the calling function (of the main thread) will modify by
either inserting new users or turning off existing watched users.
For instance,
mysh % watchuser cshen mysh % watchuser smith mysh % watchuser joe mysh % watchuser smith off
This thread should prin a message "USER has logged on TTY from HOST." to the terminal when it FIRST detects that a user from the watch list logs on to a new tty. Sample code to obtain the list of currently logged in users can be found here.
Upon exiting from the shell, the watchuser
thread
should be cancelled via pthread_cancel(3)
and "waited for" by pthread_join(3)
to prevent
memory leak. (Check out thread_cancel.c
in
sample code and compare the valgrind outputs
without and with line 33.)
Since the calling function (of the main thread) and the
watchuser
thread have access to share data
(e.g., the global linked list of users to be watched), the shared data
must be protected with a mutex lock using
pthread_mutex_lock(3)
and
pthread_mutex_unlock(3)
.
Extra Credit (5 points): Also have your thread notify when the users being watched log off of a tty. Make it clear that you do this if you do, and document it well.
strace(1)
useful.
To test '&', show running some commands in the background in your script. To test watchuser, come up with your own tests. The TAs will test each feature.
Test your shell by running the following commands in it (in order): (ignore all the watchmail commands)
pwd
ls &
ls -l &
cd /
sleep 20 &
ls & ; run before sleep is done
pid
tty
/bin/ps -lfu USERNAME ; replace USERNAME with your own
cd
cd [project test dir of your choosing]
pwd
ls -l
rm -f test1 test2 test3 test4 test5 test6 test7 test8
./test-1+2 > test1
./test-1+2 >> test2
./test-1+2 >& test3
./test-1+2 >>& test4
cat test1 test2 test3 test4
./test-1+2 > test1
./test-1+2 >> test2
./test-1+2 >& test3
./test-1+2 >>& test4
cat test1 test2 test3 test4
noclobber ; turn noclobber on
./test-1+2 > test5
./test-1+2 >> test6
./test-1+2 >& test7
./test-1+2 >>& test8
cat test5 test6 test7 test8
./test-1+2 > test5
./test-1+2 >> test6
./test-1+2 >& test7
./test-1+2 >>& test8
cat test5 test6 test7 test8
grep hello < test8
grep error < test8
rm -f test9 test10 test11 test12
noclobber ; turn noclobber off
./test-1+2 > test9
./test-1+2 >> test10
./test-1+2 >& test11
./test-1+2 >>& test12
cat test9 test10 test11 test12
ls | fgrep .c ; show pipes works
./test-1+2 | grep hello
./test-1+2 |& grep hello
./test-1+2 | grep output
./test-1+2 |& grep output
./test-1+2 |& grep error
pid ; zombie avoidance checking
/bin/ps -lfu USERNAME | grep defunct ; replace USERNAME with your username
You need to tar up your source code and submit the tar file so that your shell can be tested and graded. To do this,
zip -r YourLoginName_4 YourLoginName_4