You are on page 1of 190

COMP302

Operating Systems
including:
Introduction to Unix,
C programming in a Unix environment,
Operating Systems Theory
Hugh Murrell
February, 2006

Contents
1 Introduction to Unix

1.1

Credits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.2

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.2.1

Commands . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.2.2

Some common commands . . . . . . . . . . . . . . . . . . . .

1.2.3

The vi editor . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

1.2.4

Electronic mail . . . . . . . . . . . . . . . . . . . . . . . . . . 11

1.3

1.4

1.5

Processes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.3.1

Filename shorthand . . . . . . . . . . . . . . . . . . . . . . . . 17

1.3.2

Input-output redirection . . . . . . . . . . . . . . . . . . . . . 18

1.3.3

Pipes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

1.3.4

Processes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

1.3.5

The environment . . . . . . . . . . . . . . . . . . . . . . . . . 22

The UNIX file system . . . . . . . . . . . . . . . . . . . . . . . . . . . 23


1.4.1

The file system . . . . . . . . . . . . . . . . . . . . . . . . . . 23

1.4.2

Displaying the contents of files . . . . . . . . . . . . . . . . . . 24

1.4.3

Permission . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

1.4.4

Setting permissions . . . . . . . . . . . . . . . . . . . . . . . . 26

1.4.5

Running sequences of commands . . . . . . . . . . . . . . . . 27

1.4.6

Changing owners . . . . . . . . . . . . . . . . . . . . . . . . . 27

1.4.7

Inodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

The X-Windows system . . . . . . . . . . . . . . . . . . . . . . . . . 28


1.5.1

What is the X Window System? . . . . . . . . . . . . . . . . . 28

ii

CONTENTS

1.6

1.5.2

Why X . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

1.5.3

History of X . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

1.5.4

Starting the window manager . . . . . . . . . . . . . . . . . . 30

1.5.5

The fvwm window manager . . . . . . . . . . . . . . . . . . . 31

1.5.6

The .fvwmrc file . . . . . . . . . . . . . . . . . . . . . . . . . 31

1.5.7

The .Xresources and .Xdefaults files . . . . . . . . . . . . . 33

Networking and the Internet on UNIX machines . . . . . . . . . . . . 33


1.6.1

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

1.6.2

Protocols . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

1.6.3

Ethernet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

1.6.4

Types of cabling . . . . . . . . . . . . . . . . . . . . . . . . . 36

1.6.5

TCP/IP Internet addresses . . . . . . . . . . . . . . . . . . . . 37

1.6.6

Gateways and Routers . . . . . . . . . . . . . . . . . . . . . . 38

1.6.7

Telnet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

1.6.8

Anonymous ftp . . . . . . . . . . . . . . . . . . . . . . . . . . 39

1.6.9

The World Wide Web . . . . . . . . . . . . . . . . . . . . . . 41

2 ANSI C for Programmers on UNIX Systems

43

2.1

Credits: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

2.2

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

2.3

Compilation Stages . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46

2.4

Variables and Literals . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

2.5

Aggregates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48

2.6

Constructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

2.7

Exercises 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53

2.8

Contractions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

2.9

Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

2.10 Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.11 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
2.12 Exercises 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2.13 Keywords, Operators and Declarations . . . . . . . . . . . . . . . . . 69

CONTENTS

iii

2.13.1 Keywords . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
2.13.2 Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
2.13.3 Declarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2.14 Memory Allocation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
2.15 Input/Output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
2.15.1 File I/O under Unix . . . . . . . . . . . . . . . . . . . . . . . 74
2.15.2 Interactive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
2.16 Source File organisation . . . . . . . . . . . . . . . . . . . . . . . . . 78
2.16.1 Preprocesser Facilities . . . . . . . . . . . . . . . . . . . . . . 79
2.16.2 Multiple Source Files . . . . . . . . . . . . . . . . . . . . . . . 80
2.16.3 Make . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
2.17 Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
2.17.1 Utilities and routines . . . . . . . . . . . . . . . . . . . . . . . 83
2.17.2 Some Common mistakes . . . . . . . . . . . . . . . . . . . . . 84
2.18 Exercises 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
2.19 More information . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
2.20 Sample answers to exercises . . . . . . . . . . . . . . . . . . . . . . . 97
2.21 Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
2.21.1 Command Line arguments . . . . . . . . . . . . . . . . . . . . 97
2.21.2 Using qsort, random numbers and the clock . . . . . . . . . . 98
2.21.3 Calling other programs . . . . . . . . . . . . . . . . . . . . . . 98
2.21.4 Linked Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
2.21.5 Using pointers instead of arrays . . . . . . . . . . . . . . . . . 100
2.21.6 A data filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
2.21.7 Reading Directories . . . . . . . . . . . . . . . . . . . . . . . . 102
2.21.8 Queens: recursion and bit arithmetic . . . . . . . . . . . . . . 103
2.22 More on Arrays, Pointers and Malloc . . . . . . . . . . . . . . . . . . 104
2.22.1 Multidimensional Arrays . . . . . . . . . . . . . . . . . . . . . 104
2.22.2 realloc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
2.23 Signals and error handling . . . . . . . . . . . . . . . . . . . . . . . . 107

iv

CONTENTS

2.24 ANSI C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108


2.24.1 Converting to ANSI C . . . . . . . . . . . . . . . . . . . . . . . 108
2.25 Maths . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
2.25.1 Fortran and C . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
2.25.2 Exercises 1

. . . . . . . . . . . . . . . . . . . . . . . . . . . . 112

2.25.3 Exercises 2

. . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

2.25.4 Exercises 3

. . . . . . . . . . . . . . . . . . . . . . . . . . . . 116

3 Operating Systems Theory


3.1

3.2

3.3

3.4

121

Process Synchronization . . . . . . . . . . . . . . . . . . . . . . . . . 121


3.1.1

Common synchronization problems . . . . . . . . . . . . . . . 122

3.1.2

Mutual exclusion . . . . . . . . . . . . . . . . . . . . . . . . . 123

3.1.3

Semaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126

3.1.4

Producer/Consumer problem via semaphores . . . . . . . . . . 127

3.1.5

Reader/Writer problem via semaphores . . . . . . . . . . . . . 128

3.1.6

Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

InterProcess Communication under UNIX . . . . . . . . . . . . . . . 131


3.2.1

Shared Memory . . . . . . . . . . . . . . . . . . . . . . . . . . 131

3.2.2

Semaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133

3.2.3

Group Project 2006: Musical Chairs: . . . . . . . . . . . . . . 138

3.2.4

Previous Group Projects . . . . . . . . . . . . . . . . . . . . . 138

Deadlock . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
3.3.1

A definition for deadlock . . . . . . . . . . . . . . . . . . . . . 147

3.3.2

Resource Allocation Graphs . . . . . . . . . . . . . . . . . . . 147

3.3.3

Resource allocation examples . . . . . . . . . . . . . . . . . . 148

3.3.4

Deadlock Prevention . . . . . . . . . . . . . . . . . . . . . . . 149

3.3.5

Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
3.4.1

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152

3.4.2

F.C.F.S. Scheduling . . . . . . . . . . . . . . . . . . . . . . . . 152

3.4.3

S.T.F. Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . 153

CONTENTS

3.5

3.6

3.4.4

Priority Scheduling . . . . . . . . . . . . . . . . . . . . . . . . 154

3.4.5

Preemptive Scheduling . . . . . . . . . . . . . . . . . . . . . . 155

3.4.6

Round Robin Scheduling . . . . . . . . . . . . . . . . . . . . . 156

3.4.7

Scheduling Tasks on more than one Processor . . . . . . . . . 156

3.4.8

Preemptive Schedules for more than one Processors . . . . . . 161

3.4.9

Scheduling Dependent Tasks . . . . . . . . . . . . . . . . . . . 163

Virtual Memory and Paging . . . . . . . . . . . . . . . . . . . . . . . 166


3.5.1

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166

3.5.2

Demand Paging . . . . . . . . . . . . . . . . . . . . . . . . . . 167

3.5.3

Some Common Demand Paging Algorithms . . . . . . . . . . 167

3.5.4

The Optimality of Beladys Algorithm . . . . . . . . . . . . . 169

Computer Security . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171


3.6.1

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171

3.6.2

Encryption Systems . . . . . . . . . . . . . . . . . . . . . . . . 171

3.6.3

Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172

3.6.4

Introduction to Number Theory . . . . . . . . . . . . . . . . . 173

3.6.5

The Discrete Logarithm Problem . . . . . . . . . . . . . . . . 178

3.6.6

The Diffie-Hellman Key exchange procedure . . . . . . . . . . 179

3.6.7

The Code Protection Problem . . . . . . . . . . . . . . . . . . 180

3.6.8

The Rivest-Shamir-Adleman public key system

3.6.9

Authentication and Digital Signatures . . . . . . . . . . . . . 181

3.6.10 Secure Shell Environment:

. . . . . . . . 180

. . . . . . . . . . . . . . . . . . . 182

3.7

Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184

3.8

Appendices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
3.8.1

Appendix A: Quick reference for the vi editor . . . . . . . . . 185

3.8.2

Appendix B: Regular Expressions . . . . . . . . . . . . . . . . 185

CONTENTS

Chapter 1

Introduction to Unix
1.1

Credits

These notes closely resemble an introduction to UNIX course maintained by Luis


Balona at the South African Astronomical Observatory. This is no accident and
I would like to thank Luis for giving me permission to use his notes as a base for
constructing this set.

1.2

Introduction

The UNIX operating system started off in 1969 in an attempt to design a portable
operating system. In other words, the idea was to have UNIX running on any type
of machine. Up to that time, each brand of computer such as IBM, DEC, etc. used
its own particular software. If you switched machines, you had to re-learn all the
commands. The idea was that with UNIX all this would disappear and it didnt
matter what machine was being used - the commands would be identical.
At that time there was no computer screen. Instead, you entered commands on
a device similar to a typewriter which printed on paper roll. This was called a
terminal. The concept of a terminal is still used, but usually in a different context.
By the 1980s, video screens started replacing terminals. At first, the video screen
acted just like a terminal in the sense that only pure text could be displayed. For
most purposes this is quite adequate.
However, the visual impact of graphics is so high that very soon software was developed which could display graphics on the screen. For example, Microsoft developed
its windows operating system to replace the purely text-based DOS. In the same way,
the developers of UNIX created additional software which could be used to display
graphics. This is called the X-windows system. Today, most users of UNIX use
the X-windows rather than basic UNIX. Note that X-windows is part of UNIX (it
doesnt replace UNIX). As such it can run on any machine which supports UNIX

CHAPTER 1. INTRODUCTION TO UNIX

provided the hardware (graphics screen) is available.


Your instructor for the Operating Systems course has access to a unix box on his
desk:
hughm.cs.unp.ac.za

A 2 year old pentium running RedHat 10.0


used by Hugh Murrell as a UNIX work station

The web-site for this course can be found at:


hughm.cs.unp.ac.za/ murrellh

follow the links to the


operating systems course

Students taking the Operating Systems course will be given UNIX accounts on the
following UNIX machine
mars.cs.unp.ac.za

A pentium running redhat

Once you have been given an account you will be able to log in to mars from the
senior-lab by running telnet or putty. For example:
telnet mars.cs.unp.ac.za
Supply your user name and password to log in and use the UNIX command exit
to log out. Note that you will have to login to your lab machine in the usual way
before you can run telnet. We will be studying some basic UNIX commands and we
assume that you have logged in to your UNIX machine and have the UNIX prompt
on your screen.

1.2.1

Commands

Associated with commands in UNIX is a manual page. This is a document which


tells you in full detail what that particular command does. For example, the command which lists the files in a directory is called ls. To find out what it does and
how it works, you can look at the manual page on your screen by typing:
man ls
The command man picks out the page of the manual which deals with the command.
Of course, this is all very well, but ls isnt a very obvious command and you would
be very lucky to guess that this could be the command to list the file names in your
directory. Fortunately, there is a way in which you might be able to find out the
name of the command. This is the apropos command. For example, if you are
looking for a command which lists files, you could type in:

1.2. INTRODUCTION

apropos files
You will get the following among a long list:

...
lorder (1)
lpr (1)
ls (1)
m4 (1)
make (1)

- Finds the best order for member files in an object library


- Sends files to spooling daemon for printing
- Lists and generates statistics for files
- Preprocesses files, expanding macro definitions
- Maintains up-to-date versions of target files and performs
shell commands
makedepend (1X) - create dependencies in makefiles
...
Among this lot is the command you are looking for and a short description of what
it does.
The manual page of ls is typical of the manual page for all UNIX commands. You
will find associated with ls a set of options which are used to modify the behaviour
of the command. For example, ls by itself simply lists the files in the current directory (try it). However, you may want more information about these files, for
example when they were created or how large they are. This is done by adding the
option -l to the command:
ls -l
There are many other options associated with ls which act in the same way. Look
at the manual page and try them out.

1.2.2

Some common commands

Here is a list of commands which are most often used. I suggest you look at the
manual pages of each of these commands for more details and experiment with some
of the various options. The term standard output refers to the screen. Later on
you will see how standard output can be re-directed to a file or device.
cal - Displays a calendar

Typing cal without any arguments displays a calendar for the current month. Typing cal 1996 displays a calendar for the year 1996. cal 6 1996 displays a calendar
for June 1996.

CHAPTER 1. INTRODUCTION TO UNIX

cat - Concatenates or displays files

Typing cat foo displays the contents of a file foo on the screen. It takes no notice
of the fact that you can only see one screen at a time. A better way of displaying a
file is to use the more command. The value of cat is its ability to concatenate files.
For example:
cat foo1 foo2 foo3 > foo123
produces file foo123 which consists of file foo1 follwed by foo2 and foo3.

clear - Clears terminal screen

Thats all it does!

cd - Changes the current working directory

The command cd foo changes the current directory to foo. cd .. takes you to the
directory above the current working directory. cd without arguments takes you to
your home directory.

cmp - Compares two files

cmp foo1 foo2 prints nothing if file foo1 is identical to file foo2. If there are
differences, it prints the byte and line number where the differences occur.

compress, uncompress - Compresses and expands data

The command compress foo compresses the file foo to file foo.Z which is smaller
in size. The original file, foo is deleted. To restore foo, use uncompress foo.Z.

cp - Copies files

cp foo1 foo2 creates a new file foo2 which is an identical copy of foo1. If foo2
already exists, it is lost. The normal usage is cp foo1path/foo1 . which takes file
foo1, residing in another directory foo1path, and creates a file of the same name
in the current directory. Another use is to copy whole directory trees to another
location. For example, cp -r foopath . copies everything in foopath, including
its subdirectories and their contents, to the current directory.

1.2. INTRODUCTION

du - Displays a summary of disk usage

This command lists the number of blocks of disk usage in the current directory and
all subdirectories.

echo - Writes its arguments to standard output

For example echo "Hello World!" will display the message Hello World!. What
appears to be a rather useless command at first turns out to have a great many uses,
as you will find out.

find - Finds files matching an expression

This command searches for a file matching a name or part of a name in the current
directory and its subdirectories. It has a rather obscure syntax. For example, to
search for files in directory /user2/vk which have the characters foo somewhere
in there names, type:
find /user2/vk -name "*foo*" -print
Files with names such as foo1, newfoo, old.foo etc will all be listed.

grep - Searches a file for patterns

This is one of the most useful commands. It allows you to look for a string of characters inside files. For example, suppose you have forgotten the name of the file in
which you were writing a letter to Karen. Suppose you remember that in the file
somewhere you had mentioned Karen by name. Then you may type:
grep -i Karen *
which will search for all files in the current directory looking for the name Karen
inside them. The -i flag instructs grep to ignore capitalization, so that it will find
names such as Karen, KAREN, karen etc.

kill - Sends a signal to a running process

kill 999 stops and deletes the process with process number 999. Use this command
to stop a job running in the background, but to use it you need to know its process

CHAPTER 1. INTRODUCTION TO UNIX

number. For more details see ps.

ln - Link to a file

UNIX has a rather peculiar feature - the ability to link to a file. Essentially, this
means that you can have one copy of a file sitting in a certain directory but you can
have what amounts to a copy of the same file in another directory. The difference
is that this copy takes up almost no disk space if it is a link. For example, you
may have a very large file, foo sitting in directory foo1dir, but you need to use it
another directory otherdir. You could, of course, use the cp command to make a
duplicate of foo in otherdir, but this will use the same amount of disk space as
the original file foo. To conserve disk space, you might decide to make a link to
foodir/foo in directory otherdir using:
ln -s foodir/foo foo
which creates what appears to be file foo in otherdir. However, it is not a file, but
a link. But it behaves just as if it were the original file foodir/foo. Deleting the
link using rm does not delete the original file in foodir/foo.

lpr - Sends files to spooling daemon for printing

The standard UNIX print command is lpr. However, in our lab the user must
download print files to their local Microsoft machine and print from there. That
way all accounting is kept in place.

ls - Lists and generates statistics for files

One of the most common commands. It just displays a listing of all files in the
current direcory. Some of the more useful options are as follows:
ls -l - Gives full information for each file.
ls -1 - Lists files, one per line.
ls -F - Appends a slash if the file is a directory.
ls -a - Lists invisible files as well (files begining with a dot).
The options may be concatenated, e.g. ls -laF. It is annoying that there is no
option for listing only directories. One way of doing this is the command ls -F |
grep /. This uses two commands linked by a pipe (the symbol |). You will learn
about pipes later.

1.2. INTRODUCTION

mkdir - Makes a directory

The same as the familiar DOS command. mkdir foo creates subdirectory foo in
the current directory.

more - Displays a file one screenful at a time

The standard way of displaying the contents of files on the screen, one screen at a
time, e.g. more foo. Press the spacebar to view another screen or type q to quit.

mv - Moves files and directories

This command is useful to rename files or directories. For example, mv foo1 foo2
renames file or directory foo1 to foo2. Too bad if foo2 already exists; it is deleted
without warning.

passwd - Changes password file finformation

Use passwd to change your password. It will ask you for the new password and for
a confirmation.

ps - Displays current process status

This command is normally used to get the process number of some job you want to
kill. For example, suppose you are logged in twice and you want to be only logged
in at the current terminal. Suppose your user name is vk. First, you find all the
processes associated with vk using:
ps -a | grep vk
The number in the first column is the process number, say it is 999. Next you kill
the process using kill 999. There are many other uses for it, but this is one of the
more common ones. Of course, you need to be sure that the process number is the
correct one or you may find you have killed some other process instead of your login
shell. Fortunately, you can only kill processes that belong to you (unless you are the
superuser).

pwd - Displays the pathname of the current working directory

Tels you in what directory you happen to be in, but gives the full path name.

10

CHAPTER 1. INTRODUCTION TO UNIX

rm - Removes (unlinks) files and directories

Very useful, but dangerous command. The command rm foo deletes the file foo;
rm * deletes all files in the current directory, but not subdirectories. You should use
rm -i * if you want to be prompted before each file is deleted. rm -r foo deletes
the directory foo and all its contents (including subdirectories).

tail - Writes last few lines of a file to standard output

tail foo displays the last 10 lines of file foo on the screen.

tar - Manipulates tape archives

This command was originally developed to spool data on or off mag tapes, but has
other uses which have nothing to do with tapes. To use this command to read
data from a tape, the tape must have been written with tar (perhaps on another
machine).
The usage for reading a tape is tar xvf tape.device.name. The tape device name
is usually something like /dev/rmt1h, but can be omitted if it is the default name
/dev/rmt0h. In what follows I will assume the tape device is the standard one and
omitt its name
To write the whole of the current directory to tape, use tar cv .. To write subdirectory foo, use tar cv foo.
To list the contents of a tape archive (does not write to disk), usr tar t.
A very common, non-tape usage is to bundle-up a whole directory tree into a single
file. You may want to do this when copying a directory tree to another machine,
for example. The command is: tar cvf foo.tar foo where foo is the directory
and foo.tar is the name of the file to contained the bundled-up directory. Often
this is compressed to give foo.tar.Z. After transfer to another machine, it may
be un-bundeled using tar xvf foo.tar (you need to uncompress it first if it was
originally compressed). This command creates the original directory tree on the new
machine.

w - Prints a summary of current system activity

Use this command to find out who is logged on.

1.2. INTRODUCTION

1.2.3

11

The vi editor

One of the most common things you will want to do is to create files and to modify
them. You do this by using an editor. There are many kinds of editors. In the early
days of hardcopy terminals, it was important to minimize time spent in printing to
paper. The editor would do this by operating on one line at a time - a line editor.
Most of the common UNIX editors started in this way, but gradually changed to
accomodate screen editing.
There are two standard UNIX editors vi and emacs. People who use the one
violently condemn the other. Editors are a matter of personal taste. Most people
use neither vi or emacs, though both are available on the system and you can read
how to use them with the aid of the respective manual pages. We suggest that you
use vi, the standard in UNIX editing. At the end of this document, in appendix A,
you will find a short introduction to vi editing. Read it and try editing some text
files and save them in your home directory on your assigned UNIX box.

1.2.4

Electronic mail

Electronic communications is one of the most important tasks done by computers.


UNIX boxes come with easy to use mail servers which allow users to communicate
with each other using electronic mail or e-mail. Users are also connected to the
internet via UNINET, which links universities, technicons and research institutes.
This allows us to communicate with anyone in the world who has access to the
internet.
Most of you on this course will already have e-mail accounts and so wont need
another e-mail account. However you should try out UNIX e-mail to give yourself
an idea of how e-mail operates on a unix box.
There are many programs which can be used to send and receive mail messages.
The most common ones in UNIX are called mail and mailx. The latter is used by
many people, but most prefer another program called pine which is freely available
on the internet. We will discuss how to use pine for e-mail.
pine displays your options in menus at the bottom of each screen, so you do not need
to memorize commands. On-screen messages appear on a line above the command
menu to give you warnings or information as you make a choice. Help is instantly
available to provide information about the task you are performing.

Starting and quitting pine

To start pine, simply type:

12

CHAPTER 1. INTRODUCTION TO UNIX

pine
After starting pine, the Main Menu screen appears. Each screen has a similar
layout: the top line tells you the screen name and additional useful information,
below that is the work area (on the Main Menu screen, the work area is a menu of
options), then the message/prompt line, and finally the menu of commands.
When you want to leave pine, type Q (Quit).

The Main Menu

The Main Menu lists pines main options. The letter you must type to enter your
choice is to the left of each option or command name. You can usually type either
uppercase or lowercase letters, and you should not press Enter to enter commands.
Here is what the Main Menu looks like:
PINE 3.90
?
C
I
L
A
S
Q

MAIN MENU

HELP
COMPOSE MESSAGE
FOLDER INDEX
FOLDER LIST
ADDRESS BOOK
SETUP
QUIT

Folder:INBOX
-

2 Messages

Get help using pine


Compose and send a message
View messages in current folder
Select a folder to view
Update address book
Configure or update pine
Exit the pine program

Copyright 1989-1994. PINE is a trademark of the University of Washington.


[Folder "INBOX" opened with 2 messages]
?
O

Help
OTHER CMDS

P PrevCmd R RelNotes
L [ListFldrs]
N NextCmd

From the Main Menu, you can read online help, write and send a message, look at
an index of your mail messages, open or maintain your mail folders, update your
address book, configure or update pine, and quit pine.

Getting Help in pine

To read the online help, use the Help command at the bottom of each screen. For
example, at the Main Menu screen, type ? (Help). Because the help text is context
sensitive, you never see all of it at once - only the part that relates to the pine
feature you are using. To exit the online help, type E (Exit Help).

1.2. INTRODUCTION

13

Writing a Message in pine

To write a message, type C (Compose) to see the Compose Message screen.


PINE 3.90

COMPOSE MESSAGE

Folder:INBOX

2 Messages

To :
Cc :
Attchmnt:
Subject :
----- Message Text -----

^G Get Help ^X Send


^R Rich Hdr ^Y PrvPg/Top ^K Cut Line
^O Postpone
^C Cancel
^D Del Char ^J Attach
^V NxtPg/End ^U UnDel Line ^T To AddrBk

In the command menu above, the cap character is used to indicate the Control key.
This means you must hold down the Control key while you press the letter for each
command. Press Ctrl-G (Get Help) to see additional commands. To move around,
use the arrow keys. To correct errors, use Backspace or Del.
You might start experimenting in pine by sending yourself a message. The following
section shows you how.

Writing and sending a test message to yourself

To write and send a test message to yourself, type C (Compose) to see the Compose
Message screen. In the To: field, type your email address and press Enter. In the
Cc: field, press Enter. In the Attachment: field, press Enter. In the Subject:
field, type Test and press Enter. Below the Message Text line, type This is a
test.
You need to know the e-mail address of the receiver before you can send e-mail. For
example use murrellh@ukzn.ac.za if you want to send a message to me. Here is
what the screen might look like:

PINE 3.90

COMPOSE MESSAGE

To : murrellh@ukzn.ac.za
Cc :
Attchmnt:

Folder:INBOX

2 Messages

14

CHAPTER 1. INTRODUCTION TO UNIX

Subject : Test
----- Message Text ----Tell me how much you are enjoying the course here.

^G Get Help ^X Send


^R Read File ^Y Prev Pg ^K Cut Text
^O Postpone
^C Cancel
^J Justify ^W Where is ^V Next Pg ^U UnCut Text ^T To Spell

To send your message, press Ctrl-X (Send). You are asked:


Send message?
Type y (yes) or press Enter. The message is sent, and a copy is saved to your
sent-mail folder. (If you type n (no) the message is not sent, and you can continue
to work on it.)
This test message is very simple. There are, of course, other options you can use as
you compose a message. A few are summarized in the next section, and complete
information about options for the Compose Message screen is available in online
help. As you compose a message, you can press Ctrl-G (Get Help) at any time to
see details about your current task.

Inserting a plain text file

If you want to send a plain text file with your message, you can insert the file in
the body of your message using the Ctrl-R (Read in a File) command.

Listing messages

pine stores messages sent to you (including those you send to yourself) in your
INBOX folder. Messages remain in your INBOX folder until you delete them or
save them in other folders.
To see a list of the messages you have received in your INBOX folder, type I at
the pine Main Menu. If you have any messages, they are listed as shown in the
following example:

PINE 3.90

FOLDER INDEX

Folder:INBOX

Message 3 of 3 NEW

1.2. INTRODUCTION

D 1 Jan 10
+ A 2 Jan 10
+ N 3 Jan 11

15

Mu Li
Christine Smith
To: jhughes

? Help
M Main Menu
O OTHER CMDS V [ViewMsg]

(486) Proposal
(500) NSF
(448) Test

P PrevMsg
N NextMsg

Prev Page
Spc Next Page

D Delete
U Undelete

R Reply
F Forward

The selected message is highlighted. The first column on the left shows the message
status. It may be blank, or it may contain N if the message is new (unread), +
if the message was sent directly to you (it is not a copy or from a list), A if you
have answered the message (using the Reply command), or D if you have marked
the message for deletion. The rest of the columns in the message line show you the
message number, date sent, sender, size, and subject. For details, press ? (Help).
Most of the commands you need to handle your messages are listed at the bottom
of the screen. You can type O (Other Commands) to see the additional commands
that are available. You do not need to see these commands on the screen to use
them. That is, you never need to type O as a prefix for any other command.

Viewing a message

At the Folder Index screen, use the arrow keys to highlight the message you want
to view. Type V (ViewMsg) or press Enter to read a selected message. To see the
next message, press N (NextMsg). To return to the index, press I (Index).

Replying to a message

To reply to a message that you have selected at the Folder Index screen or that
you are viewing, type R (Reply). You are asked whether you want to include the
original message in your reply. Also, if the original message was sent to more than
one person, you are asked if you want to reply to all recipients. Think carefully
before you answer - it may be that you do not want your reply to be sent to more
than just the author of the message. It is always a good idea to verify that the
addresses in the To: and Cc: fields are correct before you send a message.

Folders

Incoming messages may quickly accumulate in your INBOX folder. Imagine what it
would be like to find one hundred messages there. If you use email often, this may

16

CHAPTER 1. INTRODUCTION TO UNIX

happen sooner than you expect. How should you organize the messages you wish to
save?
A pine folder, like a folder in your file cabinet, is a storage place for messages. As
you use email, you accumulate many messages and can organize them into different
folders by topic, correspondent, date, or any other category that is meaningful to
you. You can create your own folders, and pine automatically provides three:
The INBOX folder: Messages sent to you are listed in this folder. When you
first start pine and go to the Folder Index screen, you are looking at the list
of messages in your INBOX folder. Every incoming message remains in your
INBOX until you delete it or save it in another folder.
The saved-messages folder: Copies of messages you save are stored in this
folder unless you save them to other folders you create yourself.
The sent-mail folder: Copies of messages you send are automatically stored in
this folder. This is convenient if you cannot remember whether you actually
sent a message or if you want to send a message again.

Using the address book

As you use email, you build a list of email correspondents. Some of their addresses
may be difficult to type or remember. You can use the pine Address Book to store
email addresses for individuals or groups, to create easily remembered nicknames
for these addresses, and to quickly retrieve an email address when you are composing
a message. Here is a sample page from an Address Book:
PINE 3.90

ADDRESS BOOK

Folder:INBOX

Message 1 of 3

gomez
Gonzalez, George
ggonz@unixz.university.ca
mu Li, Mu
muli@u.university.edu
chris
Smith, Christine K. cksmith@art.somewhere.edu
rt Research Team
DISTRIBUTION LIST:
gomez
chris
jhughes@art.somewhere.edu
? Help
M MainMenu P PrevEntry
O OTHER CMDS E [Edit]
N NextEntry

- PrevPage
Spc NextPage

D Delete
A Add

S CreateList
Z AddToList

There are two ways to set up an individual address in your Address Book. You can
add an address manually or take it more easily from an incoming message. With
either method, you specify nicknames for your correspondents. You can also set up
a group (list) address in your Address Book, but only manually.

1.3. PROCESSES

17

To add an individual address manually, first make a note of the address. At the
pine Main Menu, type A (Address Book). Type A (Add). Follow the instructions.
(Press Ctrl-G if you need help.)
To take an individual address from a message you are viewing or have selected in
the index, type T (Take Address). The T command is not visible on your screen
unless you type O (Other Commands), but you need not see it to use it. Follow the
instructions. (Press Ctrl-G if you need help.)

Printing a message

To print a message, save the message to a file and then ftp the file to your local
hard drive and use your local operating system to print the file. Do not use the
standard UNIX commands, lp or lpr to print the file. If you do, your printouts will
appear on the departmental printer and our technical staff will get very cross with
you. For the same reason you must not use the print command built into pine.

Exercise

Set up an address list for your tutorial group and then try it out by sending a test
message to everyone in your group.

1.3
1.3.1

Processes
Filename shorthand

Suppose youre typing a large document like a book. You might have separate files
for each chapter, called ch1, ch2, etc. Or, if each chapter were broken into sections, you might have files called ch1.1, ch1.2, etc. What if you wanted to print
the whole book? You could say lpr ch1.1, lpr ch1.2 ... but this would soon
get rather boring. This is where filename shorthand comes in. If you say:
cat ch*
the asterisk, *, is taken to mean any string of characters, so ch* is a pattern that
matches all filenames that begin with ch. The above command will print to the
screen all the chapers of the book in alphabetical order. The * can be anywhere and
can occur several times. Thus

18

CHAPTER 1. INTRODUCTION TO UNIX

rm *.save
removes all files that end in .save. The strings ch* and *.save are examples of
regular expressions. Many unix command allow their parameters to be specified as
regular expressions. To find out more about regular expressions read appendix B at
the end of these notes.

1.3.2

Input-output redirection

Most of the commands we have seen so far produce output on the terminal screen.
The terminal screen is called standard output. In the same way, the keyboard is called
standard input. There is also standard error. This is the place where system error
messages are displayed. Normally, this is also the screen so that standard output
and standard error are the same (it would be most unfortunate if error messages
disappeared into some obscure file).
It is a feature of UNIX that standard input, output or error can nearly always be
replaced by a file. For example, ls normally lists the files in your current directory
on the screen. If you prefer this listing to be a file called foo, you would type:
ls > foo
Now, foo will contain the file listing that would normally go to the screen. The
symbol > means redirect the output to the file that follows. The file foo will be
created if it doesnt exit or the previous contents overwritten if it does. No output
appears on your screen.
The symbol >> operates in the same way as >, except that it stands for add to
the end of the following file. For example,
cat foo1 foo2 foo3 >> temp
will append files foo1, foo2, foo3, in that order, to the end of temp. If temp
doesnt exist, it will be created.
Some commands produce output on standard error, even when they work properly.
For example, find will do this if a directory you are searching is protected. It may
be desireable in this case to re-direct standard error to a file. The command:
find /usr -name foo -print 2> foo.error

1.3. PROCESSES

19

will direct all error messages from this command to the file foo.error. The file descriptor number for standard error is 2. Sometimes you want both standard output
and standard error directed to the same file. Here is how you do it:
find /usr -name foo -print 1> foo.outerror 2>&1
This doent make all that much sense, but it works because the file descriptor number
for standard output is 1. The file descriptor number for standard input is 0.
In a similar way, the symbol < alows you to redirect input which would normally be
from the keyboard to a file. For example, if file foo contains a list of users produced
by the command w > foo, you could use:
grep vk < foo
to display information for user vk only.
It is also possible to include standard input for a command along with the command
itself. In other words, if you have a program which requires some keyboard input,
you can include it as in this example:
grep "$*" <<Junk
search pattern 1
search pattern 3
search pattern 3
Junk
What is happening here is that the input is on three separate lines, search pattern
1, search pattern 2, search pattern 3. The block of input is demarcated by
the word Junk at the begining and at the end. This can be any unique character
or string of characters it doesnt have to be Junk. The symbol "$*" tells grep to
look for standard input within the lines demarcated by Junk. This might seem very
complicated and rather useless, but it can be very useful indeed. This construction
is called a here document. It means that standard input is right here instead of the
keyboard.
1.3.3

Pipes

In the last section we have shown how the output from one command (w > foo) can
be used as input to another command (grep vk < foo) using the intermediate file
foo. You can do away with such intermediate files by using a pipe. In this example,
you could more conveniently type:

20

CHAPTER 1. INTRODUCTION TO UNIX

w | grep vk
to get the same result. The symbol | is called a pipe and is used to connect the
standard output of one command to the standard input of another command. Any
number of commands may be connected by pipes.

1.3.4

Processes

When you log in, a program know as a shell takes over. When you type in commands,
you are communicating with the shell. The shell is an interface between you and
the fundamental system which does all the work called the kernel. As far as you are
concerned, it is the shell that is doing the work, but in reality all the shell does is
translate your commands to something the kernel can understand. There are many
types of shell, all doing the same thing but in slightly different ways. We will talk
about this later.
One of the things the shell can do is to run more than one program at a time. For
example, suppose you have a huge, time consuming program called foo. You could
run it and wait for several munites for it to complete. This is inconvenient because
you wont be able to use that terminal screen until the program has finished. It
is better to set this big program running in the background. This allows you to
continue working with the computer. When the program finishes, the shell will alert
you by printing an appropriate message on the screen.
As an example, suppose you want to locate a certain file foo on the disk. There
may be thousands of files, so that the find command may take a long time before
finishing. You may set it running in the background as follows:
find / -name foo -print > foo.list &
The ampersand sign, &, at the end of a command line says to the shell start this
command running, then take further commands from the terminal immediately,
that is, dont wait for it to complete. The command will begin, but you can do
something else while it is running. Directing the output to file foo.list keeps it
from interfering with whatever you are doing at the same time.
Notice that as soon as you start this command, the shell responds with:
[1] 8413
or some such number. The number 8413 is the process-id of the job. If the program

1.3. PROCESSES

21

running in the background is out of control, you can kill it using


kill 8413
If you dont know the process number, you have to look for it using the ps command.
It is important to distinguish between programs and processes. find is a program;
each time you run find, or any program, it creates a new process. There is an
unique process-id number associated with each running process. The shell is itself
a process. When you log in, you set the shell program running. When you logout,
you kill the shell process. You can log yourself out by using kill if you know what
the process id of the shell is (run ps to find out).
When you logout, all running processes are killed. This includes any processes that
you might have set running in the background using &. Sometimes the program
takes so long to run to completion that you may want it running even after you have
logged out. The command nohup was created to deal with this situation. If you say
nohup command &
the command will continue to run if you log out. Any output from the command
will be saved in a file called nohup.out.
Sometimes you start a program running normally, but decide that it was a bad idea
and it would have been better to run it in the background. This can be done without
re-starting the program by typing Ctrl-Z. In other words, hold down the Ctrl key
on the keyboard and type Z. This temporarily halts the program. Then you continue
execution of the program in the background by typing:
bg
Now you can do other things. To return the background process to the foreground,
type
fg
at any time.

22

1.3.5

CHAPTER 1. INTRODUCTION TO UNIX

The environment

One of the virtues of the UNIX system is that there are several ways to bring it
closer to your personal taste. This is done by tailoring the environment. The
method for tailoring your environment depends on what shell you are running.
Unix provides for many diferent shells. Here are the three most common shells:
Shell
bash
csh
tcsh

Name
GNU Bourne Shell
C Shell
Tc Shell

Executed at login
.profile
.cshrc and .login
.cshrc and .login

At UND users will run bash by default. bash is the GNU version of the Bourne
shell sh based and it has more powerful features. To see all the features available
consult the man pages.
When you log on, the bash reads a file called profile stored in a special directory
(/etc) which can only be modified by the superuser. Then it reads a file called
.profile in your home directory. This is one of the invisible files (because it
starts with a dot). You are not allowed to change /etc/profile, which sets up
the basic environment for all users, but you may change .profile to suit your own
taste.
One of the most common environment variables is PATH. This specifies the path
that the shell will take to look for programs or commands. In general, the PATH
environment variable in /etc/profile is set up so that the shell first looks in the
current directory for the program or command. If it cannot find it in the current
directory, it will look in /bin, then in /usr/bin, then in /usr/local/bin. This is
an example of a path. You can display the path by typing:
echo $PATH
If you want the shell to search for some particular place in your directory for a program, you can add to the search path by including these lines in your .profile file:
PATH=$PATH":/user1/vk/foo"
export PATH
where /user1/vk/foo is the full pathname of the directory foo where the command
is stored. The command export ensures that processes spawned by the current
process have the new PATH environment.
You may want to look at all the environmental variables that have been set. Do this
using: env

1.4. THE UNIX FILE SYSTEM

1.4
1.4.1

23

The UNIX file system


The file system

Everything in the UNIX system is a file. A file is just a sequence of bytes (a byte
is a unit of data 8 bits long think of it as a character). This is true not only of
disk files, but also of tape files, line printer files, etc. Because most of the UNIX
commands deal with files, a good understanding of the file structure is important.
When a UNIX system is created on an empty disk, two directories are created.
These are called /, the root directory, and /usr, the user directory. All directories,
including /usr can be thought of as subdirectories of /.
The machine expects to see the kernel as a file in the root directory. (If you remember, the kernel is a program which executes commands passed to it by the shell.)
This file is called vmunix on most machines. When the machine boots (i.e. starts
up), it loads the kernel into memory.
There are several directories which are of vital importance and which are present on
all UNIX machines.
/bin: This subdirectory of / contains most of the unix commands, such as ls,
grep, more, etc. (Remember, a command is just a program which you run.)
/etc: This subdirectory of / contains important files (not programs) needed by the
system. This includes files such as passwd, (the username and password file),
hosts (a file listing the addresses of all machines on the network), printcap
(a file listing the characteristics of printers connected to the system), motd,
message of the day the file containing the text which is displayed when a
user first logs in, profile, which sets up the environment for all users, etc.
/dev: A directory containing device drivers for various peripherals (disks, tapes,
etc.). Rather than having special system routines to, for example read a magnetic tape, there is a file called /dev/rmt0h. Inside the kernel, references to the
file are converted into hardware commands to access the tape.
/tmp: A directory used for temporary storage. When you edit a file, for example,
the editor saves a copy of the original file in this directory which has write and
read permission set for all users. When the editor has finished its job, it deletes
this temporary file.
/usr/bin: This subdirectory of /usr contains other important unix commands.
In some systems, such as on ours, it is this directory which actually contains
all the UNIX commands: /bin is just a link to /usr/bin.
/usr/man: This subdirectory of /usr contains the manual pages in eight subdirectories: man1, man2, ....
We will discuss some of these directories and files in later chapters. In small systems,
individual users are allocated space in the /usr directory. In other words, the home

24

CHAPTER 1. INTRODUCTION TO UNIX

directory of vk, for example, will be a subdirectory of /usr and will have the path
/usr/vk. On our system, we have separate disks set aside to hold private user
directories. In this case, a separate file system is created for each disk (/user1 and
/user2 on our machine). This filesystem is then mounted as a subdirectory of /, the
root directory. This is done by creating empty subdirectories user1 and user2 in /
and then using these as mount points for the two disks. Each disk, then, appears as
a subdirectory of root when it is mounted. You do the same thing when you mount
a magneto-optical disk or cdrom on our system.
A single disk can, in fact, be subdivided so that to the system it appears as different
disks. This is called partitioning. Our system disk is divided into four partitions: /,
/usr, /proc and /catalogs which all share the same physical disk. On booting,
/usr, /proc and /catalogs and other file systems are mounted on root. /proc is
used by the system as memory swapping storage; /catalogs is a file system I created
to accommodate astronomical catalogues. The kernel reads the file /etc/fstab to
determine what filesystems to mount. The command df lists the filesystems and
their mount points.

1.4.2

Displaying the contents of files

The od command allows any file to be displayed as a sequence of bytes (a bag of


bytes). Normally, many files cannot be displayed on the screen because they are
binary files. od converts each byte to a printable character. To display the file foo,
use the command:
od -c foo or od -cb foo
The former displays the contents as characters, the latter as octal numbers. As an
exercise, you should display the contents of directory names, links, etc. Note that
each one of these is just a bag of bytes and nothing more.

1.4.3

Permission

Every file has a set of permissions associated with it, which determine who can do
what with the file. If you keep your love letters in a directory, you probably do not
want anybody to read them. To do this you need to change the read permission of
the directory. But let me warn you that the superuser can still read them!
When you login, you type your username and password. The system actually recognises you by a number, called the user id or uid. Besides the uid, you are assigned
a group identification or gid which places you in a class of users. On our system,
all users have been assigned the same gid (which is represented by a number but
given the name users), but of course the uids are different. It may be desirable in

1.4. THE UNIX FILE SYSTEM

25

some large systems which have, say physicists and astronomers, to have a gid for
physicists (say phys) and a different one for astronomers (say astros).
The file /etc/passwd is the password file; it contains all the login information about
each user. You can discover your uid and gid, as does the system, by looking at this
file:
aavdw:4OeLd.nWLZV6g:24:15:Audrey van der Wielen:/user2/aavdw:/bin/ksh
vk:mKaffnwrg8azY:30:15:Veronique K:/user2/vk:/bin/ksh
The fields are separated by colons and laid out like this:
login-id:encrypted-password:uid:gid:your-name:login-directory:shell
Note that the password appears here in encrypted form. Anyone can read it, but it
is impossible to decode it, so it is useless to try. On our system all users have the
same gid = 15 (users).
When you try to read, write or execute a file, the kernel looks at the permissions
set for the file and decides on this basis whether you are allowed to do any of these
three these operations. As file owner, you have one set of read, write and execute
permissions. Your group has a separate set. Everyone else (world) has a third set.
When you use the command ls -l to display your files, the output looks something
like this:
-rw-r--r--rw-r--r--rwxr-xr-x

1 lab
1 lab
1 lab

users
users
users

184 Nov 20 11:09 foo


33 Nov 18 11:53 foo.list
3086 Nov 20 08:47 foox

These lines tell us that all three files are owned by uid lab, gid users. The first file,
foo, is 184 bytes long, was created on November 20 at 11:09 and has only one link
the number just before the uid.
The string -rw-r--r-- is how ls represents permissions on the file. The very
first - indicates that it is an ordinary file. If it were a directory, there would be
a d here. The next three characters encode the owners (based on the uid lab)
read, write and execute permissions. The next three characters encode the group
permissions. Finally, the last three characters encode permissions for everyone else.
In this example, lab can read and write to foo, but group and world can only read.
This is fine - you dont want some other user to overwrite your file. You could do this
by changing the permissions of foo to -rw-rw-rw-. Anyone can read the contents
of all three files. Also, everyone can read and execute foox.
Anyone can execute the passwd command to change her password. This modifies

26

CHAPTER 1. INTRODUCTION TO UNIX

the password file /etc/passwd. Dont get confused by these two files with identical
names one is a command /bin/passwd, the other is a file /etc/passwd. Since
the file /etc/passwd can only be modified by the superuser, you may wonder how
anyone can change his password. If you do a ls -l /bin/passwd you get:
-rws--x--x

3 root

bin

16384 May 20

1996 /bin/passwd

This tells us that only the owner, root, has permission to write to the file; everyone
else can only execute the file. But note the execute permission character for the
owner, which is supposed to be x, has been replaced by s. This tells the system that
when /bin/password is executed by anyone, it must behave as if that person where
root. In other words, the uid is set to root on execution. This is called making the
command set-uid. Since on execution, /bin/passwd has root privileges, it can
modify the /etc/passwd file.
Directory permissions behave a little differently, but the basic idea is the same. Here
is the output of ls -ld /usr/bin:
drwxr-xr-x 5 root system 7168 Oct 18 10:44 /usr/bin
An r field means that you can read the directory, so that you can find out what
files it contains using ls. A w means you can create or delete files in this directory,
because that requires modifying and therefore writing the directory file. The x field
does not mean execute in this case. It stands for search. If a field is set to --x,
you can no longer use ls to list the contents of the directory or read any files, but
you can access any file that you know is there. Similarly, with r-- users can see
(ls) the files, but not use the contents of a directory.
1.4.4

Setting permissions

The chmod (change mode) command changes permissions on files. The syntax of
this command is very clumsy. The most often used options are:
chmod +x foo
which allows everyone to execute the file foo, and:
chmod -w foo
which turns off write permission to everyone, including the owner of the file. Apart
from the superuser, only the owner can change permissions on a file.

1.4. THE UNIX FILE SYSTEM

27

These options are fine for simple permission changes, but get very complex when
several permission fields have to be changed at the same time. In this case it is much
easier applying another option of chmod using numerical values. To use these you
need to know something about octal notation. Each of the three permission fields,
rwx, can be set or unset if there is a one or zero in that particular bit position. For
example 001 010 100 stands for --x-w-r--; 011 110 101 stands for -wxrw-r-x,
etc.
Now, a triplet such as 011 can be converted to a single octal number. To do this,
you must multiply each bit by the numbers 4, 2, and 1 respectively and add up the
result. For example:
110 = 1 4 + 1 2 + 0 1 = 6;
011 = 0 4 + 1 2 + 1 1 = 3.
The octal number that represents -wx rw- r-x or 011 110 101 is 365; --x -w- r-is represented by 124 etc. The command to change these permissions is therefore
chmod 365 foo and chmod 124 foo respectively.

1.4.5

Running sequences of commands

Sometimes it is useful to be able to run a file containing a sequence of commands.


For example, if you need to run these commands very frequently, it is easier if these
commands are stored in a file. To run the sequence all you need to do is type in the
name of the file. However, before this can be done, the file must be made executable
using chmod +x.

1.4.6

Changing owners

Only the superuser can change the ownership of a file. This is done using:
chown vk foo
which changes the owner of file (or directory) foo to user vk. The wild card symbol
may be used: chown vk *. All files and subdirectories in directory tree foodir may
be changed using chown -R vk foodir.

28

1.4.7

CHAPTER 1. INTRODUCTION TO UNIX

Inodes

A file has several components: a name, contents and administrative information


such as permissions and modification times. The admin information is stored in the
inode (should be called i-node, but the hyphen has fallen away).
It is important to understand the concept of inodes because in a sense, inodes are
the files. All the directory hierarchy does is provide convenient names for files. Files
are recognised by the computer not by their names, but by their inode numbers
(i-numbers). You can obtain the i-number of foo using ls -i foo. If foodir is a
subdirectory, you can display the i-numbers of all files in the directory using ls -i
foodir. A directory name is nothing more than a file giving the i-number for each
file belonging to it.
A link is just a filename which has the same i-number as another file.

1.5
1.5.1

The X-Windows system


What is the X Window System?

The X Window System permits a user sitting at one machine to run programs on
a remote machine but still interact with the program locally. X is in effect one
way for different systems to interface with each other. It will let a program run on
one computer and yet display its output on another computer, even when the other
computer is of a different make. The program will display its output on the local
machine, accept keyboard and mouse input from the local machine, but will execute
on the CPU of the remote machine. At large institutes the local machine will be a
PC running LINUX. The remote machine is usually a fast large UNIX device like a
Dec Alpha.
In summary, X is a distributed, intelligent, device independent, operating system
independent, windowing system.

1.5.2

Why X

Most computer systems have long been based around the character terminal. These
are typically connected to the central host computer via serial lines running at
between 1200 and 19200 baud. (Baud is a term which stands for bits per second;
if you divide by eight you get the speed in characters per second.) With fairly
basic terminals, such as the VT100, you have a single screen of 80 characters by 24
lines. On a serial line, then, a complete screen refresh would take between 1 and 16
seconds. Running a program on a remote machine is not difficult, all that you have
to do is to log in to the remote machine and run it. The output will come directly
to your screen.

1.5. THE X-WINDOWS SYSTEM

29

The situation would be somewhat different, however, if you were wanting to run
graphics. A basic graphics terminal with 512 x 512 x 4 bit-planes resolution would
require 128 kB of data for a complete screen refresh. Obviously this would make
use of such a terminal rather tedious over a serial line, requiring over a minute for
each screen refresh even at 19200 baud. Even if the terminal was directly attached
to the host computer via ethernet (10 Megabit/sec), the network load would be
unacceptable. With a more advanced graphics screen, eg 1280x1024x8 bit-planes
requiring 1280 kB per screen, the situation would be even worse.
The solution to the above problem is to provide an intelligent terminal that understands how to draw graphics objects without requiring bit maps to be continuously
sent across the network. In other words, if the machine were to tell the terminal to
draw a circle with radius of 100 pixels, it would simply send the command draw
a circle with radius 100 pixels instead of sending full details of which screen pixel
has to be turned on or off to draw the circle.

1.5.3

History of X

In 1984 the Massachusetts Institute of Technology (MIT) formed Project Athena.


The goal was to take the existing assortment of incompatible workstations from
different vendors and develop a network of graphical workstations that could be
used as teaching aids. The solution was a network that could run local applications
while being able to call on remote resources. They thus created the first operating
environment that was truly hardware and vendor independent - the X Window
System.
By 1986 outside organisations were asking for X. In 1988, MIT officially released
version 11 release 2. The X Consortium now handles all the development of X and
the most recent version is release 6, which was released in September 1995. X has
now reached the level of success that most UNIX vendors incorporate it as standard
into their own windowing systems, and it is widely available for other platforms such
as the Macintosh and the PC.
Given that X is the universal cure for all ills, why should anyone ever not use X?
The device independence and distributed nature of X does incur an overhead. On
most UNIX workstations approximately 16 MB of RAM is recommended for good
performance in X. (DOS PC implementations of X windows normally require 2MB
of RAM to even start and recommend 4MB of RAM for reasonable performance). X
also takes up more disk space than custom, hardware dependent windowing systems.
However, given that disk and memory prices are forever becoming cheaper, these
limitations of X are not overwhelming ones. As far as I know all UNIX workstation
vendors are currently selling their machines with an X based windowing system as
standard.
When X was designed, there was no specification on what the graphics should look
like - this was left entirely to the programmers. As a result, several different styles

30

CHAPTER 1. INTRODUCTION TO UNIX

have emerged, among which these are the most popular:

Open Look style - on Sun workstations.


Motif - on most other vendors workstations.
CDE - Common Desktop Environment. This seems to be the emerging standard.

1.5.4

Starting the window manager

When you first switch on your LINUX machine, it boots up a normal (text) version
of UNIX. To run X windows, you login at the LINUX console and type the command:
startx
This command reads certain startup files which can be tailored to produce the look
that you want. The first file that startx looks for is called .xinitrc in your home
directory. If it does not exist, it looks for a file called xinitrc in the directory
/usr/X11R6/lib/X11/xinit. These files determine the kind of window manager
that should be run. The window manager is the program that manages the windows!
Here is an extract from an .xinitrc file
xrdb -load $HOME/.Xresources
xsetroot -solid SteelBlue &
oclock -geometry 75x75-0-0 &
xterm -geometry +0+60 -ls &
exec fvwm
The first line says that the file in the users home directory called .Xresources
should be loaded. We discuss this file below.
Next it says that the background window (the root window) on which all other
windows are to be displayed should have a solid steel blue colour. This is what all
LINUX users use as a default, but you can change the default colour of your root
window by creating .xinitrc and modifying this line.
The third line says that a clock face with size 75 pixels horizontaly, 75 pixels vertically and positioned at the bottom right hand corner of the root window.
The fourth line says that an x-terminal window should be created located at the
very left hand margin (+0) and 60 pixels from the top of the screen.
Finally, the actual window manager fvwm is executed.

1.5. THE X-WINDOWS SYSTEM

31

Its quite safe to create this file and to play around with various settings. If you delete
.xinitrc from your home directory, you will get the standard setup, so nothing is
lost.
The are many types of window managers besides fvwm (Feeble Virtual Window
Manager). Here are some for you to try: twm, olvwm, olwm. olvwm and olwm are
examples of the Open Look style.

1.5.5

The fvwm window manager

Fvwm puts a decorative border around most windows. This border consists of a bar
on each side, and a small L shaped section on each corner. There is an additional
top bar is called the title bar, and is used to display the name of the window. The
top, side and bottom bars are collectively known as the side-bars. The corner pieces
are called the frame.
Unless the standard defaults files are modified, pressing mouse button 1 in the title
or side-bars will begin a move operation on the window. Pressing button 1 in the
corner frame pieces will begin a resize operation. Pressing button 2 anywhere in the
border brings up an extensive list of window operations.
Fvwm provides multiple virtual desktops for users who wish to use them. The screen
is a viewport onto a desktop which is larger than (or the same size as) the screen.
Sticky windows are windows which transcend the virtual desktop by Sticking
to the screens glass. They always stay put on the screen. This is convenient for
things like clocks, so you only need to run one such gadget, and it always stays with
you.
Window geometries are specified relative to the current viewport. That is xterm
-geometry +0+0 will always show up in the upper-left hand corner of the visible
portion of the screen.
A geometry specified as something like xterm -geometry -5-5 will generally place
the windows lower right hand corner 5 pixels from the lower right hand corner of the
visible portion of the screen. Not all applications support window geometries with
negative offsets.
On startup, fvwm will search for a file named .fvwmrc in the users home directory.
Failing that, it will look for /usr/lib/X11/fvwm/system.fvwmrc for system-wide
defaults.

1.5.6

The .fvwmrc file

This files allows you to specify what is to be placed on the root window when it
starts up. As an excercise, you should copy the system-wide default file to .fvwmrc

32

CHAPTER 1. INTRODUCTION TO UNIX

in your home directory, and change various options:


cp /usr/X11R6/lib/X11/fvwm/system.fvwmrc .fvwmrc
There is no danger in editing .fvwmrc, since all you need to do to restore your initial
configuration is to delete it (in which case the system-wide file system.fvwmrc will
be read). I will discuss a few interesting features which you may want to change.
Near the begining of .fvwmrc you will see these lines:

HiForeColor
HiBackColor

Yellow
#c06077

The HiForeColor and HiBackColor options set the foreground and background
colours of the title bar text and the title bar itself respectively. You can use a
normal colour name to specify the colour, or you can use the hexadecimal notation
(e.g. #c06077). In this notation, c0 is the amount of red, 60 is the amount of blue
and 77 is the amount of green which makes the total colour. Remember this is in
hex (base-16) notation in which a = 10, b = 11,... f = 15 (#ff0000, for example,
means pure red). A list of the available colours can be produced using the command
showrgb.
Further down in the file, you will see how to define and position the virtual desktop:
#set the desk top size in units of physical screen size
DeskTopSize 3x3
# and the reduction scale used for the panner/pager
DeskTopScale 36
# Use the Fvwm Pager
Pager 5 5
The DeskTopSize defines the total number of virtual screens that you require. The
default is nine (3x3), but you may decide to have more, say 5x5. The DeskTopScale
is the reduction factor for the virtual desktop. Finally, you place the virtual desktop
5 pixels from the left hand side and 5 pixels from the top edge of the screen (the
default). You could position it at near the bottom right hand corner using Pager
-5 -5.
This is how windows are started:
#

1.6. NETWORKING AND THE INTERNET ON UNIX MACHINES

33

# Put here things you want to see on start up. Here I put up a window
# showing the date and time, wait till it has poitioned itself.
#
Exec "I"
xclock -d -bg white -update 1 &
Wait "I"
xclock
EndFunction
#
The first thing that is done on startup is to display a digital clock showing the date
and time (this is the time by your PC which could be out). Read about the various
obtions using man xclock. It waits a little while for the clock to come up.
1.5.7

The .Xresources and .Xdefaults files

These two files are essentially the same thing, except that .Xresources is used at
normally when the window manager starts up for the first time, while .Xdefaults
is used at any time. We will assume that you do not have a .Xresources file in
your LINUX home directory.
The .Xdefaults file allows you to customise your xterm and other windows. If such
a file exists, it overwrites whatever options are set in the actual command line. Here
is an example:
XTerm*Background:
linen
XTerm*Foreground:
black
XTerm*font:
9x15
XTerm*saveLines:
1000
XTerm*HiForeColor: white
XTerm*HiBackColor: #c06077
XTerm*geometry:
+50+100
Although the default xterm might have quite different settings, it will be created
with the settings specified above. Most of the terms are self explanatory, but full
details may be obtained by consulting the xterm man page.

1.6
1.6.1

Networking and the Internet on UNIX machines


Introduction

When computers first became available, the typical institute consisted of one single
large, expensive machine which catered for many users, perhaps hundreds of users
at a university campus. This single machine is usually called a mainframe. Each
user was connected to the mainframe by means of a serial, low speed, line and a

34

CHAPTER 1. INTRODUCTION TO UNIX

terminal (which simply displayed the information in text mode). No connection


existed between the mainframe and any other computer.
This is quite adequate for many purposes and was the only practical solution at a
time when computers were very expensive items. By the mid 1970s, experiments
were made in connecting computers to each other. The benefit of having computers
talking to each other is that instead of duplicating valuable and expensive resources
on each machine, it makes it possible for these resources to be made available on
all machines which are on the network. The connections that were made in those
early days involved computers situated in close proximity to each other (in the same
building). The connections were accomplished by connecting each computer with
coaxial cable. This allows much higher data transfer rates than the serial lines which
connect terminals to computers.
Local area networks (LANS) are those networks usually confined to a small geographic area, such as a single building or a college campus. LANs are not necessarily
simple in design, however, as they may link many hundreds of systems and service
many thousands of users. The development of various standards for networking protocols and media has made possible the proliferation of LANs worldwide for business
and educational applications.

1.6.2

Protocols

Network protocols are standards that allow computers to communicate. A typical


protocol defines how computers should identify one another on a network, the form
that the data should take in transit, and how this information should be processed
once it reaches its final destination. Protocols also define procedures for handling
lost or damaged transmissions or packets. IPX, TCP/IP, DECnet, AppleTalk and
LAT are examples of network protocols. At SAAO we used DECnet and LAT, but
these have now been abandoned and only TCP/IP is used. TCP/IP is also the
protocol used by the Internet.
Although each network protocol is different, they all use the physical cabling in
the same manner. This common method of accessing the physical network allows
multiple protocols to peacefully coexist, and allows the builder of a network to use
common hardware for a variety of protocols. This concept is known as protocol
independence, meaning that the physical network doesnt need to concern itself
with the protocols being carried.

1.6.3

Ethernet

Ethernet is the most popular LAN technology in use today. Other LAN types
include Token Ring, Fiber Distributed Data Interface (FDDI), and LocalTalk. Each
has its own advantages and disadvantages. Ethernet strikes a good balance between
speed, price and ease of installation. These strong points, combined with wide

1.6. NETWORKING AND THE INTERNET ON UNIX MACHINES

35

acceptance into the computer marketplace and the ability to support virtually all
popular network protocols, makes Ethernet the perfect networking technology for
most computer users today.
An important part of designing and installing an Ethernet is selecting the appropriate Ethernet medium for the environment at hand. There are four major types
of media in use today: ThickWire, thin coax, unshielded twisted pair and fiber optic. Each type has its strong and weak points. Careful selection of the appropriate
Ethernet medium can avoid recabling costs as the network grows.
Ethernet media are used in two general configurations or topologies: bus and
star. These two topologies define how nodes are connected to one another. A
node is an active device connected to the network, such as a computer or a piece of
networking equipment, for example, a repeater (hub), a bridge or a router.
A bus topology consists of nodes strung together in series with each node connected
to a long cable or bus. Many nodes can tap into the bus and begin communication
with all other nodes on that cable segment. A break anywhere in the cable will
usually cause the entire segment to be inoperable until the break is repaired. The
diagram below shows four computers connected together in a bus topology. A cable
break between 1 and 2, for example, renders the whole system unworkable until it
has been repaired.

Star media links exactly two nodes together. The primary advantage of this type
of network is reliability. If a point to point segment has a break, it will only affect
the two nodes on that link. Other nodes on the network continue to operate as if
that segment were nonexistent. The diagram below shows computer 1 connected to
a hub 2 which in turn links three other computers 3, 4, 5 in a star topology. A
break between the hub and 3 will not affect the connection between the remaining
computers, 1, 4, 5.

3
1

4
5

36

1.6.4

CHAPTER 1. INTRODUCTION TO UNIX

Types of cabling

ThickWire, or 10BASE-5 Ethernet, is generally used to create large backbones.


A network backbone joins many smaller network segments into one large LAN.
ThickWire makes an excellent backbone because it can support many nodes in a
bus topology and the segment can be quite long. It can be run from workgroup
to workgroup where smaller departmental networks can then be attached to the
backbone. A ThickWire segment can be up to 500-m long and have as many as 100
nodes attached. ThickWire is a thick, hefty, coaxial cable, and can be expensive
and difficult to work with. A thick coaxial cable is used because of its immunity
to common levels of electrical noise, helping to ensure the integrity of the network
signals.
Thin coax, or 10BASE-2 Ethernet, offers many of the advantages of ThickWires
bus topology with lower cost and easier installation. Thin coax coaxial cable is
considerably thinner and more flexible than ThickWire, but it can only support
30 nodes, each at least 0.5-m apart. Each segment must not be longer than 185
m. Subject to these restrictions, thin coax still can be used to create backbones,
albeit with fewer nodes. A thin coax segment is actually composed of many lengths
of cables, each with a BNC type connector on both ends. Each cable length is
connected to the next with a T connector wherever a node is needed. Nodes can
be connected or disconnected at the T connectors as the need arises with no ill
effects on the rest of the network. Thin coaxs low cost, reconfigurability, and bus
topology make it an attractive medium for small networks, for building departmental
networks to connect to backbones and for wiring a number of nodes together in the
same room, such as a computer lab.
Unshielded twisted pair, or UTP, cable offers many advantages over the ThickWire
and thin coax media. Because ThickWire and thin coax are coaxial cables, they
are relatively expensive and require some care during installation. UTP is similar
to, if not the same as, the telephone cable. A UTP or 10BASE-T Ethernet, uses a
star topology. Generally a computer is located at one end of the segment, and the
other end is terminated in a central location with a repeater or hub. UTP segments
are limited to 100 meters, but UTPs point-to-point nature allows the rest of the
network to function correctly if a break occurs in a particular segment.
Fiber Optic, or 10BASE-FL Ethernet, segments are similar to twisted pair. Fiber
optic cable is more expensive, but it is invaluable for situations where electronic
emissions and environmental hazards are a concern. The most common situation
where these conditions threaten a network is in LAN connections between buildings.
Lightning strikes can wreak havoc and easily destroy networking equipment. Fiber
optic cables effectively insulate networking equipment from these conditions since
they do not conduct electricity. Fiber optic cable is used between domes at Sutherland and to connect the electronics workshop and the computer room in Cape Town.
The Ethernet standard allows for fiber optic cable segments up to 2km long.

1.6. NETWORKING AND THE INTERNET ON UNIX MACHINES

1.6.5

37

TCP/IP Internet addresses

Information is sent by one computer to another in packets. Each packet has the
address of the machine for which it is destined. Only that machine gets the packet.
This is the same as sending a letter with the address written on the envelope. A
packet is of fixed size, so the information being sent probably consists of a large
number of packets. These packets may or may not arrive in the proper sequence.
The machine which is receiving the packets has the responsibility of assembling them
in the correct order.
An IP (Internet Protocol) address uniquely identifies a node or host connection to
an IP network. System administrators or network designers assign IP addresses to
nodes. IP addresses are configured by software; they are not hardware specific. An
IP address is a 32 bit binary number usually represented as 4 fields each representing
8 bit numbers in the range 0 to 255 (sometimes called octets) separated by decimal
points.
An IP address consists of four decimal numbers (called octets) separated by dots.
For example, the IP address of hughm.cs.unp.ac.za is 143.128.82.130. Each of
the four numbers can be in the range 0 255. The IP address consists of two parts:
one part identifies the domain and the other the node. All machines on the same
LAN have the same domain address. For example, the domain address for computer
science staff is 143.128.82.nnn. When a new computer is added to our LAN, a new
node address nnn must be allocated. That means we can have a maximum of 256
computers or network devices attached to the computer science staff LAN.
Obviously, this is not sufficient for the whole of CompSci on the PmB campus.
To cater for CompSci we have been allocated a domain class which in this case is
143.128.nnn.nnn. This allows CompSci to have a maximum of 256 256 = 65536
computers in their network. The class of the address determines which part belongs
to the domain address and which part belongs to the node address. Classes can be
distinguished by the first number of the IP address. If that number is between:

1 and 126 it is a Class A address.


128 and 191 it is a Class B address.
192 and 223 it is a Class C address.
224 and 239 it is a Class D address.
240 and 255 it is a Class E address.
127 is reserved for loopback and is used for internal testing on the local machine.
The following list shows which part belongs to the domain (D) and which part
belongs to the node (n).

38

CHAPTER 1. INTRODUCTION TO UNIX

Class A DDD.nnn.nnn.nnn
Class B DDD.DDD.nnn.nnn
Class C DDD.DDD.DDD.nnn
150.215.17.9 is a Class B address so its domain is defined by the first two octets
and its node is defined by the last 2 octets. Class D addresses are reserved for
multicasting and Class E addresses are reserved for future use so they should not be
used.
It is obvious that Class A networks can accommodate a huge number of nodes, Class
B networks a smaller number. Our Class C network can accommodate a maximim
of 256 nodes (because there are 8 bits in the node and 28 = 256). Class A and
Class B networks might be assigned to large institutes like Universities. There is
a central registry in the USA which assigns domain addresses. Of course, no two
domain addresses can be the same. Each machine in a domain must be assigned
a unique node number. This is the responsibility of the network manager. A list
of all the addresses in use at compsci in PmB can be found on the name server
eagle.und.ac.za
It is not easy to remember these numerical addresses, therefore names can been
assigned to each address. For example, our domain name is cs.unp.ac.za: the
ac stands for academic and za is the country code. The domain name must be
registered by a central registry in the USA. In the United States, the domain names
do not contain the country code and have the following suffixes:
com - COMmercial
edu - EDUcation
gov - GOVernment
net - NETwork
org - ORGanization
Other countries have their own country codes: United Kingdom - uk, France - fr,
etc. The node names can be chosen at will by the network manager. For example,
our machine has been called mars, so instead of specifying its numerical address,
143.128.82.4, it is easier to remember mars.cs.unp.ac.za.
Although people use these easy to remember names, machines actually need the
numerical address. A computer which can translate an alphabetical address to a
numerical address and vice versa is called a nameserver.
1.6.6

Gateways and Routers

A machine which connects a LAN to the Internet is called a gateway. The gateway
machine is responsible for routing packets which are destined for a domain outside

1.6. NETWORKING AND THE INTERNET ON UNIX MACHINES

39

the local domain. These machines are called routers. Our gateway used to be a
PC running a public domain program called PCROUTE. This PC has now been
replaced by a commercial CISCO router doing the same job, but much faster.

1.6.7

Telnet

A basic Internet service is the provision of interactive login to a remote host. Telnet
is both a protocol and a program that enables you to do so. It is the standard
TCP/IP remote login protocol.
You must know the address of the remote host computer before you can initiate a
session with telnet. Once you know the address, you can use telnet. Most remote
hosts require you to have an account to log in (you must have a user id and a
password). However, some remote hosts do not require that users have accounts.
Users can log in with a general user id such as info (or some other word that is
published in guides to the Internet). Passwords are usually not required.
If you are in telnet mode (i.e., the telnet > prompt is on the screen and you want
to return to the UNIX prompt without initiating an interactive session, type quit
and press return.

1.6.8

Anonymous ftp

A great deal of useful information is stored in files at computers throughout the


country and the world. Many of these file are freely available to users of the Internet.
A simple method for transferring such files from a remote computer to a users
computer is anonymous ftp. Anonymous ftp allows a user to transfer files without
having an account at the remote computer (i.e. the user is anonymous.)
To access an anonymous ftp site you must know the address of the site. For example,
ftp.sun.ac.za, is the address of the Stellenbosch University ftp server. To connect
to this server, for example, type:
ftp ftp.sun.ac.za
You will be asked for your username; type anonymous. As a password, type your
e-mail address, e.g. jones@mars.cs.unp.ac.za. Once you have gained access to
the site, the ftp > prompt returns and acknowledges that the system is ready to
use.
Once you have accessed the ftp site, to transfer a file, you may have to change directories to the directory that your file is located in. Many sites store public access
files in a directory called pub. To access this directory you would type cd pub at
the ftp > prompt. It is a good idea to list the contents of the directory before

40

CHAPTER 1. INTRODUCTION TO UNIX

attempting to transfer a file. This is done by ls at the ftp > prompt. When you
have determined that the file you want to retrieve is there, you can transfer it to
your computer using the get command. Before you do this, however, you need to
know whether the file you are transferrin is an ASCII file or a binary file. It is nearly
always safe to transfer in binary mode, and most ftp sites have this as the default
mode. To be on the safe side, you should enforce binary mode transfer. Here is how
you would transfer the file foo:
ftp
200
ftp
200
150
226
137
ftp

> binary
Type set to I.
> get foo
PORT command successful.
Opening BINARY mode conection for foo (137 bytes).
Transfer complete.
bytes received in 2.37 seconds (0.3 Kbytes/s).
>

The commands that can be used within ftp are the same, or similar, to the UNIX
commands which do the same thing. Here is a list of the most commonly used
commands:
ls: lists the contents of the active directory.
cd foo: enables the user to change to directory foo.
cd ..: allows the user to return to the previous directory.
lcd foo: changes to directory foo on the local machine.
binary: chnages to binary mode transfer.
axcii: changes to ASCII mode transfer.
get foo: transfers file foo to the local machine.
mget *: transfers all files to the local machine.
put foo: transfers file foo from the local machine.
mput *: transfers all files in the local machine.
When using mput or mget, you are prompted after each file. To avoid this, you
should start your ftp session with the command:
ftp -i ftp.sun.ac.za
To view a document, foo, while still connected to the ftp site, type:

1.6. NETWORKING AND THE INTERNET ON UNIX MACHINES

41

ftp > get foo | less

1.6.9

The World Wide Web

The World Wide Web is a graphics interface to the internet. Prior to 1993, the
only way of accessing the internet was through telnet and ftp which supports only
text display. By 1993, software was available which allowed pictures to be viewed
on the screen, much like Microsoft Windows or X-windows. The software which
enables this is called a browser. Netscape and Microsofts Internet Explorer are the
two most popular Web browsers. The original broweser, Mosaic has been largely
superceded by these two, but is still used.
The WWW is an electronic web of files connected by hypertext links. Hypertext
links are connections that let you move from one file to another with a keystroke
or a click on your mouse. The actual location is irrelevant - you can be reading
a document from a computer in France one minute, and follow a link to a related
document in New Zealand. There are all kinds of files on the WWW. Text files are
the most common, but you will also find sound files, downloadable graphics files,
programs ... everything!
Information on the WWW is organized on pages. Each page has a particular topic,
and contains hypertext links. You scroll up and down a page, and use hypertext
links to bring you to different places on a page or to a new page altogether. You can
use the back and forward buttons (see arrows at top of Web browser) to move to a
previous or next page. Hypertext links show up as colored, underlined, or highlighted
phrases and words in text. When you click on the link, your Web browser retrieves
the file it is linked to.
To use the WWW under UNIX in PmB, you need to be logged on to mars.cs.unp.ac.za
via an X windows session and then you can run the mozilla browser which comes
with LINUX.
Once you have started your web browser to connect to some website, you need to
know its URL (its uniform resource location or in plain English, its address). The
URL is given as a string of the form:
http://hughm.cs.unp.ac.za/ murrellh/index.html
where hughm.cs.unp.ac.za is the address of the particular site. The document you
wish to look at is stored in directory /home/murrellh/public html and is called
index.html. This form is very often used. Most of the documents have names which
end in .html or .htm because they are written in hypertext markup language. If
the address is given without a document name, then by default it loads a document
called index.html or home.html.

42

CHAPTER 1. INTRODUCTION TO UNIX

Very often, you do not need to know the URL of anything - you simply follow the
hypertext links. But you need a good starting place. One of the best is yahoo which
allows you to search indices grouped by subject matter:
http://www.yahoo.com
It also provides a search engine which allows you to enter a word or phrase describing
the document you wish to see. There are many other search engines available, one
of the most effective being altavista.dec.com.
Hypertext documents are not the only things you can view with your browser.
Instead of the text based ftp session described above, you may prefer the same
thing done on your browser. For example, to start an anonymous ftp session on
ftp.sun.ac.za, you open:
ftp://ftp.sun.ac.za
The advantage is that it enables you just to point and click files, or whole directories,
that you wish to download.

Chapter 2

ANSI C for Programmers on


UNIX Systems
2.1

Credits:

Tim Love
Cambridge University Engineering Department.
Adapted for use in Operating Systems course by: Hugh Murrell
University of KwaZulu-Natal, Computer Science.
This chapter aims to: Introduce C by providing and explaining examples of common programming
tasks.
Enable the reader to learn from available source code by clarifying common
causes of incomprehension.
Coverage is not uniform: pedantry will be selective, aimed at describing aspects of C
which are not present in other languages or are different to what a programmer from
another language might expect. For a full description of C refer to one of the many
books in the bibliography. The first part of the document is an informal introduction
to C. After the first set of exercises a more comprehensive description of some features
is given. After the final set of exercises selected topics are covered. Note that the
exercises and examples form an integral part of the course, containing information
not duplicated elsewhere. The original version of this document is available by ftp
from svr-ftp.eng.cam.ac.uk:misc/.

List of Demo Programs

44

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

Program
Page Description
basics.c
45
basics
strings.c
60
strings
array.c
63
2D arrays
mallocing.c 73
malloc
files.c
74
file i/o
line nums.c 76
filter

2.2. INTRODUCTION

2.2

45

Introduction

Cs popularity has increased as Unix has become more widespread. It is a flexible,


concise and small language, with a mix of low-level assembler-style commands and
high-level commands. Its much used with the X graphics system and increasingly
for numerical analysis. The first de facto standard C was as described in [?] and is
often known as K&R C . The current standard is ANSI C [?] in which the source
contained in this document is written. Check your local documentation to see how
to compile the code. In this documentation cc -Aa will be used.
To those who have programmed before, simple C programs shouldnt be too hard
to read. Suppose you call this program basics.c
#include <stdio.h>
#include <stdlib.h>
int mean(int a,int b)
{
return (a + b)/2;
}
int main()
{
int i, j;
int answer;
/* comments are done like this */
i = 7;
j = 9;
answer = mean(i,j);
printf("The mean of %d and %d is %d\n", i, j, answer);
exit (0);
}
Note that the source is free-format and case matters.
All C programs need a main function where execution begins. In this example some
variables local to main are created and assigned (using = rather than :=. Also
note that ; is a statement terminator rather than a separator as it is in Pascal).
Then a function mean is called that calculates the mean of the arguments given it.
The types of the formal parameters of the function (in this case a and b) should be
compatible with the actual parameters in the call. The initial values of a and b are
copied from the variables mentioned in the call (i and j).
The function mean returns the answer (an integer, hence the int before the function
name), which is printed out using printf. The on-line manual page describes printf
fully. For now, just note that the 1st argument to printf is a string in which is

46

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

system
include files

system libraries

?
?
Source -Preprocessor
- Compiler -Assembler - Object - Loader - Executable
File
File
6
3

users
users object files
include files
Figure 2.1: Compilation Stages

embedded format strings; %d for integers, %f for reals and %s for strings. The
variables that these format strings refer to are added to the argument list of printf.
The \n character causes a carriage return.
C programs stop when
The end of main is reached.
An exit() call is reached.
The program is interrupted in some way.
The program crashes

**

This program can be compiled using cc -Aa -o basics basics.c. The -o


option renames the resulting file basics rather than the default a.out. Run it by
typing basics. A common mistake that beginners make is to call their executable
test. Typing test is likely to run the test facility built into the shell, producing
no input, rather than the users program. This can be circumvented by typing
./test but one might just as well avoid program names that might be names of
unix facilities. If youre using the ksh shell then typing whence program name will
tell you whether theres already a facility with that name.

2.3

Compilation Stages

First the Preprocessor cpp is run. This strips out comments and interprets directives (lines with a # character in the first column). The #include directive
read in the named file, looking for the file in the directory /usr/include. Include
files have a .h suffix by convention, and shouldnt contain executable code, only
definitions and declarations. /usr/include/stdio.h and /usr/include/stdlib.h
should always be included. Other useful include files are /usr/include/limits.h
and /usr/include/math.h which define characteristics of the machine and the
maths implementation. Further preprocessor directives to do with macros, etc, will

2.4. VARIABLES AND LITERALS

47

be introduced later. If you want to see how the code looks after pre-processing, try
typing cc -Aa -E basics.c
After the preprocessor comes the compiler, which after a pass or 2 produces assembler
code. This is assembled to produce an object file, in this case basics.o.
Finally the link-loader produces the executable from the object file and any other
object you mention. The standard C library is automatically consulted. Any other
libraries that your code needs have to be mentioned on the compile line. E.g., ending
the line with -lm links in the maths library, -lX11 links in X graphics functions.
Note that its not sufficient to just have #include <math.h> at the top of the file
if youre doing maths. This just supplies enough information so that the compiler
can do its work correctly. It doesnt tell the link-loader where the maths routines
actually are. You need to have -lm on the command line before an executable can
be produced.
The stages of compilation can be seen if a -v flag is given to the compiler.

2.4

Variables and Literals

Variable names cant begin with a digit. Nor can they contain operators (like .
or -). They have to be declared before being used. The available scalar types are
char, short, int, long, float, double and long double. chars and the various
lengths of integers can be signed (the default) or unsigned.
Often you dont explicitly have to convert types. If you operate on variables of
different types, type conversion takes place auomatically. E.g. if you add an int
and a float then the int will be converted to a float and the result will be a
float.
unsigned int i;
float f = 3.14;
i = f;
will automatically set i to 3, truncating the real value. To explicitly convert types
use casting; E.g.
i = (unsigned int) f;
Most conversions preserve the numerical value but occasionally this is not possible
and overflow or loss of precision results. C wont warn you if this happens.
**
The length of an integer isnt the same on all machines. sizeof (int) will return
the number of bytes used by an integer.

48

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

The scope of a variable is the block (the function, or { } pair) its declared in, or
the remainder of the file its declared in. Variables declared outside of a block and
functions can be accessed from other files unless theyre declared to be static.
Variables declared in functions can preserve their value between calls if they are
defined as static, otherwise theyre automatic, getting recreated for each call of
the function.
Character values can be written in various ways :- E.g. on machines that use ASCII
A

\101

\x41

all represent 65; the first using the ASCII A character, the second using octal and
the third hexadecimal. The newline character is \n.
Integer and real values can be variously expressed:15L
015
0xF7
15.3e3F
15.3e3
15.3e3L

2.5

long integer 15
octal integer 15
Hex (base 16) number F7
15.3 103 , a float
15.3 103 , a double
15.3 103 , a long double

Aggregates

Variables of the same type can be put into arrays.


char letters[50];

**

defines an array of 50 characters, letter[0] being the 1st and letter[49] being
the last character. C has no subscript checking; if you go off the end of an array C
wont warn you.
Multidimensional arrays can be defined too. E.g.
char values[50][30][10];
defines a 3D array. Note that you cant access an element using values[3,6,1];
you have to type values[3][6][1].
Variables of different types can be grouped into a structure (like a record in Pascal).
struct person {

2.5. AGGREGATES

49

int age;
int height;
char surname[20];
} fred, jane;
defines 2 structures of type person each of 3 fields. Fields are accessed using the .
operator. For example, fred.age is an integer which can be used in assignments
just as a simple variable can.
typedef creates a new type. E.g.
typedef struct{
int age;
int height;
char surname[20];
} person;
create a type called person and
typedef struct{
double real;
double imaginary;
} complex;
creates a complex type. Note that typedef creates new variable types but doesnt
create any new variables. These are created just as variables of the predefined types
are:person fred, jane;
Structure may be assigned, passed to functions and returned, but they cannot compared, so
person fred, jane;
...
fred = jane;
is possible (the fields of jane being copied into fred) but you cant then go on to
do
if (fred == jane)
fprint("The copying worked ok\n");
you have to compare field by field.

50

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

As you see, new variable types are easily produced. What you cant do (but can
in C++ and Algol68) is extend the meaning of an existing operator (overload it)
so that it works with the new variable type: you have to write a specific function
instead.
A union is like a struct except that the fields occupy the same memory location
with enough memory allocated to hold the largest item. The programmer has to
keep a note of what the union is being used for. What would the following code
print out?
...
union

person {
int age;
int height;
char surname[20];
} fred;
fred.age = 23;
fred.height = 163;
printf("Fred is %d years old\n", fred.age);
...
If fred started at memory location 2000, then fred.age, fred.height and fred.surname
would all begin at memory location 2000 too, whereas in a struct the fields wouldnt
overlap. So setting fred.height to 163 overwrites fred.age (and the 1st 4 characters of fred.surname) making fred 163 years old.

2.6

Constructions

C has the following loop and selection constructs:-

Selection
...
if (i==3) /* checking for equality; != tests for inequality */
/* no braces needed for a single statement */
j=4;
else{
/*the braces are necessary if the
clause has more than one statement
*/
j=5;
k=6;
}

2.6. CONSTRUCTIONS

51

...
...
/* switch is like the case statement in pascal.
The values that the switching variable is compared with
have to be constants, or default.
*/
switch(i){
case 1: printf("i is one\n");
break; /* if break wasnt here, this case will
fall through into the next.
*/
case 2: printf("i is two\n");
break;
default: printf("i is neither one nor two\n");
break;
}
...

Loops
...
while(i<30){
something();
...
}

/* test at top of loop */

...
do {
something();
} while (i<30); /* test at bottom of loop */
...
The for construction in C is very general. In its most common form its much like
for in other languages.
...
for(i=0; i<5; i=i+1){
something();
}
...
The general form of for is

52

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

for ([expression1]; [expression2]; [expression3])


something();
where all the expressions are optional. The default value for expression2 is 1
(true). Essentially, the for loop is a while loop. The above for loop is equivalent
to
...
expression1; /* initialisation */
while (expression2){ /* condition */
something();
expression3;
/* code done each iteration */
};
...
E.g. the 2 fragments below are equivalent. i is set to 3, the loop is run once for
i=3 and once for i=4, then iteration finishes when i=5.
for (i = 3; i < 5; i=i+1)
total = total + i;

i = 3;
while(i < 5){
total = total + i;
i=i+1;
}

Within any of the above loop constructions, continue stops the current iteration
and goes to the next and break stops the iterations altogether. E.g. in the following
fragment 0 and 2 will be printed out.
...
i=0;
while (i<5){
if (i==1){
i = i+1;
continue;
}
if (i==3)
break;
printf("i = %d\n", i);
i=i+1;
}
...
If you want a loop which only ends when break is done, you can use while(1)
(because 1 being non-zero, counts as being true) or for(;;).
The { } symbols are used to compound statements. You can declare variables at
the start of any compound statement. For instance, if youre worried about the
scope of an index variable in a for loop, you could do the following.

2.7. EXERCISES 1

53

{int i;
for (i=1;i<5;i++)
printf("i is %d\n",i);
}

2.7

Exercises 1

(Sample solutions are on page 112)


1. pascal has a function called odd, that given an integer returns 1 if the number
is odd, 0 otherwise. Write an odd function for C and write a main routine to
test it. (hint You can use the fact that in C, if i is an integer then (i/2)*2
equals i only if i is even).
2. Write a routine called binary that when supplied with a decimal number, prints
out that number in binary, so binary(10) would print out 1010
void binary(unsigned int number){
/* print decimal number in binary */
...
}
Then write a main routine to test it. Dont worry about leading zeroes too
much at the moment. Note that binary returns a void, i.e. nothing.
3. Write a routine called base that when supplied with a decimal number and a
base, prints out that number to the required base, so base(10,3) would print
out 101
void base(unsigned int number, unsigned int base){
/* print decimal number to the given base */
...
}
Then write a main routine to test it.
4. Print a table of all the primes less than 1000. Use any method you want. The
sieve method is described here:- aim to create an array number such that if
numbers[i] == PRIME then i is a prime number. First mark them all as being
prime. Then repeatedly pick the smallest prime you havent dealt with and
mark all its multiples as being non prime. Print out the primes at the end.
Heres a skeleton:#include <stdio.h>
#include <stdlib.h>
#define PRIME 1
/* Create aliases for 0 and 1 */
#define NONPRIME 0

54

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

int numbers[1000];
void mark_multiples(int num){
/* TODO: Set all elements which represent multiples of num to NONPRIME. */
}
int get_next_prime(int num){
/* find the next prime number after num */
int answer;
answer = num+1;
while (numbers[answer] == NONPRIME){
answer= answer +1;
if (answer == 1000)
break;
}
return answer;
}
main(){
int i;
int next_prime;
/* TODO: Set all the elements to PRIME. Remember, the 1st element is
numbers[0] and the last is numbers[999] */
/* TODO: 0 and 1 arent prime, so set numbers[0] and numbers[1]
to NONPRIME */
next_prime = 2;
do{
mark_multiples(next_prime);
next_prime = get_next_prime(next_prime);
} while(next_prime < 1000);
/* TODO: Print out the indices of elements which are still set to PRIME */
exit(0);
}

The TODO lines describe what code you need to add in.
You can speed up this program considerably by replacing 1000 where appropriate by something smaller. See page 93 for details.

2.8. CONTRACTIONS

2.8

55

Contractions

C veterans use abbreviated forms for expressions. Some are natural and widely
adopted, others merely lead to obscurity even if they produce faster code (and
often they dont) they waste future programmers time.
i++ is equivalent to i=i+1. This (and the i-- decrementing operator) is a
common contraction. The operation can be done after the variable is used, or
(by using --i, ++i) before, so
...
i = 4;
printf("i = %d\n", i++)
and
...
i = 4;
printf("i = %d\n", ++i)
will both leave i as 5, but in the 1st fragment 4 will be printed out while in
the 2nd 5 will.
i+=6 is equivalent to i=i+6. This style of contraction isnt so common, but can
be used with most of the binary operators.
Assignment statements have a value the final value of the left-hand-side so
j = (i=3+4) will set i then j to 7, and i = j = k = 0 will set k, then j, then
i to zero. This feature should be used with caution.
The , operator is used between 2 expressions if the value of the 1st expression
can be ignored. Its a way to put 2 or more statements where normally only
one would go. E.g.
for(init(3),i=0,j+0; i<100; i++,j++)
This feature is often over-used too.
Expressions with comparison operators return 1 if the comparison is true, 0 if
false, so while(i!=0) and while(i) are equivalent.
The if (cond) exp1; else exp2; construction can be abbreviated using
(cond)?exp1:exp2. The following fragments are equivalent.
...
if (a==6)
j=7;
else
j=5;
...
(a==6)?j=7:j=5;
...
This notation should be used with discretion.

56

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

2.9

Functions

C has no procedures, only functions. Their definitions cant be nested but all except
main can be called recursively. In ANSI C the form of a function definition is
<function type> <function name> ( <formal argument list> )
{
<local variables>
<body>
}
E.g.
int mean(int x, int y)
{
int tmp;
tmp = (x + y)/2;
return tmp;
}
In K&R C the same function would be written as
int
int
int
{
int

mean(x,y)
x;
y;
tmp;

tmp = (x + y)/2;
return tmp;
}
Note that the formal argument declarations are differently placed. This are the most
visible difference between ANSI C and K&R C . Programs exist to convert between
the 2 forms of formal argument declaration (see the comp.lang.c newsgroup for
details).
The default function type is extern int and the default type for the formal arguments is int but depending on these defaults is asking for trouble; they should be
explicitly declared.
Functions end when
execution reaches the closing } of the function. If the function is supposed to
return something, the return value will be undefined.

2.10. POINTERS

57

a return statement is reached, returning control to the calling function.


an exit statement is reached, ending execution of the whole program.
Just as return can return a value to the calling routine, so exit returns a value
to the Unix environment. By convention, returning a zero means that the program
has run successfully. Better still, return EXIT_SUCCESS or EXIT_FAILURE; theyre
defined in stdlib.h.
All parameters in C are passed by value. To perform the equivalent of Pascals
pass by reference you need to know about pointers.

2.10

Pointers

Even if you dont use pointers yourself, the code youll learn from will have them.
Suppose i is an integer. To find the address of i the & operator is used (&i).
Setting a pointer to this value lets you refer indirectly to the variable i. If you have
the address of a pointer variable and want to find the variables value, then the
dereferencing operator * is used.
...
int i;
/* The next statement declares i_ptr to be a pointer at
an integer. The declaration says that if i_ptr is
dereferenced, one gets an int.
*/
int *i_ptr;
i_ptr = &i; /* initialise i_ptr to point to i */
/* The following 2 lines each set i to 5 */
i = 5;
*iptr = 5; /* i.e. set to 5 the int that iptr points to */
Pointers arent just memory addresses; they have types. A pointer-to-an-int is int*
and is of a different type to a pointer-to-a-char (which is char*). The difference
matters especially when the pointer is being incremented; the value of the pointer
is increased by the size of the object it points to. So if we added
iptr=iptr+1;
in the above example, then iptr wouldnt be incremented by 1 (which would make
it point somewhere in the middle of i) but by the length of an int, so that it would
point to the memory location just beyond i. This is useful if i is part of an array.
In the following fragment, the pointer steps through an array.

58

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

...
int numbers[10];
int *iptr;
int i;
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;
iptr = &numbers[0]; /* Point iptr to the first element in numbers[] */
/* now increment iptr to point to successive elements */
for (i=0; i<3; i++){
printf("*iptr is %d\n", *iptr);
iptr= iptr+1;
}
...
Pointers are especially useful when functions operate on structures. Using a pointer
avoids copies of potentially big structures being made.
typedef struct {
int age;
int height;
char surname[20];
} person;
person fred, jane;
int sum_of_ages(person *person1, person *person2){
int sum; /* a variable local to this function. */
/* Dereference the pointers, then use the . operator to get the
fields */
sum = (*person1).age + (*person2).age;
return sum;
}
Operations like (*person1).age are so common that theres a special, more natural
notation for it: person1->age.
To further illustrate the use of pointers lets suppose that in the first example on
page 45 we wanted to pass to the function mean the variable where we wanted the
answer stored. Since we no longer need mean to return a value, we can make it
return void. Lets first try:#include <stdio.h>

2.10. POINTERS

59

#include <stdlib.h>
void mean(int a, int b, int return_val )
{
return_val = (a + b)/2;
printf("return_val in mean in %d\n",return_val);
}
main()
{
int i, j;
int answer;
i = 7;
j = 9;
mean(i,j, answer);
printf("The mean of %d and %d is %d\n", i, j, answer);
}
This wont work. Although return val is set to the right value in mean, answer
isnt. Remember, return val and answer are separate variables. The value of
answer is copied into return val when mean is called. The mean function doesnt
know where answer is stored, so it cant change it. A pointer to answer has to be
given to mean().
#include <stdio.h>
#include <stdlib.h>
/* Note the form of the ptr_to_answer declaration below. It
says that if you dereference ptr_to_answer you get an
int. i.e. ptr_to_answer is a pointer to an int.
*/
void mean(int a,int b, int *ptr_to_answer)
{
*ptr_to_answer = (a + b)/2;
}
main()
{
int i, j;
int answer;
i = 7;
j = 9;
mean(i,j, &answer); /* Note that now were passing a pointer
* to answer
*/

60

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

printf("The mean of %d and %d is %d\n", i, j, answer);


}
Theres a special value for null pointers (NULL) and a special type for generic pointers
(void*). In K&R C , casting a pointer from one type to another didnt change its
value. In ANSI C however, alignment is taken into account. If a long can only
begin at an even memory location, then a pointer of type char* pointing to an odd
location will have its value changed if cast into a long*.

2.11

Strings

In C a string is just an array of characters. The end of the string is denoted by a zero
byte. The various string manipulation functions are described in the online manual
page called string, and declared in the string.h include file. The following piece
of code illustrates their use and highlights some problems
/* strings.c */
#include <stdio.h>
#include <string.h>
char str1[10]; /* This reserves space for 10 characters */
char str2[10];
char str3[]= "initial text"; /* str3 is set to the right size for you
* and automatically terminated with a 0
* byte. You can only initialise
* strings this way when defining.
*/
char *c_ptr;
/* declares a pointer, but doesnt initialise it. */
unsigned int len;
main()
{
/* copy "hello" into str1. If str1 isnt big enough, hard luck */
strcpy(str1,"hello");
/* if you looked at memory location str1 youd see these byte
values: h,e,l,l,o,\0
*/
/* concatenate " sir" onto str1. If str1 is too small, hard luck */
strcat(str1," sir");
/* values at str1 : h,e,l,l,o, ,s,i,r,\0
*/

2.11. STRINGS

61

len = strlen(str1); /* find the number of characters */


printf("Length of <%s> is %d characters\n", str1, len);
if(strcmp(str1, str3))
printf("<%s> and <%s> are different\n", str1, str3);
else
printf("<%s> and <%s> are the same\n", str1, str3);
if (strstr(str1, "boy") == (char*) NULL)
printf("The string <boy> isnt in <%s>\n", str1);
else
printf("The string <boy> is in <%s>\n", str1);
/* find the first o in str1 */
c_ptr = strchr(str1,o);
if (c_ptr == (char*) NULL)
printf("There is no o in <%s>\n", c_ptr);
else{
printf("<%s> is from the first o in <%s> to the end.\n",
c_ptr, str1);
/* Now copy this part of str1 into str2 */
strcpy(str2, c_ptr);
}
}
Usually str1 would be used instead of &str1[0] to refer to the address of the
first element of the character array, since C defines the value of an array name to
be the location of the first element. In fact, once youve set c ptr to str, the 2
variables behave similarly in most circumstances.
There is not really any difference in the behaviour of the array subscripting
operator [] as it applies to arrays and pointers. The expressions str[i] and
c_ptr[i] are both processed internally using pointers. For instance, str[i] is
equivalent to *((str)+(i)).
Array and pointer declarations are interchangeable as function formal parameters. Since arrays decay immediately into pointers, an array is never actually
passed to a function. Therefore, any parameter declarations which look like
arrays, e.g.
int f(char a[])
{
...
}
are treated by the compiler as if they were pointers, so char a[] could be
replaced by char* a. This conversion holds only within function formal parameter declarations, nowhere else. If this conversion bothers you, avoid it.

62

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

Because the distinction between pointers and arrays often doesnt seem to matter,
programmers get surprised when it does. Arrays are not pointers. The array declaration char str1[10]; requests that space for ten characters be set aside. The
pointer declaration char *c_ptr; on the other hand, requests a place which holds
a pointer. The pointer is to be known by the name c_ptr, and can point to any
char (or contiguous array of chars) anywhere. str1 cant be changed: its where
the array begins and where it will always stay.
You cant pass whole arrays to functions, only pointers to them. To declare such
pointers correctly you need to be aware of the different ways that multi-dimensional
arrays can be stored in memory. Suppose you created a 2D array of characters as
follows:char fruits[3][10] = {"apple", "banana", "orange"};
This creates space for 3 strings each 10 bytes long. Lets say that fruits gets
stored at memory location 6000. Then this will be the layout in memory:
6000 a
6010 b
6020 o

p
a
r

p
n
a

l
a
n

e \0 . .
n a \0 .
g e \0 .

.
.
.

.
.
.

If you wanted to write a function that printed these strings out so you could do
list names(fruits), the following routine will work
void list_names(char names[][10] ){
int i;
for (i=0; i<3; i++){
printf("%s\n", names[i]);
}
}
The routine has to be told the size of the things that names points to, otherwise it
wont be able to calculate names[i] correctly. So the 10 needs to be provided in
the declaration. It doesnt care about how many things are in the array, so the first
pair of brackets might just as well be empty. An equivalent declaration is
void list_names(char (*names)[10])
saying that names is a pointer to an array each of whose elements is 10 chars.
The above method wastes a lot of space if the strings differ greatly in length. An
alternative way to initialise is as follows:char *veg[] =

{"artichoke", "beetroot", "carrot"};

2.11. STRINGS

63

Here veg is set up as an array of pointer-to-chars. The layout in memory is


different too. A possible layout is:Address
6000
6004
6008
...
9000
9600
9700

Value
9000
9600
9700
a
b
c

r
e
a

t
e
r

i
t
r

c
r
o

h
o
t

o k
o t
\0

e \0
\0

Note that veg is the start of an array of pointers. The actual characters are
stored elsewhere. If we wanted a function that would print out these strings,
then the list names() routine above wouldnt do, since this time the argument
names wouldnt be pointing to things that are 10 bytes long, but 4 (the size of a
pointer-to-char). The declaration needs to say that names points to a character
pointer.
void list_names(char **names){
int i;
for (i=0; i<3; i++){
printf("%s\n", names[i]);
}
}
The following declaration would also work:void list_names(char *names[]){
Using cdecl (see page 84) will help clarify the above declarations.
The program below shows the 2 types of array in action. The functions to print the
names out are like the above except that
The arrays are endstopped so that the functions neednt know beforehand how
many elements are in the arrays.
The for loop uses some common contractions.

#include <stdio.h>
#include <stdlib.h>
void list_names(char (*names)[10] ){

64

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

for (; names[0][0]; names++){


printf("%s\n", *names);
}
}
void list_names2(char *names[] ){
for (; *names!=NULL; names++){
printf("%s\n",*names);
}
}
int main(int argc, char *argv[]){
char fruits[4][10] = {"apple", "banana", "orange", ""};
char *veg[] = {"artichoke", "beetroot", "carrot", (char*) NULL};
list_names(fruits);
list_names2(veg);
exit(0);
}

2.12

Exercises 2

To answer these exercises youll need to be able to get keyboard input from the user.
For the moment, use the following fragment to get a string from the user. str needs
to point to the start of an existing character array.
char * get_string(char str[])
{
printf("Input a string\n");
return gets(str);
}
Sample answers are on page 115 unless otherwise stated.
1. The following code fragment uses many of the contractions mentioned earlier.
It comes from ghostscript. Re-write it to make it more legible.
int ccase = (skew >= 0 ? copy_right :
((bptr += chunk_bytes), copy_left))
+ function;
2. Write a program that invites the user to type in a string and prints the string
out backwards (The answers in section 2.18).
3. Write your own version of strchr (see the manual page for a description).

2.12. EXERCISES 2

65

4. Write a program which reads in a string like 20C or 15F and outputs the
temperature to the nearest degree using the other scale. The easiest way to
parse the input string is to use sscanf to scan the input string for a number
and a character. It will return the number of items successfully read in.
...
int degrees;
char scale;
int return_value;
...
return_value = sscanf(str,"%d%c",&degrees, &scale);
...
5. The following program will be developed later in the handout. Suppose you
have a situation where you need to process a stream of things (they might
be scanned character images, chess positions or as in this example, strings),
some of which might be duplicates. The processing might be CPU-intensive,
so youd rather use the previously calculated values than re-process duplicate
entries. Whats needed is a look-up table.
Each entry in the look-up table needs to have a record of the original string
and the result of the processing. A structure of type Entry
typedef struct {
char str[64];
int value;
} Entry;
will do for now. For our purposes it doesnt matter much what the processing routine is. Lets use the following, multiplying all the characters values
together.
int process(char *str){
int val = 1;
while (*str){
val = val * (*str);
str++;
}
return val;
}
To get strings into the program you can use the get string function. Now
write a program that reads strings from the keyboard. If the string is new,
then its processed, otherwise its value is looked up in a table. The program
should stop when end is typed. Heres a skeleton program to get you started.
/* hash1.c */
/* TODO include standard include files */
/* The following 2 lines use the preprocessor to create aliases.

66

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

Note that these lines DONT end with a ;


*/
#define TABLE_SIZE 50
#define MAX_STR_LEN 64
typedef struct {
char str[MAX_STR_LEN];
int value;
} Entry;
char str[MAX_STR_LEN];
/* TODO Create an array of TABLE_SIZE elements of type Entry */
int process(char *str){
int val = 1;
while (*str){
val = val * (*str);
str++;
}
return val;
}
char * get_string(char str[])
{
printf("Input a string\n");
return gets(str);
}
main(){
int num_of_entries = 0;
/* TODO Use get_string repeatedly. For each string:If the string says end, then exit.
If the str is already in the table,
print the associated value
else
calculate the value, add a new
entry to the table, then print the value.
*/
}
6. The method used above can be improved upon. Firstly, it will go wrong if there
are too many strings. By choosing an arbitrarily large value for TABLE SIZE
you could overcome this problem, but the method of searching the table to see
whether an entry is new becomes very inefficient as the table grows.
A technique called hashing copes with this. First we need a hash function which
given a string produces a number in the range 0..TABLE SIZE. The following
function just adds up the value of the characters in the string and gets the

2.12. EXERCISES 2

67

remainder after dividing by TABLE SIZE.


int hashfn(char *str){
int total = 0;
int i;
while (i = *str++)
total += i;
return total % TABLE_SIZE;
}
Now, whenever a string is to be processed, its hash value is calculated and that
is used as an index into the table, which is much quicker than searching. If
that entry is empty then the string is new and has to be processed. If the entry
is occupied, then the associated value can be accessed. This method is flawed,
but well deal with that problem later.
/* hash2.c */
/* TODO include standard include files */
#define TABLE_SIZE 50
#define MAX_STR_LEN 64
#define EMPTY -1
typedef struct {
char str[MAX_STR_LEN];
int value;
} Entry;
char str[MAX_STR_LEN];
/* TODO Create an array of TABLE_SIZE elements of type Entry */
int process(char *str){ /* Same as hash1.c */
int val = 1;
while (*str){
val = val * (*str);
str++;
}
return val;
}
char * get_string(char str[]) /* Same as hash1.c */
{
printf("Input a string\n");
return gets(str);
}
int hashfn(char *str){
int total = 0;
int i;
while (i = *str++)

68

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

total += i;
return total % TABLE_SIZE;
}
void set_table_values(void){
/* TODO set all the value entries in the table to EMPTY
(Well assume that the process() routine doesnt
produce -1)
*/
}
int find_entry(char *str, int bucket){
/* TODO
if the entry in postion bucket is EMPTY then fill
the entrys fields in and return the strings
processed value, else return the value of the entry.
*/
}
main(){
int bucket;
int val;
set_table_values();
/* TODO Use get_a_string repeatedly. For each string:use the hash function to find the strings entry
in the table, then do the following
*/
bucket = hashfn(str)
val = find_entry(str,bucket);
printf("Value of <%s> is %d\n",str, val);
}
7. The problem with this method is that the hash function may produce the same
value for different strings (for example, act and cat will both map into the
same entry). A simple way of coping with such collisions is the following:If a table entry is occupied, check the string there to see if its the one being
searched for. If it is, then return the associated value. If it isnt the right string,
then look at subsequent entries until either
an entry for the string is found.
an empty entry is found.
Its been shown that all entries are full up.
Youll have to add just a few lines to the find entry routine of the previous
exercise. Remember to cycle round when the bottom of the table is reached.

2.13. KEYWORDS, OPERATORS AND DECLARATIONS

69

A more robust method (and the answer to the exercise here) is in the next set
of exercises (see section 2.18).

2.13

Keywords, Operators and Declarations

2.13.1

Keywords

You cant use the following reserved words for variable names, etc.
auto
continue
enum
if
short
switch
volatile

break
default
extern
int
signed
typedef
while

case
do
float
long
sizeof
union

char
double
for
register
static
unsigned

const
else
goto
return
struct
void

A few of these havent yet been described.


auto :- This is the default Storage Class for variables so its not explicitly used.
static, which youve already met, is an alternative class.
const :- If a variable isnt meant to change you can define it as const. E.g., If you
create an integer using const int i = 6; then a later i = 7; will be illegal.
However, if you create a pointer to i and use this to change the value, youll
probably get away with it. The main purpose of const is to help optimisers.
volatile is the opposite of const.
enum :- C has enumerated types, like pascal. E.g.
enum color {Red, Green, Blue};
Theyre not as useful as in pascal because C doesnt check if you set an enumerated type to a valid value.
register :- You can suggest to the compiler that a variable should be kept in a
register for faster access. E.g. register int i might help if i is a muchused indexing variable. An optimising compiler should use registers efficienty
anyway. Note that you cant use the & operator on a register variable.
2.13.2

Operators

At last, here is a table of operators and precedence.


The lines of the table are in order of precedence, so a * b + 6 is interpreted as
(a * b) + 6. When in doubt put brackets in!

70

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

The Associativity column shows how the operators group. E.g.< groups left to
right, meaning that a < b < c is equivalent to (a < b) < c rather than a < (b < c).
Both are pretty useless expressions.
Associativity
left to right
right to left
right to left
left to right
left to right
left to right
left to right
left to right
left to right
left to right
left to right
left to right
right to left
right to left
left to right

Operator
() [], ->, .
! (negation), ~ (bit-not)
++, --, - (unary) , * (unary), & (unary), sizeof
cast (type)
*, /, % (modulus)
- +
<<, >>
<, <=, >, >=
==, !=
& (bit-and), | (bit-or)
^ (bit-xor)
&& (logical and)
|| (logical or)
?:
=, +=, -=, /=, %=, >>=, &=
,

Bit operations
C can be used to operate on bits. This is useful for low-level programming though
the operations are also used when writing X graphics applications.
Setting a bit :- Suppose you wanted to set bit 6 of i (a long, say) to 1. First
you need to create a mask that has a 1 in the 6th bit and 0 elsewhere by doing
1L<<6 which shifts all the bits of the long 1 left 6 bits. Then you need to do
a bit-wise OR using i = i | (1L<<6).
Unsetting a bit :- Suppose you wanted to set bit 6 of i (a long, say) to 0. First
you need to create a mask that has a 0 in the 6th bit and 1 elsewhere by doing
1L<<6 then inverting the bits using the ~ operator. Then you need to do a
bit-wise AND using the & operator. The whole operation is i =i & ~(1<<6)
which can be contracted to i &= ~(1<<6).
Creating a mask for an X call :- In X graphics, masks are often created each
of whose bits represent a option that is to be selected in some way. Each bit
can be referred to using an alias that has been set up in an include file. E.g. a
mask which could be used in a call to make a window sensitive to key presses
and buttonpresses could be set up by doing
unsigned int mask = KeyPressMask | ButtonPressMask;
2.13.3

Declarations

First, a note on terminology. A variable is defined when it is created, and space is


made for it. A variable is declared when it already exists but needs to be re-described

2.14. MEMORY ALLOCATION

71

to the compiler (perhaps because it was defined in another source file). Think of
declaring in C like declaring at customs admitting to the existence of something.
C declarations are not easy to read. Any good book on C should explain how to
read complicated C declarations inside out to understand them, starting at the
variable name and working outwards back to the base type. You shouldnt need to
use complicated declarations so dont worry too much if you cant decode them.
Keep a cribsheet of useful typedefs and play with cdecl (see section 2.17.1).
ANSI C introduced the use of the void keyword in various contexts.
routine(void) the routine takes no arguments.
void routine (int i) the routine returns no value.
void *ptr ptr is a generic pointer which should be cast into a specific form
before use.
The following examples show common declarations.
int *p
int x[10]
int (*x)[10]
int *x[10]
int (*f)(int)
void (*f)(void)
int (*f[])(int)

pointer to an int
an array of 10 ints
a pointer to an array of 10 ints
array of 10 pointers to ints
pointer to a function taking and returning an int
pointer to a function taking no args and returning nothing
An array of pointers to a functions taking and returning an int

Note the importance of the brackets in these declarations. If a declaration gets too
complex it should be broken down. For example, the last example could be rewritten
as
typedef int (*PFI)(int) /* declare PFI as pointer to function that
takes and returns an int.*/
PFI f[];

2.14

Memory Allocation

Space is automatically set aside for variables when they are defined, but sometimes
you dont know beforehand how many variables youll need or just how long an array
might need to be. The malloc command creates space, returning a pointer to this
new area. To illustrate its use and dangers, heres a sequence of attempts at writing
a string reverser program.
#include <stdio.h>
#include <stdlib.h>

72

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

void print_reverse(char *str)


{
int i;
unsigned int len;
len = strlen(str) - 1; /* Why the -1? Because arrays start at 0,
so if a string has n chars, the
last char will be at position n-1
*/
for (i=len; i>=0; i--)
putchar(str[i]);
}
void main()
{
char input_str[100] /* why 100? */
printf("Input a string\n");
gets(input_str); /* should check return value */
printf("String was %s\n", input_str);
print_reverse(input_str);
}
This works, but is a bit loose (suppose the user types more than 100 characters?)
and doesnt keep a copy of the reversed string should it be needed later. The next
example shows a wrong (but not uncommon) attempt to solve this limitation.
#include <stdio.h>
/* WRONG! */
char* make_reverse(char *str)
{
int i, j;
unsigned int len;
char newstr[100];
len = strlen(str) - 1;
j=0;
for (i=len; i>=0; i--;)
newstr[j] = str[i];
j++;
/* now return a pointer to this new string */
return newstr;
}
void main()
{
char input_str[100]; /* why 100? */
char *c_ptr;

2.14. MEMORY ALLOCATION

73

printf("Input a string\n");
gets(input_str); /* should check return value */
c_ptr = make_reverse(input_str);
printf("String was %s\n", input_str);
printf("Reversed string is %s\n", c_ptr);
}
Like many flawed C programs this will work much of the time, especially if its not
part of a bigger program. The problems are that : The memory allocated for newstr when it was declared as an automatic variable in make reverse isnt permanent it only lasts as long as make reverse()
takes to execute. However, the arrays contents arent erased, theyre just freed
for later use, so if you access the array from main you might still get away with
it for a while. Making newstr a static will preserve the data but only until
its overwritten by a subsequent call.
The newly created array of characters, newstr, isnt terminated with a zero
character, \0, so trying to print the characters out as a string may be disastrous. Luckily the memory location that should have been set to zero is likely
to be zero anyway.
Lets try again.
/* mallocing.c */
#include <stdio.h>
#include <stdlib.h>
char* make_reverse(char *str)
{
int i;
unsigned int len;
char *ret_str, *c_ptr;
len = strlen(str);
/* Create enough space for the string AND the final \0.
*/
ret_str = (char*) malloc(len +1);
/*
Now ret_str points to a permanent area of memory.
*/
/* Point c_ptr to where the final
c_ptr = ret_str + len;
*c_ptr = \0;

\0 goes and put it in */

/* now copy characters from str into the newly created space.
The str pointer will be advanced a char at a time,

74

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

the cptr pointer will be decremented a char at a time.


*/
while(*str !=0){ /* while str isnt pointing to the last \0 */
c_ptr--;
*c_ptr = *str;
str++; /* increment the pointer so that it points to each
character in turn. */
}
return ret_str;
}
void main()
{
char input_str[100]; /* why 100? */
char *c_ptr;
printf("Input a string\n");
gets(input_str); /* Should check return value */
c_ptr = make_reverse(input_str);
printf("String was %s\n", input_str);
printf("Reversed string is %s\n", c_ptr);
}
The malloced space will be preserved until it is explicitly freed (in this case by
doing free(c ptr)). Note that the pointer to the malloced space is the only way
you have to access that memory: lose it and the memory will be inaccessible. It will
only be freed when the program finishes.
malloc is often used to create tree and list structures, since one often doesnt know
beforehand how many items will be needed. See section 2.21.4 for an example.

2.15

Input/Output

2.15.1

File I/O under Unix

Some file operations work on file pointers and some lower level ones use small integers
called file descriptors (an index into a table of information about opened files).
The following code doesnt do anything useful but it does use most of the file handling
routines. The manual pages describe how each routine reports errors. If errnum is
set on error then perror can be called to print out the error string corresponding to
the error number, and a string the programmer provides as the argument to perror.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>

2.15. INPUT/OUTPUT

#include <sys/stat.h>
#include <fcntl.h>
/* the man pages of the commands say which
include files need to be mentioned */
#define TRUE 1
int bytes_read;
size_t fp_bytes_read;
int fd;
/* File descriptors */
int fd2;
FILE *fp; /* File pointers */
FILE *fp2;
char buffer[BUFSIZ]; /* BUFSIZ is set up in stdio.h */
main(){
/* Use File descriptors */
fd = open ("/etc/group", O_RDONLY);
if (fd == -1){
perror("Opening /etc/group");
exit(1);
}
while (TRUE){
bytes_read = read (fd, buffer,BUFSIZ);
if (bytes_read>0)
printf("%d bytes read from /etc/group.\n", bytes_read);
else{
if (bytes_read==0){
printf("End of file /etc/group reached\n");
close(fd);
break;
}
else if (bytes_read == -1){
perror("Reading /etc/group");
exit(1);
}
}
}
/* now use file pointers */
fp = fopen("/etc/passwd","r");
if (fp == NULL){
printf("fopen failed to open /etc/passwd\n");
exit(1);
}
while(TRUE){
fp_bytes_read= fread (buffer, 1, BUFSIZ, fp);

75

76

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

printf("%d bytes read from /etc/passwd.\n", fp_bytes_read);


if (fp_bytes_read==0)
break;
}
rewind(fp); /* go back to the start of the file */
/* Find the descriptor associated with a stream */
fd2 = fileno (fp);
if (fd2 == -1)
printf("fileno failed\n");
/* Find the stream associated with a descriptor */
fp2 = fdopen (fd2, "r");
if (fp2 == NULL)
printf("fdopen failed\n");
fclose(fp2);
}
To take advantage of unixs I/O redirection its often useful to write filters: programs
that can read from stdin and write to stdout. In Unix, processes have stdin,
stdout and stderr channels. In stdio.h, these names have been associated with file
pointers. The following program reads lines from stdin and writes them to stdout
prepending each line by a line number. Errors are printed on stderr. fprintf
takes the same arguments as printf except that you also specify a file pointer.
fprintf(stdout,....) is equivalent to printf(....).
/* line_nums.c
Sample Usage :
line_nums < /etc/group
*/
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
int lineno = 0;
int error_flag = 0;
char buf[BUFSIZ]; /* BUFSIZ is defined in stdio.h */
main(){
while(TRUE){
if (fgets(buf,BUFSIZ, stdin) == NULL){
if (ferror(stdin) != 0){
fprintf(stderr,"Error during reading\n");
error_flag = 1;
}
if (feof(stdin) != 0)
fprintf(stderr,"File ended\n");
clearerr(stdin);

2.15. INPUT/OUTPUT

77

break; /* exit the while loop */


}
else{
lineno++;
/* in the next line, "%3d" is used to restrict the
number to 3 digits.
*/
fprintf(stdout,"%3d: ", lineno);
fputs(buf, stdout);
}
}
fprintf(stderr,"%d lines written\n", lineno);
exit(error_flag);
}
ferror() and feof() are intended to clarify ambiguous return values. Here thats
not a problem since a NULL return value from fgets() can only mean end-of-file,
but with for instance getw() such double checking is necessary.

2.15.2

Interactive

Output

For efficiency, writing to files under Unix is usually buffered, so printf(....) might
not immediately produce bytes at stdout. Should your program crash soon after
a printf() command you might never see the output. If you want to force synchronous output you can
Use stderr (which is usually unbuffered) instead of stdout.
Use fflush(stdout) to flush out the standard output buffer.
Use setbuf(stdout,NULL) to stop standard output being buffered.
Input

scanf is a useful-looking routine for getting input. It looks for input of the format
described in its 1st argument and puts the input into the variables pointed to by
the succeeding arguments. It returns the number of arguments successfully read.
Suppose you wanted the user to type their surname then their age in. You could do
**
this:int age;

78

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

char name[50];
int return_val;
main(){
printf("Type in your surname and age, then hit the Return key\n");
while(TRUE){
return_val= scanf("%s %d", name, &age);
if (return_val == 2)
break;
else
printf("Sorry. Try Again\n");
}
}
If you use scanf in this way to directly get user input, and the user types in something different to what scanf() is expecting, scanf keeps reading until its entire
input list is fulfilled or EOF is reached. It treats a newline as white space. Thus users
can become very frustrated in this example if, say, they keep typing their name,
then hitting Return. A better scheme is to store user input in an intermediate string
and use sscanf(), which is like scanf() except that its first argument is the string
which is to be scanned. E.g. in
...
int ret, x, y, z;
ret = sscanf(str,"x=%d y=%d z=%d", &x, &y, &z);
...
sscanf, given a string x=3 y=7 z=89, will set the x, y, and z values accordingly
and ret will be set to 3 showing that 3 values have been scanned. If str is x+1
y=4, sscanf will return 2 and wont hang and you can print a useful message to
the user.
To read the original string in, fgets() is a safer routine to use than gets() since
with gets() one cant check to see if the input line is too large for the buffer. This
still leaves the problem that the string may contain a newline character (not just
whitespace) when using fgets. One must make annoying provisions for ends of lines
that are not necessary when input is treated as a continuous stream of characters.

2.16

Source File organisation

The needs of large-scale organisation and support for many platforms may make
modules incomprehensible unless some understanding of the overall structure is
gained first.

2.16. SOURCE FILE ORGANISATION

2.16.1

79

Preprocesser Facilities

The preprocessor has some useful options.


sourcefile inclusion :#include "defines.h"
...
#include <defines.h>
The difference between these two variants is that with the included file in quotes,
it is first looked for in the directory of the source file. In each case, the standard include directories on the system are searched as well as any directories
mentioned on the command line after the -I flag. See the cpp man page for
more details.
macro replacement :Note that these macros are expanded before the compiler is called. They aid
legibility. In the first example below, a simple substitution is done. In the
second, an in-line macro is defined, whose execution should be faster than the
equivalent function.
#define ARRAY_SIZE 1000
char str[ARRAY_SIZE];
...
#define MAX(x,y) ((x) > (y) ? (x) : (y))
int max_num;
max_num = MAX(i,j);
...
conditional inclusion :Blocks of code can be conditionally compiled according to the existence or
value of a preprocessor variable. A variable can be created using the #define
preprocessor directive or using the -D option at compilation time. The first
two examples shows how debugging statements can easily be switched on or
off. The final example shows how blocks of code can be de-activated.
#ifdef DEBUG
printf("got here\n");
#else
something();
#endif /*DEBUG*/
...
#if defined(DEBUG)
#define Debug(x) printf(x)
#else
#define Debug(x)

80

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

#endif
if ( i == 7 ){
j++;
Debug(("j is now %d\n", j));
}
#if 0
/* this code wont reach the compiler */
printf("got here\n");
#endif
2.16.2

Multiple Source Files

Modularisation not only makes the source more easy to manage but it speeds up
re-compilation: you need only recompile the changed source files. Also, by keeping
the I/O components in one file (and perhaps the text to be printed into another) one
can more easily convert the software to run on other machines and in other natural
languages.
By default, functions and variables defined outside of functions can be accessed from
other files, where they should be declared using the extern keyword. If however the
variable is defined as static, it cant be accessed from other files. In the following
example, i, j and the function mean are created in file1.c but only i can be
accessed from file2.c.
/* file1.c */
int i;
static int j;
static int mean(int a, int b){
...

/* file2.c */
extern int i;

Names of external variables should be kept short; only the first 6 initial characters
are guaranteed to be significant (though in practise the first 255 character often are).
You should keep to a minimum the number of global variables. You can use include
files to manage your global variables.
1. Construct a globals.h file with all of your #defines and variable declarations
in it. Make sure all variables are defined as externs. Include this file in all the
relevant source files.
2. In the file that contains your main(), you again have all the variable definitions,
minus the externs. This is important if they are all defined extern, the linker
will not be able to allocate memory for them.
You can achieve this with the help of the pre-processor if your globals.h looks like
this:-

2.16. SOURCE FILE ORGANISATION

81

#ifdef LOCAL
#define EXTERN
#else
#define EXTERN extern
#endif
EXTERN int num_of_files;
..
In this way, the EXTERN becomes extern in every file that includes globals.h.
The trick is then to have
#define LOCAL
#include "globals.h"
in the file containing the main routine.
If youre calling a routine in one file from another file its all the more important
for the formal parameters to be declared correctly. Note especially that the declaration extern char *x is not the same as extern char x[] one is of type
pointer-to-char and the other is array-of-type-char (see section 2.11).
2.16.3

Make

If you have many source files you dont need to recompile them all if you only
change one of them. By writing a makefile that describes how the executable is
produced from the source files, the make command will do all the work for you. The
following makefile says that pgm depends on two files a.o and b.o, and that they in
turn depend on their corresponding source files (a.c and b.c) and a common file
incl.h:
pgm: a.o b.o
cc -Aa a.o b.o -o pgm
a.o: incl.h a.c
cc -Aa -c a.c
b.o: incl.h b.c
cc -Aa -c b.c
Lines with a : are of the form
target : dependencies
make updates a target only if its older than a file it depends on. The way that
the target should be updated is described on the line following the dependency line
(Note: this line needs to begin with a TAB character).

82

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

Heres a more complex example of a makefile for a program called dtree. First
some variables are created and assigned. In this case typing make will attempt to
recompile the dtree program (because the default target is the first target mentioned). If any of the object files it depends on are older than their corresponding
source file, then these object files are recreated.
The targets neednt be programs. In this example, typing make clean will remove
any files created during the compilation process.

# Makefile for dtree


DEFS = -Aa -DSYSV
CFLAGS = $(DEFS) -O
LDFLAGS =
LIBS = -lmalloc -lXm -lXt -lX11 -lm
BINDIR = /usr/local/bin/X11
MANDIR = /usr/local/man/man1
OBJECTS_A = dtree.o Arc.o Graph.o

#using XmGraph

ARCH_FILES = dtree.1 dtree.c Makefile Dtree Tree.h TreeP.h \


dtree-i.h Tree.c Arc.c Arc.h ArcP.h Graph.c Graph.h GraphP.h
dtree: $(OBJECTS_A)
$(CC) -o dtree $(LDFLAGS) $(OBJECTS_A) $(LIBS)
Arc.o: Arc.c
$(CC) -c $(CFLAGS) Arc.c
Graph.o: Graph.c
$(CC) -c $(CFLAGS) Graph.c
dtree.o: dtree.c
$(CC) -o dtree.o -c $(CFLAGS) -DTREE dtree.c
install: dtree dtree.1
cp dtree $(BINDIR)
cp dtree.1 $(MANDIR)
clean:
rm -f dtree *.o core tags a.out

2.17. DEBUGGING

2.17

Debugging

2.17.1

Utilities and routines

83

Some compilers have flags to turn on extra checking. gcc for example has a -Wall
option which gives a list of suspicious constructions as well as the usual compile
errors.
There are also routines that are useful
When a system call fails it generally sets an external variable called errno to
indicate the reason for failure. Using perror() (which takes a string as an
argument) will print the string out and print the error message corresponding
to the value of errno
assert() is useful for putting diagnostics into programs. When it is executed,
if the expression it takes as an argument is false (zero), assert prints the
expressions value and the location of the assert call. See the on-line manual
page for more details.
If using these fail, try some of the following. If your machines lacking any of these
programs, look for public domain versions.
lint :- is a program which gives the sort of warning messages about unused variables and wrong number of arguments that non-C compilers usually give.
lint takes most of the same arguments as the compiler. It needs special libraries which are already provided.
cflow :- To show which functions call which, use cflow. This produces an indented
output which also gives an idea of function call nesting. An ansi-ized, much enhanced version is available by ftp from sunsite.unc.edu:/pub/linux/devel/C
cb :- To standardise the indentation of your program, send it through cb, a C
beautifier;
cb ugly.c > lovely.c
cxrefs :- tells you where variables and functions are mentioned. Its especially
useful with multi-file sources.
adb :- I only use adb to see why a core dump happened. If myprog causes a core
dump then
adb myprog
$c
will show you what functions return addresses were on the stack when the
crash happened, and what hex arguments they were called with. Quit using $q

84

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

Symbolic Debuggers :- dbx, xdb, or gdb may be available to you. They are
symbolic source-level debuggers under which you can run a program in trace
mode allowing you to use breakpoints, query values, etc. To use this you will
have to first compile your program with the -g flag.
cdecl :- This program can help with C declarations. See man page for details.
Some examples:unix: cdecl declare fptab as array of pointer to function returning int
int (*fptab[])()
unix:
cdecl explain int (*fptab[])()
declare fptab as array of pointer to function returning int

cdecl is available from archives in comp.sources.unix/volume6.


2.17.2

Some Common mistakes

C is based on the principle that programmers know what theyre doing, so it lets
them get on with it and doesnt get in their way. Throughout this document common
errors have a Warning sign in the margin. A checklist of more errors is given here.
Miscellaneous

A common mistake is to type = instead of ==.


if (i=3)
return 1;
else
return 0;
will always return 1 because the assignment i=3 has the value 3 and 3 is true!
gccs warning option can alert you to this. You might also try to get into the
habit of writing expressions like if (3==i) to safeguard yourself from this kind
of error.
Comments in C cant be nested. Use the preprocessor directives to temporarily
comment out blocks of code. Suppose you had the following code.
if (i=6)
z=mean(x,y); /* get the xy mean */
mean(z,y);
If you decided not to risk running mean you might do
/* comment this fragment out
if (i=6)
z=mean(x,y); /* get the xy mean */
mean(z,y);
*/

2.17. DEBUGGING

85

but it wouldnt work because the first /* would be matched by the */ on


the mean(x,y) line (the /* on that line being ignored), and mean(z,y);
wouldnt be commented out at all. In this case the final */ would be flagged
as an error, but you wont always be so lucky.
...
i = 3;
j = 10;
while (i<100);
i = i+j;
...
This while loop will go on for ever. The semicolon after the while condition is
a null statement which forms the body of the loop so i will always be 3. Take
away that semicolon and i = i+j becomes the body, which is probably what
was intended.
When you have an if-else statement nested in another if statement, always put
braces around the if-else. Thus, never write like this:
if (foo)
if (bar)
win ();
else
lose ();
(the else matches the closest if), always like this:
if (foo)
{
if (bar)
win ();
else
lose ();
}
Dont be fooled by indentation. In the following fragment only the execution
of the j = 7; statement is conditional upon the value of i.
...
if (i==7)
j = 7;
k = 7;
...
The order of operations in an expression isnt guaranteed to be left-to-right. A
line like
a[i++] = b[i++];
will have different results according to whether or not the i on the left-hand
side is calculated before the right-hand side is evaluated.

86

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

The order of operator precedence sometimes surprises people.


...
FILE *fp;
...
if (fp=fopen(filename, "r") == NULL)
return (NULL);
Here the intention is to try opening a file, then compare the resulting fp to
NULL to see if fopen failed. Unfortunately, what actually happens first is the
test (fopen(filename, "r") == NULL) which has an integer result (non-zero
if the statement is true). This result is then assigned to fp. The compiler should
warn you about this problem. The code should have some extra brackets:...
FILE *fp;
...
if ((fp=fopen(filename, "r")) == NULL)
return (NULL);
The following wont work as expected because the ~ character needs to be
interpreted by the shell.
if ((fp=fopen("~/data", "r")) == NULL)
return (NULL);
Youll have to find out your home directory (use getenv("HOME")) and append
to it.
scanf takes pointers to the variables that are going to be set. The following
fragment will cause a crash
...
int i;
scanf("%d",i); /* this should be scanf("%d",&i) */
The most uncomfortable bugs are those that seem to move as you hunt them
down. Put in some printf() statements and they just disappear or seem to.
This could mean that youre writing off the end of an array or that one of your
pointers has gone astray. You can protect against this by doing something like
#define BUFLEN 10
int x[BUFLEN], y;
...
if (y >= BUFLEN || y<0)
[error code here]
else
x[y] = 255;
...

2.17. DEBUGGING

87

Theres a big difference between \0 and "\0". Suppose you had


char str[100];
char *str_ptr;
str_ptr = str;
then str ptr and str would both point to the first element in the array. Suppose you wanted to initialise this string by making the first element a zero byte.
You could do
strcpy(str_ptr, "\0") /* or strcpy(str_ptr, "") */
or
*str_ptr = \0;
but
str_ptr = "\0";
would do something quite different. It would create a string in your executable
(namely "\0") and set str ptr to point to it with potentially disastrous effects.
Turning on optimisation may change the behaviour of your program, especially
if the program isnt perfect. For instance, if optimisation re-positions a variable
into a register its less likely to be 0 initially, so if youve not initialised variables
before use you might get a surprize.
A function that returns a pointer either (1) takes a pointer as a parameter or
(2) uses malloc to allocate memory to store the data in or (3) returns a pointer
to a static buffer. As the user of a function, you must know which of the three it
is in order to use the function; the manual page describing the function should
give you this information.
declaration mismatch

getchar returns an integer, not a char as you might expect. If the integer
value returned is stored into a character variable and then compared against
the integer constant EOF, the comparison may never succeed, because signextension of a character on widening to integer is machine-dependent. Read the
manual page before using a function.
Suppose a function reverse takes a string. If the K&R C programmer accidentally writes
reverse (str)
{
char *str;
...
}

88

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

rather than
reverse (str)
char *str;
{
...
}
the compiler might not warn the programmer that the formal parameter str has
the default type int and a local variable str is created which isnt initialised.
In the next example, it looks as if the programmer meant to define 2 pointers
to integers. In fact, ptr2 is being defined as an integer.
int*

ptr1, ptr2;

In K&R C the following code would crash; (ANSI C does automatic type conversion)
int mean(num1, num2)
int num1, num2;
{
...
}
int i, answer;
float f;
/* deliberate mistake! */
answer = mean(f,j);
printf("The mean of %f and %d is %d\n", f, j, answer);
C functions usually get given arguments via the stack. Calling functions put
values on the stack then peel the same number of bytes off when returned to,
so it doesnt matter to K&R C if the subfunction doesnt use or declare all the
arguments that it is given. It doesnt even matter if it declares more arguments
than given by the caling function as long as it doesnt write to these values.
Were it to do so, it might well overwrite the address that the called function
should return to. Such a problem might not be recognised for quite a while,
and isnt easy to track down. This is where lint (see 2.17.1) becomes useful
If in one source file you have int array[100] and you want to use this array
from another source file, you mustnt declare as extern int *array but as
extern int array[]. An explanation of why this is so comes from Chris
Volpe (volpecr@crd.ge.com)
When you declare int array[100]; the compiler sets aside storage for 100
ints, at say, address 500. The compiler knows that array is an array, and
when it tries to generate code for an expression like array[3], it does the
following: It takes the starting address of the array (500), and adds to that
an offset equal to the index (3) times the size of an int (typically 4) to get an

2.17. DEBUGGING

89

address of 500+3*4=512. It looks at the int stored at address 512 and theres
the int.
When you give an external declaration in another file like extern int *array;,
the compiler takes your word for it that array is a pointer. The linker resolves
the symbol for you as an object that resides at address 500. But since you lied
to the compiler, the compiler thinks theres a pointer variable stored at address
500. So now, when the compiler sees an expression like array[3], it generates
code for it like this: It takes the address of the pointer (500) and, assuming
theres a pointer there, reads the value of the pointer stored there. The pointer
will typically reside at address 500 through 503. Whats actually in there is indeterminate. There could be a garbage value stored there, say 1687. The compiler
gets this value, 1687, as the address of the first int to which the pointer points.
It then adds the scaled index offset (12) to this value, to get 1699, and tries to
read the integer stored at address 1699, which will likely result in a bus error or
segmentation violation.
The thing to remember about all this is that even though array[index] and
pointer[index] can be used interchangeably in your source code, the compiler
generates very different object code depending on whether you are indexing off
an array identifier or a pointer identifier.
malloc

malloc() allocates memory dynamically. The standard malloc() and free() functions need to be efficient and cant check the integrity of the heap on every call.
Therefore, if the heap gets corrupted, seemingly random behaviour can occur. The
following code wont work.
char *answer;
printf("Type something:\n");
gets(answer);
printf("You typed \"%s\"\n", answer);
The pointer variable answer, which is handed to the gets function as the location
into which the response should be stored, has not been set to point to any valid
storage. That is, we cannot say where the pointer answer points. Since local
variables are not initialized, and typically contain garbage, it is not even guaranteed
that answer starts out as a null pointer.
The simplest way to correct the question-asking program is to use a local array,
instead of a pointer, and let the compiler worry about allocation:
#include <string.h>
char answer[100], *p;
main(){

90

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

printf("Type something:\n");
fgets(answer, 100, stdin);
if((p = strchr(answer, \n)) != NULL)
*p = \0;
printf("You typed \"%s\"\n", answer);
}
Note that this example also uses fgets instead of gets (always a good idea), so that
the size of the array can be specified and fgets will not overwrite the end of the
array if the user types an overly-long line, though unfortunately for this example,
fgets does not automatically delete the trailing \n, as gets would.
Alignment problems can arise if malloc is used carelessly. Processors have different
rules about (for instance) whether a long can be stored starting at an odd memory
location. If you try to break these rules, your program will crash giving little or no
clue why. The HP RISC chips only permit a double to start at an address divisible
by 8, so trying something like
char *block = (char*) malloc(sizeof(double));
double d = 1.2;
* (double*)block = d;
is likely to crash.
Find the bug

What looks wrong with these programs?


#include <stdio.h>
#include <stdlib.h>
main()
{
int i;
for (i=0; i<10; i=i+1);
printf("i is %d\n",i);
}
#include <stdio.h>
#include <stdlib.h>
main()
{
int numbers[10];
int i;
for (i=1;i<=10;i++)
numbers[i]=i;
for (i=1;i<=10;i++)

2.17. DEBUGGING

printf("numbers[%d]=%d\n", i, numbers[i]);
}
#include <stdio.h>
#include <stdlib.h>
main()
{
int i;
for (i=0; i<10; i=i+1)
if (i=2)
printf("i is 2\n");
else
printf("i is not 2\n");
}
#include <stdio.h>
#include <stdlib.h>
main()
{
int i;
for (i=0; i<10; i=i+1)
if (i<2)
printf("%d is less than 2\n",i);
printf("and %d is not equal to, 2 either\n",i);
}
#include <stdio.h>
#include <stdlib.h>
main()
{
int i;
i = 0;
while (i < 10);
i = i + 1;
printf("Finished. i = %d\n",i);
}
#include <stdio.h>
#include <stdlib.h>
main()
{
int i;
for (i=0; i<10; i=i+1)
switch(i){
case 0: printf("i is 0\n");
case 1: printf("i is 1\n");
default: printf("i is more than 1\n");
}

91

92

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

}
#include <stdio.h>
#include <stdlib.h>
main()
{
int i;
for (i=0; i<10; i=i+1)
/* check the value of i*/
switch(i){
/* is i 0?
case 0: printf("i is 0\n");
break;
/* is i 1?
case 1: printf("i is 1\n");
break;
/* now the default case */
default: printf("i is more than 1\n");
}
}
#include <stdio.h>
#include <stdlib.h>
main()
{
int i;
i=3;
i=i+2*i++;
printf("i is now %d\n",i);
}
the following code works on some machines but crashes on others ...
#include <stdio.h>
#include <stdlib.h>
typedef struct {
double *par;
double *pos;
double *vel;
} ajoint;
main()
{
ajoint *joint;
joint = (ajoint *) malloc(sizeof(ajoint) + sizeof(double));
joint->pos = (double*) (joint +1);
*(joint->pos) = 0;
}

2.18. EXERCISES 3

2.18

93

Exercises 3

1. Improve your primes program so that


It stops searching for primes
in the range 0 to n once it has marked all the

multiples of primes n
It can take as an argument a number to show the upper bound of the
primes to print out.
2. Put 10 integers in a file, one per line. Write a program that reads the numbers
then prints their sum and and average.
3. Read the 1st 10 uids from /etc/passwd, save them in an array of strings and
sort them using qsort.
4. Take a simple program (the malloc example on page 73 will do) and break it
up into 2 or 3 source files. See if you can compile them into an executable.
Try adding static to variable and function definitions to see what difference
it makes. Write a makefile for it.
5. Write a program to count the number of ways that 8 queens can be placed on a
chess board without any 2 of them being on the same row, column or diagonal.
6. Hashing - First a solution to the last hash exercise.
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 50
#define MAX_STR_LEN 64
#define EMPTY -1
typedef struct {
char str[MAX_STR_LEN];
int value;
} Entry;
char str[MAX_STR_LEN];
/* Create an array of elements of type Entry */
Entry table[TABLE_SIZE];
int process(char *str){
int val = 1;
while (*str){
val = val * (*str);
str++;
}
return val;
}

94

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

char * get_string(char str[])


{
printf("Input a string\n");
return gets(str);
}
int hashfn(char *str){
int total = 0;
int i;
while (i = *str++)
total += i;
return total % TABLE_SIZE;
}
void set_table_values(void){
/* set all the value entries in the table to EMPTY
(here we assume that the process() routine doesnt
produce -1)
*/
int i;
for (i =0;i<TABLE_SIZE;i++)
table[i].value= EMPTY;
}
int find_entry(char *str, int bucket){
if (table[bucket].value == EMPTY){
strcpy(table[bucket].str,str);
table[bucket].value = process(str);
}
else{
if (strcmp(table[bucket].str,str)){
bucket = (bucket +1)% TABLE_SIZE;
return find_entry(str, bucket);
}
}
return table[bucket].value;
}
main(){
int bucket;
int val;
set_table_values();
/* Use get_string repeatedly. For each string:use the hash function to find the strings entry

2.18. EXERCISES 3

95

in the table.
*/
while(get_string(str)){
if (! strcmp(str,"end")){
printf("Program ended\n");
exit(0);
}
bucket = hashfn(str);
val = find_entry(str,bucket);
printf("Value of <%s> is %d\n",str,val);
}
}
Another approach to collisions is for each entry in the hash table to be the
beginning of a linked list of items that produce the same hash function value.
First we need to alter the Entry structure so that it includes pointer to another
Entry. Theres a slight complication here in that we cant define a pointer to
something which isnt defined yet, so we introduce a tag name to the structure.
typedef struct _entry {
int value;
struct _entry *next;
char str[20];
} Entry;
New entry structures can be generated using the following routine.
Entry *create_an_entry(void){
Entry *entry;
entry = (Entry*) malloc(sizeof (Entry));
return entry;
}
find entry needs to be re-written.
int find_entry(Entry ** entry, char *str){
if (*entry == NULL){
*entry = create_an_entry();
set_entry(*entry,str);
return (*entry)->value;
}
else{
if ((*entry) -> value != EMPTY){
if (!strcmp ((*entry) ->str, str)){
printf("Valueue for <%s> already calculated\n",str);
return (*entry) -> value;
}
else{

96

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

printf("Theres a collision: <%s> and <%s> share\n",


(*entry) ->str, str);
printf("the same hashfn valueue\n");
find_entry(&((*entry)->next),str);
}
}
else{
printf("<%s> is a new string\n",str);
set_entry((*entry),str);
return (*entry)->value;
}
}
}
The initial table can now be
/* Create an array of elements of type Entry */
Entry *table[TABLE_SIZE];
These entries need to be initialised to NULL.
Now write a program with the following main routine to test all this out.
main(){
int bucket;
int value;
set_table_values();
/* Use get_string repeatedly. For each string:use the hash function to find the strings entry
in the table.
*/
while(get_string(str)){
if (! strcmp(str,"end")){
printf("Program ended\n");
exit(0);
}
bucket = hashfn(str);
value = find_entry(&(table[bucket]), str);
printf("Valueue of <%s> is %d\n",str,value);
}
}
This program could be further elaborated
At the moment, if a string is long enough it will be too big for the array.
Change the Entry definition to:-

2.19. MORE INFORMATION

97

typedef _entry {
int val;
Entry *entry;
char *str;
}
and change the code so that correctly sized space for each string is created
using malloc.
A hash function should be quick to calculate and provide an even spread
of values to minimize collisions. Add some diagnostics to the program and
improve the hash function.

2.19

More information

The comp.lang.c newsgroup has a great deal of information. Read the Frequently
Asked Questions if nothing else.
Tutorials :- a set of tutorials written by Christopher Sawtell is available from
The gopher.eng.cam.ac.uk gopher in CUED help/languages/C/Tutorials.
paris7.jussieu.fr:/contributions/docs by anon-ftp.
Style Guides and Portability Guides appear on the net from time to time. Read
comp.lang.c for details.
Look in just about any ftp archive for source code. The code at ftp.funet.fi
in /pub/languages/C/Publib has routines to manipulate sets, stacks, etc than
you may find educational.
On the World Wide Web access URL
http://club.eng.cam.ac.uk/help/tpl/languages/C.html

2.20

Sample answers to exercises

2.21

Examples

2.21.1

Command Line arguments

#include <stdio.h>
#include <stdlib.h>
/* This shows how args can be read from the Unix command line */
int main(int argc, char *argv[]){
int i;
printf("The arguments are\n", argc);
for (i=1; i<argc; i++)
printf("%d %s\n",i, argv[i]);
exit (0);
}

98

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

2.21.2

Using qsort, random numbers and the clock

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
* compile on HPs using c89 -D_HPUX_SOURCE -o filename filename.c */
#define NUM 10
int comp(const void *a, const void *b )
{
return *(int *)a - * (int *)b;
}
int main(int argc, char *argv[])
{
int numbers[NUM];
int i;
srand48((long)time(NULL));
printf("\nUnsorted numbers are:-\n");
for (i=0; i< NUM; i++){
numbers[i]= 1000 * drand48();
printf("%d: %3d\n", i, numbers[i]);
}
/* See the qsort man page for an explanation of the following */
qsort((void*) numbers, (size_t) NUM, sizeof(int), comp);
printf("\nSorted numbers are:-\n");
for (i=0; i< NUM; i++)
printf("%d:%3d\n", i, numbers[i]);
exit(0);
}

2.21.3

Calling other programs

The commands used from the command line can be called from C.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
FILE *popen();
FILE *fp;
char string[32];
/* First use the system() call. Output will go to stdout. */
system("date");
/* Now capture the output of date using popen() */
fp = popen("date","r");
if (fp == NULL)
fprintf(stderr,"Cannot run date\n");
else{

2.21. EXAMPLES

99

fgets(string, 32, fp);


printf("The date command returns [%s]\n", string);
pclose(fp);
}
}

2.21.4

Linked Lists

The following program creates a singly linked list. Pointers are maintained to the
head and tail of the list.
Initially
head

NULL

tail

NULL

After : tail=add list item(5);


head

- val
next

5
NULL
6

tail

After : tail=add list item(7);


head

- val

- val

next

next

tail
Figure 2.2: Linked List

#include <stdio.h>
typedef struct _list_item {
int val;
struct _list_item *next;
} list_item;
/* prototypes */
list_item *add_list_item(list_item *entry, int value);
void print_list_items(void);

7
NULL
6

100

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

list_item *head=NULL;
list_item *tail=NULL;
main(int argc, char *argv[])
{
tail=add_list_item(tail,5);
tail=add_list_item(tail,7);
tail=add_list_item(tail,2);
print_list_items();
}
list_item *add_list_item(list_item *entry, int value)
{
list_item *new_list_item;
new_list_item=(list_item*)malloc(sizeof(list_item));
if (entry==NULL){
head=new_list_item;
printf("First list_item in list\n");
}
else {
entry->next = new_list_item;
printf("Adding %d to list. Last value was %d \n",value,entry->val);
}
new_list_item->val = value;
new_list_item->next = NULL;
return new_list_item;
}
void print_list_items(void)
{
list_item *ptr_to_list_item;
for (ptr_to_list_item= head;ptr_to_list_item!= NULL;
ptr_to_list_item=ptr_to_list_item->next) {
printf("Value is %d \n", ptr_to_list_item->val);
}
}

2.21.5

Using pointers instead of arrays

#include "stdio.h"
char *words[]={"apple","belt","corpus","daffodil","epicycle","floppy",
"glands","handles","interfere","jumble","kick","lustiness",
"glands","handles","interfere","jumble","kick","lustiness",
"glands","handles","interfere","jumble","kick","lustiness",
"glands","handles","interfere","jumble","kick","lustiness",
"glands","handles","interfere","jumble","kick","lustiness",
"glands","handles","interfere","jumble","kick","lustiness",
"glands","handles","interfere","jumble","kick","lustiness",
"glands","handles","interfere","jumble","kick","lustiness",
"glands","handles","interfere","jumble","kick","lustiness",
"mangleworsel","nefarious","oleangeous","parsimonious",NULL};

void slow(void)
{

2.21. EXAMPLES

101

int i,j,count=0;
for (i=0; words[i] != NULL ; i=i+1)
for (j=0; j <= strlen(words[i]) ; j=j+1)
if(words[i][j] == words[i][j+1])
count= count+1;
printf("count %d\n",count);
}
void fast(void)
{
register char **cpp; /* cpp is an array of pointers to chars */
register char *cp;
register int count=0;
for (cpp= words; *cpp ; cpp++)
for (cp = *cpp ; *cp ; cp++)

/* loop through words. The final


NULL pointer terminates the loop */
/* loop through letters of a word.
The final \0 terminates the loop */

if(*cp == *(cp+1))
count++;
printf("count %d\n",count);
}

/*count the number of double letters, first using arrays, then pointers */
void main(int argc, char *argv[]){
slow();
fast();
}

2.21.6

A data filter

The program reads from stdin an ASCII file containing values of a variable y for
integral values of x running from 0 to n-1 where n is the number of values in the file.
There may be several values on each line. The program outputs the x, y pairs, one
pair per line, the y values scaled and translated.
#include <stdio.h>
#include <stdlib.h>
int answer;
float offset;
float scale;
char buf[BUFSIZ];
int xcoord = 0;
char *cptr;
int transform(int a)
{
return a * scale + offset + 0.5;
}

char* eat_space(char *cptr){


/* This while loop skips to the nonspace after spaces.
If this is the end of the line, return NULL
While a space, keep going
*/

102

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

while (*cptr == ){
if (*cptr == \0)
return NULL;
else
cptr++;
}
return cptr;
}

char * next_next_num(char *cptr){


/* This while loop skips to the 1st space after a number.
If this is the end of the line, return NULL
While NOT a space, keep going
*/
while (*cptr != ){
if (*cptr == \0)
return NULL;
else
cptr++;
}
/* Now move to the start of the next number */
return eat_space(cptr);
}
int main(int argc, char *argv[])
{
offset = 2.3;
scale = 7.5;
while(1){
/* if we havent reached the end of the file ...*/
if(fgets(buf, BUFSIZ,stdin)!= NULL){
/* initialise cptr to point to the first number ...*/
cptr = eat_space(buf);
do{
/* convert the representation of the num into an int */
sscanf(cptr,"%d", &num);
/* print x and y to stdout */
printf("%d %d\n",xcoord, tranform(num));
/* skip to the start of the next number on the line */
cptr=next_next_num(cptr);
xcoord++;
}while ( cptr!=NULL);
}
else{
exit(0);
}
}
}

2.21.7

Reading Directories

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>

2.21. EXAMPLES

103

#include <sys/stat.h>
#define REQUEST_DIR "/"
int main(int argc, char *argv[]){
FILE *fp;
DIR *dirp;
struct dirent *dp;
struct stat buf;
dirp = opendir(REQUEST_DIR);
chdir(REQUEST_DIR);
/* Look at each entry in turn */
while ((dp = readdir(dirp)) != NULL) {
/* Now stat the file to get more information */
if (stat(dp->d_name, &buf) == -1)
perror("stat\n");
if (S_ISDIR(buf.st_mode))
printf("%s is a directory\n", dp->d_name);
else if (S_ISREG(buf.st_mode))
printf("%s is a regular file\n", dp->d_name);
}
(void) closedir(dirp);
}

2.21.8

Queens: recursion and bit arithmetic

This program counts the number of ways that 8 queens can be placed on a chess
board without any 2 of them being on the same row, column or diagonal. It was
written by M. Richards at cl.cam.ac.uk
#include <stdio.h>
int count;
void try(int row, int ld, int rd){
if (row == 0xFF)
count++;
else{
int poss = 0xFF & ~(row | ld | rd);
while (poss){
int p = poss& -poss;
poss = poss -p;
try(row+p, (ld+p)<<1, (rd+p)>>1);
}
}
}
int main(int argc, char *argv[]){
printf("Eight Queens\n");
count = 0;
try(0,0,0);
printf("Number of solutions is %d\n", count);
exit(0);

104

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

2.22

More on Arrays, Pointers and Malloc

2.22.1

Multidimensional Arrays

The elements of aai[4][2] are stored in memory in the following order.


aai[0][0]aai[0][1]aai[1][0]aai[1][1]
aai[2][0]aai[2][1]aai[3][0]aai[3][1]
*aai is of type int[]. Note that:aai[1][2] == *( (aai[1])+2) == *(*(aai+1)+2)
and that numerically
aai == aai[0] == &aai[0][0]
*aai can be used as a pointer to the first element even though it is of type array 4
of int because it becomes pointer to int when used where a value is needed.
But *aai is not equivalent to a pointer. For example, you cant change its value. This
distinction can easily and dangerously be blurred in multi-file situations illustrated
in the following example. In
extern int *foo;
foo is a variable of type pointer to int. foos type is complete, (sizeof foo) is
allowed. You can assign to foo. But given
extern int baz[];
baz is a variable of type array UNKNOWN-SIZE of int. This is an incomplete
type, you cant take (sizeof baz). You cannot assign to baz, and although baz
will decay into a pointer in most contexts, it is not possible for (baz == NULL) ever
to be true.
The compiler will allow you to mix the array/pointer notation and will get it right,
but it needs to know what the reality is. Once you declare the array/pointer correctly, you can then access it either way.

2.22. MORE ON ARRAYS, POINTERS AND MALLOC

2.22.2

105

realloc

Suppose we have a simple array, and a subfunction for adding items to it:
#define MAXELTS 100
int array[MAXELTS];
int num_of_elements = 0;
install(int x)
{
if(num_of_elements >= MAXELTS){
fprintf(stderr, "too many elements (max %d)\n", MAXELTS);
exit(1);
}
array[num_of_elements++] = x;
}
Lets see how easy it is to remove the arbitrary limitation in this code, by dynamically
re-allocating the array:
#include <stdlib.h>
int *array = NULL;
int nalloc = 0;
int num_of_elements = 0;
install(x)
int x;
{
if(num_of_elements >= nalloc){
/* Were out of space. Reallocate with space for 10 more ints */
nalloc += 10;
array = (int *)realloc((char *)array, nalloc * sizeof(int));
if(array == NULL){
fprintf(stderr, "out of memory with %d elements\n",
num_of_elements);
exit(1);
}
}
array[num_of_elements++] = x;
}
If you want to be true-blue ANSI, use size t for nalloc and num of elements.

106

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

When dynamically allocating a multidimensional array, it is usually best to allocate


an array of pointers, and then initialize each pointer to a dynamically-allocated
row. The resulting ragged array can save space, although it is not necessarily
contiguous in memory as a real array would be. Here is a two-dimensional example:
/* create an array of pointers */
int **array = (int **)malloc(nrows * sizeof(int *));
if (array == NULL){
fprintf(stderr,"Out of memory\n");
exit(1);
}
for(i = 0; i < nrows; i++){
/* create space for an array of ints */
array[i] = (int *)malloc(ncolumns * sizeof(int));
if (array[i] == NULL){
fprintf(stderr,"Out of memory\n");
exit(1);
}
}
You can keep the arrays contents contiguous, while making later reallocation of
individual rows difficult, with a bit of explicit pointer arithmetic:
int **array = (int **)malloc(nrows * sizeof(int *));
if (array == NULL){
fprintf(stderr,"Out of memory\n");
exit(1);
}
array[0] = (int *)malloc(nrows * ncolumns * sizeof(int));
if (array[0] == NULL){
fprintf(stderr,"Out of memory\n");
exit(1);
}
for(i = 1; i < nrows; i++)
array[i] = array[0] + i * ncolumns;
In either case, the elements of the dynamic array can be accessed with normal-looking
array subscripts: array[i][j].
If the double indirection implied by the above schemes is for some reason unacceptable, you can simulate a two-dimensional array with a single, dynamically-allocated
one-dimensional array:
int *array = (int *)malloc(nrows * ncolumns * sizeof(int));

2.23. SIGNALS AND ERROR HANDLING

107

However, you must now perform subscript calculations manually, accessing the i,j
th element with array[i * ncolumns + j].

2.23

Signals and error handling

Various signals (interrupts) can be received by your program. See the signal.h
include file for a list. You can trap them if you wish, or simply ignore them. E.g.
#include <signal.h>
...
/* this will ignore control-C */
signal(SIGINT, SIG_IGN);

The following code sets a timebomb. After Timer is called, the program will
continue execution until n milliseconds have passed, then normal execution will be
interrupted and onalarm() will be called before normal execution is resumed.
#include <signal.h>
static void onalarm(void)
{
something();
signal(SIGALRM,SIG_DFL);
}
...
void Timer(int n)
/* waits for n milliseconds */
{
long usec;
struct itimerval it;
if (!n) return;
usec = (long) n * 1000;
memset(&it, 0, sizeof(it));
if (usec>=1000000L) { /* more than 1 second */
it.it_value.tv_sec = usec / 1000000L;
usec %= 1000000L;
}
it.it_value.tv_usec = usec;
signal(SIGALRM,onalarm);

108

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

setitimer(ITIMER_REAL, &it, (struct itimerval *)0);


}
This same method can be used to catch emergency signals like SIGBUS (bus error)
too.

2.24

ANSI C

In 1983, the American National Standards Institute commissioned a committee,


X3J11, to standardize the C language. After a long, arduous process, including
several widespread public reviews, the committees work was finally ratified as an
American National Standard, X3.159-1989, on December 14, 1989, and published
in the spring of 1990. For the most part, ANSI C standardizes existing practice,
with a few additions from C++ (most notably function prototypes) and support
for multinational character sets (including the much-lambasted trigraph sequences).
The ANSI C standard also formalizes the C run-time library support functions.
The published Standard includes a Rationale, which explains many of its decisions,
and discusses a number of subtle points, including several of those covered here.
(The Rationale is not part of ANSI Standard X3.159-1989, but is included for
information only.) The Standard has been adopted as an international standard,
ISO/IEC 9899:1990, although the Rationale is currently not included.
2.24.1

Converting to ANSI C

Many K&R C programs compile with an ANSI C compiler without changes. Where
changes are required, the compiler will nearly always tell you. A list of differences
between K&R C and ANSI C is in [?]. The most important are
Function prototyping :- Function prototypes arent mandatory in ANSI C, but
they improve error checking. Their use enables certain ANSI C features which
otherwise, for backward compatibility, are suppressed.
Parameter Passing : Floats are passed as floats (in K&R C floats are converted to doubles when passed to a function)
Arguments are automatically cast into the right form for the called function. Without the function prototyping the following program wouldnt
work because mean is expecting 2 integers.
#include <stdio.h>
#include <stdlib.h>
int mean(int a,int b)
{
return a + b;

2.25. MATHS

109

}
main()
{
int i;
float f;
int answer;
i = 7;
f= 5.3;
/* deliberate mistake! */
answer = mean(f,j);
printf("%f + %d = %d\n", f, j, answer);
}
Standardisation :- The standard include files for ANSI C are
assert.h
ctype.h
errno.h
float.h
limits.h
locale.h
math.h
setjmp.h
signal.h
stdarg.h
stddef.h
stdio.h
stdlib.h
string.h
time.h

Assertions
Character identification
Error handling
Max and Min values for floats
limits for integral types
Internationalisation info
Advanced math functions
Non-local jump
Exception handling
Variable numbers of arguments
Standard definitions
Input/Output
General Utilities
String Manipulation
Date and Time functions

If you want to support both ANSI C and K&R C , you can use the following construction
#ifdef __STDC__
/* ANSI code */
#else
/* K and R code */
#endif

2.25

Maths

If youre using any of the maths routines remember that youll need to mention the
maths library on the compile line (otherwise the maths code wont be linked in) and

110

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

that youll need to include math.h (otherwise the values returned by the routines
could be misinterpreted).
Before you start writing much maths-related code, check to see that it hasnt all been
done before. Many maths routines, including routines that offer arbitrary precision
are available by ftp from netlib.att.com. Also see the CUED help/languages/C/math_routines
file in the gopher.eng.cam.ac.uk gopher for a long list of available resources.
One problem when writing numerical algorithms is obtaining machine constants. On
Suns they can be obtained in <values.h>. The ANSI C standard recommends that
such constants be defined in the header file <float.h>. Suns and standards apart,
these values are not always readily available.
The NCEG (Numerical C Extensions Group) is working on proposing standard
extensions to C for numerical work, but nothings ready yet, so before you do any
heavy computation, especially with real numbers, I suggest that you browse through
a Numerical Analysis book. Things to avoid are
Finding the difference between very similar numbers (if youre summating an
alternate sign series, add all the positive terms together and all the negative
terms together, then combine the two).
Dividing by a very small number (change the order of operations so that this
doesnt happen).
Multiplying by a very big number.
Common problems that you might face are :Testing for equality :- Real numbers are handled in ways that dont guarantee
expressions to yield exact results. Its risky to test for exact equality. Better is
to use something like
d = max(1.0, fabs(a), fabs(b))
and then test fabs(a - b) / d against a relative error margin. Useful constants in float.h are FLT_EPSILON, DBL_EPSILON, and LDBL_EPSILON, defined
to be the smallest numbers such that
1.0f + FLT_EPSILON != 1.0f
1.0 + DBL_EPSILON != 1.0
1.0L + LDBL_EPSILON != 1.0L
respectively.
Avoiding over- and underflow :- You can test the operands before performing
an operation in order to check whether the operation would work. You should
always avoid dividing by zero. For other checks, split up the numbers into
fractional and exponent part using the frexp() and ldexp() library functions
and compare the resulting values against HUGE (all in <math.h>).

2.25. MATHS

111

Floats and Doubles :- K&R C encouraged the interchangeable use of float and
double since all expressions with such data types where always evaluated using
the double representation a real nightmare for those implementing efficient
numerical algorithms in C. This rule applied, in particular, to floating-point
arguments and for most compilers around it does not matter whether one defines
the argument as float or double.
According to the ANSI C standard such programs will continue to exhibit the
same behavior as long as one does not prototype the function. Therefore, when
prototyping functions make sure the prototype is included when the function
definition is compiled so the compiler can check if the arguments match.
Keep in mind that the double representation does not necessarily increase
the precision. Actually, in most implementations the worst-case precision
decreases but the range increases.
Do not use double or long double unnecessarily since there may a large
performance penalty. Furthermore, there is no point in using higher precision if the additional bits which will be computed are garbage anyway.
The precision one needs depends mostly on the precision of the input data
and the numerical method used.
Infinity :- The IEEE standard for floating-point recommends a set of functions
to be made available. Among these are functions to classify a value as NaN,
Infinity, Zero, Denormalized, Normalized, and so on. Most implementations provide this functionality, although there are no standard names for the
functions. Such implementations often provide predefined identifiers (such as
_NaN, _Infinity, etc) to allow you to generate these values.
If x is a floating point variable, then (x != x) will be TRUE if and only if x
has the value NaN. Many C implementations claim to be IEEE 748 conformant,
but if you try the (x!=x) test above with x being a NaN, youll find that they
arent.
In the mean time, you can write your own standard functions and macros, and
provide versions of them for each system you use. If the system provides the
functions you need, you #define your standard functions to be the system
functions. Otherwise, you write your function as an interface to what the
system provides, or write your own from scratch.
See matherr(3) for details on how to cope with errors once theyve happened.

2.25.1

Fortran and C

Here are some opinions that experienced programmers have given for why fortran
has not been replaced by C for numerical work:
C is definitely for wizards, not beginners or casual programmers. Usually
people who are heavily into numerical work are not hacker types. They are

112

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

mathematicians, scientists, or engineers. They want to do calculations, not


tricky pointer manipulations. fortrans constructs are more obvious to use,
while even simple programs in C tend to be filled with tricks.
fortran is dangerous to use, but not as dangerous as C. For instance, most
fortran compilers have subscript checking as an option, while I have never
encountered a C compiler with this feature. The ANSI standard for function
prototypes will give C an edge over fortran in parameter mismatch error.
There is a large body of well tested mathematical packages available for fortran, that are not yet available in C; for example the IMSL package. However,
this situation is improving for C.
In studies done at Cray Research, they found it took significantly longer for
their programmers to learn C and the number of errors generated in coding in
C (as opposed to fortran) was much higher.
C is hard to optimize, especially if the programmer makes full use of Cs
expressivity. Newer C features (like the const keyword etc) and new software
technology are improving the situation.
Some (old) implementations of C still have too many system dependent aspects
(e.g. round up or down when dividing negative integers).
Whether or not the switch to C is worthwhile will depend on whether its quirks
outweigh the benefits of having more modern data typing and control structures.
ANSI C goes a long way to removing the quirks but for the time being fortran is
probably more portable and will run faster on supercomputers without tweaking. On
the other hand fortran may be harder to maintain, and it is a poor fit to algorithms
that are best expressed with types more involved than n-dimensional arrays. When
Fortran9X becomes commonplace, perhaps the decision will be easier to make.

2.25.2

Exercises 1

#include <stdio.h>
#include <stdlib.h>
int odd(int number){
/* return 0 if number is even, otherwise return 1 */
if ( (number/2)*2 == number)
return 0;
else
return 1;
}
int main(){
int i;
i = 7;
printf("odd(%d) = %d\n",i,odd(i));
}

2.25. MATHS

#include <stdio.h>
#include <stdlib.h>
void binary(unsigned int number){
/* print decimal number in binary */
unsigned int power_of_2;
power_of_2=1;
/* Find the greatest power of 2 which isnt more
than the number
*/
while (power_of_2<= number)
if (power_of_2*2>number)
break;
else
power_of_2=power_of_2*2;
/* Now print out the digits */
while(power_of_2>0){
if( number/power_of_2 == 1){
printf("1");
number = number - power_of_2;
}
else
printf("0");
power_of_2=power_of_2/2;
}
printf("\n");
}
int main(){
unsigned int i;
i=187;
printf("%d in binary is ",i);
binary(i);
}

#include <stdio.h>
#include <stdlib.h>
void base(unsigned int number, unsigned int base){
/* Print number to a specified base */
unsigned int power_of_base;
power_of_base=1;
/* Find the greatest power of base which isnt more
than the number
*/
while (power_of_base<= number){
if (power_of_base*base>number)
break;
else
power_of_base=power_of_base*base;
}
/* Now print out the digits */
while(power_of_base>0){
printf("%1d", number/power_of_base);
number = number - power_of_base * (number/power_of_base);
power_of_base=power_of_base/base;
}
printf("\n");
}

113

114

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

int main(){
base(87,2);
base(100,8);
}

#include <stdio.h>
#include <stdlib.h>
#define PRIME 1
/* Create aliases for 0 and 1 */
#define NONPRIME 0
int numbers[1000];
void mark_multiples(int num){
/* Set all elements which represent multiples of num to NONPRIME */
int multiple = num *2;
while (multiple < 1000){
numbers[multiple] = NONPRIME;
multiple = multiple + num;
}
}

int get_next_prime(int num){


/* find the next prime number after num */
int answer;
answer = num+1;
while(numbers[answer] == NONPRIME){
answer= answer + 1;
if (answer == 1000)
break;
}
return answer;
}

main(){
int i;
int next_prime;
/* Set all the elements to PRIME.*/
for(i=0;i<1000;i++){
numbers[i] = PRIME;
}
/* 0 and 1 arent prime, so set numbers[0] and numbers[1] to false */
numbers[0] = NONPRIME;
numbers[1] = NONPRIME;
next_prime = 2;
do{
mark_multiples(next_prime);
next_prime = get_next_prime(next_prime);
}while(next_prime < 1000);
/* Print out the indices of elements which are still set to PRIME */
for(i=0;i<1000;i++)
if (numbers[i] == PRIME)
printf(" %d ",i);

2.25. MATHS

exit(0);
}

2.25.3

Exercises 2

int ccase;
if (skew >= 0){
ccase = copy_right + function;
}
else{
bptr = bptr + chunk_bytes;
ccase = copy_left + function;
}

char *strchr(const char* str, int c)


{
while(*str !=\0){
if (*str == c)
return str;
else
str++;
}
return NULL;
}

#include <stdio.h>
#include <stdlib.h>
char * get_string(char str[])
{
printf("Input a string: ");
return gets(str);
}
main(){
int degrees;
char scale;
int return_value;
char string[1023];
while(1){
printf("Please type in a string like 20C or 15F\n");
printf("Use control-C to quit\n");
get_string(string);
return_value = sscanf(string,"%d%c",&degrees, &scale);
if (return_value != 2){
printf("Theres a mistake in your input. Try again.\n");
continue;
}
if (( scale == f)|| (scale == F))
printf("%s is %dC\n",string,((degrees-32)*5)/9);
else
if (( scale == c)|| (scale == C))
printf("%s is %dF\n",string, (degrees*9)/5+32);
else{
printf("Unable to determine whether you typed C or F\n");
printf("Try again.\n");
}
}

115

116

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

2.25.4

Exercises 3

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
/* This version of the primes program takes an optional
argument from the command line. Because it uses sqrt()
it needs the maths library.
*/
#define PRIME 1
/* Create aliases for 0 and 1 */
#define NONPRIME 0
#define DEFAULT_RANGE 1000
int maxprime;
int *numbers;
int range;
void usage(void){
printf("usage: prime [max]\n");
}

/* Set all elements which represent multiples of num to NONPRIME */


void mark_multiples(int num){
int multiple = num;
while (multiple+num <= range){
multiple = multiple+num;
numbers[multiple]= NONPRIME;
};
}
/* find the next prime in the range after num */
int get_next_prime(int num){
int answer;
answer = num+1;
while (numbers[answer] == NONPRIME){
answer= answer +1;
if (answer == maxprime)
break;
}
return answer;
}
main(int argc, char *argv[]){
int i;
int next_prime;
/* If more than 1 arg has been given , flag an error */
if (argc > 2){
usage();
exit(1);
}
/* If one arg has been given, try to read it as an integer
(sscanf returns the number of successfully scanned items)

2.25. MATHS

*/
if (argc == 2){
if (sscanf(argv[1],"%d",&range) != 1)
range = DEFAULT_RANGE;
}
else
range = DEFAULT_RANGE;
maxprime = sqrt (range);
/* Instead of a fixed size array, malloc some space */
numbers = (int*) malloc( (range+1)* sizeof (int));
/* Set all the elements to PRIME.*/
for (i=0;i<range;i++)
numbers[i]= PRIME;
/* 0 and 1 arent prime, so set numbers[0] and numbers[1] to false */
numbers[0] = NONPRIME;
numbers[1] = NONPRIME;
next_prime = 2;
do{
mark_multiples(next_prime);
next_prime = get_next_prime(next_prime);
} while(next_prime <= maxprime);
/* Print out the indices of elements which are still set to PRIME */
for (i=0;i<range;i++)
if (numbers[i]== PRIME)
printf("%d\n",i);
exit(0);
}

#include <stdio.h>
#include <stdlib.h>
#define NUMBERCOUNT 10 /* the number of numbers */
int main()
{
/* Read 10 numbers from a file called data */
int numbers[NUMBERCOUNT];
FILE *fp;
int i, return_value;
float total;
char line[100];
fp = fopen("data","r");
if (fp == NULL){
fprintf(stderr,"Cannot open data for reading. Bye.\n");
exit(1);
}
/* Read the numbers in */
for(i=0;i<NUMBERCOUNT;i++){
if (fgets(line,100,fp) == NULL){
fprintf(stderr,"End of file reached too early. Bye.\n");
exit(1);
}
return_value = sscanf(line,"%d", numbers+i);
if (return_value !=1){

117

118

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

fprintf(stderr,"Cannot parse line %d of data. Bye.\n",i+1);


exit(1);
}
}
fclose(fp);
/* Now calculate the total */
total=0.0;
for(i=0;i<NUMBERCOUNT;i++){
total=total+numbers[i];
}
printf("The total is %.3f. The average is %.3f\n",
total,total/NUMBERCOUNT);
}

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define UIDCOUNT 10
#define MAXUIDLENGTH 20
main()
{
/* Sort the first 10 uids in the password file */
char uids[UIDCOUNT][MAXUIDLENGTH];
char *cptr;
FILE *fp;
int i, return_value;
float total;
char line[100];
fp = fopen("/etc/passwd","r");
if (fp == NULL){
fprintf(stderr,"Cannot open /etc/passwd for reading. Bye.\n");
exit(1);
}
/* Read each of the first ten lines into the line array.
Replace the first : by a \0. Copy the resulting
truncated string into the uids array
*/
for(i=0;i<UIDCOUNT;i++){
if (fgets(line,100,fp) == NULL){
fprintf(stderr,"End of file reached too early. Bye.\n");
exit(1);
}
cptr = strchr(line,:);
if (cptr == NULL){
fprintf(stderr,"Strange line in /etc/passwd. Bye.\n");
exit(1);
}
*cptr = \0;
strncpy(uids[i],line,MAXUIDLENGTH);
}
/* See the qsort man page for an explanation of the following.
Note that strcmp doesnt precisely match the man pages
requirements, so you may get a warning message on compiling
*/

2.25. MATHS

qsort((void*) uids, (size_t) UIDCOUNT, MAXUIDLENGTH, strcmp);


/* Print the sorted list */
for(i=0;i<UIDCOUNT;i++){
printf("%s\n", uids[i]);
}
}

#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 50
#define MAX_STR_LEN 64
#define EMPTY (-1)
typedef struct _entry {
int value;
struct _entry *next;
char str[20];
} Entry;

char str[MAX_STR_LEN];
/* Create an array of elements of type Entry */
Entry *table[TABLE_SIZE];
int process(char *str){
int value = 1;
while (*str){
value = value * (*str);
str++;
}
return value;
}
char * get_string(char str[])
{
printf("Input a string\n");
return gets(str);
}
int hashfn(char *str){
int total = 0;
int i;
while (i = *str++)
total += i;
return total % TABLE_SIZE;
}

void set_table_values(void){
/* set all the entries in the table to NULL
*/
int i;
for (i =0;i<TABLE_SIZE;i++)
table[i] = NULL;
}
void set_entry(Entry *entry, char *str){
strcpy(entry->str,str);
entry->value = process(str);

119

120

CHAPTER 2. ANSI C FOR PROGRAMMERS ON UNIX SYSTEMS

}
Entry *create_an_entry(void){
Entry *entry;
entry = (Entry*) malloc(sizeof (Entry));
return entry;
}

main(){
int bucket;
int value;
set_table_values();
/* Use get_string repeatedly. For each string:use the hash function to find the strings entry
in the table.
*/
while(get_string(str)){
if (! strcmp(str,"end")){
printf("Program ended\n");
exit(0);
}
bucket = hashfn(str);
value = find_entry(&(table[bucket]), str);
printf("Value of <%s> is %d\n",str,value);
}
}

Chapter 3

Operating Systems Theory


3.1

Process Synchronization

A process is a program whose execution has started but not yet terminated. At any
one moment a process need not be actually executing.
Three possible states for a process are:
Running: the process is executing on a processor.
Ready: the process is ready to execute but all the processors are in use.
Blocked: the process is waiting for some event to occur before it can continue
with its execution.
An operating system must maintain a data structure that describes the current
status of all live processes. This structure is called a process control block or pcb and
commonly contains the following fields:
Name: an identifier for the process.
State: running, ready or blocked.
Re-Start Info: program counter, register contents, interrupt masks, etc.
Priorities: for deciding who gets the resources next.
Permissions for access control of resources.
Ownerships: list of resources currently held by process.
Accounting: time used, memory held, I/O volume logged, etc.
During the life of any particular process an operating system must perform operations that can result in a change to the information in the PCB of the process.
Typical operations include:

122

CHAPTER 3. OPERATING SYSTEMS THEORY

Create: start a new PCB.


Delete: remove a PCB.
Signal: Indicate that a specific event has occured.
Wait: Stop execution until an event is signaled.
Schedule: Assign a runable process to an available processor.
Change-Priority: to influence future resource acquisition rights.
Suspend: either suspend-ready or suspend-blocked depending on the current state
of the process.
Resume: change suspend-ready to ready or suspend-blocked to blocked.

3.1.1

Common synchronization problems

A set of processes is called determinate if given the same input, the same results are
produced regardless of the order of execution of the processes. Determinate systems
are easy to control, the operating system can let them execute in any order and the
results are always the same. However in real life sets of processes are usually not
determinate and the operating system must synchronize their execution in order
that a prefered result is attained. Some common synchronization problems are:
Mutual exclusion problem: In many computer systems, processes must cooperate with each other when accessing shared data. The designer must ensure
that only one process at a time modifies the shared information. During the
execution of such critical sections of code mutual exclusion must be ensured.
Producer-Consumer problem: In this problem a set of producer processes provide messages to a set of consumer processes. They all share a common pool
of space into which messages may be placed by the producers and removed by
the consumers.
Reader-Writer problem: In this problem reader processes access shared data
but do not alter it while writer processes change the contents of the shared
data. Any number of readers should be allowed to proceed concurrently in the
absence of a writer, but writers must insist on mutual exclusion while in their
critical section.
It turns out that if one can solve the mutual exclusion problem then all the other
common sychronization problems are solvable. First we will examine some historical
attempts to solve the mutual exclusion problem via software then we will introduce
the hardware concept of a semaphore which provides a modern solution.

3.1. PROCESS SYNCHRONIZATION

3.1.2

123

Mutual exclusion

Consider a system of two cooperating processes P0 and P1 . Each process has a


segment of code, called a critical section, in which the process may be reading or
writing common variables. The important feature of such a system is that when one
process is executing in its critical section the other process must be prevented from
executing its critical section. We say that the execution of critical sections must be
mutually exclusive in time.
To solve the mutual exclusion problem we must design a protocol which the process
must use to cooperate and ensure mutual exclusion. Each process must request
permission to enter its critical section by executing socalled entry code and after
completing its critical section must execute exit code so that the next process can
enter its critical section. The following constraints must be observed by any practical
mutual exclusion solution.
1 Only basic machine language instructions are atomic.
2 No assumptions may be made concerning the relative execution speeds of the
cooperating processes.
3 When one process is in a non-critical section of code it may not prevent the
other process from entering its critical section.
4 When both processes want to enter their critical section the decision about
which one to grant access to cannot be postponed indefinately.
Our first attempt at a solution to the mutual exclusion problem with constraints is
to let both processes share a common variable called turn initialized to 0 or 1, and
then use the protocol that if turn = i then process Pi is allowed to execute in its
critical section. Each cooperating process would loop as follows:
program P(i)
common variable: turn: 0..1 = 0;
repeat
while turn <> i do nothing;
critical section
turn = j;
non-critical section
until false;

124

CHAPTER 3. OPERATING SYSTEMS THEORY

This solution ensures that only one process at a time can be in its critical section,
however, constraint number 3 is not satisfied since strict alternation of processes in
the execution of thier critical section is required.
The problem with this attempted solution is that it fails to remember the state of
each process but remembers only which process is next. To remedy the situation
we could use a common flag for each process. The idea is for a process to set its
flag before entering its critical section and only do this if the other processs flag is
unset. The cooperating processes would loop as follows:
program P(i)
common variables:
flag: array[0..1] of boolean = {false, false};
repeat
while flag[j] do nothing;
flag[i] = true;
critical section
flag[i] = false;
non-critical section
until false;
Unfortunately this algorithm does not ensure mutula exclusion. Consider the following sequence of events:
P0 enters the while statement and finds flag[1]=false.
P1 enters the while statement and finds flag[0]=false.
P1 sets flag[1]=true and enters its critical section.
P0 sets flag[0]=true and enters its critical section.
This sequence of events allows P0 and P1 to enter their critical sections at the same
time and mutual exclusion is not ensured. The problem is with the non-atomic
nature of the entry code. It does not help to interchange the order of the assignment
and the while loop in the entry code since in that case both processes may exclude
each other indefinately and thus violate constraint number 4.
It appears that no simple solution to the mutual exclusion problem exists but a
correct solution was discovered in 1964 by the dutch mathematition, Dekker. This
solution combines both of the previous attempts as follows:

3.1. PROCESS SYNCHRONIZATION

125

program P(i)
common variables:
flag: array[0..1] of boolean = {false, false};
turn: 0..1 = 0;
repeat
flag[i] = true;
while flag[j] do
if turn = j then
begin
flag[i] = false;
while turn = j do nothing;
flag[i] = true;
end;
critical section
turn = j;
flag[i] = false;
non-critical section
until false;
We leave it to the reader to convince himself that Dekkers solution ensures mutual
exclusion and that indefinite blocking cannot occur.
When more that two processes are involved in the mutual exclusion problem the
solution is more complicated. In 1965 another Dutch mathematition, Dijkstra solved
the n process problem. His solution was refined by Knuth and then DeBruijn, and
finally by Eisenberg and McGuire in 1972 to produce the following n process solution
that satifies not only all the constraints 1 to 4, but is also fair in the sense that every
process can eventually enter its critical section even if access requirements are greater
than total time allows.
program P(i)
common variables:
flag: array[0..n-1] of (idle, want, in) = {idle, ...};
turn: 0..n-1 = 0;
ordinary variables:
j: integer;
repeat
repeat

126

CHAPTER 3. OPERATING SYSTEMS THEORY

flag[i] = want;
j = turn;
while j <> i do
if flag[j] <> idle then
j = turn
else
j = j+1 mod n;
flag[i] = in;
j = 0;
while ( j<n ) and ( j=i or flag[j]<>in ) do inc(j);
until (j >= n) and ( turn = i or flag[turn] = idle);
turn = i;
critical section
j = turn+1 mod n;
while (j<>turn) and (flag[j] = idle) do j = j+1 mod n;
turn = j;
flag[i] = idle;
non-critical section
until false;
We leave it to the reader to convince himself that the Eisenberg and McGuire solution
ensures mutual exclusion and that indefinite blocking cannot occur.
3.1.3

Semaphores

In modern computer instruction sets, the problem of entry and exit code for the
mutual exclusion is solved by supplying an atomic instruction that does the job.
The data structure associated with this instruction is called the semaphore and a
full definition is as follows:
A semaphore is an integer variable, S, and an associated group of waiting processes,
Q(S), upon which only two operations, P and V , may be performed.
P (S) if S 1 then S = S 1 else the executing process places itself in Q(S) and
goes to sleep.
V (S) if Q(S) is non-empty then wake up one waiting process and make it available
for execution else S = S + 1.
The operating system must offer P and V as indivisible instructions. This means
that once they start executing they cannot be interrupted until they have completed. Note also that in the definition of V (S), no rules are laid down to identify

3.1. PROCESS SYNCHRONIZATION

127

which waiting process is reactivated. In most operating systems this decision is


implementation dependent.
To solve the mutual exclusion problem using a semaphore the following scheme can
be used:
shared vars:

S: semaphore = 1;

Process i:
loop
...
P(S)
...
critical section
...
V(S)
...
non-critical section
...
endloop

Process j:
loop
...
P(S)
...
critical section
...
V(S)
...
non-critical section
...
endloop

To see that this scheme will work consider the following two senarios:
1: Process i goes in and out of its critical section while processes j, k, ... do
not attempt entry. That is: S = 1; i : P (S), S = 0; i enters; i:V(S), S = 1;
i exits; and the initial configuration is restored.
2: Prosess i goes in, j attempts entry and k, l, ... are not interested. That
is: S = 1; i : P (S); S = 0; i enters; j : P (S); j waits; i : V (S); i exits, j
enters; j : V (S); S = 1; j exits; and the initial configuration is restored.

3.1.4

Producer/Consumer problem via semaphores

In this problem we have many producers producing messages which are consumed
by many consumers. Howeve, there are only a finite number of message buffers:
shared vars: nrfull: semaphore = 0
nrempty: semaphore = N
mutexP: semaphore = 1
mutexC: semaphore = 1
buff: array [0..N-1] of message
in, out: 0..N-1 = 0
producer i:
loop

consumer j:
loop

128

CHAPTER 3. OPERATING SYSTEMS THEORY

...
create a message m

...

P(mutexP)
{one producer at a time}

P(mutexC)
{one consumer at a time}

P(nrempty)
{wait for an empty cell}
buff[in] = m
in = in + 1 mod N

m = buff[out];
out = out + 1 mod N

V(nrfull)
{signal a full cell}

V(nrempty)
{signal an empty cell}

V(mutexP)
{let the next producer in}
...
endloop

3.1.5

P(nrfull)
{wait for a message}

V(mutexC)
{let next consumer in}
consume message
endloop

Reader/Writer problem via semaphores

In this problem we have a number of writer programs that must exclude all readers
and other writers when in their critical section. We also have a number of reader
routines who can perform their read operations concurrently but writer routines
must be excluded. The following semaphore solution gives priority to the readers:
shared vars:

mutexW, mutexR: semaphore = 1


nr: integer = 0

reader i:

writer j:

loop
...
P(mutexR)
{readers enter one at a time}

loop
...
P(mutexW)
{wait}

if nr=0 then P(mutexW)


{first reader inhibits writers}
nr = nr+1

critical section
for writers
V(mutexW)
{signal}

V(mutexR)
{allow other readers in/out}

endloop

3.1. PROCESS SYNCHRONIZATION

129

critical reader section


P(mutexR)
{readers exit one at a time}
nr = nr-1
if nr=0 then V(mutexW)
{last out allows writers in}
V(mutexR)
{allow other readers in/out}
endloop

3.1.6

Exercises

1: Use a semaphore with P and V operations to control the trafic flow at the
intersection of two one-way streets. The following rules should be satisfied:
Only one car can be crossing at any given time.
When cars are approaching the intersection from both directions they
should take turns at crossing so as to prevent indefinite postponements
in either street.
A car approaching from one street should always be allowed to cross the
intersection if there are no cars approaching from the other street.
A solution to this problem is two algorithms for crossing. One algorithm for
cars comming from one direction and another algorithm for cars comming from
the other direction.
2: Consider a barbershop that has three barber chairs, three barbers, one till,
and one cashier. The shop also contains a sofa that can seat four waiting
customers, and standing room area for further waiting customers. Assume that
at most twenty customers can be inside the barbershop at any one time.
A customer enters the shop, provided it is not full and once inside takes a seat
on the sofa or stands if the sofa is fully occupied. When a barber is free the
customer who has been on the sofa for the longest is served and if there are any
standing customers the one who has been in the shop the longest takes a seat
on the sofa. Whan a customers haircut is finished the cashier accepts payment
and gives the customer his recipt. Because there is only one till payment is
accepted for one customer at a time. The barbers divide their time between
cutting hair and sleeping in their barber chair if there are no customers to be
served.
Solve this concurrency problem by writing three algorithms. One each for
customers, barbers and cashiers. Make a table of all the semaphores you use
indicating what the P and V operations denote for each.

130

CHAPTER 3. OPERATING SYSTEMS THEORY

3: There are five philosophers sitting at a round table. On the table are five
plates, five forks (one to the left of each plate), and a bottomless serving bowl
of spaghetti at the center of the table. The philosophers spend their time either
thinking or eating. Thinking is easy as the philosopher does not require any
utensils to do it. Eating on the other hand requires two forks, one from the
left and one from the right. On completion of an eating period the philosopher
will replace the two forks in their original positions and return to thinking.
Design an algorithm for a philosopher to follow that allows all philosophers to
think and eat to their hearts content.

3.2. INTERPROCESS COMMUNICATION UNDER UNIX

3.2

131

InterProcess Communication under UNIX

UNIX now offers new powerful interprocess communication (IPC) facilities. These
include message queues, shared memory segments and semaphores. In this section we
show how to call UNIX IPC routines to implement shared memory and semaphores.
UNIX offers two shell commands to monitor the current IPC state and to delete
unwanted IPC structures. The formats are:
ipcs [options]
ipcrm [options]
Use the man pages to get information on the available options.
To tackle the group project layed out later on in this document, you will have to
implement shared memory and semaphores in Perl. Make sure you have read the
intro Perl documentation and completed the intro Perl tutorials before you start
with IPC.

3.2.1

Shared Memory

Shared memory is a section of main computer memory which can be shared by one
or more independent processes. Shared memory is attached to the data segment of
a process and can appear at a different address in each process. Shared memory
accesses by different processes must be synchronized through the use of semaphores.
UNIX provides the system call shmget to create a shared memory segment along
with its associated data structures. The segment is then attached to a processes
address space through the use of the system call, shmat. shmget returns the shared
memory identifier shmid which is later used by the system call shmctl to update
the contents of shared memory. The system call, shmdt is used to detach a shared
memory segment from a processes address space.
Full specifications for the above system calls can be found in the man pages. The
following sample program, written in C, (the source of which can be downloaded
from my website) shows you how to create and use a shared memory segment.
#include
#include
#include
#include
#include
#define
#define
#define
#define

<stdio.h>
<stdlib.h>
<sys/types.h>
<sys/ipc.h>
<sys/sem.h>

ERROR (-1)
SMSIZE 512 *1
RECSIZE 10
NORECS 50

132

CHAPTER 3. OPERATING SYSTEMS THEORY

int myseg;
/* char *buf[NORECS][RECSIZE]; */
char *buf;
extern char *shmat();
main()
{ char c[2];
printhelp();
while(1)
{
printf(" command (h = help) ... ");
scanf("%s",c);
if(c[0] == q ) {
printf("quitting \n");
exit(0); };
if(c[0] == c ) {
printf("Creating a shared segment .... \n");
myseg = startSeg();
printf("Segment %d created \n",myseg); };
if(c[0] == w) {
printf("Writing \n");
writeSeg(); };
if(c[0] == r) {
printf("Reading \n",myseg);
readSeg(); };
if(c[0] == d) {
printf("Deleting shared segment %d\n",myseg);
stopSeg(myseg); };
if(c[0] == h) {
printhelp(); };
}
}
printhelp()
{ printf("\n");
printf(" c --- Create the shared segment \n");
printf(" w --- Write to segment \n");
printf(" r --- Read from segment \n");
printf(" d --- Delete the shared segment \n");
printf(" q --- Quit this program \n");
printf(" h --- print this Help \n");
printf("\n"); }

int startSeg()
{ int key, shmid;
key = 200;
if ( ( shmid = shmget( (key_t) key, SMSIZE, IPC_CREAT | IPC_EXCL | 0664 ) ) == ERROR )
{ printf(" Unable to create segment, trying to get an existing one... \n");
if( ( shmid = shmget( (key_t) key, SMSIZE, 0664 ) ) == ERROR )
printf(" Cant do that either. \n"); }
if( (buf = shmat(shmid, 0, 0)) == (char *) ERROR )
{ printf(" Unable to attach SM segment \n"); };
return shmid;
}
stopSeg(shmid)
int shmid;
{ shmdt(shmid); /* first detatch from the segment */
shmctl(shmid, IPC_RMID, 0); /* then destroy the segment */
}

3.2. INTERPROCESS COMMUNICATION UNDER UNIX

133

writeSeg()
{ int i;
char rec[RECSIZE];
printf("enter record number ... ");
scanf("%d",&i);
printf("enter a short string for record %d \n ",i);
scanf("%s",rec);
printf("the string entered was ...%s...\n",rec);
memcpy(buf+(i*RECSIZE),rec,RECSIZE); }
readSeg()
{ int i;
char rec[RECSIZE];
printf("enter record number ... ");
scanf("%d",&i);
memcpy(rec,buf+(i*RECSIZE),RECSIZE);
printf("the string from record %d was ...%s...\n",i,rec);
}

3.2.2

Semaphores

UNIX supplies a system call, semget to set up a semaphore with its associated
data structure. semget returns a unique positive integer known as the semaphore
identifier, semid. The semid is subsequently used by the semop system call which
updates the values in the semaphore data structure.
The UNIX semaphore structure contains the variables, semval, semzcnt and sempid.
The variable semval is a non-negative integer whose value is changed by the semop
system call. semval corresponds to the semaphore integer described above in these
notes. semzcnt is an unsigned short integer that represents the number of processes
that are suspended waiting for semval to reach zero. sempid holds the id of the
process that performed the last semaphore operation on this semaphore.
Full specifications for semget and semop can be found in the man pages. The following sample program, written in C, (the source of which can be downloaded from
my website) shows you how to create and destroy a semaphore and also how to
implement the P and V mutual exclusion operators.
#include
#include
#include
#include
#include

<stdio.h>
<stdlib.h>
<sys/types.h>
<sys/ipc.h>
<sys/sem.h>

#define ERROR (-1)


int mysem, myval;
main()
{ char c[1];
printhelp();
while(1)

134

CHAPTER 3. OPERATING SYSTEMS THEORY

{
printf(" command (h = help) ... ");
gets(c);
if(c[0] == q ) {
printf("quitting \n");
exit(0); };
if(c[0] == c ) {
printf("Creating a semaphore .... \n");
mysem = startSem();
printf("Semaphore %d created \n",mysem); };
if(c[0] == p) {
printf("Doing P(%d) \n",mysem);
P(mysem); };
if(c[0] == v) {
printf("Doing V(%d) \n",mysem);
V(mysem); };
if(c[0] == d) {
printf("Deleting semaphore %d\n",mysem);
stopSem(mysem); };
if(c[0] == n) {
myval = semValue(mysem);
printf(" The current numeric value of semaphore %d is %d \n",mysem,myval); };
if(c[0] == h) {
printhelp(); };
if(c[0] == r) {
printf("starting random semaphore grabs ... \n\n");
randomtest(); };
}
}
printhelp()
{ printf("\n");
printf(" c --- Create the semaphore \n");
printf(" p --- perform P(sem) \n");
printf(" v --- perform V(sem) \n");
printf(" n --- print Numeric value of semaphore \n");
printf(" r --- test Random semaphore grabs \n");
printf(" d --- Delete the semaphore \n");
printf(" q --- Quit this program \n");
printf(" h --- print this Help \n");
printf("\n"); }
randomtest()
{ int sec, count, ppid;
count = 5;
ppid = getpid();
srand(ppid);
sec = rand();
sec = sec - (sec / 10) * 10;
printf("\n random semaphore tests using %d second delays \n",sec);
while (count > 0)
{
printf("count = %d \n",count);
P(mysem);
printf(" I
G O T
I T \n");
sleep(sec);
printf(" letting go \n");
V(mysem);
sleep(sec);
count = count - 1;
}

3.2. INTERPROCESS COMMUNICATION UNDER UNIX

135

}
int startSem()
{ int key, semid;
key = 100;
if ( ( semid = semget( (key_t) key, 1, IPC_CREAT | IPC_EXCL | 0664 ) ) == ERROR )
{ printf(" Unable to create semaphore, trying to get an existing one... \n");
if( ( semid = semget( (key_t) key, 1, 0664 ) ) == ERROR )
printf(" Cant do that either. \n"); }
else
V(semid);
return semid;
}
stopSem(semid)
int semid;
{ semctl( semid, 0, IPC_RMID, 0);
}
int semValue(semid)
int semid;
{ return semctl( semid, 0, GETVAL, 0); }
csemop(semid, val)
int semid, val;
{ struct sembuf sval;
sval.sem_num=0;
sval.sem_op = val;
sval.sem_flg=0;
/* printf(" Performing semaphore operation %d on semaphore %d ... ",val,semid); */
if( semop(semid, &sval, 1) == ERROR )
printf(" \n Unable to perform semaphore operation %d on semaphore %d \n",val,semid);
/* else
printf(" SUCESSFULLY \n"); */
}
P(semid)
int semid;
{ csemop(semid, -1); }
V(semid)
int semid;
{ csemop(semid, 1); }

The above sample codes will help you solve IPC probles. To get a nice window
like interface on aa ascii terminal, it is advisable to learn the ncurses library. This
library offers a host of system calls that allows you to obtain keystrokes from the
user and update an ascii terminal screen as appropriate to your application. Further
documentation on ncurses can be found on the web. We include a short demo
program with these notes:
/*
* ncurses_demo.c
* ===============
* hello world with half delay keyboard input
* */
#include <stdlib.h>
#include <stdio.h>

136

#include <unistd.h>
#include <curses.h>
#include <time.h>

CHAPTER 3. OPERATING SYSTEMS THEORY

/* for sleep() */

int main(void) {
int i,row,col;
int h, w;
int et;
time_t s, t;
char time_str[20];
/* initialize ncurses */
time(&s);
if (initscr() == NULL ) {
fprintf(stderr,"error initialising ncurses \n");
exit(EXIT_FAILURE);
}
/* display hello world in center of screen,
* call refresh() to update screen and sleep */
mvaddstr(13,30,"pausing for 6 secs");
/* display some chairs at random positions on screen */
srand(getpid());
getmaxyx(stdscr,h,w);
for (i=0;i<10;i++){
row = (int) ((float)h*rand()/(RAND_MAX+1.0));
col = (int) ((float)w*rand()/(RAND_MAX+1.0));
mvaddstr(row,col,"@");
}
refresh();
/* now get input from the keyboard to drive cursor */
cbreak(); /* one character at a time */
noecho(); /* supress auto echo */
keypad(stdscr, TRUE);
row = (int) (25.0*rand()/(RAND_MAX+1.0));
col = (int) (80.0*rand()/(RAND_MAX+1.0));
mvaddstr(row,col,"*");
refresh();
sleep(6);
/* now go into an inf loop reading keys and taking action */
halfdelay(1); /* allows getch to return ERR after 1/10 sec */
while (TRUE) {
int ch = getch();
switch (ch) {
case KEY_UP: {
mvaddstr(row,col," ");
row--;
if(row<0)row=0;
mvaddstr(row,col,"*");
refresh();
break;
}
case KEY_DOWN: {
mvaddstr(row,col," ");
row++;
if(row>h-1)row=h-1;
mvaddstr(row,col,"*");
refresh();
break;
}
case KEY_LEFT: {
mvaddstr(row,col," ");
col--;

3.2. INTERPROCESS COMMUNICATION UNDER UNIX

if(col<0)col=0;
mvaddstr(row,col,"*");
refresh();
break;
}
case KEY_RIGHT: {
mvaddstr(row,col," ");
col++;
if(col>w-1)col=w-1;
mvaddstr(row,col,"*");
refresh();
break;
}
case q: {
/* clean up and exit */
delwin(stdscr);
endwin();
refresh();
printf("thanks for playing \n");
return EXIT_SUCCESS;
break;
}
case ERR: {
/* no input yet */
time(&t);
et = t - s;
sprintf(time_str,"time elapsed -> %d secs",et);
mvaddstr(13,28,time_str);
refresh();
break;
}
}
}
}

137

138

CHAPTER 3. OPERATING SYSTEMS THEORY

3.2.3

Group Project 2006: Musical Chairs:

Consider the game of Musical Chairs. The game is controlled by a Disc Jockey whos
task it is to play short segments of music.
The game starts with n players who dance around n 1 chairs while a disc jockey
plays a short segment of music. When the music stops playing the n players all
scramble for the n 1 chairs and the unsuccessful player, (the one who did not
get a chair) is out of the game. Once the unsuccessful player leaves the game the
music restarts, the remaining players start dancing, a single chair is removed from
the dance floor and the next round continues with n 1 players competing for n 2
chairs.
In the final round of the game, 2 players compete for 1 chair and the successful
player in the final round is declared the overall winner of the game.
Make use of ncurses, semaphores and shared memory to simulate the Musical Chairs
game. Write one code for the disc jockey to follow and one code for the players to
follow. Make sure that you clearly specify all shared memory data with their initial
values.
3.2.4

Previous Group Projects

Group Project 2004: Snakes:

Design and code in Perl the game Snakes to run on a UNIX system from telnet
terminals. Make use of UNIX shared memory to store the current state of the
SnakePit. Make use of UNIX semaphores to solve all synchronization problems
that appear in your design.
The class will be split into groups. Each group must submit a complete solution to
the Snakes game. Each solution will consist of two Perl programs, a Monitor and
a Player. During the playing of Snakes, one copy of the Monitor program will be
running while many copies of the Player program may be running.
In the game of Snakes, the Monitor will set up a new game and allow a Player to
join as a new snake in the SnakePit. As each player joins the game he is allowed
to choose the type of snake he represents. Common types could be:
Mamba
Puffadder
Cobra
Python
Each snake type will have different fighting skills. It is up to your group to define
the set of fighting skills for each snake type.

3.2. INTERPROCESS COMMUNICATION UNDER UNIX

139

When the game starts Players move their snakes in the snake pit using the arrow
keys. Snakes can attack other snakes when their bodies intersect and the outcome
of the encounter will be determined by the current attributes of the two snakes
involved in the encounter. Snakes can grow in length when they consume food that
they encounter in the snakepit. Food may be thrown into the pit at random times
determined by the monitor.
At any one time, the Monitor displays the state of the whole snakepit. Players on
the other hand can only see part of the snakepit. (ie: in the imediate vacinity of
their snake). A players snake should be centered in his field of view and if a player
decides to move his snake to the right say then the snakepit and its contents should
move to the left.
Specifications for each snake type should be loaded at the start from an ascii text
file so that the group can experiment with different specifications to determine those
that result in the best game.
Player programs send their game commands to the monitor. The monitor must stack
up incomming commands against each player. The monitor executes the commands
by cycling through the players stacks and executing the command at the bottom of
each players stack (if any). In this way it is possible for a quick thinking player to
get more than one command executed before his slower counterpart executes any.
After executing each command the state of the battle must be updated and the
new state reflected on the monitor screen and all the players screens. Access to the
command stack must be controlled through the use of semaphores.
The group must decide how a game is won and what messages to display on a players
screen when he wins the game or when his snake dies before the game is over.
At the end of this project each group will be asked to demonstrate their SNAKES
game to the rest of the class. Each group must also hand in a design document,
outlining the design of their SNAKES game, especially how semaphores were used
to solve the multi-user clashing problem. Each group must also construct a web-site
that delivers their SNASKES game to the world as a gzipped tar file containing
code, installation instructions and a players user manual.

BattleShips: 2002

Design and code into Perl the game BattleShips to run on a UNIX system from
telnet terminals. Make use of UNIX shared memory to store the current state of
the BattleShips board. Make use of UNIX semaphores to solve all synchronization
problems that appear in your design.
The class will be split into 6 groups according to machine name. Each group must
submit a complete solution to the BattleShips game. Each solution will consist of
two Perl programs, a Monitor and a Player. During the playing of BattleShips,
one copy of the Monitor program will be running while many copies of the Player

140

CHAPTER 3. OPERATING SYSTEMS THEORY

program may be running.


In the game of BattleShips, the Monitor will set up a new game and allow a Player
to join one of two fleets taking part in a battle. As each player joins a fleet, he is
allowed to choose the type of ship he represents. Common types are:

Battleship
Destroyer
Cruiser
Frigate
Submarine

When the game starts Players move their ships on the board using the arrow
keys. Players can also fire missiles at other ships taking part in the battle. The
different ships will have different attributes. For example, Battleships can only turn
slowly but have relatively large fire-power. Ships must return to their home port to
replenish supplies when these run low.
At any one time, the Monitor displays the state of the whole battle. Players on
the other hand can only see part of the battle board. (ie: in the imediate vacinity of
their ship). Some ships may be able to fire missiles further than their field of view.
Ships from the same fleet should be able to pass messages to each other.
Specifications for each ship should be loaded at the start from an ascii text file so
that the players can experiment with different specifications to determine those that
result in the best game.
The board should consist of an ascii window on the world. The world is all sea
apart from two islands which have home ports for the ships in their fleets. Use ascii
characters to represent the ships and the islands and the ports. The world is round
and any ship sailing over an edge appears at the opposite edge.
Players send their battle commands to the monitor. The monitor must stack up
incomming commands against each player. The monitor executes the commands by
circling the players and executing the command at the bottom of each players stack
(if any). In this way it is possible for a quick thinking player to get more than one
command executed before his slower counterpart executes any. After executing each
command the state of the battle must be updated and the new state reflected on
the monitor screen and all the players screens.
Each group will be awarded a mark for their project. Individual members of a group
could be asked a spot quiz on their projects to determine their contribution to the
group effort.

3.2. INTERPROCESS COMMUNICATION UNDER UNIX

141

Sychronized Clocks, 2001

Consider the following one-player game: There are nine clocks in a 3*3 array (figure
1). The goal is to return all the dials to 12 oclock.
|-------|
|
|
|---O
|
|
|
|-------|
A

|-------|
|
|
|---O
|
|
|
|-------|
B

|-------|
|
|
|
|
O
|
|
|
|-------|
C

|-------|
|
|
|
O
|
|
|
|
|-------|
D

|-------|
|
|
|
O
|
|
|
|
|-------|
E

|-------|
|
|
|
O
|
|
|
|
|-------|
F

|-------|
|
|
|
O
|
|
|
|
|-------|
G

|-------|
|
|
|
O---|
|
|
|-------|
H

|-------|
|
|
|
O
|
|
|
|
|-------|
I

(Figure 1)

There are nine different allowed ways to turn the dials on the clocks. Each such way
is called a move. Select for each move a number 1 to 9. That number will turn the
dials 90 (degrees) clockwise on those clocks which are affected according to figure
2 below.
Move
1
2
3
4
5
6
7
8
9

Affected clocks
ABDE
ABC
BCEF
ADG
BDEFH
CFI
DEGH
GHI
EFHI

(Figure 2)

For example let a time be represented by a number accoring to following table:

142

0
1
2
3

=
=
=
=

CHAPTER 3. OPERATING SYSTEMS THEORY

12 oclock
3 oclock
6 oclock
9 oclock

then the sequence of moves, (5,8,4,9), will return all the clocks to noon as follows:
3 3 0
2 2 2
2 1 2

5->

3 0 0
3 3 3
2 2 2

8->

3 0 0
3 3 3
3 3 3

4 ->

0 0 0
0 3 3
0 3 3

9->

0 0 0
0 0 0
0 0 0

Your operating systems group must construct a multi-player version of this game.
The multi-player version of the game should allow for a maximum of six players.
Each player should view a screen showing the state of all nine clocks.
The object of the multi-player version of the clocks game is still to solve the clock
problem, but by allowing any of the players to make moves at any time. Whenever
a move is made the system should allocate a score for that move and update that
players total score accordingly. Once the clock puzzle has been solved, the player
with the highest total score wins.
Scoring could be done as follows: When a player makes a move, his (or her) total
score gets incremented by:
no_of_zeros(after the move) - no_of_zeros(before the move)
Note that this increment could be negative.
In your design of the multi-player clocks game, you should allow for all moves occuring in some time interval after an accepted move, to be aborted. Also as soon as
a move is accepted by the system, all players should be warned that no new moves
will be accepted until the time interval has ellapsed.
Use Perl to design and code the game Clocks to run on your UNIX box from
telnet terminals. Write two programs, one called ClockWatcher to act as an
administrator for your game, and the other called ClockPlayer for each player to
run. Make use of UNIX shared memory to store the current state of the game. Make
use of UNIX semaphores to solve all synchronization problems that appear in your
design. Make use of the ncurses library to display the current state of the game on
a telnet terminal. Make sure that all your programs have adequate help available
to users. Produce a group report for your project in the form of a website on your
machine. Your game should be distributable from your website as a tar archive
containing appropriate README and INSTALL files.

3.2. INTERPROCESS COMMUNICATION UNDER UNIX

143

Game of Life, 2000

The Game of Life is the most well-known cellular automaton, invented by John
Conway and popularized in Martin Gardners Scientific American column starting
in October 1970. The game was originally played by hand with counters, but implementation on a computer greatly increased the ease of exploring patterns.
The Life cellular automaton is run by filling in a number of cells on a 2-D grid.
Each generation then switches cells on or off depending on the state of the cells that
surround it in the previous generation. The rules are defined as follows. All eight of
the cells surrounding the current one are checked to see if they are on or not. Any
cells that are on are counted, and this count is then used to determine what will
happen to the current cell.
1 Death: if the count is less than 2 or greater than 3, the current cell is switched
off.
2 Survival: if (a) the count is exactly 2, or (b) the count is exactly 3 and the
current cell is on, the current cell is left unchanged.
3 Birth: if the current cell is off and the count is exactly 3, the current cell is
switched on.
A pattern which does not change from one generation to the next is known as a
still life , and is said to have period 1. Conway originally believed that no pattern
could produce an infinite number of cells, and offered a $50 prize to anyone who
could find a counterexample before the end of 1970 (Gardner 1983, p. 216). Many
counterexamples were subsequently found, including guns and puffer trains .
A Life pattern which has no father pattern is known as a Garden of Eden (for obvious
biblical reasons). The first such pattern was not found until 1971, and at least 3 are
now known. It is not, however, known if a pattern exists which has a father pattern
, but no grandfather pattern.
Similar cellular automaton games with different rules are HexLife and HighLife.
HashLife is a life algorithm that achieves remarkable speed by storing subpatterns
in a hash table, and using them to skip forward, sometimes thousands of generations
at a time.
For your group project this year you must design and implement a 2-player game
of life. Player 1 starts with 8 blue counters and player 2 starts with 8 red counters.
The game takes place on a 19 x 19 life board. At the start of the game players
place 5 counters consecutively on the life board at locations of their choice. The
game of life algorithm then takes over using the same rules as above except that the
following further rule is implemented:
4 Conversion: The color of a surviving or new-born cell is determined by the
majority of the colors in the 9-cell neighbourhood of that cell. The neighbourhood is as it was in the previous generation. In the event of a tie the color of

144

CHAPTER 3. OPERATING SYSTEMS THEORY

a surviving cell does not change and the color of a new-born cell is determined
at random.
Once the game of life is running, players may interrupt the run up to 3 times in
order to place one of their remaining counters. The game ends after a fixed number
of iterations and the majority surviving color is declared the winner.
Design and code into C the game TwoLife to run on your UNIX box from telnet
terminals. Make use of UNIX shared memory to store the current state of the game.
Make use of UNIX semaphores to solve all synchronization problems that appear
in your design. Make use of the ncurses library to display the current state of the
game on a telnet terminal.

TicTackToe, 1999

If ticktacktoe is played on an unlimited checkerboard with the goal of obtaining


five markers in a row then the game is known as Go-Moku. Although it is widely
believed that a first player winning statergy exists this has not been proven.
Design and code a multi-player version of Go-Moku. Your design must allow players
to alternately place markers on a rectangular board until one of them obtains five
markers in a row (horizontal, vertical or diagonal).
When a game is in progress each player must be able to view the current state of the
game on his workstation. Also a game coordinator must be able to view the game
on his workstation without taking part as a player.
Your solution must allow for two to four players and one game coordinator. The
final product must detect a win or a drawn situation.
If a player takes too long to play when it is his turn then the system must make a
move for him. (Just use the nearest non-filled square to his last move). The timeout
parameter must be settable by the game co-ordinator.
The class will be split into a number of groups. Each group must submit a complete
solution to the Go-Moku problem. Each groups solution will be demonstrated to the
rest of the class towards the end of the semester. Programs must be written in C to
run on a UNIX system with telnet terminals. The current state of the game must
be stored in shared memory and UNIX semaphores must be employed to solve any
synchronization problems in your design.

Game of 24, 1997

Design and code into C the game MATH 24 to run on a UNIX system from telnet
terminals. Make use of the UNIX shared memory to store MATH 24 questions,

3.2. INTERPROCESS COMMUNICATION UNDER UNIX

145

answers and score boards. Make use of UNIX semaphores to solve all synchronization
problems that appear in your design.
The class will be split into 5 groups of 10 students. Each group must submit
a complete solution to the MATH 24 game. Each solution will consist of two C
programs, a QuizMaster and Players. During the playing of MATH 24, one copy
of the QuizMaster will be running while many copies of the Players program may
be running.
In the game MATH 24 the QuizMaster selects 4 integers at random in the range
1 to 12 and presents them to all the Players. The Players combine the integers
using the binary operations +, , and and any legal combination of brackets.
The resultion expression must evaluate to 24 hence the name of the game. The rules
of the game state that all four integers must be used once and only once in this
expression. The first of the Players to produce a correct expression wins the game.
Note that in general a solution is not unique so in your design allow the QuizMaster
to allocate points to the Players depending on what order they solve the problem.
(for example: 6-points for the first correct solution, 4-points for the second correct
solution, etc.) Your QuizMaster should allow the game to run continuously, with
a time-limit on each round and a score-board available to the Players indicating
their correct solutions and current points. You may like to implement a negative
score to penalize Players that submit incorrect expressions.
Each group will be awarded a mark for their project. Members of a group will be
asked a spot quiz on their projects and some combination of group mark and quiz
mark will make up the final project mark.
In order to write the Players program it is necessary to be able to timeout the
user if she is too slow in entering a solution to the MATH 24 game. Under UNIX,
timeouts can be implemented using the signal system call. You can use the man
pages to get full specifications for signal calls.

Auctioneering, 1995

Design and code an auctioneering system. Make use of semaphores to solve any
sychronization problems that appear in your design.
The class will be split into groups. Each group will produce an auctioneer program
and a bidder program. Each group will have a group-leader who will oversee all
design questions.
The auctioneer program should be able to:
Set up descriptions and reserve prices of items to be sold. This set-up proceedure takes place before the auction starts.
Monitor bidder registrations before the auction.

146

CHAPTER 3. OPERATING SYSTEMS THEORY

End bidder registrations.


Start the bidding on each item.
Monitor the bidding on each item.
End the bidding on each item.
Log the highest bid and the successful bidder for each item.
Restart in the case of a power failure.
The bidder program should be able to:
Register as an official bidder in the upcomming auction.
Display a description of the current item up for sale.
Take a bid from the user and attempt to make it.
Inform the user as to the success of the bid.
Display the reserve price, current highest bid and current successful bidder.
Resart in case of a power failure.
Each group will be awarded a mark for their project. Members of a group will be
asked a spot quiz on their projects and some combination of group mark and quiz
mark will make up the final project mark.

3.3. DEADLOCK

3.3

147

Deadlock

At the end of the previous section we saw that a simple sychronization algorithm
can end up in a deadlocked state when more than one processes are competing for a
few resources.

3.3.1

A definition for deadlock

A set of processes is in a state of deadlock when every process in the set is waiting
for a resource that can only be released by another process in the set.
A deadlock situation may arise iff the following necessary condition holds:
circular hold and wait: There must exist a set of waiting processes {p1 , p2 , . . . , pn }
such that p1 is waiting for a resource held by p2 , p2 is waiting for a resource that
is held by p3 , . . ., pn is waiting for a resource that is held by p1 . The resources
involved must be held in a non-sharable mode.

3.3.2

Resource Allocation Graphs

Deadlocks can be described more precisely in terms of a directed bipartite graph


G(V, E), called a resource allocation graph, where the set of vertices V is partitioned
into two types, P = {p1 , p2 , . . . , pn } the set of processes and R = {r1 , r2 , . . . , rm }
the set of resource types.
Each element in the set E of edges is an ordered pair (pi , rj ) or (rj , pi ). If (pi , rj ) E
then there is a directed edge from process pi to resource type rj indicating that
process pi has requested an instance of resource type rj and is currently waiting for
that resource. If (rj , pi ) E then there is a directed edge from resource type rj
to process pi indicating that an instance of resource type rj has been allocated to
process pi . These edges are called request edges and assignment edges respectively.
Pictorally we represent each process pi as a circle and each resource type as a square.
Since a resource type rj may have more than one instance we represent each such
instance as a dot within the square. A request edge points to a square while an
assignment edge starts at one of the dots and points to a circle.
When a request edge is fulfilled it is instantaneously transformed into an assignment
edge which is deleted when the resource is released.
Here are some facts about resource allocation graphs:
If G contains no cycles then no process in the system is deadlocked.
If G contains a cycle then a deadlock may exist.

148

CHAPTER 3. OPERATING SYSTEMS THEORY

If each resource type has exactly one instance then a cycle implies that a deadlock has occurred.
If any resource involved in a cycle has more than one instance then the cycle
does not necessarily imply a deadlock.
A system is deadlocked iff when resources are partitioned according to instances
there is no way to draw request edges without introducing a cycle.

3.3.3

Resource allocation examples

3.3. DEADLOCK

3.3.4

149

Deadlock Prevention

The only way to prevent deadlock from occuring is to ensure that a circular hold
and wait condition never occurs. We will investigate three methods for doing this:

Prevention by Preemption

To prevent the hold and wait condition we allow implicit preemption. If a process
that is holding some resources requests another resource that cannot be immediately allocated to it then all resources currently held are preempted and implicidly
released. The preempted resources are added to the list of resources for which the
process is waiting. The process will only be restarted when it can regain all its old
resources as well as the new one that it requested.
This is not the best deadlock prevention scheme available. For example if a line
printer is continually preempted the the operator whould have a terrible time sorting
out which printed pages belonged to which process.

Prevention by Linear Ordering of Resources

In order to ensure that the circular hold and wait condition never happens one can
impose a linear ordering of all resource types. To do this one must define a 1 1
function F that maps resource types to integer numbers. For example suppose our
resource types are card readers, disk drives, tape decks and line printers and:
F (cr) = 1
F (dd) = 5
F (td) = 7
F (lp) = 12
and suppose that we insist that each process can only request resources in increasing
order of enumeration. To do this we require that whenever a process requests a
resource rj it first releases any resource ri such that F (ri ) > F (rj ). If this protocol
is followed then a circular hold and wait condition cannot happen.
To see this assume that a circular hold and wait condition exists with {p0 , p1 , . . . , pn1 }
involved. Assume that pi is waiting for resource ri which is held by pi+1 , (mod n on
all the indecies). Thus since pi+1 is holding ri while requesting ri+1 we must have
F (ri ) < F (ri+1 for all i. In other words: F (r0 ) < F (r1 ) < F (r2 ) < . . . < F (rn1 ) <
F (r0 ). This is a contradiction and thus the circular hold and wait cannot exist if
the protocol is adhered to.

150

CHAPTER 3. OPERATING SYSTEMS THEORY

The Bankers Algorithm

The following deadlock prevention scheme ensures that a circular hold and wait
condition cannot occur by demanding that at any time the system is in a safe state.
More formally, a system of processes is in a safe state if there exists a sequence,
{p0 , p1 , . . . , pn1 } such that for each pi the resources which pi can still request can
be satisfied by the available resources plus the resources held by all the pj with j < i.
To check whether or not a collection of processes is in a safe state the operating
system must maintain several data structures containing the current state of resource
allocations.. Let n be the number of processes and m the number of resource types.
The bankers algorithm will require the following data structures:
Available: A vector of length m with Available[j] = k if there are currently k
instances of resource type rj available.
Max: An nm matrix defining the maximum demand for each resource type by
each process. If M ax[i, j] = k then process pi may request at most k instances
of resource type rj .
Allocation: An n m matrix defining the number of resources of each type
currently allocated to each process. If Allocation[i, j] = k then process pi is
currently allocated k instances of resource type rj .
Need: An n m matrix indicating the remaining need of each process. If
N eed[i, j] = k then process pi may need k more instances of resource type rj in
order to complete its task. Note that N eed[i, j] = M ax[i, j] Allocation[i, j].
Given that the above data structures are kept up to date by the operating system,
the algorithm for checking whether or not the system is in a safe state is quite simple:
1: Let W ork and F inish be vectors of length m and n respectively. Initialize
W ork = Available and F inish[i] = F alse for all i.
2: Find an i such that:
a: F inish[i] = F alse
b: N eed[i] W ork
If no such i exists then goto step 4.
3: Set:
a: W ork = W ork + Allocation[i]
b: F inish[i] = T rue
and goto step 2.
4: if F inish[i] = T rue for all i then state is safe.
Now to complete the bankers algorithm we must specify what is to be done when
a request for resources comes in from process pi . Suppose pi issues a request vector
Request[i] with Request[i, j] = k if process pi wants k more instances of resource
type rj . The operating system must then take the following action:

3.3. DEADLOCK

151

1: If Request[i] > N eed[i] then we have an error since the process has requested
more resources than initially allowed for in M ax[i].
2: If Request[i] > Available the process pi must wait.
3: The operating system pretends to allocate the resources as follows:
a: Available = Available Request[i].
b: Allocation[i] = Allocation[i] + Request[i].
c: N eed[i] = N eed[i] Request[i]
If the resulting resource state is safe then the allocations are made and the
process pi continues. If the new state is unsafe then pi must wait and the old
state is restored.

3.3.5

Exercise

1) Consider the following resource allocation data for five processes competing for
four resource types.

Available = 1 5 2 0

0
1
M ax = 2
0
0

0
7
3
6
6

1
5
5
5
5

2
0
6
2
6

0
1
Allocation = 1
0
0

0
0
N eed = 1
0
0

0
7
0
0
6

0
5
0
2
4

0
0
2
0
2

0
0
3
6
0

1
0
5
3
1

2
0
4
2
4

a) Show that this system in a safe state?


b) If a request, [0, 4, 2, 0], comes in from the second process, will the operating
system grant it?

152

CHAPTER 3. OPERATING SYSTEMS THEORY

3.4

Scheduling

3.4.1

Introduction

CPU scheduling deals with the problem of deciding which of the processes in the
READY queue is to be allocated the CPU. The criteria used for comparing different
CPU scheduling algorithms include:
Utilization: The idea is to keep the CPU as busy as possible. In real systems
CPU utilization could vary from 40 to 90 percent.
Throughput: One way to measure the work done by the CPU is to count the
number of tasks that are completed per unit of time.
Turnaround Time: The interval of time from submission to completion. Includes, waiting time, executing time and I/O time.
Waiting Time Some scheduling algorithms just try to minimize the waiting
time rather than the complete turnaround time.
Response Time: The time from submission of a request until the first response
is produced. Often used as a criteria in interactive systems.
In most case an average measure is minimized.

3.4.2

F.C.F.S. Scheduling

By far the simplest scheduling algorithm is First-Come-First-Served. The implementation is easily managed with a First-In-First-Out READY queue.
The performance of FCFS is however often quite poor. Consider the following three
tasks with known CPU burst times. We can compute the average turnaround time
to service these three CPU bursts:
Task Burst Time
T1
24
T2
3
T3
3
If the tasks arrive in the order 1, 2 and 3 to a FCFS scheduler then the average
turnaround time can be computed with the aid of a Gantt chart.
T1

T2

T3

and the Average turn-around time, or AT T , can be computed as:

AT T =

24 + 27 + 30
= 27
3

3.4. SCHEDULING

153

On the other hand, if the tasks arrive in the reverse order, 3 then 2 then 1, a much
better result is obtained.
T3

T2

T1

AT T =

3 + 6 + 30
= 13
3

Thus we conclude that the average turnaround time for FCFS scheduling is not in
general minimal.
In addition consider the performance of FCFS in a dynamic situation. Assume 1
CPU bound tasks and many I/O bound tasks. The following scenario often results:
The CPU bound task gets the CPU and holds it. The I/O bound tasks all wait
for the CPU in the READY queue. The I/O tasks are IDLE.
The CPU task finishes and moves to an I/O device. All the I/O tasks finish
quickly and now wait in the I/O queue. The CPU is now IDLE.
There is a convoy effect as these two situations repeat resulting in plenty of IDLE
time. The way around this problem is to allow shorter tasks to go first!

3.4.3

S.T.F. Scheduling

In Shortest-Task-First scheduling the CPU is assigned to that task with the smallest
next CPU burst time. For example:
Task Burst Time
T1
6
T2
3
T3
8
T4
7
T2

T1

T4

T3

AT T =

3 + 9 + 16 + 24
= 13
4

Theorem
STF scheduling is optimal in that it yields the minimum average waiting time.
Proof (due to P. Somaroo, 1995.)

154

CHAPTER 3. OPERATING SYSTEMS THEORY

Let the execution time for task Ti be denoted as ti then:


AW T =
=

0+t1 +(t1 +t2 )++(t1 +t2 ++tn1 )


n
(n1)t1 +(n2)t2 ++2tn2 +tn1
n

which is minimised if ti < tj whenever i < j since ti has a larger weight than tj .
Q.E.D.
Although STF is optimal in the shortest waiting time sense, it is unimplementable.
There is no way that the operating system knows the length of the next CPU burst
time.
One approach is to try and approximate STF scheduling. We try to predict the
length of the next CPU burst by considering the burst time history of the task.
For example let tn be the length of the nth CPU burst and let n be our predicted
value for the nth CPU burst. Then let
n+1 = tn + (1 )n
where is a parameter satisfying 0 1 which controls the relative weight of
recent and past history in our prediction.
If = 0 then n+1 = n and our estimate never changes. If = 1 then n+1 = tn and
only the most recent CPU burst time is taken into account. However if 0 < < 1
then the following expansion shows how past history is incorporated.
n+1 = tn + (1 )tn1 + (1 )2 tn2 +
We see that each successive term has less weight than its predecessor. Note that the
initial 0 can be a system constant.

3.4.4

Priority Scheduling

In this type of scheduling a priority is associated with each task and the CPU is
allocated to the task with the highest priority. Equal priority tasks are scheduled
according to FCFS. Note that STF scheduling is just priority scheduling with the
priority set to 1 .
Priorities can be defined either internally or externally.
Internal priority algorithms will use factors such as, time limits, memory requirements, number of open files and ratio of I/O to CPU bursts to compute the priority
of a given task.
External priority algorithms will use factors such as, funds, sponsers and politics to
allocate priorities.

3.4. SCHEDULING

155

A major problem with priority scheduling is starvation A priority scheduling algorithm can leave some low priority task waiting indefinitely. A solution to this
problem is aging. Gradually increase the priority of a task that stays in the system
for a long time.
Rumor has it that when they closed down the IBM 7094 at MIT in 1973 they found
a low-priority task that had been submitted in 1967.

3.4.5

Preemptive Scheduling

FCFS, STF and PRIORITY scheduling algorithms are non-preemptive by nature.


Once the CPU has been allocated to a process it stays allocated until the process
releases the CPU, (either by terminating or by requesting I/O).
FCFS is intrinsically non-preemptive but the other two can be modified to be preemptive algorithms. For example if a new task arrives in the queue with a shorter
expected burst-time (or a higher priority) than the currently executing task, then
the currently executing task is preempted and the new task is assigned to the CPU.
Consider the following example:
Task Arrival Time
T1
0
T2
1
T3
2
T4
3

Burst Time
8
4
9
5

Using a preemptive scheduling algorithm results in:


T1 T2

T4

T1

T3

AT T =

17 + 4 + 24 + 7
= 13
4

where as for a non-preemptive schedule we have the following situation:


T1

T2

T3

T4

AT T =

8 + 11 + 19 + 23
1
= 15
4
4

We see that preemption results in an improved turn-around time. However it must


be noted that preemption has a roll-out roll-in overhead which has not been reflected
in the above calculations:

156

3.4.6

CHAPTER 3. OPERATING SYSTEMS THEORY

Round Robin Scheduling

A Round Robin scheduling algorithm is usually used for time sharing systems. A
small unit of time, called a time-slice is defined. The READY queue is treated as
a circular queue and the CPU scheduler goes around the READY queue allocating
the CPU to each process for one time-slice.
For example:
Task Burst Time
1
24
2
3
3
3
A Round Robin schedule with a time-slice of 4 is:
T1

T2

T3

T1

T1

AT T =

T1

T1

T1

30 + 7 + 10
2
= 15
3
3

Note that an infinite time-slice is equivalent to FCFS scheduling while a very small
time slice is equivalent to each process running on its own processor at n1 the speed
of the real processor. Again there are overheads with roll-out that have not been
taken into account.

3.4.7

Scheduling Tasks on more than one Processor

In this section we investigate algorithms for scheduling tasks on more than one
processor. We assume that the tasks are independent and can run in any order
we wish. However, once a task starts it must run to completion so we will not be
looking at preemptive algorithms in this section. When processing on more than
one processor the measure that is usually optimized is the total throughput time.
No optimal algorithm is known for minimizing total throughput time. We will
investigate one heuristic algorithm.
We define the largest processing time schedule, or LPT schedule, as the result of
an algorithm which, whenever a processor becomes free, assigns that task whose
execution time is the largest of those tasks not yet assigned.. For cases when there
is a tie, an arbitrary tie-breaking rule can be employed. Consider the following
example:

3.4. SCHEDULING

157

Task Processing Time


T1
6.5
T2
4
T3
3.5
T4
3
T5
2
T6
1
T7
1
An LPT schedule for this set of tasks on 3 processors turns out to be an optimal
schedule for 3 processors:
1 2 3

4 5 6

7 8

P1
P2
P3
We see that processor 1 finishes last and the total throughput time for the set of
tasks scheduled in this way is:

t(LP T ) = 7.5
The LPT schedule is not always an optimal schedule. Consider the following example on 3 processors:
Task Processing Time
T1
8
T2
6.5
T3
6
T4
4
T5
3
T6
2.5
T7
2.5
T8
1
An LPT schedule for this set of tasks has a total throughput of 12.
1 2 3

4 5 6

7 8 9

10 11

12

P1
P2
P3
t(LP T ) = 12
whereas an optimal schedule, an OPT schedule, has a total throughput time of 11.5.

158

CHAPTER 3. OPERATING SYSTEMS THEORY

1 2 3

4 5 6

7 8 9

10 11

12

P1
P2
P3
t(LP T ) = 11.5
Just how good is LPT scheduling as compared to an optimal schedule? After a certain amount of experimentation about the possible shortcomings of an LPT schedule
one usually arrives at the following example for the case of 2 processors:
Task Processing Time
T1
3
T2
3
T3
2
T4
2
T5
2
A 2 processor LPT schedule for this set of tasks has a total throughput of 6.5
1 2 3

4 5 6

P1
P2
whereas the OPT schedule has a total throughput of 6.
1 2 3

4 5 6

P1
P2
Constructing LPT and OPT schedules for the following set of tasks gives a worst
case scenario when 3 processors are involved:
Task Processing Time
T1
5
T2
5
T3
4
T4
4
T5
3
T6
3
T7
3
For the worst case scenario on m processors consider 2m + 1 tasks with ti =
2m F loor( i+1
) for i = 1, 2, . . . , 2m and with t2m+1 = m. It can be verified by
2
constructing Gannt charts of the LPT and OPT schedules that:
4
1
t(LP T )
=
t(OP T )
3 3m

3.4. SCHEDULING

159

The main result of this subsection is a generalization of the preceeding result:


Theorem
Given any set of independent tasks and m identical processors:
t(LP T )
4
1

t(OP T )
3 3m
Proof
The theorem is trivally true for m = 1 since in that case t(LP T ) = t(OP T ). So let
m 2.
Assume the theorem is false. Contrary to the theorem assume that we have a
minimal set of tasks, {T1 , T2 , , Tn }, with execution times, {t1 , t2 , , tn } and
assume that the tasks are ordered so that t1 t2 tn . With these assumptions
the LPT schedule will allways assign the tasks in numerical order.
Now assume that Tk finishes last in the LPT schedule with k < n. then an LPT
schedule for the set of tasks {T1 , T2 , , Tk } would complete at the same time as an
LPT schedule for the tasks {T1 , T2 , , Tn } and this smaller set of tasks would also
invalidate our theorem. But we assumed that our n tasks was a minimal set so we
have a contradiction and can safely assume that k = n and Tn finishes strictly last
in the LPT schedule for {T1 , T2 , , Tn }.
We shall now show that any OPT schedule for {T1 , T2 , , Tn } can have at most
two tasks per processor. First we note that
n
1 X
t(OP T )
ti
m i=1

Now let n denote the starting time of Tn in an LPT schedule for {T1 , T2 , , Tn }.
Since no processor can be idle before Tn begins execution we have:

X
1 n1
ti
m i=1

and hence:

n + tn
t(LP T )
=
t(OP T )
t(OP T )

n1
X
tn
1
+
ti
t(OP T ) mt(OP T ) i=1

160

CHAPTER 3. OPERATING SYSTEMS THEORY

n
X
(m 1)tn
1
+
ti
mt(OP T ) mt(OP T ) i=1
(m 1)tn

+1
mt(OP T )

and since the theorem does not hold for {T1 , T2 , , Tn } we have:
4
1
(m 1)tn

<
+1
3 3m
mt(OP T )
from which we obtain:
t(OP T ) < 3tn
.
Therefore, since Tn has the least execution time we conclude that if the theorem is
violated then no processor can execute more than two tasks in an optimal schedule
for {T1 , T2 , , Tn }
To complete the proof we will show that a two task at most per processor OPT
schedule can be transformed into an LPT schedule without increasing the total
throughput time which contradicts our assumption that the theorem was invalid.
Consider the following four types of transformations on schedules:
Type A:

Pi
Pj

Pi
Pj

Pi
Pj

Type B:

Pi
Pj
Type C:

Pi
Type D:

Pi

3.4. SCHEDULING

Pi
Pj

161

Pi
Pj

To turn an OPT schedule into an LPT schedule without increasing the total throughput time we first employ type A and C transforms to ensure that the m longest tasks
are scheduled first with the longest on processor 1 and the mth longest on processor
m. We then employ transforms of type A and B to ensure that the (m + 1)th longest
task is scheduled second on processor m while the (m + 2)th longest task is scheduled
second on processor m 1. We carry on in reverse order up the list of processors
untill all tasks are scheduled in an almost LPT fashion.
The only situation that could prevent a true LPT schedule from being generated
would be if one of the tasks scheduled second on a higher numbered processor completed before one of the tasks scheduled first on a lower numbered processor. In this
case a simple downward shuffle of type D remedies the problem.
Now, as none of the transforms used increase throughput time, the total throughput
time of the resulting LPT schedule will be the same as the total throughput time of
the original OPT schedule and the contradiction is established.
This completes the proof of the theorem.

3.4.8

Preemptive Schedules for more than one Processors

If we introduce preemption and remove the restriction that once a task has begun
it must run to completion. we shall find that in general total throughput times can
be improved. For a simple example, consider three taks of unit execution time that
must be scheduled on two processors.
An optimal non-preemptive schedule is as follows:
1 2 3
P1
P2
whereas an optimal preemptive schedule would be:
1 2 3
P1
P2
Note that in a preemptive schedule, tasks may be stopped and restarted at will and
on any processor but there must not be an overlap of scheduled execution time for
any one task.
For any set of independent tasks the following preemptive scheduling result has been

162

CHAPTER 3. OPERATING SYSTEMS THEORY

known for some time:


Theorem
Consider an optimal m-processor preemptive schedule for a set of tasks, {T1 , T2 , , Tn },
with the execution time of Ti given by ti , then the total throughput time for the
preemptive schedule, tmin , is given by:

tmin = max{maxi {ti },

n
1 X
ti }
m i=1

Proof
It is clear that tmin must be a lower bound for the total throughput time since no
schedule can terminate in less time than it takes for the longest task to complete,
and since a schedule can not be more efficient than to keep all the processors buzy
throughout the duration of the schedule.
To see that tmin can actually be achieved by a preemptive schedule consider the
following construction:
Sort the list of tasks into execution time order with the longest execution time first
so that

tmin = max{t1 ,

n
1 X
ti }
m i=1

Now generate a schedule with total throughput time of tmin by scheduling T1 on the
first processor, T2 in the remaining time on the first processor and any extra time
on the second processor, T3 in the remaining time on the current processor with any
extra on the next processor. Continue in this way until all tasks are scheduled. Note
P
that all processors will be fully booked if t1 m1 ni=1 ti but there will be free time
on the higher numbered processors if this is not the case.
For an example of this consider the following set of tasks:
Task Processing Time
T1
6
T2
4.5
T3
4
T4
3.5
T5
3
A 3 processor preemptive optimal schedule for this set of tasks has a total throughput
of:

3.4. SCHEDULING

163

tmin =

6 + 4.5 + 4 + 3.5 + 3
=7
3

and can be constructed as


1 2 3

4 5 6

7 8

P1
P2
P3
3.4.9

Scheduling Dependent Tasks

Let T = {T1 , T2 , . . . , Tn } be a set of tasks. Suppose the tasks are constrained to run
in some order of precedence. The precedences are specified as a directed graph, G
whose nodes are the set of tasks T and whose directed edges are members of T T
with Ti Tj if task Ti must complete before task Tj starts. If there is a sequence
of edges Ti Tj . . . Tk then we say that Ti is a predecessor of Tk and that Tk
is a successor of Ti . Two tasks are said to be independent if neither is a successor
of the other. Independent tasks may be executed in any order or even at the same
time if more than one processor is available. Dependent tasks must be executed in
the order specified by the directed edges in the precedence graph.
In the previous section we were concerned with scheduling sets of independent tasks
on m processors. In this section we will investigate two algorithms for scheduling
dependent tasks on m processors. The particular dependence is always given by a
precedence graph.
A-Scheduling

This scheduling algorithm involves a list ordering sub-algorithm as follows:


To compare two lists L and L0 do the following:
Step 1 Sort the two lists into decreasing order.
Step 2 If L is longer in length than L0 then swap the names of the lists.
Step 3 Compare element by element until two unequal elements in the same position
are found, call them lj and lj0 . We say that L < L0 if lj < lj0 else L > L0 .
Step 4 If all elements are equal up until the last element of L then L < L0 if L0 is
longer in length than L else the lists are equal.
and a labelling sub-algorithm which is designed to give each of the n tasks in the
precedence graph a label:
Step 1: An arbitrary task T with no successor is chosen and given the label, 1.

164

CHAPTER 3. OPERATING SYSTEMS THEORY

Step 2: Suppose for some k, the set of labels, 1, 2, . . . , k 1 have already be assigned.
Consider the set of tasks that have not yet received a label but whose successors
have already been labeled. To each of these tasks attach a list of labels of its
successors and choose the task T with the smallest successor list to receive the
label k. Note that the smallest successor list is chosen according to the list
ordering algorithm above.
Step 3: Repeat step 2 until all tasks in the precedence graph have received labels.
Once labels have been assigned to each task the scheduling algorithm is simple:
A-Schedule: Whenever a processor becomes free assign that task all of whose predecessors
have already been executed and which has the largest label among those tasks
not yet assigned.
Theorem
A-schedules for two processor systems are optimal when all tasks have equal execution times .
Proof
The proof of the fact that A-schedules are optimal under the above conditions is
rather difficult and will not be attempted in this course. The interested student is
refered to ***.
Examples

1) Consider a set of 22 tasks that satisfy the following precedence relations and
all have unit execution times. {T1 T4 , T2 T4 , T3 T4 , T4 T5 , T5 T6 ,
T5 T7 , T6 T9 , T6 T10 , T7 T10 , T8 T11 , T8 T12 , T9 T12 ,
T10 T12 , T10 T13 , T10 T14 , T12 T15 , T12 T16 , T13 T15 , T13 T16 ,
T14 T15 , T14 T16 , T15 T17 , T16 T17 , T16 T18 , T16 T19 , T18 T20 ,
T18 T21 }.
a) Draw the precedence graph for this set of tasks.
b) Label the tasks according to the labeling algorithm.
c) Produce an A-schedule for the tasks on two processors.
2) Show that an A-schedule is not necessarily optimal when three processors are
involved. Use the following precedence relations to provide a counter example.
Assume all tasks have equal execution times. {T1 T4 , T2 T4 , T3 T4 ,
T4 T6 , T5 T7 , T5 T8 , T5 T9 , T5 T10 , T5 T11 , T5 T12 }.
3) Show that an A-schedule is not necessarily optimal when the tasks involved
do not have equal execution times. Use the following precedence relations to
provide a counter example. Assume that all tasks except task T3 execute in unit
time while task T3 require two units to execute. {T1 T4 , T1 T5 , T2 T4 ,
T2 T5 , T3 T5 }

3.4. SCHEDULING

165

B-Scheduling

The B-schedule is optimal on any number of processors for sets of tasks, each of unit
execution time, whose precedence graphs are singly rooted trees. Each task in the
tree except for the root task has exactly one successor task. The structure of the
tree must be such that the independent tasks are the leaves of the tree while the
root of the tree is a task that can only start once all the other tasks in the set have
been completed.
The B-schedule requires the concept of a level which is as follows: The root of a tree
is at level 0. All tasks that are predecessors of the root are at level 1. All tasks that
are predecessors of level 1 tasks are at level 2. etc. etc.
B-Schedule: Whenever a processor becomes free, assign that task if any, all of whose predecessors have already executed and which is at the highest level of those tasks
not yet assigned. If there is a tie the an arbitrary tie-breaking rule may be
used.
Example

1) Consider a set of 12 tasks that satisfy the following precedence relations and all
have unit execution times. {T1 T3 , T2 T3 , T3 T9 , T4 T9 , T5 T10 ,
T6 T10 , T7 T10 , T8 T11 , T9 T11 , T10 T12 , T11 T12 }
a) Draw the precedence tree for this set of tasks.
b) Which task is the root task.
c) Produce an B-schedule for this set of tasks on three processors.

166

3.5
3.5.1

CHAPTER 3. OPERATING SYSTEMS THEORY

Virtual Memory and Paging


Introduction

We consider a system consisting of two memory levels, main and auxiliary. At time
t = 0 assume that one program is residing in auxilliary memory. The program is
divided into n pages each consisting of c contiguous addresses. The program must
run in a main memory consisting of m page frames. If m < n then a paging algorithm
is required to calculate what page must be in which page frame at any particular
time t.
Each time the program makes a reference we are only interested in the index of
the page or page frame referenced and not with the individual words within the
page. Therefore if we regard N = {1, 2, 3, . . . , n} as the set of pages and M =
{1, 2, 3, . . . , m} as the set of page frames then at each moment of time there is a
page map, ft : N M {0} such that
(

ft (x) =

y if page x resides in page frame y at time t


0 if page x is missing from M at time t

When the processor generates an address the hardware computes a memory location = ft (x)c + , where x and are determined from = xc + with 0 < c.
Note that if c is a power of 2 then the hardware can be organized to make this
computation very efficient. If ft (x) = 0 then the hardware generates a page fault
interrupt.
When a page fault interrupt occurs the operating system must find the missing page
in auxiliary memory, place it in main memory, update the map ft , and attempt the
reference again. This is the task of the paging algorithm.
Now suppose that the average time to access a word in a page in main memory is
M and that the average time to transfer a page from auxiliary memory to main
A
memory is A then an important system parameter is the ratio =
. On most
M
4
operating systems > 10 but good paging algorithms should not rely on this
assumption.
Now a programs paging behavior is described by its page reference sequence:
= r1 , r2 , . . . , rt , . . . ,
where rt = i if page i is referenced at the tth reference. Corresponding to the
reference sequence is a sequence of real times
a 1 , a2 , . . . , a t , . . . ,
such that at is the actual time at which reference rt is made. The real time ellapsed
between reference rt and reference rt+1 is given by:

3.5. VIRTUAL MEMORY AND PAGING

at+1 at =

167

M if rt+1 is in memory
M + A otherwize

Now the fault rate, F (), is defined as the number of page faults encountered while
processing reference sequence normalized by the length of . The expected ellapsed
time for a reference is thus:
E[at+1 at ] = M (1 F ()) + (M + A )F () = M (1 + F ())
Thus minimizing F () for all possible will minimize the running time of the
program.

3.5.2

Demand Paging

In our study of paging algorithms we will only deal with so-called demand paging.
Only the missing page is fetched from auxilliary memory and page replacements only
occur when main memory is full. In the abstract a demand paging algorithm, A, is
a mechanism for processing a reference sequence,
= r 1 , r 2 , . . . , rt , . . . ,
and generating a sequence of memory states,
S0 , S 1 , . . . , S t , . . . .
Each memory state St is the set of pages from N which reside in M at time t. The
memory states satisfy the following conditions:

S0 = ,
St =

St1

St N
+r

t1
t

S
+
r
t1
t rs

kSt k m ,
if
if
if

rt St1
rt 6 St1
rt 6 St1

rt St

and

and kSt1 k < m


and kSt1 k = m and rs St1

Note that rt is the page demanded by the next instruction in the program and rs is
the page chosen for overwriting by the operating systems replacement policy.

3.5.3

Some Common Demand Paging Algorithms

Before we discuss specific paging algorithms we require four further definitions to


do with a reference sequence:
= r 1 , r 2 , . . . , rt , . . . .

168

CHAPTER 3. OPERATING SYSTEMS THEORY

Firstly, the forward distance dt (x) at time t for page x is the distance to the first
reference to x after time t:
(

dt (x) =

k if rt+k is the first occurrence of x in rt+1 , rt+2 , . . .


if x does not appear after rt

Secondly, the backward distance bt (x) is the distance to the most recent reference to
x before time t:
(

bt (x) =

k if rtk is the last occurrence of x in r1 , r2 , . . . , rt


if x does not appear in r1 , r2 , . . . , rt

Thirdly, the reference arrival time lt (x) denotes the last time before time t that the
reference x was fetched from auxilliary memory.
lt (x) = max{i tkSi Si1 = x}
And fourthly, the reference frequency #t (x) denotes the number of references to x
in r1 , r2 , . . . , rt ,
In the following examples of demand paging algorithms we assume that kSt1 k = m
and that rt 6 St1 . Also let R(St1 ) denote the page in St1 that is replaced so that:
St = St1 + rt R(St1 )
Different replacement rules, R, will give rise to different demand paging algorithms:
LRU Least Recently Used: The page in St1 that is replaced is the one with the
largest backward distance:
R(St1 ) = y bt1 (y) = max{bt1 (z) | z St1 }
LFU Least Frequently Used: The page in St1 that is replaced is the one having
received the least use. (The tie-breaking rule is usually LRU)
R(St1 ) = y #t1 (y) = min{#t1 (z) | z St1 }
FIFO First In First Out: The page replaced is the one that has been in memory for
the longest time:
R(St1 ) = y lt1 (y) = min{lt1 (z) | z St1 }
LIFO First In First Out: The page replaced is the one that has been in memory for
the shortest time:
R(St1 ) = y lt1 (y) = max{lt1 (z) | z St1 }
BEL Beladys Optimal Algorithm: The page replaced is the one with the largest
forward distance in the sequence rt+1 , rt+2 , . . ..
R(St1 ) = y dt1 (y) = max{dt1 (z) | z St1 }

3.5. VIRTUAL MEMORY AND PAGING

169

If two or more pages have infinite forward distance then the page with the
smallest page number is chosen for replacement. This rule cannot effect the
fault-rate performance as any page with infinite forward distance is never used
again.
Note that Beladys algorithm is unrealizable since it requires a look into the future
operation of the program. However it does provide a useful benchmark against which
to measure the performance of the other realizable algorithms.

3.5.4

The Optimality of Beladys Algorithm

Theorem
Beladys demand paging algorithm is optimal in the sense that it results in the
minimum achievable paging cost when processing any reference sequence . (paging
costs are measured in units of page replacements and they only start mounting up
once memory is full)
Proof
Let > denote a linear ordering of the references in such that y > z if y has greater
forward distance than z at time t.
Let Ck (S + rt y, t) denote the cost of processing the references, rt+1 , rt+2 , . . . , rt+k
starting from state S at time t. Note that page rt is entering S and overwriting page
y. For Beladys algorithm to be optimal we must show that for all k:
y > z Ck = Ck (S + rt z, t) Ck (S + rt y, t) 0
since if this is the case then the y to choose to obtain minimal achievable cost is just
the y with greatest forward distance. We will show that Ck = 0 1 by induction
on k. The result is trivial for k = 0 since we are then considering processing
the next zero page references and any algorithm is optimal. Now suppose that
y > z Cj = 0 1 for j = 0, 1, . . . , k 1 we must show that the same statement
is true when j = k. There are three cases to be considered:
Case 1: rt+1 S y z In this case we have:
Ck = Ck (S + rt z, t) Ck (S + rt y, t)
= Ck1 (S + rt z, t + 1) Ck1 (S + rt y, t + 1)
which is 0 1 by the induction hypothesis.
Case 2: rt+1 = z In this case we have:
Ck = Ck (S + rt z, t) Ck (S + rt y, t)
= 1 + Ck1 (S + rt z + rt+1 u, t + 1) Ck1 (S + rt y, t + 1)
= 1 [Ck1 (S + rt y, t + 1) Ck1 (S + rt u, t + 1)

170

CHAPTER 3. OPERATING SYSTEMS THEORY

where by the induction hypothesis u has greatest forward distance in S + rt z.


So u y in the linear ordering and the term in square brackets is either 0 if
u = y or 0 1 if u > y by the induction hypothesis. So again in this case Ck
is 0 1.
Note: We need not consider the case rt+1 = y since we assume that y > z rt+1
Case 3: rt+1 6 S + rt In this case we have:
Ck = Ck (S + rt z, t) Ck (S + rt y, t)
= [1 + Ck1 (S + rt z + rt+1 u, t + 1)] [1 + Ck1 (S + rt y + rt+1 v, t + 1)]
= Ck1 (S + rt z + rt+1 u, t + 1) Ck1 (S + rt y + rt+1 v, t + 1)
where u has greatest forward distance in S + rt z and v has the greatest
forward distance in S + rt y. Now let s be the element of S + rt z y with
the greatest forward distance then there are three possibilities in the ordering
of s, y and z.
a) s > y > z In this case u = v = s and Ck reduces to:
Ck1 ((S + rt + rt+1 s) z, t + 1) Ck1 ((S + rt + rt+1 s) y, t + 1)
which is 0 1 by the induction hypothesis.
b) y > s > z In this case u = y and v = s and Ck reduces to:
Ck1 ((S + rt + rt+1 y) z, t + 1) Ck1 ((S + rt + rt+1 y) s, t + 1)
which is 0 1 by the induction hypothesis since s > z.
c) y > z > s In this case u = y and v = z and Ck reduces to:
Ck1 (S + rt z + rt+1 y, t + 1) Ck1 (S + rt y + rt+1 z, t + 1)
which is 0.
Thus by induction Ck is 0 1 for all k and the optimality of Beladys algorithm is
established.

3.6. COMPUTER SECURITY

3.6
3.6.1

171

Computer Security
Introduction

Computer Security has been the subject of intensive research since multi-user operating systems were first introduced. Its importance continues to grow as more
sensitive information is stored, transmitted and processed by computers. Some applications include the military, banks, credit bureaus and hospitals. Security flaws
of computer systems and approaches to penetration have been enumerated in the
literature. Here are some of the more common flaws:

The system does not authenticate itself to the user. A common way to steal
passwords is for an intruder to leave a running process which masquerades as
the standard system logon. After an unsuspecting user enters an identification
and a password, the masquerader records the password, gives an error message
(identical to the standard one provided by the logon process in the case of a
mistyped password) and aborts. The true logon process is left to take care of
any retry.
Improper handling of passwords. Passwords may not be encrypted, or the
table of encrypted passwords may be exposed to the general public, or a weak
encryption algorithm may be used.
Improper implementation A security mechanism may be well thought out but
improperly implemented. For example, timely user abortion of a system process
may leave a penetrator with system administrator access rights.
Trojan horse: A borrowed program may surreptitiously access information that
belongs to the borrower and deliver this information to the lender.
Clandestine code: Under the guise of correcting an error or updating an operating system code can be embedded to allow subsequent unauthorized entry to
a system
In this section we will study cryptographic methods for access control and message
protection.

3.6.2

Encryption Systems

An encryption system is an encryption procedure executed by a sender, which takes


a message (called the plain-text) and a small piece of information (called the key)
and creates an encoded version of the message (called the cipher-text). The ciphertext is transmitted along an open line to a receiver who must then use a decrypting
procedure together with the key to recover the plain-text. The key is arranged in
advance between sender and receiver.

172

CHAPTER 3. OPERATING SYSTEMS THEORY

When we consider the quality of an encryption system, we assume that a third-party


trying to decode the message knows the encryption and decryption procedures and
has a copy of the cipher-text. The only thing missing is the key. We also assume
that the sender does not spend time trying to contrive a difficult to read message
but relies entirely on the encryption system to provide all the needed security.
A more demanding standard for measuring the quality of an encryption system is
that it should be safe against a chosen plain-text attack. It is often possible for the
third party to process a known message through the encryption procedure and thus
obtain a plain-text cipher-text pair from which it may be possible to deduce the key.

3.6.3

Examples

Simple Substitution

This system involves a simple letter-for-letter substitution method. The key is a


rearrangement of the 26 letters of the alphabet. For example if the key is given as:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
actqgwrzdevfbhinsymujxplok
and the plain-text message starts as follows
THE SECURITY OF THE RSA ENCODING SCHEME ...
then the cipher-text message will read:
uzg mgtjyduo iw uzg yma ghtiqdhr mtzgbd ...
Most messages can de decoded without the key by looking for frequently occuring
pairs of letters. (TH and HE are by far the most common pairings to be found in
most english messages). Once these letters have been itentified the rest usually fall
into place easily. Of course this system is useless against a known plain-text attack.

The Vigenere Cipher

This cipher works by replacing each letter by another letter a specified number of
positions further down the alphabet. For example, J is 5 positions further down
from E and D is 5 positions on from Y. The key in this cipher is a sequence of shift
ammounts. If the sequence is of length 10 the the first member of the key is used
to process the letters in positions 1, 11, 21, ... in the plain-text. The second
member of the key is used to process the letters in positions 2, 12, 22, ... and
so on. For example if we use the key:

3.6. COMPUTER SECURITY

173

3 1 7 23 10 5 19 14 19 24
and the plain-text message starts as follows
THE SECURITY OF THE RSA ENCODING SCHEME ...
then the cipher-text message will read:
wil pohnfbrb pm qrj kgt cqdvassz gvfhnl ...
This type of cipher was considered very secdure up until 1600 AD but it is not very
difficult to crack. If the length of the key is known then a guess at the first key
element coupled with a table showing possible two letter combinations in positions
1,2 and 11,12 and 21,22 etc will usually reveal the second element of the key. The
same technique can be used to get the rest of the key. Again this cipher is useless
against a known plain-text attack.

One-Time Pads

In the previous example, if the key-sequence is long enough then the cipher becomes
harder and harder to crack. In the extreen case when the key-sequence is as long
as the plain-text itself the cipher is theoretically unbreakable (since for any possible
plain-text there is a key for which the given cipher-text comes from that plaintext). This type of cipher has reportedly been used by spies, who were furnished
with notebooks containing page after page of randomly generated key-sequences.
Note that it is essential that each key-sequence be used only once, (hence the name
one-time pad.
One-time pads seem practical when an agent is communicating with a central command. They become less attractive if several agents need to communicate with each
other.

3.6.4

Introduction to Number Theory

To ensure that cipher systems are safe one usually resorts to Number Theory. Before
presenting some number theoretic cipher systems we must revise our number theory
background.

Congruences

The congruence a b mod n says that when divided by n, a and b have the same
remainder. For example:

174

CHAPTER 3. OPERATING SYSTEMS THEORY

100 34 mod 11 ,

6 10 mod 8

In the second example we are using 6 = 8(1) + 2. Note that we always have
a b mod n for some 0 n n 1, and we are usually only concerned with that
b.
If a b mod n and c d mod n then we can add or multiply:
a + c b + d mod n ,

ac bd mod n

.
Division however does not always work:
6 18 mod 12 ,

3 6 9 mod 12

The Greatest Common Divisor

For any two numbers, a and b, the number (a, b) is the largest number which divides
a and b evenly. For example:
(56, 98) = 14 and (76, 190) = 38
(a, b) is called the greatest common divisor of the integers a and b.
Theorem 1 For any two non-zero integers a and b, there are two other integers
x and y, with the property that (a, b) is the smallest positive integer that can be
expressed as:
(a, b) = ax + by
Proof: Consider the set S of all integers that can be written in the form ax+by, and
let S + be the set of all positive integers in S. Now the set S contains the integers
a, a, b and b, so the set S + is not empty. Thus S + must have a least element,
call this element d. We must show that d = (a, b).
First by the division algorithm there are integers q and r such that a = dq + r, with
0 r < d. Thus r = a dq and since d S + we have r = a (ax + by)q and a
little algebra gives r = a(1 xq) + b(nq). Thus r is in S but since 0 r < d and
since d is the smallest element of S + we must have r = 0. So d|a.
Similarily it can be shown that d|b.

3.6. COMPUTER SECURITY

175

Now suppose that c|a and c|b, then a = cu and b = cv. Thus d = ax + by =
cux + cvy = c(ux + vy) which shows that c|d. Thus d is the greatest common divisor
of a and b. So d = (a, b).
Euclids Algorithm

Given a and b the equation ax + by = d can be solved by making a sequence of


simplifying substitutions. Suppose a < b then divide a into b getting a quotient q
and remainder r so that b = aq + r. Now rewrite:
ax + by = d
as
ax0 + ry = d where x0 = x + qy
Now try to solve the equation ax0 + ry = d by employing the same technique, r < a
so divide r into a getting a quotient and remainder and rewrite the equation, etc
etc. Eventually one ends up with an equation of the form sx0 + ty 0 = d where one
of s and t is 0 while the other is d. Consider the following example where we are
trying to compute (30, 69).
30x + 69y
30x0 + 9y
3x0 + 9y 0
3x00 + 0y 0

=
=
=
=

d
d [x0 = x + 2y]
d [y 0 = y + 3x0 ]
d [x00 = x0 + 3y 0 ]

from the last line of this reduction we can read off x00 = 1 and y 0 = 0 is a solution if
d = 3. Note that back-substitution will give x = 7 and y = 3 and the solution to
the original problem is:
(30, 69) = 3 = (30)(7) + (69)(3)
It is important to realize that this process is feasable on a computer even if a and
b are several hundred digits long. It is easy to show that the larger of the two
coefficients decreases by 12 every two equations. Thus in twenty iterations the larger
coefficient will decrease by a factor of 210 < 103 . The greatest common divisor of
two 600 digit numbers could be computed in no more than 4000 iterations.
Corollary 2 If p is prime and if ar as mod p and if a 6 0 then r s mod p.
Proof: Since p is prime we have (a, p) = 1 so there are integers x and y such that
ax + py = 1. Hence ax 1 mod p and r (1)r axr xar xas s mod p.

176

CHAPTER 3. OPERATING SYSTEMS THEORY

Corollary 3 If p is prime and a 6 0 mod p then for any b there is a y with ay


b mod p.
Proof: In the preceeding proof we found an x with ax 1 mod p. Now just take
y = bx and the result follows.
Powers modulo a Prime

The sequence a, a2 , a3 , . . . , modp has many applications in cryptography. Before


looking at the theoritical properties of such a sequence you should convince yourself
that it is feasable to compute ab mod p even if a and b are several hundred digits
long. The trick is to compute a2 , a4 , a8 , . . . using mod p arithmetic at each step until
you are almost there.
For example to compute 432687 mod 987 we note that:
687 = 512 + 128 + 32 + 4 + 2
so that in
432687 = (4322 )(4324 )(43232 )(432128 )(432512 )
(81)(639) . . . (858) mod 987
204 mod 987
Note that even if the numbers are several hundred digits long then although special
routines must be written to handle the modulo multiplications, these calculations
with exponents will be feasable.
We will now develope a series of theorems involving powers of numbers in some
modulo arithmetic field.
Theorem 4 Suppose b 6 0 and let d be the smallest number such that bd 1. Then
for any e > 0, be 1 implies d|e.
Proof: If d 6 |e then e = dq + r for some 0 < r < d and br bedq be (bd )q 1
which contradicts the definition of d.
Theorem 5 There are at most d solutions to a polynomial congruence of degree d:
0 xd + 1 xd1 + . . . + d 0 mod p
Proof: This theorem is proved in the same way as the corresponding theorem in
ordinary algebra: If x = is a solution the the polynomial can be written as (x )
times a polynomial of degree d 1 which by induction has at most d 1 solutions.

3.6. COMPUTER SECURITY

177

Primitive Roots

In cryptography we often work in a field mod p where p is some large prime number.
We will be interested in elements of this field whose powers that take on all possible
values in the field. Such an element is called a primitive root. Here is a more formal
definition:
Definition: a is called a primitive root of p if for every b between 1 and p 1 there
is an x between 1 and p 1 such that ax b mod p.
Primitive roots are only useful if we know they exist. The following theorem which
is quite hard to prove ensures the existance of a primitive root for appropriately
chosen p.
Theorem 6 If p is prime then a primitive root a exists for modulo p arithmetic.
Proof: Choose any a 6 0 and let d be the smallest positive number for which
ad 1, (there must be such a number since aK aL implies aKL 1). If d = p 1
then a is a primitive root. If d < p 1, we will find a0 and d0 with d0 > d such that
0
(a0 )d 1 and the process can be repeated until a primitive root is found.
We have ad 1 so the sequence a, a2 , a3 , . . . , ad 1 consists of d different solutions
to the polynomial equation xd 1. If d < p 1 then let b be any non-member of
the sequence. Let e be the smallest positive number with be 1. If e > d then we
can take a0 = b and d0 = e and we are done so from now on assume that e d. Also
e
note that e does not divide d so that (d,e)
> 1.
Now let a0 = a(d,e) b and let c =

d
(d,e)

and let d0 = ce > d.

We must show that ce is the smallest number with the property that (a0 )ce 1.
First note that (a0 )ce (ad )e (be )c 1
d
Also note that (c, e) = ( (d,e)
, e) = 1 so by Euclid there exist integers K and L such
that cK + eL = 1.

Now assume that (a0 )x 1, then we have 1 (a0 )cx bcx . So cx = eM for some
integer M and x = (cK + eL)x = e(KM + Lx). So x = ey for some integer y We
will show that y is divisable by c and we are done.
(a0 )x 1

(a(d,e) b)ey 1
a(d,e)ey 1
(d, e)ey = dN for some integer N
ey = cN
y = (cK + eL)y = c(Ky + LN )

Thus x is divisable by ce so ce is the smallest number with the property that (a0 )ce

178

CHAPTER 3. OPERATING SYSTEMS THEORY

1 but as already mentioned ce > d and hence a0 is a better candidate than a for a
primitive root. This completes the proof and some corollaries follow easily.
In the following three corollaries assume that p is prime:
Corollary 7 If a is a primitive root of p then ap1 1 mod p.
Proof: We know that ad 1 for some d between 1 and p 1. If d < p 1 then the
sequence of powers of a would start repeating before all the numbers between 1 and
p 1 were obtained and then a would not be a primitive root.
Corollary 8 For any b 6 0 we will always have bp1 1 mod p.
Proof: Let a be a primitive root then using the previous corollary we have bp1
(ax )p1 (ap1 )x 1.
Corollary 9 If x y mod p 1 then bx by mod p.
Proof: For some integer r we have y = r(p 1) + x thus by (bp1 )r bx bx mod p.
3.6.5

The Discrete Logarithm Problem

The existance of a primitive root a for any prime p shows that the equation
ax b mod p
has a solution for any b 6 0. We have seen that given the left hand side of this
equation it is usually feasable to compute the right hand side even when the integers
involved are large. Going the other way however is much harder. Given a primitive
root a and any element b the computation of x to satisfy the above equation is called
the discrete logarithm problem. x is called the discrete logarithm of b with respect to
the primitive root a modulo prime p. Many modern encryption systems are based
on the fact that no efficient way of computing discrete logarithms is known.
To make use of the discrete logarithm problem to build an encryption system one
must have a reliable method of finding at least one primitive root a given any prime
p. A little analysis shows that in most cases it will be sufficient to choose a at
random and then test for primitivity. If a turns out to be not primitive then choose
another a at random.
The analysis goes as follows: It is easy to show that if a is a primitive root then ax
is a primitive root if (x, p 1) = 1.
Firstly:
(ax )n 1 mod p anx 1

3.6. COMPUTER SECURITY

179

(p 1)/nx
nx = r(p 1)
Thus:
(x, p 1) = 1

Ax + B(p 1) = 1
Anx + Bn(p 1) = n
Ar(p 1) + Bn(p 1) = n
(Ar + Bn)(p 1) = n
p 1/n

and so we have shown that (ax )n 1 (p 1)/n which means ax is a primative


root.
Now suppose p 1 has a prime factor decomposition:
p 1 = (p1 )1 (p2 )2 . . . (pn )n
then the number of primitive roots would be given by the number of xs relatively
prime to p 1:
#(x0 s) = (p 1)(

p1 1 p2 1
pn 1
)(
)...(
)
p1
p2
pn

For example if p = 1223 then p 1 = 1222 = (2)(13)(47) and


1 12 46
#(x0 s) = 1222( )( )( ) 0.45
2 13 47
so choosing a at random would succeed 45% of the time. This is an example of a
probabilistic algorithm and problems in cryptography are often solved by means of
them.

3.6.6

The Diffie-Hellman Key exchange procedure

As a first example of how the intractability of the discrete logarithm problem may
be used in a cryptographic setting consider the problem of two people, A and B,
trying to agree on a secret key knowing that a third party, C, is listening to all
communications between them.
The technique is as follows: A and B agree publically on a large prime p and a
primitive root a. These numbers will also be known to C. Then A secretly chooses
a large number while B secretly chooses a large number . Then a mod p and
a mod p are computed by A and B respectively and publically announced. The
secret key which will be known only to A and B can then be computed by them as:
secret key

= (a ) mod p = (a ) mod p

180

CHAPTER 3. OPERATING SYSTEMS THEORY

Note that for C to compute the secret key he would have to determine either or
from his knowledge of p, a, a and a . In other words he would have to solve the
discrete logarithm problem for large modulo arithmetic which no one to this date
has been able to do.

3.6.7

The Code Protection Problem

As another example of the use of the intractability of the discrete logarithm problem
consider the following scheme for protecting code against piracy.
The author of the code selects a large prime p with primitive root a and stores these
as constants in the code. The author also chooses a secret number c for that copy
of the code and stores ac mod p as a constant in the code.
At startup the code computes a machine identity h. This identity could be a combination of the BIOS id and manufacturing date together with the hard disk id and
formatting date.
The code then computes ah mod p and makes this number known to the user by
displaying it on the screen. The user then phones the author and tells him over the
phone the number displayed on the screen.
If that user is currently paid-up then the author then computes a password (ah )c mod
p and then phones the user back to inform him of his password.
The user enters the password from the keyboard and the code computes (ac )h mod p
to determine if access is granted.
Note that the user cannot compute the password before the code is run since neither
c nor h is obtainable from non-executing code. Naturally tracing must be prohibited
to prevent the password being detected at access determination time.

3.6.8

The Rivest-Shamir-Adleman public key system

The idea of public key encryption is to allow a receiver to set up a system so that
anyone can send him an encoded message, but only the receiver will be able to
decode it. The plan is as follows:
The receiver chooses two large primes p and q. He then computes a number e that
is relatively prime to both p 1 and q 1. In other words:
(e, p 1) = (e, q 1) = 1
. He also computes another number d such that:
ed 1 mod (p 1) and ed 1 mod (q 1)

3.6. COMPUTER SECURITY

181

Finally the receiver computer the product of p and q:


n = pq
The receiver keeps p, q and d secret and publishes e and n. To send a message M < n
to this receiver, any member of the public can compute M e mod n and transmit M e
to the receiver safe in the knowledge that no evesdropper can recover M from M e .
Rivest, Shamir and Alderman showed that the receiver can recover M from M e by
computing (M e )d mod n. This public key encryption technique has become widely
used and is known as RSA encryption.
To show that RSA encryption is feasable we must show that it is feasable to compute
e and d from knowledge of p and q and we must also show that (M e )d M mod n.
Lastly the reader must be convinced that it is extremly hard to compute p and q
from n so that the secrecy of d is guarenteed.
Firstly to get e such that (e, p 1) = (e, q 1) = 1 just choose e to be prime and
greater than p2 and 2q .
Secondly to find d such that ed 1 mod (p 1) and ed 1 mod (q 1) we solve
ex + (p 1)(q 1)y = 1
for x any y via Euclids algorithm with back substitution and let d = x.
Thirdly to show that (M e )d M mod p we use the last corollary from the section
on number theory which states that if x y mod p 1 then bx by mod p. We have
ed 1 mod (p1) so the corollary tells us that M ed M 1 mod p. Similarily M ed
M 1 mod q Thus M ed M is divisable by both p and q so (M e )d M mod (pq = n)
Lastly to convince yourself that the factors p and q can remain secret even if n is
knownconsider the fact that the crude approach of dividing n by all numbers up
until n would take approximately 1050 steps for a 100 digit n and in the last 100
years many famous mathematicians have been unable to devise a significantly better
factoring algorithm.

3.6.9

Authentication and Digital Signatures

A problem with public key encryption is that it is easy for a troublemaker C to send
a message to A pretending to be B. This problem can be solved if both A and B
have published encryption keys. The solution is as follows:
Suppose B wants to send a message M to A. He first encrypts the message using
his own private decryption key dB to get M dB mod nB . He then prepends his name
and encrypts the result using A0 s public encryption key to get (B + (M dB mod
nB ))eA mod nA This mess is sent to A via an open line. A decrypts the mess using
his private decryption key dA and discovers Bs name at the begining of an encrypted
message. A then decrypts the rest of the message using B 0 s public encryption key

182

CHAPTER 3. OPERATING SYSTEMS THEORY

eB . If the result makes sense A is secure in the knowledge that only someone knowing
B 0 s private decryption key dB could have sent the message.

3.6.10

Secure Shell Environment:

ssh2 (Secure Shell) is a program for logging into a remote machine and executing
commands in a remote machine. It is intended to replace rlogin and rsh, and provide
secure, encrypted communications between two untrusted hosts over an insecure
network. X11 connections and arbitrary TCP/IP ports can also be forwarded over
the secure channel.
ssh2 connects and logs into the specified hostname. The user must prove his identity
to the remote machine using some authentication method.
Public key authentication is based on the use of digital signatures. Each user creates
a public / private key pair for authentication purposes. The server knows the users
public key, and only the user has the private key. The filenames of private keys that
are used in authentication are set in .ssh2/identification. When the user tries to
authenticate himself, the server checks .ssh2/authorization for filenames of matching
public keys and sends a challenge to the user end. The user is authenticated by
signing the challenge using the private key.
If other authentication methods fail, ssh2 will prompt for a password. Since all
communications are encrypted, the password will not be available for eavesdroppers.
When the users identity has been accepted by the server, the server either executes
the given command, or logs into the machine and gives the user a normal shell on
the remote machine. All communication with the remote command or shell will be
automatically encrypted.
If no pseudo tty has been allocated, the session is transparent and can be used to
reliably transfer binary data.
The session terminates when the command or shell in on the remote machine exits
and all X11 and TCP/IP connections have been closed. The exit status of the remote
program is returned as the exit status of ssh2.
Ssh2 automatically maintains and checks a database containing the host public keys.
When logging on to a host for the first time, the hosts public key is stored in a file
.ssh2/hostkey-PORTNUMBER-HOSTNAME.pub in the users home directory. If
a hosts identification changes, ssh2 issues a warning and disables the password
authentication in order to prevent a Trojan horse from getting the users password.
Another purpose of this mechanism is to prevent man-in-the-middle attacks which
could otherwise be used to circumvent the encryption.

3.6. COMPUTER SECURITY

183

ssh2 exercise

ssh2 has been installed on your unix box. Download ssh2 for windows from
www.ssh.com and try to establish a secure shell connection to your unix box. Remember that ssh2 has replaced ssh, the original secure shell command. Use man
ssh2 to get information on ssh2 options. Also make use of keysgen to generate
a private/public pair of keys and set up ssh so that you can start a unix session
without transmitting a password.

184

3.7

CHAPTER 3. OPERATING SYSTEMS THEORY

Further Reading

This set of notes is suppose to be self contained. The following books and articles
are not required reading for this course but they may help you to understand some
of the topics presented.
Paul Sheer, Rute Users Tutorial and Exposition, 2000.
Rute is a dependency consistant UNIX tutorial. This means that you
can read it from beginnning to end in consecutive order. This book can be
downloaded from: http://hughm.cs.unp.ac.za/ murrellh/notes/rute.ps
David Rusling, The Linux Kernel, 1999.
This book is for Linux enthusiasts who want to know how the Linux Kernel
works. It describes the principles and mechanisims that Linux uses. This
book can be downloaded from: http://hughm.cs.unp.ac.za/ murrellh/notes/tlk.ps
Silberschatz and Gavin, Operating Systems Concepts, Willey, 6th edition
A standard introduction to os concepts.
Coffman and Denning, Operating Systems Theory, Prentice Hall, 1973
This book contains proofs for many of the more difficult theorems discussed
in this course.
Maekawa and Oldehoeft, Operating Systems, Benjamin-Cumings, 1987
Not as advanced as Coffman but complements Coffman nicely.
Nishinuma and Espesser, Unix First Contact, Macmillan, 1987
Exactly what it claims to be, a first contact introduction.
Pilavakis, Unix Workshop, Macmillan, 1989
Good introduction to UNIX with an excellent chapter on inter-process
communication.
Filipski, Making Unix secure, Byte, pp. 113-128, April 1986
Describes security issues with respect to the UNIX operating system. Read
the article and in particular the password encryption scheme. The UNIX
password encryption is based on DES, the Data Encryption Standard.

3.8. APPENDICES

3.8

Appendices

3.8.1

Appendix A: Quick reference for the vi editor

3.8.2

Appendix B: Regular Expressions

185

You might also like