Lecture: 29 Mar, 2010


Justin Carlson


Process Control I

What is a program?

First, recall our (simplified) diagram of some of the things going on inside a computer.

CPUStack
RegisterValue
Program Counter
eax
ebx
ecx
edx
...
esp
privileged?

So what is a program, anyways?

In one sense, the computer is only ever running one big program. It has instructions to do things like get input from the user, and sometimes the user gives it more instructions in the form of source code, but it's still just a computer running a big set of instructions.

However, as good computer scientists, we like to abstract and decompose; it's usually more useful to think of what's going on in the computer as a large number of different programs, each of which has its own goals and execution context. And while we're at it, let's clear up a little more terminology - a program, when running on a computer, is usually called a process, at least on Unix machines.

So what are the parts of a process?

The short answer: probably a lot more things than you realize. But some of the more important pieces of a process are:

Most modern operating systems are multitasking. This means that we can run more than one process on a computer at the same time. On a Linux box, we can get a list of all the processes currently running using the command ps aux:

...
root      1468  0.0  0.0   4588   348 ?        Ss   Feb08   0:00 /usr/sbin/atieventsd
root      1507  0.0  0.0   2144   628 ?        S    Feb08   0:00 /sbin/dhclient -d -sf /usr/lib/NetworkManager/nm-dhcp-client.action -pf /var/run/dhclient-eth0.pid -lf /var/lib/
root      1530  0.0  0.0      0     0 ?        S>   Feb08   0:00 [firegl]
ntp       1678  0.0  0.0   4104   656 ?        Ss   Feb08   0:01 /usr/sbin/ntpd -p /var/run/ntpd.pid -u 114:125 -g -c /etc/ntp.conf.dhcp
root      1962  0.0  0.0   1704   252 tty1     Ss+  Feb08   0:00 /sbin/getty -8 38400 tty1
gdm       2035  0.0  0.0   3384   348 ?        S    Feb08   0:00 /usr/bin/dbus-launch --exit-with-session
root      2040  0.0  0.0   5764  1468 ?        S    Feb08   0:01 /usr/lib/devicekit-power/devkit-power-daemon
root      2134  0.0  0.0   8636   960 ?        S    Feb08   0:00 /usr/lib/gdm/gdm-session-worker
justinca  2281  0.0  0.0  54320  3088 ?        Sl   Feb08   0:01 /usr/bin/gnome-keyring-daemon --daemonize --login
justinca  2296  0.0  0.0  25664  3260 ?        Ssl  Feb08   0:03 gnome-session
justinca  2343  0.0  0.0   4708   132 ?        Ss   Feb08   0:03 /usr/bin/ssh-agent /usr/bin/dbus-launch --exit-with-session /usr/bin/pulse-session gnome-session
justinca  2346  0.0  0.0   4760  1960 ?        Ss   Feb08   0:36 /bin/dbus-daemon --fork --print-pid 7 --print-address 9 --session
justinca  2347  0.0  0.0   3384   352 ?        S    Feb08   0:00 /usr/bin/dbus-launch --exit-with-session /usr/bin/pulse-session gnome-session
...

But how does the computer manage to run so many processes when it has many more processes than processors?

Interrupts

To understand this, we need to understand, at a high level, the concept of an interrupt. (Modern) computers do not merely compute in a linear way, they react to events. An event that requires the computer to do something generates an interrupt.

An interrupt is a concept of hardware, not software. There is some physical process that triggers the interrupt - for example, every time you type a key on your keyboard, it results in an electrical signal to the processor that is interpreted as "I've been interrupted". Something similar occurs every time you move the mouse, or a packet comes in on the network, or...

So what happens when the CPU is interrupted? It's very similar to what happens when a program makes a system call. The processor program counter immediately jumps to a location in the operating system called the interrupt handler and begins executing instructions in privileged mode.

This handler does the following things:

  1. Saves all of the information needed to jump back into the interrupted process once the interrupt has been handled
  2. Does whatever actions need to be done to handle the interrupt (read which key was pressed, update the mouse position, read a packet from the network, ...)
  3. Restores the process state, derops the privileged mode, and jumps back to the process right at the point it was interrupted.

The end result is that unless the process is looking at the clock, it has no idea that the operating system just stole the CPU from it for a while, used it to get some work done, and then returned the CPU to the process.

In fact, the way interrupts are handled is so similar to the way system calls are issued that system calls are sometimes called "software interrupts", and the instruction on x86 to generate a system call is called "int", which stands for "interrupt".

The Timer Interrupt

One critical piece of hardware in any computer is the timer. A timer can be set to generate an interrupt at fixed intervals. In Linux, there is something called the schedule timer, which generates an interrupt 1000 times per second.

The timer interrupt is used by the operating system as a kind of referee whistle. Every time the whistle blows, the operating system takes the CPU away from currently running process, looks at all the processes in the system, and decides what process gets the CPU now.

This means, effectively, that the operating system is cutting time on the processor up into 1 ms chunks and dividing this among all the processes on the machine.

This is how we get away with having more processes than processors on which to run them - the operating system treats the processor as a shared resource, rationing out processor cycles to waiting processes.

task_struct

To summarize the story so far: the operating system has a large number of processes vying for time on a processor. Each process consists of, among other things:

  • The process memory
  • The program counter
  • The other registers
  • ...

In linux, all of this information is represented in something called the task_struct which can be found in include/linux/sched.h:

//From include/linux/sched.h:

struct task_struct {
        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
        void *stack;
        atomic_t usage;
        unsigned int flags;     /* per process flags, defined below */
        unsigned int ptrace;

        int lock_depth;         /* BKL lock depth */

#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
        int oncpu;
#endif
#endif

        int prio, static_prio, normal_prio;
        struct list_head run_list;
        const struct sched_class *sched_class;
        struct sched_entity se;

#ifdef CONFIG_PREEMPT_NOTIFIERS
        /* list of struct preempt_notifier: */
        struct hlist_head preempt_notifiers;
#endif

        unsigned short ioprio;
        /*
         * fpu_counter contains the number of consecutive context switches
         * that the FPU is used. If this is over a threshold, the lazy fpu
         * saving becomes unlazy to save the trap. This is an unsigned char
         * so that after 256 times the counter wraps and the behavior turns
         * lazy again; this to deal with bursty apps that only use FPU for
         * a short time
         */
        unsigned char fpu_counter;
        s8 oomkilladj; /* OOM kill score adjustment (bit shift). */
#ifdef CONFIG_BLK_DEV_IO_TRACE
        unsigned int btrace_seq;
#endif

        unsigned int policy;
        cpumask_t cpus_allowed;
...

The structure definition goes on for quite a few lines. As you can see, there is a lot that the OS is keeping track of for each process!

The kernel keeps these task_structs in various linked lists. It is a (useful) oversimplification to think of these task structs as existing in two linked lists -- one for processes that want to run on a processor at the next opportunity, and one for processes that don't need the processor right now.

Of significant note is the process id, or pid of the process. This is a number that uniquely identifies the process on the system. You can see the pid's of the processes in the system in the second column from the left of the ps listing. The most common use for this pid is referring to a process you want to kill:

kill pid

Creating a new process with fork()

All of these processes came from somewhere: clearly, there must be some way of creating new processes in the system. And, since this is something that involves changing the data structures inside the operating system, it must involve a system call.

The system call we are looking for is fork(). fork() means "create two (almost) identical copies of this process right now". This means the two processes have the same memory contents, the same registers, and the same stack contents, and the same program counter, among other things.

Consider the following simple program.

int main()
{
	fork();
	printf("Hello, world!\n");
	return 0;
}

This program results in the following output:

Hello, world!
Hello, world!

What's going on here? When the program hits the fork(), an interesting thing happens. A second complete process is created which is (nearly) identical to the first process. That means two different processes will each go on to the print statement, so we get the output twice.

Consider the following similar program:

int main()
{
	fork();
	fork();
	printf("Hello, world!\n");
	return 0;
}

This will result in "Hello world!" being printed 4 times. The first fork() will result in 2 processes. Each of those 2 processes will then fork(), which results in 4 processes. Each of those processes will print the message.

Read the following code segment, but keep in mind the adage "with great power comes great responsibility":

int main()
{
	while(1) 
		fork();
	return 0;
}

This is called a fork bomb. It will result in exponential growth in the number of processes running on the system. If the system is not configured to limit the amount of memory or number of processes that a user can have at one time, this will eat all available memory and processor time, and will eventually force you to reboot the machine.

Don't do this on a machine which is not yours. In particular, don't do this on any of the shared unix machines on campus. I don't know if they are configured with reasonable limits, but even if a fork bomb won't bring down the machine, cleaning up a fork bomb is a pain in the #&*(@ and will (justifiably) get you in trouble.

Notice I said the processes created by fork() are almost identical. They differ in a few crucial ways - most significantly their process ids and the return values of the fork() call.

The two processes from a fork() are related to each other. One of them is the parent, and the other is the child. The parent is the "original" process, with the original process id. The child is a copy of the parent, but has its own process id. Additionally, the parent has a few abilities with respect to the child -- more on that in the Wait section below. To help look after the child, the parent receives the process id of the child as its fork() return value. For the child, on the other hand, fork() returns 0.

Here's an example program:

int main()
{
        printf("%li: Before the fork()\n", (long)getpid());
        pid_t p = fork();
        printf("%li: After the fork.  For this process, fork returned %li\n", 
               (long)getpid(), (long)p);
        return 0;
}

The output of this program depends on the specific pid assigned to the child, but here is a sample run:

16946: Before the fork()
16946: After the fork.  For this process, fork returned 16948
16948: After the fork.  For this process, fork returned 0

The first line of the output tells us the starting state: we have a process which has a process id of 16946.

Then we have a fork(). This fork creates a second process, the child. We see two lines of output containing "After the fork"--one for the parent, and one for the child. The child has the new pid of 16948.

Look at the fork return values. For the parent, it got the pid of the child. For the child, it got 0.

The most common usage for fork() is not to start another copy of the current process, but to start a copy of an entirely different program. Do do that, we need exec, which we will cover in depth next time.