Professional Documents
Culture Documents
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
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
1.4.2
1.4.3
Permission . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.4.4
Setting permissions . . . . . . . . . . . . . . . . . . . . . . . . 26
1.4.5
1.4.6
Changing owners . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.4.7
Inodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
ii
CONTENTS
1.6
1.5.2
Why X . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.5.3
History of X . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
1.5.4
1.5.5
1.5.6
1.5.7
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.6.2
Protocols . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.6.3
Ethernet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.6.4
Types of cabling . . . . . . . . . . . . . . . . . . . . . . . . . 36
1.6.5
1.6.6
1.6.7
Telnet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
1.6.8
Anonymous ftp . . . . . . . . . . . . . . . . . . . . . . . . . . 39
1.6.9
43
2.1
Credits: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.2
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.3
Compilation Stages . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.4
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
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
2.25.3 Exercises 2
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
2.25.4 Exercises 3
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
3.2
3.3
3.4
121
3.1.2
3.1.3
Semaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
3.1.4
3.1.5
3.1.6
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
3.2.2
Semaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
3.2.3
3.2.4
Deadlock . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
3.3.1
3.3.2
3.3.3
3.3.4
3.3.5
Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
3.4.1
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
3.4.2
3.4.3
CONTENTS
3.5
3.6
3.4.4
3.4.5
3.4.6
3.4.7
3.4.8
3.4.9
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
3.5.2
3.5.3
3.5.4
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
3.6.2
3.6.3
Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
3.6.4
3.6.5
3.6.6
3.6.7
3.6.8
3.6.9
. . . . . . . . 180
. . . . . . . . . . . . . . . . . . . 182
3.7
3.8
Appendices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
3.8.1
3.8.2
CONTENTS
Chapter 1
Introduction to Unix
1.1
Credits
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
Students taking the Operating Systems course will be given UNIX accounts on the
following UNIX machine
mars.cs.unp.ac.za
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
1.2. INTRODUCTION
apropos files
You will get the following among a long list:
...
lorder (1)
lpr (1)
ls (1)
m4 (1)
make (1)
1.2.2
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.
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.
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 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.
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
This command lists the number of blocks of disk usage in the current directory and
all subdirectories.
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.
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.
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 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
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.
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.
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
The same as the familiar DOS command. mkdir foo creates subdirectory foo in
the current directory.
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.
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.
Use passwd to change your password. It will ask you for the new password and for
a confirmation.
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).
Tels you in what directory you happen to be in, but gives the full path name.
10
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 foo displays the last 10 lines of file foo on the screen.
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.
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
12
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 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
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.
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
COMPOSE MESSAGE
Folder:INBOX
2 Messages
To :
Cc :
Attchmnt:
Subject :
----- Message Text -----
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.
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
Subject : Test
----- Message Text ----Tell me how much you are enjoying the course here.
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
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.
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
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
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
22
1.3.5
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
1.4.1
23
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
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
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
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
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
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.
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
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
Inodes
1.5
1.5.1
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.
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
30
1.5.4
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.
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
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
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
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:
#
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
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
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
1.6.2
Protocols
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
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
Types of cabling
1.6.5
37
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:
38
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
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
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
40
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:
41
1.6.9
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
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
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/.
44
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
46
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
**
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
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
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
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
**
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
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
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();
...
}
...
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
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
54
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
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
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
...
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
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
62
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[] =
2.11. STRINGS
63
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
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",°rees, &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
2.12. EXERCISES 2
67
68
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.
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
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
Operators
70
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
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
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;
/* now copy characters from str into the newly created space.
The str pointer will be advanced a char at a time,
74
2.15
Input/Output
2.15.1
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
2.15. INPUT/OUTPUT
77
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
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
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.1
79
Preprocesser Facilities
80
#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
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:-
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
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.
#using XmGraph
2.17. DEBUGGING
2.17
Debugging
2.17.1
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
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
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
2.17. DEBUGGING
85
86
2.17. DEBUGGING
87
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
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
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
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
}
#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
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
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
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
2.21
Examples
2.21.1
#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
2.21.2
#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
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
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
- val
next
5
NULL
6
tail
- 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
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
#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++)
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;
}
102
while (*cptr == ){
if (*cptr == \0)
return NULL;
else
cptr++;
}
return cptr;
}
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
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
2.22
2.22.1
Multidimensional Arrays
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
107
However, you must now perform subscript calculations manually, accessing the i,j
th element with array[i * ncolumns + j].
2.23
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
2.24
ANSI C
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
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
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
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;
}
}
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;
}
#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",°rees, &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
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");
}
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
#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
#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
}
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
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
3.1.1
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.2
123
Mutual exclusion
124
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:
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
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
127
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
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
...
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
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:
reader i:
writer j:
loop
...
P(mutexR)
{readers enter one at a time}
loop
...
P(mutexW)
{wait}
critical section
for writers
V(mutexW)
{signal}
V(mutexR)
{allow other readers in/out}
endloop
129
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
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
131
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
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 */
}
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>
134
{
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;
}
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>
/* 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--;
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
3.2.3
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
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.
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
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.
141
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)
142
0
1
2
3
=
=
=
=
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.
143
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
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
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,
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
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 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
148
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
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.
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
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
152
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
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
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
Burst Time
8
4
9
5
T4
T1
T3
AT T =
17 + 4 + 24 + 7
= 13
4
T2
T3
T4
AT T =
8 + 11 + 19 + 23
1
= 15
4
4
156
3.4.6
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
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
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
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
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
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
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
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
4 5 6
7 8
P1
P2
P3
3.4.9
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
164
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
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) =
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:
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
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
168
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) =
Secondly, the backward distance bt (x) is the distance to the most recent reference to
x before time t:
(
bt (x) =
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 }
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
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
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
172
3.6.3
Examples
Simple Substitution
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:
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
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
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
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.
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
=
=
=
=
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
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)
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
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 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
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
p1 1 p2 1
pn 1
)(
)...(
)
p1
p2
pn
3.6.6
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
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
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 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)
181
3.6.9
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
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
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.
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
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
3.8.2
185