Operating Systems Chapter 4
Operating Systems Chapter 4
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
Operating Systems
Contents
- 1 . Introduction
- 2. Structure of operating systems
- 3. Processes in Operating Systems
- 4. Threads
- 5. File Management
- 6. Concurrency
- 7. Memory management
- 8. Scheduling
- 9. File Management systems
- 10. Networking
- 11. Introduction to InterProcess Communication
- 12. Appendix : Common Unix utilities
4.Threads
In previous sections the concept of context switch has been differentiated from that of process
switch, and it has been remarked that performing an OS service, while usually requiring a
context switch, not always makes a process switch necessary, depending on the scheduling
policy adopted by the OS.
1 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
A different way to regard the same issue consists in differentiating between two independent
attributes involved by the concept of process, namely:
The former refers to the fact that the management of a process involves, on the OS side, the
``investing'' of resources in terms of allocated main memory, OS table update, availability of I/O
streams and devices.
The latter points at the process as being the execution of a program, which may be interleaved
with that of other processes in a multiprogramming environment, and involves the management
of process states, priority, queues, etc.
Now it should be noted that, even if in traditional operating systems the above attributes
uniquely coexist within one process, nothing in the above forbids considering a collection of
programs , all
sharing resources, as a unit of dispatching. The recognition of the independence of these two
aspects has lead to the invention of
threads
or
lightweight processes
.
Though the relatively recent concept of thread has found different implementation in various
operating system, the most common one in an arrangement in which multiple threads coexist
within one process. In this case threads can be easily visualized as as parallel traces of the
same process independently executed (think of something like having multiple program
counters on the same instruction list - that of the process). This approach has been
implemented in OS/2, Sun OS, Lynx and, most notably, Mach.
In Lynx, a real-time, Posix abiding OS, processes are implemented in much the same way as is
usual in UNIX family: each process is allowed an address space in (virtual) memory which is
protected from other processes' interference, the kernel executes mostly in the user process
context and keeps in the user area of a process all the process-related information that is
2 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
needed only when the process is running - most notably the kernel stack. However there are
two major differences with other ``traditional'' unices (e.g BSD, System V, Ultrix, USD, etc.):
Lynx is a real-time operating system and allows for the spawning of ``parallel threads'', or
pthreads, within a process (because this is about the only way to be real-time for real).
Real-time means essentially that the OS guarantees that it'll honor an interrupt request within a
fixed response time, which is comparable to a context-switch delay. This is essential for most
application involving automatic control, where a computer (really a controller program sitting on
top of the OS) is put in a feedback loop between some machinery controlling a plant and
devices sensing the plant's behaviour. An unpredictable response time for the controller makes
the control design problem very difficult, if not altogether impossible. Yet traditional unices
cannot
be interrupted while executing in kernel mode (disabling the CPU's interrupt pins is about the
first thing that both system calls and system utilities do when they gain control), and hence the
interrupt response time can be as long as the execution time of the longest system routine. Lynx
can be interrupted in kernel mode, instead, and also offers some quite sophisticated ``timer''
system calls for finely tuning the timing of the application's operations.
Since fast controller's are always welcome in control application (you can always slow them
down if you want, while boosting a FIAT into a Ferrari is still the subject of basic research...),
Lynx also offer pthreads, which allow a designer to nicely decompose the control application in
parallel time-bound tasks (maybe operating at radically different time scales), without incurring
in a process switch overhead, and with the bonus of fast inter-thread communication through a
shared memory area. Each pthread has the following attributes:
Note that operations like suspension (i.e. swapping to disk) are still performed at the process
level, and they affect all the pthreads within a process when they occur.
A very common application of threads outside real-time control is on the server side of
client-server application over computer networks. In a typical scenario for these applications, a
3 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
server program sits idle listening to a communication port, waiting for service requests from
remote clients. When one arrives, according to how it's designed, it can:
- Service the request by itself, in the meantime denying service to other clients. Obviously
this works only for very simple & quick kinds of services, and when only a small number of
service requests may arrive within one service time interval.
- Spawn a child process which does the servicing. This initially improves the response time
on the client side, since the creation of a process is a quick event on a network's time scale. Yet
with an increasing number of active processes the burden can become excessive, with the OS
wasting too many a cycle switching between them. For this very reason, many OSs impose
quite stingy limits on the maximum allowable number of active processes, refusing to spawn
new ones when the limit is exceeded.
- Delegate the service to one of a pre-defined number of worker threads. This has the same
response time advantage as the previous solution, without the process switch burden.
Moreover, threading the services allows way more flexibility in the servicing application design.
For example, one can make use of the cheap inter-thread communication for chaining threads
that compute only parts of the service.
The seven-state logical process model we considered in a previous lecture can accommodate
the UNIX process model with some modifications, actually becoming a ten state model.
First, as we previously observed, UNIX executes most kernel services within a process's
context, by implementing a mechanism which separates between the two possible modes of
execution of a process. Hence our previously unique ``Running'' state must actually be split in a
``User Running'' state and a ``Kernel Running'' state. Moreover a process preemption
mechanism is usually implemented in the UNIX scheduler to enforce priority. This allows a
process returning from a system call (hence after having run in kernel mode) to be immediately
blocked and put in the ready processes queue instead of returning to user mode running,
leaving the CPU to another process. So it's worth considering a ``Preempted'' state as a special
case of ``Blocked''. Moreover, among exited processes there's a distinction between those
which have a parent process that waits for their completion (possibly to clean after them), and
those which upon termination have an active parent that might decide to wait for them sometime
in the future (and then be immediately notified of its children's termination). These last
processes are called ``Zombie'', while the others are ``Exited''. The difference is that the system
needs to maintain an entry in the process table for a zombie, since its parent might reference it
in the future, while the entry for an exited (and waited for) process can be discarded without
4 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
further fiddling. So the much talked about ``Zombie'' processes of UNIX are nothing but entries
in a system table, the system having already disposed of all the rest of their image. This process
model is depicted in fig. 5.
All processes in UNIX are created using the fork() system call. System calls in UNIX can be best
thought of as C functions provided with the standard C library. Even if their particular
implementation is depends on the particular UNIX flavor (and on hardware, for many of them), a
C API is always provided, and is consistent among the different unices, at least in the
fundamental traits.
UNIX implements through the fork() and exec() system calls an elegant two-step mechanism for
process creation and execution. fork() is used to
create the image of a process using the one of an existing one, and
exec
is used to execute a program by overwriting that image with the program's one. This separation
allows to perform some interesting housekeeping actions in between, as we'll see in the
following lectures.
5 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
#include <sys/types.h>
pid_t childpid;
...
...
creates (if it succeeds) a new process, which a child of the caller's, and is an exact copy of of
the (parent) caller itself. By exact copy we mean that it's image is a physical bitwise copy of the
parent's (in principle, they do not share the image in memory: though there can be exceptions to
this rule, we can always thing of the two images as being stored in two separate and protected
address spaces in memory, hence a manipulation of the parent's variables won't affect the
child's copies, and vice versa). The only visible differences are in the PCB, and the most
relevant (for now) of them are the following:
- The two processes obviously have two different process id.s. (pid). In a C program
process id.s are conveniently represented by variables of pid_t type, the type being
defined in the sys/types.h
header.
- In UNIX the PCB of a process contains the id of the process's parent, hence the child's
PCB will contain as parent id (ppid) the pid of the process that called fork(), while the caller
will have as ppid the pid of the process that spawned it.
- The child process has its own copy of the parent's file descriptors. These descriptors
reference the same under-lying objects, so that files are shared between the child and the
parent. This makes sense, since other processes might access those files as well, and having
them already open in the child is a time-saver.
6 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
The fork() call returns in both the parent and the child, and both resume their execution from the
statement immediately following the call. One usually wants that parent and child behave
differently, and the way to distinguish between them in the program's source code is to test the
value returned by fork(). This value is 0 in the child, and the child's pid in the parent.
Since fork() returns -1 in case the child spawning fails, a
catch-all C code fragment to separate behaviours may look like the following:
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
...
pid_t childpid;
...
childpid=fork();
switch(childpid)
7 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
case -1:
exit(1);
break;
case 0:
break;
default:
break;
The array of strings char *sys_errlist[] and the global integer variable int errno are defined in the
errno.h
8 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
header. The former contains a list of system error messages, and the latter is set to index the
appropriate message whenever an error occurs. For each system call several possible error
conditions are defined. Each of them is associated to an integer constant - defined via a
#define
directive in one system header file - whose value is exactly the one that
errno
takes when an error occurs. A sample definition (from the sys/errno.h header) is:
...
#define ENOMEM 12
...
which defines the error that might occur when a process creation fails because there's not
enough memory available.
Note that a child (i.e. a process whatsoever, since they are all children of some other process,
with the exception of processes 0, swapper and 1, init) cannot use the value returned by fork()
to know its pid, since this is always 0 in the child. A system call named
getpid()
is provided for this purpose, and another one, named
getppid()
is used to ask the system about the parent's id. Both functions take no arguments and return the
requested value in
pid_t
type, or -1 in case of failure.
In the above program fragment, a system call to exit() is made in case of failure, which causes
the program to abort (you might want to deal with the errors in a smoother way, depending on
your application, and perform some application-dependent error recovery action). We'll see later
that the exit() call returns the lower 8 bits
of its argument (1, in the above example) to a waiting parent process, which can use them to
determine the child's exit status and behave accordingly. The usual convention is to exit with 0
9 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
on correct termination, and with a meaningful (for the parent) error code on abort.
It is often the case that a parent process must coordinate its actions with those of its children,
maybe exchanging with them various kind of messages. UNIX defines several sophisticated
inter-process communication (IPC) mechanisms, the simplest of which is a parent's ability to
test the termination status of its children. A synchronization mechanism is provided via the wait()
system call, that allows a parent to sleep until one of its children exits, and then get its exit
status. This call actually comes in three flavors, one simply called
wait()
and common to all version of UNIX (that i know of), one called
waitpid()
, which is a POSIX extension, and one called
wait3()
, and it's a BSD extension.
Here's an example call to wait(): a program spawns two children, then waits for their completion
and behaves differently according to which one is finished. Try to compile and execute it (no
need to type: you can cut and paste from your web browser...).
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
10 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
{
getpid()
);
11 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
exit(0);
}
{
exit(1);
}
{
getpid()
12 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
);
exit(0);
}
{
exit(1);
}
13 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
howmany=0;
{
whichone=wait(&status);
howmany++;
if (whichone==first)
else
14 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
printf("correctlyn");
else
printf("uncorrectlyn");
}
return 0;
The first part of this example, up to the howmany=0 statement, contains nothing new: just make
sure you understand what the instruction flow is in the parent and in the children. The parent
then enters a loop waiting for the children's completion. The
wait()
system call blocks the caller process until one of its immediate children (not children's children,
or other siblings) terminates, and then returns the pid of the terminated process. The argument
to
wait()
is the address on an integer variable or the NULL pointer. If it's not NULL, the system writes 16
bits of status information about the terminated child in the low-order 16 bits of that variable.
Among these 16 bits, the
higher
15 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
The Posix and BSD extensions to wait() are useful when a parent must not block waiting for
children, but still wants to know about the children's termination status values via the wait
mechanism. We'll treat only the Posix waitpid() call, and
you are referred to the man page for the BSD call.
Here the meaning of the the return value and of the pointer to the status statptr is exactly the
same in wait().
However this call allows to specify which children should be waited for and how. Specifically, the
first argument
pid
specifies the process(es) that must be waited for. The relevant (for now) cases are:
16 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
The relevant (for now) value for the third argument is a constant called WNOHANG, that causes
the function not to suspend the caller's execution if status is not immediately available for one of
the child processes. This allows to implement a loop in which the parent can do something
useful and periodically poll the children's status as well.
Process creation
When a process starts, the O/S has to build an entry for it in the process table. The process
state will be marked as ``runnable''.
/*
* Ok, this is the main fork-routine. It copies the system process
*/
int do_fork(unsigned long clone_flags, unsigned long usp, struct pt_regs *regs)
17 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
if (!new_stack)
nr = find_empty_process();
18 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
*p = *current;
(*p->exec_domain->use_count)++;
(*p->binfmt->use_count)++;
p->did_exec = 0;
19 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
p->signal = 0;
p->tty_old_pgrp = 0;
20 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
p->mm->swappable = 0; /* don't try to swap it out before it's set up *
task[nr] = p;
SET_LINKS(p);
nr_tasks++;
21 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
p->mm->swappable = 1;
22 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
bad_fork_cleanup:
REMOVE_LINKS(p);
nr_tasks--;
bad_fork_free:
free_page(new_stack);
bad_fork:
Process suspension
23 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
wait
Suppose the file was a temporary file. After printing, it should be deleted. The child cannot
delete it, because after the ``exec'' it no longer exists. The parent cannot directly delete it
because the ``lpr'' program may still be accessing it. (Actually, an option on lpr allows removal
after printing - ignore this.) When ``lpr'' terminates, the parent can remove the file. So the parent
has to be able to decide when a child has finished.
#include
#include
This waits for a process to terminate and returns the PID of one that did. It also returns status
information.
if ((pid = fork()) == 0)
if (execlp("lpr",
"lpr",
"myfile",
24 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
fprintf(stderr,
else {
*/
;
unlink("myfile");
25 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
sleep
A process may suspend for a period of time using the sleep command
Bovenkant formulier
Process removal
26 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
exit
kill
One process can send simple messages to another using the ``kill'' command
#include
#include
The process to which the signal is sent must have the same uid or be a ``super user'' process.
The signal is often SIGINT or SIGKILL.
...
else {
27 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
*/
sleep(10);
kill(pid, SIGINT);
signal
A process can catch certain signals by installing a signal handler which is a function invoked
when the signal arrives.
Bovenkant formulier
28 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
Onderkant formulier
Process environment
A process has an entry in a table of processes. This table contains all sorts of information (see
next lecture) including user ID, current directory, etc. A set of function calls allows access to
much of this information:
Process ID
#include
pid_t getpid(void);
User ID
#include
uid_t getuid(void);
29 / 30
Operating Systems (Chapter 4)
Written by Administrator
Sunday, 04 October 2009 10:12 - Last Updated Tuesday, 26 January 2010 17:56
User name
char *getlogin(void);
30 / 30