IC221: Systems Programming (SP14)


Home Policy Calendar Syllabus Resources Piazza

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.

  1. Understand various process states, fork bomb, orphans, and zombies
  2. Monitoring process states through /proc/[pid]/state
  3. Following Process family
  4. Foreground and Background processes mechanism
  5. Waiting on a process state change using waitpid()
  6. Signally a process to continue using kill()
  7. 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 for myps directory
  • mypstree : execute for mypstree directory
  • fg-shell : execute for fg-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

forks_and_spoons.png

Figure 1: Forks and Spoons

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:

  1. Be sure you are physically in the lab – do not run this through ssh, it will crash the computer. We know who you are.
  2. 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, run bigboy 4 and in the other run the below ps 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.

  1. Open three terminal windows.
    • In window A, start top
    • In window B, you will run bigboy n where n is the initial count
    • In window C, run other commands, like ps and killall
  2. 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 replace n with an appropriate value
    • fill in the table using top and ps
    • kill the process with killall bigboy
  3. Answer Remaining questions in the README for this task
  4. 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 the pid to last_pid. If there as a previously stopped program, i.e., last_pid > 0, do not let the current process stop.
  • fg logic : In the main() function you will need to complete the logic for when a user enters fg. Generally, you should try and continue the last stopped process, if there is one, and then call my_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.