Lab 07: Fork-Bombs, Process State Monitoring, and Foreground/Background
Table of Contents
Preliminaries
In this lab you will complete a set of C programs that will expose you to termination status, fork-exec-wait loops, and tokenizing strings. There are 3 tasks, you will likely complete only task 1 in lab, and begin with task 2. You will need to finish the remaining taks outside of lab.
Lab Learning Goals
In this lab, you will learn the following topics and practice C programming skills.
- Understand various process states, fork bomb, orphans, and zombies
- Monitoring process states through
/proc/[pid]/state
- Following Process family
- Foreground and Background processes mechanism
- Waiting on a process state change using
waitpid()
- Signally a process to continue using
kill()
- Managing the foreground process group using
tcsetpgrp()
Lab Setup
Run the following command
~aviv/bin/ic221-up
Change into the lab directory
cd ~/ic221/labs/07
All the material you need to complete the lab can be found in the lab directory. All material you will submit, you should place within the lab directory. Throughout this lab, we refer to the lab directory, which you should interpret as the above path.
Submission Folder
For this lab, all ubmission should be placed in the following folder:
~/ic221/labs/07/
This directory contains 4 sub-directories; examples
, timer
,
term-status
, and mini-sh
. In the examples
directory you will
find any source code in this lab document. All lab work should be
done in the remaining directories.
- Only source files found in the folder will be graded.
- Do not change the names of any source files
Finally, in the top level of the lab directory, you will find a
README
file. You must complete the README
file, and include any
additional details that might be needed to complete this lab.
Compiling your programs with clang
and make
You are required to provide your own Makefiles for this lab. Each of
the source folders, myps
, mypstree
, and fg-shell
, must have a
Makefile. We should be able to compile your programs by typing
make
in each source directory. The following executables should be
generated:
myps
: executable formyps
directorymypstree
: execute formypstree
directoryfg-shell
: execute forfg-shell
directory
When compiling fg-shell
, you will need to link against the
readline
library. Add the -lreadline
to your compile command,
and you can refer to the previous lab (Lab 6) for an example. Y
README
In the top level of the lab directory, you will find a README
file. You must fill out the README file with your name and alpha.
Please include a short summary of each of the tasks and any other
information you want to provide to the instructor.
Test Script
You are provided a test script which prints pass/fail information
for a set of tests for your programs. Note that passing all the
tests does not mean you will receive a perfect score: other tests
will be performed on your submission. To run the test script,
execute test.sh
from the lab directory.
./test.sh
You can comment out individual tests while working on different parts of the lab. Open up the test script and place comments at the bottom where appropriate.
Working versions for comparisons
Working versions of all the programs described in this lab document can be found here:
~aviv/lab7-bin/myps ~aviv/lab7-bin/mypstree ~aviv/lab7-bin/fg-shell
Run these programs as a comparison point.
Part 1: The Manhattan Project
It is easy to make mistakes in systems programming, and those mistakes are often small but catastrophic. When you are writing code with system calls, you are directly telling the O.S. to complete a task, and while we like to think of our computers as smart, they are not. The O.S. will complete a task without questioning, even if it means imitate doom.
In this part, we are going to look at one such common mistake of
systems programming: the fork bomb. A fork bomb occurs when a process
has runaway fork()
'ing. The parent calls fork()
create a child,
then both parent and child call fork()
again, creating another child
and a grandchild, and then the entire "family" calls fork()
again,
and etc. Before you know it, exponential blow up … FORK BOMB!
… the O.S. runs out of system memory to store all the information
about the quickly multiplying processes … Boom. The system goes
down.
An egregious fork bomb
Here is a simple example of a fork bomb: (DO NOT RUN THIS PROGRAM, yet)
#include <unistd.h> int main(){ while(1){ //oops! fork(); } }
Each loop through the program, there are twice as many children,
and each of them are calling fork()
. This is the explosion, a fork
bomb, which will eventually cripple the system and crash the
operating system. In the tasks below, you are going to execute this
fork bombs in a hope to try and avoid executing fork bombs in the
future.
Task 1 littleman
For this task, change into the forkbomb directory in the lab
directory. In there, you will find the source file littleman.c
,
which you can compile with make
. Before running this program:
- Be sure you are physically in the lab – do not run this through ssh, it will crash the computer. We know who you are.
- Close and save any assignments currently open — The computer will crash, all will be lost.
Execute the program. Restart your computer. Don't run the program again.
Answer the questions in the README.
User Limiting
That was a bit severe, and clearly, this is a very, very bad thing. So bad, in fact, that fork bombs are a common payload for malware to crash systems in Denial of Service attacks. Fork bombs are not O.S. specific, and can be written in almost any language and on almost any operating system. The result is almost always the same, crash.
There is hope, though. Unix systems allow for you to set up limits
to avoid catastrophic resource hogging, like a fork bomb. One such
limitation is the number of processes allowed per user. To see
what this limit is set to by default, execute ulimit -u
in the
terminal:
#> ulimit -u 61281
You are currently unlimited, and you can create 60k+ processes. Let's change that to a limit of 10,000 which is sufficient to cause the system to slow down but not crash.
#> ulimit -u 10000 #> ulimit -u 10000
To keep you safe, it's probably best to insert this user limit into
your .bashrc
file.
Better Behaved Fork Bomb
A more likely scenario than the clear forkbomb above, is that you'll
write a more subtle fork-bomb that doesn't quite explode as
violently. Consider the program bigboy
:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char* argv[]){ if(argc<2){ fprintf(stderr, "Usage: '%s n', creates 2^n processes \n", argv[0]); exit(1); } //fork count times, read from command line int count = atoi(argv[1]); while(count){ fork(); count--; } //loop forever while(1); }
bigboy
takes an argument that indicates the number of generations
to create. The first process forks and decrements the counter. The
children fork and decrement it again, etc. All processes go into an
infinite while(1)
loop once the max number of generations have
been created. We will use bigboy
to measure the impact of a
forkbomb on the system.
Task 2 bigboy
Before startin this task, either add the ulimit
to your bashrc or
run the following bash command from the forkbomb
directory:
cat bashrc >> ~/.bashrc
In this task, you will run the program bigboy
, which is a more
management fork bomb. You will also need to be able to perform the
following actions
- Count the instances of a process with
ps
. In one terminal, runbigboy 4
and in the other run the belowps
commands.ps -C bigboy ps -C bigboy | wc -l
Note that wc -l might over report by 1 for the status line, so even though it says 17, there are only 16 instances of the process running.
- You can observe running process with
top
. The top command shows all running process on your machine, sorted by cpu usage. The top of the screen shows the total number of tasks and the number running or sleeping. Here's an example header:top - 18:56:16 up 13 days, 1:49, 3 users, load average: 0.02, 0.02, 3.08 Tasks: 179 total, 1 running, 178 sleeping, 0 stopped, 0 zombie Cpu(s): 0.0%us, 0.0%sy, 0.0%ni, 99.9%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Mem: 7862628k total, 1654436k used, 6208192k free, 408632k buffers Swap: 12753916k total, 0k used, 12753916k free, 865128k cached
- Kill all instances of process by name: You will definitely have
to do this at some point.
killall bigboy
Complete the following instructions and answer relevant questions in the README.
- Open three terminal windows.
- In window A, start
top
- In window B, you will run
bigboy n
wheren
is the initial count - In window C, run other commands, like
ps
andkillall
- In window A, start
- In the
README
you will find a table for you to record information. The first two rows are filled out as an example. Complete the following for the remaining rows.- Execute:
bigboy n
replacen
with an appropriate value - fill in the table using
top
andps
- kill the process with
killall bigboy
- Execute:
- Answer Remaining questions in the
README
for this task - Kill an remaining
bigboy
processes before starting any more tasks
Part 2: Process State Monitoring via /proc/[pid]
We survived the "nuclear" apocalypse, and now on safer ground, we
can start our investigation into process state. In this part of the
lab, you will write two small programs that, given a process id,
will retrieve status information to display … a lot like ps
and
pstree
The /proc
File System
The /proc
file system is special on Linux. It does not exist on
disc, but rather is a pseudo-file system. It's the place where the
O.S. allows users to take a peak at the internal workings. For
example, you can check out information about memory usage:
cat /proc/meminfo MemTotal: 7862628 kB MemFree: 6207280 kB Buffers: 408632 kB Cached: 865164 kB SwapCached: 0 kB Active: 634824 kB Inactive: 724844 kB Active(anon): 86628 kB Inactive(anon): 38332 kB Active(file): 548196 kB Inactive(file): 686512 kB Unevictable: 4 kB Mlocked: 4 kB SwapTotal: 12753916 kB SwapFree: 12753916 kB Dirty: 0 kB (...)
You can also see information about the processor:
cat /proc/cpuinfo processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 58 model name : Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz stepping : 9 microcode : 0xc cpu MHz : 1600.000 cache size : 8192 KB physical id : 0 siblings : 8 core id : 0 cpu cores : 4 apicid : 0 initial apicid : 0 fpu : yes fpu_exception : yes cpuid level : 13 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm ida arat epb xsaveopt pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase smep erms bogomips : 6783.84 clflush size : 64 cache_alignment : 64 address sizes : 36 bits physical, 48 bits virtual power management: (...)
If we take a look at the directory more closely, we can see that there is all sorts of information in there:
#> ls -F /proc/ 1/ 1257/ 1375/ 1637/ 1841/ 250/ 36/ 477/ 674/ acpi/ ioports sched_debug 10/ 1267/ 1393/ 1640/ 1871/ 253/ 37/ 48/ 675/ asound/ irq/ schedstat 1003/ 1339/ 14/ 1656/ 19/ 254/ 38/ 499/ 68/ buddyinfo kallsyms scsi/ 1010/ 1349/ 15/ 1662/ 1924/ 257/ 39/ 50/ 69/ bus/ kcore self@ 1014/ 1353/ 1525/ 1666/ 2/ 259/ 4/ 51/ 7/ cgroups key-users slabinfo 1021/ 1360/ 1528/ 1670/ 20/ 26/ 406/ 52/ 7059/ cmdline kmsg softirqs 1024/ 1361/ 1545/ 1695/ 2010/ 262/ 41/ 53/ 7066/ consoles kpagecount stat 1027/ 1362/ 15609/ 17/ 2040/ 268/ 412/ 54/ 7067/ cpuinfo kpageflags swaps 1038/ 1363/ 16/ 1700/ 22/ 27/ 42/ 549/ 8/ crypto latency_stats sys/ 1040/ 1364/ 16077/ 1702/ 23/ 278/ 428/ 55/ 88/ devices loadavg sysrq-trigger 1042/ 1365/ 16095/ 1704/ 2354/ 28/ 43/ 558/ 887/ diskstats locks sysvipc/ 1052/ 1366/ 16119/ 1709/ 2361/ 281/ 44/ 56/ 888/ dma mdstat timer_list 1071/ 1367/ 16130/ 1725/ 2362/ 282/ 441/ 566/ 89/ dri/ meminfo timer_stats 11/ 1368/ 16131/ 1727/ 24/ 29/ 448/ 6/ 897/ driver/ misc tty/ 1110/ 1369/ 1618/ 1729/ 245/ 3/ 45/ 623/ 9/ execdomains modules uptime 11209/ 1370/ 1623/ 1735/ 246/ 30/ 450/ 639/ 90/ fb mounts@ version 11511/ 1371/ 1624/ 1737/ 247/ 31/ 455/ 65/ 907/ filesystems mtrr version_signature 12/ 1372/ 1626/ 18/ 248/ 32/ 46/ 660/ 923/ fs/ net@ vmallocinfo 1200/ 1373/ 1632/ 1835/ 249/ 34/ 469/ 661/ 953/ interrupts pagetypeinfo vmstat 1224/ 1374/ 1635/ 1840/ 25/ 35/ 47/ 67/ 957/ iomem partitions zoneinfo
Most of this is not of interest to us, but the numerical
directories are. Every time a process is created, the kernel
creates a new directory in the /proc
file system. The process id
is the same as the directory name. For example, I can run ps and
see the process id's for the processes in my terminal.
#> ps PID TTY TIME CMD 7067 pts/1 00:00:00 bash 16133 pts/1 00:00:00 ps
I can also look in the directory for the bash process:
ls -F /proc/7067/ attr/ cmdline environ latency mem ns/ pagemap sessionid status autogroup comm exe@ limits mountinfo numa_maps personality smaps syscall auxv coredump_filter fd/ loginuid mounts oom_adj root@ stack task/ cgroup cpuset fdinfo/ map_files/ mountstats oom_score sched stat wchan clear_refs cwd@ io maps net/ oom_score_adj schedstat statm
Each of these files provide some information about that
program. For example, the comm
file is the name of the comand and
the fd/
stores the open file descriptors.
#> cat /proc/7067/comm bash #> ls -F /proc/7067/fd 0@ 1@ 2@ 255@
Parsing /proc/[pid]/stat
Of relevance to this lab task is the /proc/[pid]/stat
file. This
file contains the current status of a running process. Here is the
status for bash
:
cat /proc/7067/stat 7067 (bash) S 7066 7067 7067 34817 16543 4202496 15913 163332 0 5 35 8 176 78 20 0 1 0 112555708 21057536 1440 18446744073709551615 4194304 5111460 140734366799328 140734366797904 139797077691534 0 65536 3686404 1266761467 18446744071579207412 0 0 17 0 0 0 0 0 0 7212552 7248528 7630848 140734366801521 140734366801527 140734366801527 140734366801902 0
While this may seem like a just a bunch of numbers, to the trained
eye, there is a tone of information in here. You can read about it
using man 5 proc
. The relevant information is below, as well as
the scanf()
code for reading it.
- pid : "%d" : process id
- comm : "%s" : command name, with parenthesis
- status : "%c" : status id, such as S for sleeping/susspened, R for running
- ppid : "%d" : parent process id
In code, it's fairly straight forward to open the stat
file and scan in this information:
FILE * stat_f = fopen("/proc/7067/stat","r"); fscanf(stat_f, "%d %s %c %d", &pid, comm, &stat, &ppid); close(stat_f);
And, that is exactly what you are going to do in the next two tasks.
String Format Printing
For the tasks below, you will need to be able to perform a format
print into a sting. So far, we've been format printing to the
terminal, but you can also format print the string directly. Here
is the function deffinition for snprintf()
the string printf
function:
int snprintf(char *str, size_t size, const char *format, ...);
The first argument, str
, is a pointer to the string to store the
result of the formatting. The second argument size
is the size of
str
so we don't overflow the string buffer. The rest is the same
as other format printing. Here's a sample program:
#include <string.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char * argv[]]){ char hello[1024]; snprintf(hello, 1024, "You said, \"%s\"\n", argv[1]); printf("%s\n",hello); }
You'll find snprintf()
very useful because you'll receive a
process id from the command line, and need to open the stat file
/proc/[pid]/stat
, which means you'll need to write a number into a
string before using fopen()
. This is exactly what snprintf()
does best.
Task 3 myps
Change into the myps
directory in the lab directory, in which
you'll find the source code myps.c
. You should provide a Makefile
to compile the source code to myps
.
The myps
command will accept process id's on the command line and
will print information about those process in a tab separated
format. You should use the format print below in order to pass the
tests:
printf("%d\t%s\t%c\t%d\n", pid, clean_comm(comm), state, ppid);
The clean_comm()
function is provided for you to remove the "("
and ")". You must read all information from /proc/[pid]/stat
file.
Here is some sample usage.
#> cat &
[1] 16567
aviv@saddleback: myps $ emacs &
[2] 16568
[1]+ Stopped cat
#> $ ps
PID TTY TIME CMD
7067 pts/1 00:00:00 bash
16567 pts/1 00:00:00 cat
16568 pts/1 00:00:00 emacs
16569 pts/1 00:00:00 ps
[2]+ Stopped emacs
3> ./myps 16568
PID COMM STATE PPID
16568 emacs T 7067
#> ./myps 16568 16567 7067
PID COMM STATE PPID
16568 emacs T 7067
16567 cat T 7067
7067 bash S 7066
#> ./myps 16568 16567 7067 16569
PID COMM STATE PPID
16568 emacs T 7067
16567 cat T 7067
7067 bash S 7066
ERROR: Invalid pid 16569
#> ./myps 16568 16569 16567 7067
PID COMM STATE PPID
16568 emacs T 7067
ERROR: Invalid pid 16569
16567 cat T 7067
7067 bash S 7066
If a bad process id is provided, an error message should be printed
to stderr
.
The process family hierarchy
All processes start with the kernel, but we can also view the process
as a tree, from init down. The pstree
function is really quite good
at this. Here's an example just for the process I'm using:
pstree -ua aviv sshd └─bash sshd └─bash ├─cat ├─emacs ├─emacs myps.c └─pstree -ua aviv
If you think about it, all this is doing is traversing from a given
process to the parent process, all the way up to init. The /proc
file system has all this information, and it's fairly simple to adapt
the code above to recurse the parent-child tree.
Task 4 mypstree
Change into the mypstree
directory in the lab directory, in which
you'll find the source code mypstree.c
. You should provide a Makefile
to compile the source code to mypstree
.
The mypstree
command will accept process id's on the command line
and will print out a process tree, back to init
for each process
id. You will do this using recursion, and you will complete the
getparent()
function.
The idea is that, getparent()
will recurse from child to parent
until it reaches init, process id 1. It will then return the depth
– how many steps it took to reach init. After each return from
getparent()
it will print out what it finds.
You should use the following format prints to pass the tests:
//print the command printf("%s\n", clean_comm(comm)); //space in based on the current depth and max_depth for(i=depth;i<max_depth;i++){ printf(" "); //print a space for each depth } if(depth > 0){ printf("└─"); //nice symbol }
You'll need to ensure that depth
and max_depth
are set
properly, as well as the recursive calls make sense. Here is some
sample output of running mypstree
. Of course, the process id's
may be different on your local computer.
#> ps
PID TTY TIME CMD
7067 pts/1 00:00:00 bash
16567 pts/1 00:00:00 cat
16699 pts/1 00:00:02 emacs
16817 pts/1 00:00:00 ps
#> ./mypstree 16699
init
└─sshd
└─sshd
└─sshd
└─bash
└─emacs
#> ps -C getty
PID TTY TIME CMD
1003 tty4 00:00:00 getty
1010 tty5 00:00:00 getty
1021 tty2 00:00:00 getty
1024 tty3 00:00:00 getty
1027 tty6 00:00:00 getty
11511 tty1 00:00:00 getty
#> ./mypstree 16699 1027
init
└─sshd
└─sshd
└─sshd
└─bash
└─emacs
init
└─getty
#> ./mypstree 16699 1027 adf
init
└─sshd
└─sshd
└─sshd
└─bash
└─emacs
init
└─getty
ERROR: Invalid pid adf
#>$ ./mypstree 16699 adf 1027
init
└─sshd
└─sshd
└─sshd
└─bash
└─emacs
ERROR: Invalid pid adf
init
└─getty
Part 3: A minimalist job control shell: fg-shell
In the last task of this lab, you will adapt your mini-sh
and add
the most basic of job control. This will include the ability to stop
a foreground process and then bring the background process back to
the foreground. Essentially, just two of the job control commands:
- Ctrl-Z : stop a process
- fg : bring a proces to the foreground.
To do this, you'll need your shell to wait on process state changes of children, other than termination. You will also need to manage the terminal device driver so that the signals are properly delivered. And finally, you'll need to be able to signal a process that was stopped to be delivered. Fortunately, all of these commands are relatively easy, using them in the right order, that's the hard part.
Waiting on State Changes with waitpid()
UNTRACED
Parents can wait()
on children to terminate, or for any other
kind of state change. This includes process that are stopped or
continued. Unfortunately, the basic wait()
system call won't cut
it, and instead, we need to use the waitpid()
system call, a
slight more advanced form of wait()
.
Here is an example of how waitpid()
is used in the fg-shell
program:
if ( (pid = waitpid(-1, &status, WUNTRACED)) < 0){ perror("wait failed"); _exit(2); }
The first argument, -1
, indicates to wait for all children. Like
before, the status is written to status
, and the option
WUNTRACED
says to also return when children are either stopped or
continued.
You can see why a child process changed state, such as if it was stopped using the following macro:
WIFSTOPPED(status);
which returns true when waitpid()
returned because a child was
stopped.
Terminal Signaling with tcsetpgrp()
One thing you must do when you have processes running in the
foreground other than the shell is tell the terminal device driver
to which process to deliver terminal signals. For example, if we
are running a process in the foreground and we want terminate the
process with Ctrl-C
, we don't want the foreground process and
the shell to terminate. To ensure this doens't happen, we tell the
terminal device driver which process is in the foreground, and then
only the right process will receive the signal.
The system call that does this is tcsetpgrp()
. The "tc" stands
for "terminal control." The function takes two options, a file
descriptor for the terminal tty
, which is usually 0 for standard
input, and a pid for the foreground process group. Here's an
example of how it might be used in fg-shell
:
//reclaim terminal before returning to the shell! if(tcsetpgrp(0,getpid()) < 0){ perror("tcsetpgrp"); }
The example above will reclaim terminal control for the shell from
a child process. Note that the pid
is retrieved from
getpid()
. You could also use the child process pid
when giving
control to that child process.
Only the parent, the shell, should ever call tcsetpgrp()
, and it
should do so in the following situations:
- When control of terminal is going from the shell to the child process, i.e., the child is becoming the foreground process.
- When control of the terminal is going from the child process to the shell, i.e., the shell is becoming the foreground process.
Continuing a process usig kill()
and SIGCONT
To continue a process that was previously stopped, you must send
that process a signal that says, "hey, you, wake up and continue
what you were doing." The signal that does that is SIGCONT
, and
you can deliver that signal using the kill()
system call.
We'll discuss kill()
in more detail when covering inter-process
communication, but for this lab, you'll exclusively use it to
continue a stopped process allowing it to run in the
foreground. Here is an example usage:
kill(last_pid, SIGCONT);
Here, last_pid
, refers to the child process pid
that was most
recently stopped with Ctrl-z
.
Task 5 fg-shell
Change into the fg-shell
directory in the lab directory, in which
you'll find the source code fg-shell.c
. You should provide a
Makefile to compile the source code to fg-shell
. This compiltion
requires the readline library, so you will need to compile like so:
clang -g -Wall -lreadline fg-shell.c -o fg-shell
Opening the fg-shell.c
source file, you'll find that you need to
make edits in two locations or the program to function properly.
my_wait()
: function that executes the primary wait logic. This functions should check if the last program was stopped, and if so, save thepid
tolast_pid
. If there as a previously stopped program, i.e.,last_pid > 0
, do not let the current process stop.fg
logic : In themain()
function you will need to complete the logic for when a user entersfg
. Generally, you should try and continue the last stopped process, if there is one, and then callmy_wait()
In both situations, whichever is the foreground process must have
control of the terminal, as indicated via a call to tcsetpgrp()
.
Here is some sample output, note that the last_pid
is displayed in
the fgshell prompt for convenience.
./fg-shell
fg-shell (-1) #> ls
fg-shell fg-shell.c Makefile
fg-shell (-1) #> cat
^Z
fg-shell (17219) #> ls
fg-shell fg-shell.c Makefile
fg-shell (17219) #> fg
^Z
fg-shell (17219) #> cat
^Z
^Z
^Z
^C
fg-shell (17219) #> fg
^C
fg-shell (-1) #>
Of note: while cat
is in the background, other commands can
run. The second cat, in the foreground, could not be stopped because
there was already a cat
saved as the background process. Finally,
neither Ctrl-z
or Ctrl-c
affected the shell program while
another process was in the foreground.