CSC 262

Introduction to Processes

 

Orientation

This lab will introduce you to the basics of programming with multiple processes. You will learn how one process can split itself into two, or transform itself so that it is running an entirely different program. At the end, you will write your own version of a user shell, that will accept typed commands and run the appropriate programs.

Most of the information you will need is spelled out in the lab description below, but in a few places you are referred to one of the online manual pages. These are identified by a command name, and sometimes also with a volume number noted in parentheses afterwards. For example, wait(2). To see this manual page, type man 2 wait at the command prompt.

A word of caution: As you begin to develop your program, while you are still debugging you may accidentally make a program that creates processes uncontrollably over and over, eventually causing your machine to crash. If this happens, you will probably need to reboot and fix your program before running it again. Because of this possibility, you should not work on this project using any of the shared CS machines (i.e., beowulf or grendel). Use the lab computers instead. Also, a buggy program may create extra processes that persist after the parent process has finished. Use the ps command to detect this situation, and kill to deal with it and clean up.

Contents

  1. Processes on Linux
  2. Sending signals to processes
  3. Process creation

Processes on Linux

A process, also known as a task under Linux, is a running instance of a program. This means that if 10 users on a server are all using the same program, such as emacs, there are 10 emacs processes running on the server, although they all share the same executable code.

The processes on a UNIX system can be viewed using the ps command:

[ealtieri@italia labos]$ ps -e
  PID TTYTIME CMD
 1 ?00:00:04 init
 2 ?00:00:00 keventd
 3 ?00:00:00 ksoftirqd_CPU0
 4 ?00:00:00 kswapd
 5 ?00:00:00 bdflush
 6 ?00:00:00 kupdated
 8 ?00:00:00 khubd
 9 ?00:00:00 kjournald
14 ?00:00:00 devfsd
  128 ?00:00:00 kjournald
  ...... ...
 2322 pts/1 00:00:05 emacs
 2421 pts/2 00:00:07 emacs
 2922 ?00:00:00 aterm
 2923 pts/3 00:00:00 bash
 2979 pts/3 00:00:00 emacs
 2981 pts/3 00:00:00 ps

The -e option tells the command to show all of the processes on the system. Without this option, the command shows only the processes on the current terminal.

In the example above, the CMD column identifies the name of the running process, such as "emacs". The first column indicates the process identifier (PID) assigned to the process by the operating system. The second column shows the terminal associated with a process, or "?" if the process is not associated with any terminal (e.g. it's a terminal itself!). Finally, the third column shows the CPU time of the process.

We can see from the example above that there are three emacs processes running at the same time. Each of them corresponds to an emacs window on the screen. These processes have PIDs 2322, 2421 and 2979. Also notice the ps command at the end of the list. This is because the command itself is of course a process too.

The process ID (PID) is a unique identifier for a process. The operating system uses a 32-bit counter last_pid to keep track of the last PID assigned to a process. When a process is created, the counter is increased and its value becomes the PID of the new process. Because the counter may wrap around at some point, the kernel needs to check if the value of last_pid++ already belongs to a task, before it can assign it to a new process.

More information about the process list can be displayed using the -l option of the ps command.

[ealtieri@italia os]$ ps -l 
  F SUIDPID  PPID  C PRI  NI ADDR SZ  WCHAN TTYTIME CMD
000 S500  1518  1517  0  750 -645 11cf00 pts/1 00:00:00 bash
000 S500  1549  1518  0  710 -  2557 147d0d pts/1 00:00:02 emacs
000 R500  1874  1518  0  790 -660 - pts/1 00:00:00 ps

Recall that without the -e option, ps only shows the processes on the current terminal, in this case pts/1.

The first column (F) of the output above identifies the process flags (see manual page if you are interested). The "S" column indicates the state of a process. Possible state codes are the following:

Duninterruptible sleep (TASK_UNINTERRUPTIBLE)
Rrunnable (on run queue)  (TASK_RUNNING)
Ssleeping  (TASK_INTERRUPTIBLE)
Ttraced or stopped(TASK_STOPPED)
Za defunct ("zombie") process  (TASK_ZOMBIE)

You will notice that most of the processes on the system are sleeping, that is waiting for some kind of event, such as a mouse click or a key press. In the example above, the only running command is ps.

The output also shows the user owning the process (UID), the process identifier (PID), and the parent PID (PPID). The PPID identifies the process from which a given process originated. For example, you can see above that both emacs and ps have originated from the same bash shell (PID=1418), because their PPIDs are equal to the PID of bash. On the other hand, a process that originates from another process is called a child process.

Thanks to the PPID field, the process list can also be viewed as a tree, at the top of which lies the father of all of the processes: the init process (PID=1). This tree can be viewed with the pstree command.

[ealtieri@italia os]$ pstree 
init-+-atd
|-bonobo-moniker-
|-crond
...
|-evolution-execu
|-evolution-mail---evolution-mail---5*[evolution-mail]
|-gconfd-1
|-gdict
|-gdm---gdm-+-X
| `-gnome-session---ssh-agent
|-gmc
|-gnome-name-serv
|-gnome-smproxy
|-gpm
|-gweather
|-kbdd-+-aterm---bash-+-emacs
| | `-pstree
| |-evolution
| `-opera---opera---opera
|-keventd
...

In bold you can see the three processes from the previous ps output.

Processes whose PPID equals 1 (init) and that do not have a controlling terminal are called daemons. These processes run in the background and are not normally visible to the user.

Another useful command is top. This command provides an ongoing look at the processor activity in real time. It displays a listing of the most CPU-intensive tasks on the system, and can provide an interactive interface for manipulating processes. The GUI counterpart of this command, called gtop, is shown below. (Depending on which packages you selected while installing Linux, it may or may not be available on your system.)

gtop

question Q1. Give some examples of daemons running on your computer.
question Q2. Draw the process relationship tree for the following processes. Next to each node of the tree, write the PID of the corresponding process.
  F SUIDPID  PPID  C PRI  NI ADDR SZ  WCHAN TTYTIME CMD
000 S500  1492  1491  0  750 -641 11cf00 pts/0 00:00:00 bash
000 S500  2281  1492  0  690 -  2790 147d0d pts/0 00:00:00 emacs
000 S500  2284  1492  0  690 -767 147d0d pts/0 00:00:00 aterm
000 S500  2336  1492  0  690 -  4282 148496 pts/0 00:00:00 xmms
040 S500  2337  2336  0  690 -  4282 148496 pts/0 00:00:00 xmms
040 S500  2338  2337  0  690 -  4282 147d0d pts/0 00:00:00 xmms
040 S500  2342  2337  0  690 -  4282 121f03 pts/0 00:00:00 xmms
000 S500  2352  1492  0  690 -554 11cf00 pts/0 00:00:00 opera
000 S500  2353  2352  1  700 -  4597 147d0d pts/0 00:00:01 opera
040 S500  2354  2353  0  680 -  4597 148496 pts/0 00:00:00 opera
000 R500  2359  1492  0  790 -660 - pts/0 00:00:00 ps

Sending signals to processes

Signals are sometimes referred to as software interrupts. Similarly to hardware interrupts, signals are random interruptions in the execution of program that signal an event to that program. Examples of events could be a segmentation fault caused by the program itself (SIGSEGV), or a CTRL-C key combination sent by the user (SIGINT). A complete list of signals can be viewd by viewing the signal(7) manual page (type "man 7 signal" at the shell prompt). Signals are defined in include/asm/signal.h (line 31).

Signals are sent to a program using the kill command. For example:

[ealtieri@italia os]$ sleep 20 &
[1] 2611
[ealtieri@italia os]$ ps
  PID TTYTIME CMD
 2535 pts/4 00:00:00 bash
 2611 pts/4 00:00:00 sleep
 2612 pts/4 00:00:00 ps
[ealtieri@italia os]$ kill -s SIGINT 2611
[ealtieri@italia os]$ 
[1]+  Interruptsleep 20
[ealtieri@italia os]$

The sleep command creates a process that sleeps for 20 seconds and then terminates (the "&" symbol tells bash to run the command in the background). We can see this process in the output of ps immediately next. We then send a CTRL-C signal (SIGINT) to the sleep command using kill and the PID of sleep. The signal terminates the program. This is confirmed by the message that appears on the terminal if you press enter once after the kill command has been issued.

One of the most useful applications of signals is to request the termination of a program. There are two termination signals that can be sent to a program: SIGTERM and SIGKILL.

The following example shows how to kill emacs nicely:

[ealtieri@italia os]$ emacs &
[1] 2695
[ealtieri@italia os]$ ps
  PID TTYTIME CMD
 2535 pts/4 00:00:00 bash
 2695 pts/4 00:00:00 emacs
 2696 pts/4 00:00:00 ps
[ealtieri@italia os]$ kill -s SIGTERM 2695
[ealtieri@italia os]$ 
[1]+  Terminated emacs
[ealtieri@italia os]$

Without the -s <sig> option, kill sends a SIGTERM signal by default, so the third line above could be rewritten simply as:

kill 2695

From the point of view of a process, a signal can be either ignored, sent to a default handler, or caught by a function handler provided by the process. To make your program catch a signal you simply need to add the following code to the beginning of main():

#include <signal.h>

...

int main(void) {
  signal(SIGINT, &on_sigint);/* catch CTRL^C */
  ...

where on_sigint is the function in charge of handling the signal, and could be implemented as shown below:

/* CTRL^C handler */
void on_sigint(int signo) {
  printf("CTRL^C pressed!\n");

  /* do any necessary cleanup work here... */

  exit(0);
}
source sigint.c is a simple example showing how to catch the SIGINT (CTRL^C) signal.

question Q3. Modify signint.c so that it also catches the SIGTERM signal. Make the program display different messages for the SIGINT and SIGTERM signals.
question Q4. The counterpart of the kill bash command in C is the kill(pid_t pid, int sig) function (see the kill(2) manual page). Use this function to create a program that sends a SIGINT signal to the PID specified in the command line.
question Q5. Some signals cannot be ignored or caught by processes. One example is the SIGKILL signal, described earlier. Read the signal(7) manual page and find out which other signal cannot be ignored or caught by a process. Also find out the default action for the SIGINT signal.

Process Creation

Process creation under Linux is made possible by a few system calls. The most important is the fork() function.

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

fork() works by splitting the caller process into a parent and child process. Both the parent and the child processes share the same executable code, but have different data and stack segments. An example that uses this system call is shown below:

#include <sys/types.h>
#include <unistd.h>

int main() {
  pid_t pid;

  pid = fork();  /* the process is split here !! */

  /* 
   * Now we have two processes (child and parent) running on the
   * same executable code. The way we tell who we are is by
   * looking at the return value of fork() - 0 for the child
   * process and > 0 for the parent.
   */

  if (pid == 0) {
    /* CHILD PROCESS */
    printf("Child process!\n");
  } else {
  /* PARENT PROCESS */
    printf("Parent process!\n");
  }
  exit(0);
}

A slightly modified version of the program above could print the process list before and after the fork() function is called. This is the resulting output:

[ealtieri@italia process]$ ./a.out 
Processes before forking:
UIDPID  PPID  C STIME TTYTIME CMD
ealtieri  2242  2241  0 11:23 pts/2 00:00:00 bash
ealtieri  2980  2242  0 15:35 pts/2 00:00:00 ./a.out
ealtieri  2981  2980  0 15:35 pts/2 00:00:00 ps -f
Processes after forking:
UIDPID  PPID  C STIME TTYTIME CMD
ealtieri  2242  2241  0 11:23 pts/2 00:00:00 bash
ealtieri  2980  2242  0 15:35 pts/2 00:00:00 ./a.out// parent
ealtieri  2982  2980  0 15:35 pts/2 00:00:00 ./a.out// child
ealtieri  2983  2980  0 15:35 pts/2 00:00:00 ps -f

As you can see, after fork() the original process is split into two processes, the child and the parent. By looking at the PID and PPID of the two a.out processes we can tell that the second a.out is the child, while the first is the parent.

source fork.c uses fork() to create a child process. The program prints the list of processes before and after the forking.

question Q6. Consider the following code taken from forkq.c:
int main() {
  pid_t pid;
  int look_at_this;
  
  look_at_this = 99;
  
  if ((pid = fork()) < 0) {
    perror("fork()");
    exit(1);
  } else if (pid == 0) {
    /* CHILD PROCESS */
    look_at_this = 33;  /* <<<<<< changing look_at_this here */
    exit(0);            /* child process terminates here */
  }
  
  /* PARENT PROCESS */
  wait(NULL);  /* wait until child terminates */
  printf("The value of look_at_this is %d\n", look_at_this);
  exit(0);
}
The wait(2) function puts the parent process to sleep until the child process terminates. According to the definition of fork(), which value of look_at_this will be displayed on the screen? Explain your answer.

So far we have talked about how to fork a new process from an existing one, but what if we want to execute a different program altogether? For this purpose Linux provides the exec() family of functions (see the exec(3) manual page). Differently from fork(), the exec() functions do not create a process. The functions replace the image of the caller process with the program specified as the first argument. This means that the caller process terminates and the program specified in exec() takes its place.

For example:

#include <unistd.h>

int main() {
  /* FILE TO EXECUTEARG0*/
  execl("/usr/bin/emacs", "emacs", NULL);

  /* should never get here, unless execl() returned with an error */
  perror("execl()");
  exit(1);
}

The program above tries to execute emacs. If the execl() function succeeds, the process will be replaced by emacs and we will never reach the following lines. However, execl() may not find the specified program, return an error and continue execution of the current process, therefore we always need to provide some error handling after the call.

A slightly modified version of the program above could print the process list just before calling execl(). We can then manually check the list again with the ps command after the exec() fnction has been called.

[ealtieri@italia process]$ ./a.out &
[2] 3106
Processes before the exec() call:
UIDPID  PPID  C STIME TTYTIME CMD
ealtieri  2242  2241  0 11:23 pts/2 00:00:00 bash
ealtieri  3106  2242  0 16:14 pts/2 00:00:00 ./a.out
ealtieri  3107  3106  0 16:14 pts/2 00:00:00 ps -f
[ealtieri@italia process]$ ps -f
UIDPID  PPID  C STIME TTYTIME CMD
ealtieri  2242  2241  0 11:23 pts/2 00:00:00 bash
ealtieri  3106  2242  0 16:14 pts/2 00:00:00 emacs
ealtieri  3108  2242  0 16:15 pts/2 00:00:00 ps -f

After the call to execl(), the original process disappeared and in its place we have emacs. Notice how emacs maintains the PID and PPID of the original process.

Executing a program while continuing the current process involves two steps. First, a child process has to be forked by the parent task. Second, the child process executes the desired program using one of the exec(3) functions. This way, the child is replaced with the image of the new program, while the parent is untouched.

source exec.c shows the use of the execl() function.

question

Q7. You're familiar with so-called shell programs; these are the programs that run in a terminal window and accept commands typed by the user. Implement a small shell that repeatedly asks for the a program name and then executes that program using fork() and execlp(). The shell must terminate when the user types exit. The shell shouldn't prompt for a new program to execute until the previous one has terminated. You can use the wait(2) function to make the parent process wait for the child to terminate. (Recall that you can learn more about this function by typing man 2 wait at a terminal prompt. Also note that execlp() differs slightly from execl() as shown in the example program above; read the corresponding man page to learn more about the difference.)

Your program should also respond to signals as follows: If SIGINT (CTRL-C) is typed while a child process is executing, your program should pass along the signal to that process (and keep itself running). If no child process is currently running, your program should catch the signal and print an informative message explaining how to terminate the program (by typing exit at the prompt). On the other hand, if your program receives a SIGTERM signal, it should first send the SIGTERM signal to any child processes and then terminate itself as well. (Remember for testing that you can use the kill command from another terminal window to send signals to your shell program.)

Resources

Advanced UNIXAdvanced Programming in the UNIX Environment, by Richard W. Stevens. Addison Wesley