Beruflich Dokumente
Kultur Dokumente
INTERMEDIATE
NETWORK
PROGRAMMING
AND DESIGN
Adapted for
Open University Malaysia by: Assoc Prof Dr Nantha Kumar Subramaniam
Faculty of Information Technology and
Multimedia Communication, OUM
Glossary 106
References 108
INTRODUCTION
Welcome to CBND3203 Intermediate Network Programming and Design. This course
is the continuation from the CBND3103 Introduction to Network Programming and
Design course.
Course Objective
In general, the overall aims of this course is to develop your capability to write
network applications, discuss the underlying techniques and algorithms used in
the network programming and develop network applications under the UNIX and
Internet environments. To achieve all these, at the end of this course, you should
be able to:
Course Organization
There are six topics in this Network Programming and Design module, namely:
In order to understand the content of this course, you must analyse the course
materials and apply the concepts learned. We hope that you are able to apply the
knowledge and skills from this course throughout your career.
COURSE SYNOPSIS
In previous CBND3103 course, you learned that most networks use a client/server
model in which the client computer makes a request and the server computer
fulfils it. In a distributed computing network, both the client and the server
perform tasks. But the workload isn’t distributed evenly. Because the server is
typically more powerful and faster than the client, the server handles most of the
tasks, leaving the smaller ones for the client. Distributed computing is not limited
to two machines. Several clients and servers in a network can perform tasks.
Communication is key at all levels of a distributed computing network. At the
machine level, clients and servers must communicate with each other. For
example, when you run a distributed application (i.e. an application that splits
processing between the client and server), the various clients and servers must
communicate task-related information. At the operating system (OS) level, various
OS components need to communicate with each other.
techniques for communication between processes within the same system. It is the
principal concept for Topic 1 – Topic 2.
We start by introducing the fundamentals of network programming to help you
understand what is going on in this unit. We do this by presenting the basic
concepts of the following topics:
• programs and processes
• process environments
• useful system calls such as fork, exec, exit, wait
While some top students enjoyed studying Topic 1 – Topic 4, we did have
students who found these topics difficult to comprehend. If you find these topics
are difficult, we suggest you read through the topics to get a general understanding
of the concepts and terminologies. Also try out all the hands-on exercises. Do seek
help from your tutor if you need assistance.
The socket interface was first developed on Berkeley UNIX in 1982 as a general
mechanism for interprocess communication. It is now a very powerful tool in
network programming. Different from the IPC techniques described in Topic 1 –
Topic 4, which are usually restricted to the communications between processes on
the same host, applications developed with socket interfaces can communicate
with other hosts in remote systems. Socket techniques provide flexible ways for
programmers to develop complicated applications in the networking environment.
Because of the predominance of sockets in network programming, Topic 5
emphasizes this concept.
Various system calls for socket programming are introduced in these topics. You
can use these system calls in the development of network applications.
Table 1
Activities Totals Hours
General understanding of module 5
Reading module (see guide in Table 2) 60
Attending tutorial: 5 times of 2 hours each 10
Access OUM website 12
Work on assignment 15
Revision 18
Total 120
It is expected student will do all the above tasks in order gain good knowledge on
this subject.
The following chart gives a general guideline on the topics to be discussed on each
tutorial by the tutor:
Table 2
Topic Tutorial
1 Tutorial 1
2 Tutorial 1
3 Tutorial 2
4 Tutorial 3
5 Tutorial 4
6 Tutorial 5
Please take note that each topic contains few external readings. These readings are
compulsory for you to read and they can be tested in the examination.
TEXTBOOKS
The following set textbooks will be used:
Dean, T (2003) Enhanced Network + Guide to Networks, enhanced edn, Course
Technology, Thomson Learning.
Andrews, J (2002) i-Net Guide to Internet Technologies, 2nd edn, Course
Technology.
Alternatively, you could also refer to any other relevant textbooks found in
OUM’s Digital Library.
REQUIRED EQUIPMENT
The minimum PC configuration for this course is as listed below:
Pentium 233 MMX CPU
Microsoft Windows 95
VGA display card and colour monitor
32MBRAM
600MB free hard disk space
3.5” (1.44MB) floppy disk drive
CD-ROM Drive (8x or better), sound card and speaker
56 kbps modem
COURSE ASSESSMENT
Refer to myVLE.
techniques for communication between processes within the same system. The
basic concepts of IPC will be covered in Topic 1 and Topic 2. Also, IPC is the
principal concept for Topic 3 – Topic 4.
You may feel scared and confused when you first study Interprocess
Communication (IPC). It may at first seem very complex and too detailed to
quickly understand. Ironically, the first few topics in this module which are very
much related to IPC is one that many students find satisfying as well as
challenging, and at the end of the course, many students often say that IPC was a
highlight of the course. So, my advice to you at the beginning of this topic is to
persevere and try not to be too scared by some of the complex ideas that we
present in our initial discussions. As we start to look at and run programs, some of
the ideas will become clearer. The readings that we have included from the
Stevens book will also help you get a better understanding of some of the details
of the process environment that supports IPC, so please take the time to look at
these readings. As mentioned earlier, Topic 1 is the fundamental for you to
understand Topic 2 – Topic 4. Thus, give full attention for this topic.
to communicate with each other. Without IPC, processes can still exchange data
or other information with each other through the file system, but there are certain
limitations to the kind of communication that can occur.
IPC is a broad subject. Books have been written just on the topic alone. Among
my favourite books is John Gray’s Interprocess Communications in UNIX: The
Nooks and Crannies (1988). To get a deep understanding of IPC, a network or
system programmer needs to understand the system architecture and operating
system of the machines he or she is programming or communicating to.
We therefore start our discussion of IPC by first recalling and further elaborating
on the concepts of operating system architecture. We look at some characteristics
of system architecture such as
• system calls,
• system memory and
• process memory.
System calls
UNIX is a two-layered operating system consisting of a kernel and a series of
system programs. UNIX systems programs and utilities deliver requested
functionality to users by issuing system calls to the kernel. As shown in Figure
1.1, system calls provide the interface between executable programs and the
operating system. In the executable programs, system calls request the UNIX
operating system to directly perform some work on behalf of the invoking
process. A process consists of an executing program, its current values, state
information, and the resources used by the operating system to manage the
execution of the process. The system calls are then handled by the kernel.
System memory
The kernel provides a common environment under which to run the executable
programs. When we enter the name of a program to be run at the prompt, the file
is sought along our path variable and, when found, the executable file is loaded
into memory and executed. During the lifetime of its operation it is referred to as a
process and given a unique identifier.
The system memory within which the processes reside can be divided into two
distinct areas: user space and kernel space. A program is a user process and will
run in user space. It will be running in user mode. The kernel, by contrast,
executes in kernel space and is said to be running in kernel mode. A user process
can only access kernel space by making a system call — in other words, by asking
the kernel to perform some tasks. In doing so, the user process makes a context
switch and will, temporarily, be running in kernel mode. System calls are more
common than you may at first think. The C command printf() makes a system call
to write.
Process memory
To manage processes, computers must have efficient memory management. This
is accomplished through the use of memory management techniques, which are
described in this section.
When residing in system memory, the user process is divided into three segments:
• text,
• data and
• stack.
Data segment: This can be thought of as continuing from the text segment and is
divided into two areas: initialized (e.g. in C, variables that are declared as static or
are static) and uninitialized data. In C program language, the call to the library
routine malloc may be made to request more memory. System calls brk and sbrk
handle this by extending the size of the data segment toward the stack segment,
adding memory from the heap to the end of the uninitialized data area.
Stack segment: The location of the stack segment is system dependent and holds
register variables, automatic identifiers and function call information for the
process.
The u area
In addition to the text, data and stack segments, the operating system maintains
for each process a region called the u area (user area). This area of memory
contains information such as open files that is specific to a process alongside a
system stack segment. Were a process to make a system call, the information
needed would be stored in the system stack segment — an area not normally
accessible by the process. System calls are needed to access this information,
which is paged in and out by the kernel.
This reading describes the concept of system memory, process memory, the u
area and process memory addresses. After this reading, you should have a
better understanding of the conceptual relationship between systems and
process memory.
ACTIVITY 1.1
Run program 1.4 in the above reading. The output demonstrates the
range of addresses for identifiers of different memory storage types.
SELF-TEST 1.1
By looking at the process environment and by knowing a process ID, you can
write programs to do other functions, e.g. to stop or communicate the process.
This section looks at concepts such as process ID, parent process ID, process
group ID, user identifications and file descriptors. An understanding of these basic
concepts will help you understand how knowledge of the process environment
helps you learn IPC techniques in later sections/topics.
Process ID
A process is an instance of a program that is being executed by the operating
system. The only way a new process can be created by the UNIX system is by
issuing the fork system call. Details of the fork call can be found in the next
topic. Every process has unique process identification (process ID or PID). The
PID is an integer, typically in the range of 0 through 30000. The kernel assigns a
PID when a new process is created, and a process can obtain its PID using the
system call.
The process with process ID 1 is a special process called the init process
(whose basic responsibility is managing terminal lines and who is the parent
process of all other UNIX processes). Process ID 0 is also a special kernel process
termed either the swapper or the scheduler. In virtual memory
implementations of UNIX, the process with process ID 2 is typically a kernel
process termed the pagedaemon (responsible for paging). Other than these
special processes, no special meanings are associated with other process ID
values.
Parent process ID
Every process has a ‘parent’ process and corresponding parent process
identification (parent PID or PPID). The parent process ID is the process ID or
PID of the process’s parent. The parent is the process that created the current
process, which is its ‘child’. A process can obtain its value using the getppid
system call.
The following program prints the PID and the parent process ID of a process.
More detailed explanation of the system calls getpid and getppid can be
found in UNIX command manuals. Sun Microsystems host a very good online
UNIX command manual at the following website: http://docs.sun.com/
main()
{
printf("Process ID = %d, Parent process ID = %d\n", getpid(),
getppid());
}
The corresponding process ID and its parent process ID will be sent to the
standard output file. The output of the above program looks like this:
Process ID = 6312, Parent process ID = 2334
The process with ID 2334 is the parent or creator of the process with ID 6312.
Process group ID
The group identification (GID) is a group number which is defined in the file
/etc/group. When a process is started, its GID is set to the GID of its parent.
The effective group identification (EGID) is related to the GID in the same way
that the EUID is related to the UID. If the process tries to access a file on which it
does not have owner permission, the kernel will automatically check to see if
permission may be granted on the basis of the EGID.
The effective user identification (EUID) is the number that is used to determine
what resources the process has permission to access. In most processes, the UID
and EUID are the same; however, a program can set a flag that defines a feature:
‘when this program is executed by others users, change the EUID of the
process to be the UID of the owner of this file’.
Hence, other users can execute this program as if the owner of the program were
doing so.
A process in UNIX systems requires several files to handle different purposes, for
example a file descriptor for input tasks, another descriptor for output tasks, and
so on. In UNIX, file descriptors 0, 1, and 2 correspond to the standard input,
standard output and standard error files, respectively.
Many UNIX system calls rely on file descriptors. The low level open, close,
read, and write calls, for example, use file descriptors. In some instances,
such as the socket-programming interface, discussed in other topic, the only way
to perform input and output is through file descriptors.
A detailed explanation of the relationships of process ID, parent process ID, real
and effective user and group IDs is provided in the following reading.
ACTIVITY 1.2
Write a program to identify your process UID, EUID, GID and EGID.
SELF-TEST 1.2
What is the difference between real user ID and effective user ID?
INTRODUCTION
In the previous topic, you saw that processes are at the very heart of the UNIX
operating system. In this topic, we see how to create and control processes by
using fork, exec,exit and wait system calls.
The fork system call creates a copy of process that was executing and is unlike
any function that we’ve seen. It takes one process to call it but two processes
return from it. The process that executes the fork is called the parent process and
the other process is called the child process. Note that these names are not simply
convention — they correspond to an important relationship between the two
processes. The parent process can control certain aspects and get information
about its child processes in ways that the children cannot access their parents.
From the child’s point of view, fork always returns zero. However, fork
returns a non-zero value to the parent process that is the PID of the newly created
child. If fork fails for any reason, a value of –1 is returned. An example of the
fork system call looks like this:
Figure 2.1: Child process creation with the fork system call.
The output of the program shown in Figure 2.1 should look like this:
This is the child process.
This is the parent process.
After the fork operation, the child process routine will continue with its coding
in the child process routine (just as the parent does). After all, the child process is
an exact duplicate of the parent except for the value returned from fork. The
child process inherits several attributes from the parent process, including:
a real user ID, real group ID, effective user ID and effective group ID
a set of environment variables
open file descriptors
a current working directory
a root directory.
The child and parent can share data and exchange messages with each other in this
way, if this is what you want.
However, situations in which you would want two copies of the same program
running simultaneously are not common. In fact, a more general reason that a new
process gets ‘forked’ is so that it can execute another program by using the
exec family of system calls. The function of the exec system call is to change
the executable code that the process is running and reset the data and stack
segments to a predefined initial state. The function of the exec system call is
further explained in the next section.
The concept of the fork process system call is presented in the following
reading. It describes the fork operation in detail.
EXEC’S MINIONS
Processes generate child processes for a number of reasons. In UNIX, there are
several long-lived processes that run in the background and perform a specified
operation at predefined times or in response to certain events. These processes,
called daemon processes, frequently generate child processes to carry out the
requested service. The term daemon is a UNIX term. Many other operating
systems provide support for daemons, though they’re sometimes called other
names. Windows, for example, refers to daemons as System Agents and services.
The term comes from Greek mythology, in which daemons were guardian spirits.
Typical daemon processes include print spoolers, email handlers, and other
programs that perform administrative tasks for the operating system.
A program generates a child process because it would like to transform the child
process by changing the program code the child process is executing. A set of six
system calls under the generalized heading of exec will do this. exec is a
family of related functions (execl, execlp, execle, execv, execvp, and
execve). exec in all its forms overlays a new process image on an old process.
This implies that exec starts the execution of a new program and throws away
the current program (there’s no way to get back to the original program if exec
succeeds; if exec is successful, it doesn’t return to the original program). The
new process image is constructed from an ordinary, executable file. This file is
either an executable object file or a file of data for an interpreter. The following is
part of the function calls in the exec family.
Copyright © Open University Malaysia (OUM)
TOPIC 2 USING PROCESS 15
int execl(const char *path, const char *arg0, ..., const char
*argn, char * /* (char *) 0 */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
In general, all forms of the system functions in the exec family behave in the
same way but have their parameters specified in different formats. The first
difference in these functions is that the first two take a pathname argument
whereas the last one takes a filename argument. When a filename argument is
specified, one of the following results:
If the filename contains a slash, it is taken as a pathname.
Otherwise, the executable file is searched for in the directories specified by
the environment variable.
For simplicity of explaining the variants exec, a frequently used call execvp is
selected to demonstrate the system call. The rest of the exec family is similar;
you can browse through the manual pages to understand all of the differences
among the species of exec. In general, the execv family is easiest to program;
the execp functions take care of some other details, and execvp is a nice
combination.
The concepts of the exec and daemon process system calls are presented in
the following reading. It describes the six functions in exec operation in
detail. Also, it discusses how a daemon process can be started and the
characteristics of typical system daemons.
int pid;
pid = fork ();
if (pid == -1) {
printf ("Something is wrong!\n");
}
else if (pid == 0) {
char *args [2];
args [0] = "ls";
args [1] = NULL;
execvp (args [0], args);
exit (1);
}
else {
printf ("I’m the parent, and my child is %d.\n", pid);
}
The child process starts a new process that issues the list file system command
ls. The effect is identical to inputting ls from the UNIX command prompt. The
output of the above program looks like this:
fork-exec mpipe pipe pipe2 test.c test1.cc
fork-exec.c p1 pipe.c pipe2.c test.out
I’m the parent, and my child is 20128.
The file name in the current directory is shown, and the last message is the
printout from the parent process. The corresponding PID of the child process is
20128.
ACTIVITY 2.1
SELF-TEST 2.1
What are the differences between the child process and the parent
process?
ENDING A PROCESS
Eventually all things must come to an end. Every UNIX process terminates with
an exit value. For example, a process has been generated. If the process does not
receive a terminating signal and the system has not crashed, we will terminate a
process by calling the exit system call. When exit is called, an integer exit
status is passed by the process to the kernel. This exit status is then available to
the parent process through the wait system call (described below). By
convention, a process that terminates normally returns an exit status of zero,
whereas the nonzero values are used to indicate an error condition.
WAITING
When a program forks and the child finishes before the parent, the kernel keeps
some of its information about the child in case the parent needs it — for example,
the parent may need to check the child’s exit status. To be able to get this
information, the parent calls wait() . When this happens, the kernel can discard
the information.
You need to ensure that your parent process calls wait() (or waitpid(),
wait3() and so on) for every child process that terminates; or, on some systems,
you can instruct the system that you are uninterested in child exit states.
Once the parent has forked off a process (usually followed immediately by an
exec), the child runs along doing whatever it will while the parent also continues
to run. At some point in the future, however, the parent may become interested in
the eventual fate (or current condition) of the child. For example, when a child
terminates, either normally or abnormally, the parent will receive a special signal
transmitted by the kernel. Since the termination of a child can happen at any time
while the parent is running, this signal is the asynchronous notification from the
kernel to the parent. In order to get information about its children, the parent can
use the wait system call.
Like exec, wait comes in several formats to suit a wide range of needs and
tastes. In general, the wait3 system call is the most widely used but it is also the
most complicated, and wait and waitpid are sufficient for many uses.
As its name implies, the purpose of the wait system call is to allow the parent to
wait for the child to finish running and ‘die’ (or perish for some other reason), get
the exit status, and continue. However, with the advent of job control (which
allows each parent to control any number of children, who can all be running in
the foreground and/or background simultaneously), things are no longer quite so
simple. For example, consider the chores that the windowing system on your
computer must do. It is interested in the fate of all your individual programs, each
running in its own window, but it can’t be completely preoccupied with watching
them every moment, for it must be ready to respond to a mouse click or keyboard
event intended for it as well. To accomplish these things, wait3 is well suited.
Returning once again to the previous example, we modify it so that the parent
waits for something to happen to the child process to finish before continuing.
(Note that we’re not checking to find out what happened, i.e. whether the child
exited, crashed or merely stopped for some random reason.)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
main()
{
int pid;
pid = fork ();
if (pid == -1) {
printf ("Something is wrong!\n");
}
else if (pid == 0) {
char *args [2];
args [0] = “ls”;
args [1] = NULL;
execvp (args[0], args);
return 0;
}
else {
int status;
/* To issue the first ps -elf in the command prompt manually*/
sleep(5);
printf(“waiting for the exit of the child\n”);
wait(&status);
/* To issue the second ps -elf in the command prompt manually*/
sleep(5);
printf (“I’m the parent, and my child is %d.\n”, pid);
}
/* To issue the third ps -elf in the command prompt manually*/
}
Note that when a child exits, UNIX will not remove the process until the parent
has seen the exit status and any other related information. This means that each
fork should have a matching wait, and that wait should eventually wait until
the child is dead. Processes that have exited but have not had their exit status
reported to their parents are left inactive. The process in this status is called the
zombie process. That is, they cannot be killed and cannot be removed until their
parents wait for them (or their entire ancestor processes exit, thereby removing the
possibility that any process will ever wait for them). They will clutter up the
system, causing other users to grumble.
Let’s use the above program to illustrate the effect of system call wait in the
parent process. The application runs into three stages:
1 The child exits but the parent does not clean up the child’s entry in the
process table.
2 The child’s entry in the process table has been removed by the parent
process.
3 The end of the waiting program.
You can verify this status according to the instructed ps command. The output of
the status should look like Figure 2.5 below:
First : ps -efl
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
8 S pkser 20120 20118 0 40 20 50814cd0 137 50814ea0 13:45:09 pts/3 0:00 -csh
8 Z pkser 20469 20468 0 0 0:00 <defunct>
8 S pkser 20401 20399 0 41 20 5083ccd8 137 5083cea8 16:09:03 pts/4 0:00 -csh
8 S pkser 20468 20120 0 60 20 508f0cc8 139 508f0e98 16:24:42 pts/3 0:00 waiting
Second : ps -efl
8 S pkser 20120 20118 0 40 20 50814cd0 137 50814ea0 13:45:09 pts/3 0:00 -csh
8 S pkser 20401 20399 0 41 20 5083ccd8 137 5083cea8 16:09:03 pts/4 0:00 -csh
8 S pkser 20468 20120 0 45 20 508f0cc8 139 508f0e98 16:24:42 pts/3 0:00 waiting
Third : ps -efl
8 S pkser 20120 20118 0 40 20 50814cd0 137 508711ce 13:45:09 pts/3 0:00 -csh
8 S pkser 20401 20399 0 41 20 5083ccd8 137 5083cea8 16:09:03 pts/4 0:00 -csh
In the first ps command, you will get a zombie process. For the example in
Figure 2.4, a zombie process with bold text is shown in Figure 2.5. The
corresponding item in the last column of the result is defunct, which means the
process is left inactive. The reason is that the child has completed its task but its
entry is still recorded in the process table. At this moment, the child becomes a
zombie and the status flag in the second column is Z to indicate that it is a zombie.
After sleeping for five seconds, the parent process starts to work again and will
remove the zombie entry. This is why the defunct does not show in the second ps
command. Finally, the last ps command only lists out the user log-on shell. The
waiting program exits.
You can learn more about the prototype and return values of the system call
wait in this reading.
ACTIVITY 2.2
SELF-TEST 2.2
What are the three conditions for which wait returns a process ID as its
return value?
In this topic, we see how to create and control processes by using fork,
exec,exit and wait system calls.
INTRODUCTION
This topic focuses the discussion on interprocess communication (IPC) to help
you in developing programs that consist of processes that use the program codes
presented in this topic and the previous topics. If you would like to know more
system calls or commands provided in the UNIX OS, you are encouraged to refer
to a UNIX manual or use the help command man in UNIX for a detailed
explanation of other system calls.
SIGNALS
One primitive interprocess communication technique involves the use of signals.
Signals can be considered as interrupts that are sent from the system to a process.
The process has no control over when they arrive. Signals are sometimes called
‘software interrupts’. Signals usually occur asynchronously. This implies that
processes do not know ahead of time exactly when a signal will happen. Different
events in a system will generate different signals. A signal will be sent to a
process when the event occurs in the system. Signals can either be sent by:
one process to another process (or to itself); or
by the kernel to a process.
Examples of such events include hardware faults, timer expiration and terminal
activity, as well as the invocation of special system calls such as kill. In some
circumstances, the same event generates signals for multiple processes. A process
may request a detailed notification of the source of the signal and the reason why
it was generated.
All UNIX programs have a full set of signal handlers. Each signal has a name and its
default action. The default action can be classified into four types:
exit
exit with core image
ignore
stop process.
A core image is to the dumping of the system memory image to a file. All of the
signal handlers that we’ve been using in all of the programs to date are the default
signal handlers. However, we can also choose to include tailor-made handlers in
programs. The signals currently defined by <signal.h> are shown in the
following table.
The default action for each signal is selected from those shown in Table 3.2
below.
Exit When it gets the signal, the receiving process is to be terminated with all the
consequences.
Core When it gets the signal, the receiving process is to be terminated with all the
consequences. The core file contains all of the process information pertinent to
debugging: contents of hardware registers, process status and process data. In
addition, a ‘core image’ of the process is dumped in the current working
directory.
Stop When it gets the signal, the receiving process is to stop. When a process is
stopped, all threads within the process also stop executing.
Ignore When it gets the signal, the receiving process is to ignore it. This is identical to
setting the handler to ignore.
An example of this is the SIGINT (or interrupt) signal, which you have seen
many times. If your program is running out of control, the general technique for
stopping it is to press ‘control-C’. Of course, there’s nothing in the program that
you wrote that knows anything about ‘control-C’. Instead, what happens is that
when you type ‘control-C’, UNIX notes this and knows that the default action to
take when a ‘control-C’ is typed is to send an SIGINT signal to all the processes
attached to that terminal. When UNIX processes receive an SIGINT, the default
action is to die, so your program exits.
#include <signal.h>
The signal prototype requires two arguments as input, and it always returns the
previous value of func for the specified signal. The first parameter is the signal
value. The second parameter is a pointer to a function that returns void and takes
a single integer as its argument: The return value of signal itself is a pointer to
a function that returns void and takes a single integer argument. There are two
special values for the func argument: SIG_DFL to specify that the signal is to
be handled in the default way, and SIG_IGN to set a signal that is pending causes
the pending signal to be discarded. Any queued values pending are also discarded;
the resources used to queue them are released and made available to queue other
signals. The second parameter to signal is a pointer to the function to call
whenever the signal is delivered. The return value is the previous signal handler
that was installed before the new one was added.
As the first example of this we’ll write a program that ignores ‘control-C’. When
it receives an INT signal, it will simply ignore it. We’ll just install the default
‘ignore it’ signal handler, which is named SIG_IGN.
#include <signal.h>
Figure 3.1: Section of program code to illustrate the signal system call
On many systems, once a signal handler has been invoked it reverts to its default.
If this is the situation, then the second ‘control-C’ would kill this program. To fix
this, it is necessary to set the SIGINT to handler after the first handle is called, as
shown in Figure 3.2.
Note that a parent process can send signals to its child processes at will using the
kill system call. All signals, other than SIGKILL, can be ignored. The
SIGKILL signal is special because the system administrators need to have a
guaranteed way of terminating any process. We recommend that you reference
kill in a UNIX manual for information about what each signal is used for.
Blocking signals
A global mask specifies which signals are currently blocked. The system call
sigblock adds the signals specified in the mask to the set of signals currently
being blocked from delivery. Signals are blocked if the appropriate bit in the mask
is a 1. The system calls sigsetmask and sigpause can be used to unblock
signals by restoring the original mask. In the file <signal.h> is a call
sigmask that makes it convenient to set the signal mask for the call to
sigblock. sigmask is provided to construct the mask for a given signal mask
number.
Figure 3.3: Pseudo code for uninterrupted section with signal masking
This reading provides a detailed description of each signal event. If you would
like to design a single handler, you have to understand the meaning of each
signal event.
Optional
The following website provides information on the signalling concept for the
Linux OS. Information about SIGKILL, SIGSTOP and blocked is
introduced for your reference.
ACTIVITY 3.1
SELF-TEST 3.1
PIPES
In the previous section, we addressed primitive techniques for communicating by
using signals. In this section, we explore interprocess communications techniques
using designed interprocess facilities. First, we introduce two system calls — dup
and dup2 — used in pipes. Then we discuss the pipes interprocess
communications techniques — unnamed pipes and FIFOs.
#include <unistd.h>
int dup2(int fildes, int fildes2);
dup returns a new file descriptor having the following in common with the
original open file descriptor fildes. However, dup2 causes the file descriptor
fildes2 to refer to the same file as fildes. fildes is a file descriptor
referring to an open file, and fildes2 is a non-negative integer less than the
current value for the maximum number of open file descriptors allowed the
calling process. If fildes2 already referred to an open file, not fildes, it is
closed first. If fildes2 refers to fildes, or if fildes is not a valid open file
descriptor, fildes2 will not be closed first.
As a simple example of this call (just using dup and not using pipe), consider
the following command (which again is portable to just about any UNIX shell):
sort < foo > bar
This command invokes the sort program, but instead of reading input from the
user’s keyboard (as it would ordinarily do), input is read from a file named foo.
Similarly, instead of the output being displayed on the screen, it is placed in a file
named bar. This is a relatively simple example, because only one process is
involved. The only thing that we need to do is somehow fiddle with this process
so that its input file descriptor (fd 0) is reading from foo instead of the keyboard,
so that the output file descriptor (fd 1) writes to bar instead of the screen.
int pid;
In Figure 3.4, two file descriptors, input_fd and output_fd, were created for
input/output operations. The input_fd is used as an input file. It is opened with
the O_RDONLY, which stands for read only. The output_fd is the output file of
the program. It is opened with the O_WRONLY, which stands for write only. If
output_fd exists, the O_CREAT has no effect, but O_TRUNC will remove all
data corresponding to this descriptor. Otherwise, a new file descriptor is created.
The new file descriptor returned by dup is guaranteed to be the lowest numbered
available file descriptor. In our example, since we just closed file descriptors 0
and 1, we know that the first thing we duplicate will go into 0 and the next into 1.
However, this is a little risky (imagine that you really wanted to duplicate
something into fd 6 but didn’t know whether or not fds 0 through 5 existed).
To get around this, there is another function called dup2 that takes two file
descriptors as its arguments. You can read about dup2 in the following reading.
The definitions of the dup and dup2 system call are introduced in this
reading.
ACTIVITY 3.2
Complete the program code given in the above paragraph for the dup
function, given that the input file contains the following five lines:
This is a boy.
What is the time?
123
Abc
023
What is the output of the result?
SELF-TEST 3.2
Unnamed pipes
It is well known that users in the UNIX platform can pipe the result of a
program to the input of another. The syntax is: prog1 | prog2. This is called
piping the output of one program to another, because the mechanism used to
transfer the output is called a pipe. A simple example of this compound command
is: who | grep root. The first program who will show all users who have
logged into the system. Instead of having the result of the command who sent to
the standard output stdout, the output is redirected as the standard input of the
grep command.
The effect of the compound command ‘who | grep root’ can be similar
to the following:
root console Tue 8pm 4days 4:39 2:47 xlock
The UNIX shell does the steps shown above to create two processes with a pipe
between them. This corresponds to the fact that the standard output (stdout) of
the first process is the standard input (stdin) of the second process. Its
mechanism can be shown as in Figure 3.5.
Pipes provide a one-way flow of data from one process to another. They are a
form of descriptor that has been used in UNIX. The processes communicating
over a pipe must be related; typically, they are parent and child. A disadvantage of
using a pipe is that it may be too slow: Data have to be transferred from the
writing process to the kernel and back again to the reader, although no I/O is
performed.
pipe() creates an I/O mechanism called a pipe and returns two file descriptors.
The two descriptors are not equivalent. The descriptor whose index is returned in
the low word of the array is filedes[0] which is open for reading, whereas the
filedes[1] in the high end is open only for writing. A pipe is a stream
communication mechanism; that is, all messages sent through the pipe are placed
in order and reliably delivered. When the read process asks for a certain number
of bytes from this stream, the process will receive as many bytes as are available
in the pipe, up to the amount of the request. For writing data to a particular pipe,
data may have come from a single system call write() or from multiple calls to
write() which have been concatenated.
The simple example shown below illustrates the generation, writing and reading
from a pipe.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main()
{
int filedes[2];
char buf[30];
if (pipe(filedes) == -1) {
perror("pipe");
exit(1);
}
As you can see, pipe() takes an array of two integers as an argument. Assuming
no errors, it connects two file descriptors and returns them in the array. The first
element of the array is the reading end of the pipe; the second is the writing end.
The output of this program will send three statements to the terminal:
"writing to file descriptor #4"
Pipes are typically used to communicate between two related processes in the
following way. Let us now examine a program that creates a pipe for
communication between the parent and its child process. First, a process creates a
pipe and then forks to create a copy of itself, as shown in Figure 3.7. The
parent’s descriptor table is copied into the child’s table.
A parent process makes a call to the system routine pipe. The returns of the two
descriptors filedes are used as the two ends of the pipe in the process’s
descriptor table. After the fork, both the parent’s and the child’s descriptor table
points to the pipe. The child can then use the pipe to send a message to the parent.
Next, the parent process closes the read end of the pipe and the child process
closes the write end of the pipe. This provides a one-way flow of data between the
two processes as shown in Figure 3.8.
The corresponding program to implement Figures 3.7 and 3.8 is shown below in
Figure 3.9.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#define DATA "This is the first message for the PIPE program" main()
{
int childpid, pipe1[2];
char buf[1024];
if (pipe(pipe1) < 0)
perror("can’t create pipes");
close(pipe1[1]);
}
}
Note that all the pipes shown so far have been one-way communication
mechanisms. In order to provide two-way communication between the parent and
child processes, two pairs of pipe descriptors have to be created for these
processes. One of the descriptor pairs is used for the parent to send data to child
and the other pair is used for the child to the parent. The program structure of a
two-way communication can be constructed in a similar way to the pseudo code
shown in Figure 3.10.
/* Child Process */
......
Figure 3.10: A pseudo code of two-pipe communication between child and parent
process
The following online guide describes the pipe and fork system calls, giving
many suitable examples and answers. You can duplicate a copy of the given
program code and try to compile the program yourself to reinforce your
understanding of the piping concept. This Web page also provides a sufficient
help manual for applicable system functions such as fork(), dup(),
exec(), pipe(), and so on. If you find understanding the programming
syntax or logic difficult, you should refer to the corresponding help manual
provided in the URL.
Optional
The following Web page provides a good description of the IPC pipe
mechanism in Linux OS.
Rusling, D (1996–98) ‘Interprocess communication mechanisms: pipes’ in
The Linux Kernel, pp. 2–4: http://www.eee.hku.hk/LDP/LDP/tlk/ipc/ipc.html
ACTIVITY 3.3
NAMED PIPES
UNIX provides for a second type of pipe called a named pipe or FIFO (we use the
terms interchangeably). FIFO stands for ‘first in, first out’. A UNIX FIFO is
similar to a pipe. It is a one-way flow of data, and the first byte written to it is the
first byte read from it. Unlike pipes, however, a FIFO has a name associated with
it, allowing unrelated processes to access a single FIFO. Indeed, FIFOs are also
called named pipes. A named pipe looks and acts like a file. It has a file name,
with permissions and a path, and you can open it for reading or writing.
The pathname is a normal UNIX pathname, and this is the name of the FIFO. The
argument specifies the file type and access mode (read and write permissions
for the owner, group and other users). The file type is specified in the mode,
which must be set to one of the values shown below in Table 3.3.
S_IFDIR directory
The following example shows how to make a named pipe within a C program
using mkfifo .
If you don’t have mkfifo() , you’ll have to use mknod(). mknod() is able to
make a directory, or a special or ordinary file, by the path name pointed to by path
argument of mknod(). You can also initialize the file type and permission of the
created file. This call is normally reserved to the superuser to create new device
entries, but any user can create a FIFO.
#include <sys/types.h>
#include <sys/stat.h>
Once a FIFO is created, it must be opened for reading or writing, using either the
open () system call or one of the standard I/O open functions. Note that it takes
three system calls to create a FIFO and open it for reading and writing
(mknod(), open(), and write()) whereas the single pipe() system call
does the same thing.
main()
{
int childpid, readfd, writefd;
In the above program, we first call mknod() to create the FIFOs, realizing that
they might already exist. After the fork , both processes must open each of the
two FIFOs as desired. The parent process removes the FIFOs with the unlink()
system call, after waiting for the child to terminate.
The order of the open() calls is important and avoids a deadlock condition.
When the parent opens FIFO1 for writing, it will block the writing. This
implies it waits until the child opens it for reading. If the first call to open() in
the child were for FIFO2 instead of FIFO1 , then the child would wait for the
parent to open FIFO2 for writing. Each process would be waiting for the other
and neither would proceed. This is called a deadlock.
There is a flag in the system call open which can specify the opening operation
not to block. The way to do this is to call open with the O_NDELAY flag set in
the mode argument:
readfd = open(FIFONAME, O_RDONLY|O_NDELAY)
This will cause open to return –1 if there are no processes that have the file open
for reading.
Note that when we used pipes to implement this example, the client and server
had to originate from the same process, since pipes cannot be shared between
unrelated processes. With FIFOs, however, we do not have this restriction.
FIFO is able to support communication between unrelated processes, but the later
is not. Hence, it gives us greater flexibility with interprocess communication.
However, this creates another problem for using the FIFO technique in
programming. In the simplest situation of one process Proc1 writing to a FIFO
and another one Proc2 reading from the FIFO, everyone is happy with the
connection. However, imagine that a new process Proc3 would like to use the
existing FIFO to talk to Proc2. This creates a problem if Proc2 is not able to
sort the data from the two writers.
One remedy is to define the same amount of data every time (let’s say, 1024
bytes). Then Proc2 reads 1024 bytes at a time, and Proc1 and Proc3 also
write the 1024 bytes data unit. This approach can solve part of the problem. That
is, every time Proc2 reads from the FIFO, it is guaranteed that the whole block
of data is either from Proc1 or Proc3. However, there is no way to identify
which writer sent which packet. A more practical approach is to attach a header
on every piece of data sent from the writers. A unique identifier should be
included as an identity of the data unit, which can let the reader determine which
writer sent the packet. By writing a data unit as a structure such as:
typedef structure {
short identifier; /* To specify the corresponding
writer*/
char data_unit[1024]; /* Actual data */
}
the structure design for process communication is more intelligent. In fact, there is
another IPC technique called ‘message queues’ which applies this approach, as is
explained further on.
The following Web page describes the concept and technique of FIFOs. It
demonstrates how a new FIFO is created. It gives two programs that will send
data through a FIFO. You can duplicate a copy of the given programs code
and try to compile the programs yourself to reinforce your understanding of
the FIFO concept.
ACTIVITY 3.4
1. In this activity, you are required to create a FIFO by using the UNIX
command mknod. Follow the instructions shown below in Table 3.4
to learn how to send the output of a process to the input of another
process via FIFO. Explain the result of this activity.
Table 3.4: Creating and using a FIFO with mknod , more and who
Step Instruction Description
1 # mknod testfifo p This step is to create a FIFO with name
testfifo.testfifo
2 # more testfifo To show the content of testfifo. At this
stage there is no output on the screen. The
reason is that the content of testfifo is
emPte.
3 Open another window and
perform the following step in
that window in that window
4 # who > testfifo who is a UNIX command to show the
existing login users in the system. The
output of who is redirected to the FIFO
testfifo.
2. Compile the speak.c and tick.c program given at the Reading 3.13
URL:
Hall, B (1997) ‘FIFOs’ in Beej’s Guide to Unix Interprocess
Communication, Version 0.9.3: http://www.ecst.csuchico.edu/~beej/
guide/ipc/fifos.html (14 May 1997, cited 8 July 1998).
Try to understand the logic of the program. Practice the speak and
tick programs. What is the output of these?
SELF-TEST 3.3
INTRODUCTION
This topic is divided into two: Message queues and semaphores.
The examples shown so far in the previous topics for unnamed pipes and FIFOs
have used the stream I/O model. There are no message boundaries, and reads
and writes do not examine the data at all. This type of data is a stream of bytes
with no interpretation by the system. This may cause problems, particularly in
multiple writers and readers with the same named pipe. To solve this, we can use
message queue. A message queue is like a pipe and is used to transfer messages
between processes in a UNIX system.
Semaphores, on the other hand, are not used to exchange data between processes;
instead they are used to synchronize two or more processes. They prevent two
processes from simultaneously accessing a shared resource.
MESSAGE QUEUES
The examples shown so far for unnamed pipes and FIFOs have used the stream
I/O model. There are no message boundaries, and reads and writes do not
examine the data at all. This type of data is a stream of bytes with no
interpretation by the system. This may cause problems, particularly in multiple
writers and readers with the same named pipe.
A message queue is like a pipe and is used to transfer messages between processes
in a UNIX system. Unlike a pipe and FIFO, however, it retains message
boundaries and hence is a much more flexible way for many processes to use the
same IPC. A message is typically a small amount of data (~500 bytes) that is sent
to a message queue; any process with appropriate permissions can receive
messages from a queue.
System V allows a process to pass messages to any other active process in the
system. In the System V implementation of message queues, all messages are
stored in the kernel and have an associated message queue identifier or message
descriptor. This identifier identifies a particular queue of messages. Processes can
read or write messages to arbitrary queues, each queue identified uniquely via its
message queue identifier. Unlike other IPC mechanisms such as pipes or FIFOs,
where it makes no sense to have a writer process unless a reader process exists as
well, it is possible for a writer process to write a message to a queue, exit and
have a reader process retrieve the message at some later time.
Every message on a queue should be a structure that has, at the very least, two
fields:
a long integer (used to multiplex messages in a single queue); and
a buffer to hold the message text (the text can contain binary or ASCI data).
typedef struct {
long mtype /* message type */
char message[MSG_BUF]; /* The actual message */
} Message;
The key argument is a system-wide unique identifier describing the queue you
want to connect to (or create). Every other process that wants to connect to this
queue will have to use the same key. The msgflag value is a combination of
the constants used to set the access right of the message queue. To create a new
queue, this field must be set equal to IPC_CREAT bit-wise OR with the
permissions for this queue where OR is a logical operation. A simple example of
msgget is shown below:
key = ftok("/tmp/testing", "abc"); /* To define a key with
project ‘abc’*/
msqid = msgget(key, 0666 | IPC_CREAT); /*To create a new
message queue */
The permission flag is set as 0666. It stands for -rw-rw-rw- for the queue,
meaning everyone is allowed to read or write to the queue. The IPC_CREAT will
cause the message queue to be created if it does not already exist. And now we
have msqid, which will be used to send and receive messages from the queue.
Once a message queue is opened, a writer inserts data in the queue using a
msgsnd() system call.
int msgsnd(int msqid, Message *ptr, int length,
int flag);
The length argument to msgsnd specifies the length of the message in bytes. The
flag argument can be specified as either IPC_NOWAIT or as zero. The
IPC_NOWAIT value allows the system call to return immediately if there is no
room on the message queue for the new message.
Alternatively, a reader reads data from the queue using a msgrcv() system call.
int msgrcv(int msqid, Message *ptr, int length,
long msgtype, int flag);
The ptr argument is like the one for msgsnd() and specifies where the received
message is stored. The third argument is length, which specifies the size of the
data portion of the structure pointed to by ptr. This is maximum amount of data
that is returned by the system call. If the MSG_NOERROR bit in the flag argument
is set, this specifies that if the actual data portion of the received message is
greater than length, just to truncate the data portion and return without an error.
Not specifying the MSG_NOERROR flag causes an error return if length is not
large enough to receive the entire message.
The long integer msgtype argument specifies which message in the queue is
desired.
If msgtype is zero, the first message on the queue is returned. Since each message
queue is maintained as a FIFO list, a msgtype of zero specifies that the oldest
message on the queue is to be returned.
If msgtype is greater than zero, the first message with a type equal to msgtype is
returned.
If msgtype is less than zero, the first message with the lowest type that is less than
or equal to the absolute value of msgtype is returned.
The call msgctl() includes options to set and return parameters associated with
a message queue identifier and remove identifiers (with cmd = IPC_RMID ).
int msgctl(int msqid, int cmd, struct msqid_ds *buff);
The following program sections demonstrate the use of message queues. A server
runs first and creates a message queue. It also populates the message queue with
some text. A client can be run later; on invocation, it opens the message queue,
reads the data left for it by the server, deletes the message queue and exits.
Figure 4.1 shows a header file for client/server message program (msg.h). Figure
4.2 and Figure 4.3, respectively, show a message server program and a message
client program.
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <memory.h>
#include <sys/errno.h>
/* message structure */
#define MSG_BUF 512
typedef struct _Message {
long mtype;
char message[MSG_BUF];
} Message;
#include "msg.h"
return 0;
}
#include "msg.h"
return 0;
}
After a message queue is created it will exist in the system between program calls.
If you do not use msgctl() , you could leave the message queue hanging
around. There is a UNIX command to find this out. The command is called ipcs.
An example of ipcs is shown as below.
# ipcs
IPC status from <running system> as of Thu Mar 5 19:35:42 1998
T ID KEY MODE OWNER GROUP
Message Queues:
q 50 0x4200766d -Rrw-rw-rw- pkser mygroup
Shared Memory facility not in system.
Semaphore facility not in system.
This tells us that there is one message queue owned by the user named ‘pkser’; its
identifier is 50 and it was created with 0666, which gives read/write privileges to
everyone. Any number of pkser’s processes could access this message queue.
The implementation of the message queues is like a link list. You can realize
the mechanism of kernel to store the message queue identifier to the system.
This reading shows the message queues implementation and demonstrates the
message queue structures in kernel. Finally, it discusses the use of arguments
in msgsnd system call.
This Web page clearly explains the concept and technique of the message
queue. It shows how to connect to a queue and send/receives messages.
Finally, it describes how to destroy a message queue to save system resources.
This Web page gives two programs that will communicate using message
queues. You can duplicate a copy of the given programs code and try to
compile the programs yourself to reinforce your understanding of the message
queue concept.
ACTIVITY 4.1
Compile the kirk.c and spock.c provided at the Reading 4.15 URL:
Hall, B (1997) ‘Message queues’ in Beej’s Guide to Unix Interprocess
Communication, Version 0.9.3: http://www.ecst.csuchico.edu/~beej/
guide/ipc/mq.html (14 May 1997, cited 8 July 1998).
SELF-TEST 4.1
The use of the shared memory is usually accompanied by the semaphore. The
fastest way of moving data between processes is not moving the data at all —
both can share some memory. When data are written to a shared memory
segment, they are immediately available to the other process. The only trick in
using shared memory is synchronizing access to a given region among multiple
processes. If one process is placing data into the shared memory, the other
processes should not access the corresponding data in the memory.
Just as in other IPC techniques, a user identifies the shared memory by a system
identifier. A shared memory identifier (shmid) is a unique positive integer
created by a shmget system call. Each shmid has a segment of memory
(referred to as a shared memory segment) and a data structure associated with it.
Details of the concepts of semaphore and shared memory are not covered in this
course. If you are interested in knowing more about these topics, please refer to
the following readings.
Optional
A visit to the following Web page is highly recommended. The content is
clear and easy to understand, and the author, Brian Hall, is familiar with the
differences between the semctl function on the Linux OS and HPUX (HP
UNIX) OS.
The next topic discusses the Socket, a programming technique for building
network application.
INTRODUCTION
Networking is essential in today’s computer environments. The benefits of
networking are numerous. A user in a local host can easily access shared
resources provided by a distant host. In order to accomplish this, the processes in
the local host and in the remote host have to establish communication. The
process in the local host is considered as the client requests certain services from
the remote host. In this situation, the remote host is called the server, which is
regarded as the services provider to the client.
server process that may be running on a remote system. The main advantage of a
distributed software model is that, even in a heterogeneous environment (which
means concerned systems involving different types of hardware and software),
each part of the program can be developed independently of the others, thus
taking advantage of special-purpose hardware and software. Besides the telnet
application, many applications such as File Transfer Protocol (ftp), electronic
mail, Web server, and clients etc. can be developed by socket programming.
Certainly, you can design processes running on the same system to communicate
via sockets. Does this mean the socket programming techniques can totally
replace those IPC techniques? Indeed, you may consider the functionality of IPC
to be a subset of socket programming. However, you should also bear in mind that
IPC and sockets are totally different mechanisms and each has its own pros and
cons. The implementation of sockets is much more complicated than that of IPC,
and so it is less efficient than IPC in providing a communication channel between
processes. If two processes are residing on the same system, IPC is preferred over
sockets to provide the communication channel.
It is common for a client/server application that the server process has to serve
client requests from both the remote and local systems. With sockets, all client
requests can be handled. However, this may not be an optimal design, as sockets
also handle the client requests from the same system. A better design may be
implementing both sockets and IPC on the server process — that is use sockets to
handle client requests from remote systems while using IPC to handle the requests
from the local system.
Figure 5.1 below shows two applications using sockets. The differences between
sockets and normal file descriptors occur in the creation of a socket and through a
variety of special operations to control a socket. These operations are different
between sockets and normal file descriptors because of the additional complexity
in establishing network connections compared with normal disk access.
From a programmer’s point of view, a socket looks and behaves much like a file
descriptor, because commands such as read and write work with sockets in the
same way they do with files and pipes. As far as your application is concerned,
the socket is the end of the channel. When you create the socket, the socket
routines return a file descriptor used when accessing the endpoint.
The next reading introduces the Berkeley socket interface. This may be quite
helpful to you in the elementary stage. It compares network I/O to file I/O. It also
shows some of the steps required to use sockets and compares it with other
techniques such as message queues and FIFOs, which you learned in Topic 4.
You can see the complexity imposed by the networking routines compared with
message queues and FIFOs.
Connection-oriented mode
Figure 5.3 shows a time line of the typical scenario that takes place for a
connection-oriented transfer: First the server is started, and then sometime later a
client is started that connects to the server. A brief description of the mechanism
is explained below the figure.
On the server
Create a socket. This action is like establishing a connection using a telephone.
The socket routine returns a socket descriptor.
bind a socket. This is the assignment of the phone number to the socket.
listen to the socket. This is similar to plugging the telephone into the socket.
accept the connection. This process waits for the request of the connection from
the client.
read, send, recv and write operation. Communication is started.
On the client
Create a socket. This is the client’s telephone.
bind a socket. This stage is optional for the client process, because the server
does not have to dial back to the client.
connect the server socket. The connection is established with this system call.
read, send, recv and write operation.
Connectionless mode
For a client-server using a connectionless protocol, the system calls are different.
Figure 5.3 shows these system calls.
The client does not establish a connection with the server, but the client side needs
to know the socket information of the server. The client then sends a datagram to
the server using the sendto system call, which requires the address of the
destination (the server) as a parameter. Similarly, the server does not have to
accept a connection from a client. Instead, the server just issues a recvfrom
system call that waits until data arrive from a client. The recvfrom returns the
network address of the client process, along with the datagram, so the server can
send its response to the correct process. Compared with the connection-oriented
Socket types
Sockets are categorized according to the communication properties visible to a
user. Processes are presumed to communicate only between sockets of the same
type, although nothing prevents communication between sockets of different
types, should the underlying communication protocols support this. Four types of
socket are currently available to a user.
A stream socket (SOCK_STREAM) provides for the bi-directional, reliable,
sequenced, and unduplicated flow of data without record boundaries. Aside
from the bi-directionality of data flow, a pair of connected stream sockets
provides an interface nearly identical to that of pipes. SOCK_STREAM type of
socket has the ability to queue incoming connection requests, which is a lot
like having ‘call waiting’ for your telephone. If you are busy handling a
connection, the connection request will wait until you can deal with it.
A datagram socket (SOCK_DGRAM) supports bi-directional flow of data
which are not necessarily sequenced, reliable, or unduplicated. That is, a
process receiving messages on a datagram socket may find messages
duplicated and possibly in an order different from the order in which it was
sent. An important characteristic of a datagram socket is that record
boundaries in data are preserved. Datagram sockets closely model the
facilities found in many contemporary packet-switched networks such as the
Ethernet.
A raw socket (SOCK_RAW) is a protocol under the transport layer. Raw
sockets are not intended for the general user; they have been provided mainly
for those interested in developing new communication protocols.
A sequenced packet socket (SOCK_SEQPACKET) is similar to a stream socket
and maintains message boundaries. However, TCP/IP does not support
message boundaries; SOCK_SEQPACKET does not exist in TCP/IP.
Another potential socket type that has interesting properties is the reliably
delivered message socket. The reliably delivered message socket has similar
properties to those of a datagram socket but with reliable delivery. There is
currently no support for this type of socket, but a reliably delivered message
protocol similar to Xerox’s Packet Exchange Protocol (PEX) may be simulated at
the user level.
The most commonly used sockets are the first two mentioned above, so we deal
mostly with the SOCK_STREAM and SOCK_DGRAM in this topic.
UNIX domain
In the UNIX domain, a socket is addressed by a UNIX path name that may be up
to 105 characters long. The binding of a path name to a socket results in the
allocation of an inode and an entry of the path name into the file system. This
necessitates removing the path name from the file system (using the unlink
system call) when the socket is closed. The created file is only used to provide a
name for the socket and does not play a role in the actual transfer of data. When
using sockets in the UNIX domain, it is advisable to only use path names for
directories (such as /tmp) directly mounted on the local disk. The UNIX domain
only allows interprocess communication for processes working on the same
machine. The structure sockaddr_un used to define the UNIX address format
can be found in <sys/un.h>.
struct sockaddr_un {
short sun_family; /* AF_Unix */
char sun_path[105]; /* pathname */
};
Internet domain
In the DARPA Internet domain, addresses consist of two parts — a host address
(consisting of a network number and a host number) and a port number. This host
address allows processes on different machines to communicate. The port number
in turn is like a mailbox that allows multiple addresses on the same host. The
structure sockaddr_in describing an address in the Internet domain is defined
in the file <netinet/in.h>.
struct in_addr {
u_long s_addr; /* 32-bit netid/hostid,
network byte ordered*/
};
struct sockaddr_in {
short sin_family; /* AF_INET */
u_short sin_port; /* 16-bit port number */
/* network byte ordered */
struct in_addr sin_addr; /* 32-bit netid/
hostid */
/* network byte ordered */
char sin_zero[5]; /* unused */
};
The declaration of the data type is useful in binding a name to a socket. The step
to bind a socket is introduced in a later section. The structure sockadd_in
defines what you need to specify a socket in system. For example, if you declare
my_addr with sockadd_in, the my_addr.sin_family will store the
domain. Meanwhile, the IP and port number of the socket to bind are saved in
my_addr.sin_port and my_addr.sin_addr.s_addr. A simple
initialization of the my_addr is given below:
my_addr.sin_family = AF_INET; /* host byte order */
The above lines introduce two new system calls, htons and inet_addr. The
htons call is used to convert the port number 1555 into a number with the
format of network byte order. The use of inet_addr is a simple convert an IP
with ‘dot’ format to a 32-bit number with network byte order.
ntohls converts 16-bit network byte order value into host order
SELF-TEST 5.1
But just before going into those details, it may be helpful for your understanding
if you can get an overall picture of how the system calls are positioned in a typical
connection-oriented socket program. The following reading shows a complete
program listing of a simple client/server program with a detailed analysis of the
code. The program is called chef/cook and uses connection-oriented
communication. You may just want to browse through those descriptions for the
system calls, as they are described in detail in this section.
To introduce the system calls required for socket communication, we start from
those on the server program. Typically, the server process has to execute the
following system calls before a client can connect to a server process:
socket() — for creating a socket
bind() — for assigning a name to the socket
listen() — for specifying the maximum number of pending client
connections
You should keep in mind that the sequence of executing these system calls is
important, as there are dependencies between them. You cannot assign a name to
a socket before its creation, right?
This call requests that the system create a socket in the specified family and of
the specified type. The family can be AF_Unix (UNIX internal protocols) or
AF_INET (Internet protocols). The AF_ prefix stands for address family.
Not all combinations of socket family and type are valid. The following list
shows the valid combinations, along with the actual protocol that is selected by
the pair.
AF_Unix AF_INET
SOCK_STREAM Yes TCP
SOCK_DGRAM Yes UDP
SOCK_RAW IP
SOCK_SEQPACKET
The boxes marked ‘Yes’ are valid but don’t have handy acronyms. The empty
boxes are not implemented.
This call returns a small positive integer called a socket descriptor, which can be
used as a parameter to reference the socket in subsequent system calls. Socket
descriptors are similar to file descriptors returned by the open system call. Each
open or socket call will return the smallest unused integer. Thus, a given number
denotes either an open file or a socket. Socket and file descriptors may be used
interchangeably in many system calls. For example, the close system call is used
to destroy sockets.
ACTIVITY 5.1
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, int addrlen);
The first argument is a socket descriptor that was returned by the socket system
call. The second argument is a pointer to a protocol-specific address, and the third
argument is the size of this address structure. Client applications are not required
to be bound to a specific address or local port number — the operating system
takes care of that.
SELF-TEST 5.2
ACTIVITY 5.2
If you wanted to bind the name ‘/tmp/foo’ to a UNIX domain socket,
what should the code be?
It is usually executed after both the socket and bind system calls and
immediately before the accept system call. The sockfd argument is a file
descriptor corresponding to a socket. The socket must be a type of
SOCK_STREAM or SOCK_SEQPACKET. The backlog argument specifies how
many connection requests can be queued by the system while it waits for the
server to execute the accept system call. Most systems silently limit this number
to about 20; you can probably get away with setting it to 5 or 10.
If the accept() system call is successful, it creates a new socket that will be
used for exchanging data with the client. The call accept() returns the new
socket descriptor to the calling program. The original server socket continues to
listen for incoming connection request messages.
{
close(sockfd); /* child */
doit(newsockfd); /* process the request */
exit(0);
}
close(newsockfd); /* parent */
}
When a connection request is received and accepted, the process forks, the child
process serves the connection and the parent waits for another connection request.
All five elements of the 5-tuple associated with newsockfd have been filled in on
return from accept.
SELF-TEST 5.3
ACTIVITY 5.3
After introducing the system calls that are necessary for preparing the server
process to connect with the client processes, we now come to discuss the system
calls that have to be executed in the client process before it can connect to the
server process. Basically, this involves two system calls — socket() and
connect(). The socket() system call in the client process is also used to
create a socket, and the connect() system call is used for making connection to
the server process. As the socket() system call is introduced above, only the
connect() system call is described below.
#include <sys/types.h>
#include <sys/socket.h>
The sockfd is a socket descriptor that was returned by the socket system call.
The second and third arguments are a pointer to a socket address and its size,
respectively. Typically, in connection-oriented applications the client program
executes the connect system call, which results in a connection between the
local and remote systems.
SELF-TEST 5.4
ACTIVITY 5.4
Suppose you are required to get a socket file descriptor and then
connect to ‘123.45.6.10’ on port ‘23’. What will be the program code?
(Hint: In order to convert an IP address into a 32-bit integer with
network byte order, the inet_addr call should be used. This routine can
interpret character strings representing numbers expressed in the
Internet standard ‘.’ notation, returning numbers suitable for use as
Internet addresses.)
Once the socket for the communication is created and bound and the
establishment of the communication channel is ready, the next step is to transfer
data between the client hosts and the servers. Various system calls are provided by
UNIX systems for Input/Output operations. The simplest way to perform the
read/write operations is to apply the standard read() and write() system
calls. As well, send() and recv() system calls may be used. They are also
described below.
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
int read(int sockfd, char *buff, int size);
#include <unistd.h>
int write(int sockfd, char *buff, int size);
Let’s briefly review the use of these system calls. The sockfd argument is an
open descriptor. size bytes are read or written from the file associated with
sockfd into the buffer pointed to by buff. Note that write blocks until data
are transferred to the socket buffers.
These two functions are for communicating over stream sockets or connected
datagram sockets.
#include <sys/types.h>
#include <sys/socket.h>
For the send socket call, sockfd is the socket descriptor you want to send data to
(whether it’s the one returned by socket or the one you got with accept).
buff is a pointer to the data you want to send, and size is the length of that data
in bytes. The flag argument is what differentiates the send routine from the
write system call. If the flag argument is 0, the send routine behaves exactly as
the write system call. If it is not 0, it can be logical OR of the following values:
MSG_OOB Send out-of-band (urgent) data on sockets that support
this notion. The underlying protocol must also support
‘out-of-band’ data. Only SOCK_STREAM sockets created
in the AF_INET address family support out-of-band data.
MSG_DONTROUTE The SO_DONTROUTE option is turned on for the duration
of the operation. Only diagnostic or routing programs use
it.
For the recv socket call, sockfd is the socket descriptor to read from, buff is
the buffer to read the information into, size is the maximum length of the buffer,
and flag can again be set to 0. recv() returns the number of bytes actually
read into the buffer, or -1 on error. The flags argument is what differentiates the
recv from the read system call. If the flag argument is 0, the recv routine
behaves exactly as the read system call. Otherwise, the flags parameter is
formed by ORing one or more of the following:
MSG_OOB Read any ‘out-of-band’ data present on the socket rather than the
regular ‘in-band’s data.
MSG_PEEK ‘Peek’ at the data present on the socket; the data are returned but
not consumed, so that a subsequent receive operation will see the
same data.
This prevents any more reads and writes to the socket. Anyone attempting to read
or write the socket on the remote end will receive an error.
After performing all of the necessary communication between the client and
server processes, the connection may be closed using the close() system call.
This will prevent any more reads and writes to the socket. Anyone attempting to
read or write the socket on the remote end will receive an error.
SELF-TEST 5.5
1. What is the function of send system call? When would you use
send system call or write system call?
2. What is the function of recv system call? When would you use
recv system call or read system call?
The application is a client/server time machine. The client process connects to the
server host to request the return of the timestamp from the server. The implement
of the client process is listed in Figure 5.2. The client created a socket and
connects to the server’s socket named serv_addr. After the setup of the
connection, data transmission can start and then the client can read the timestamp
from the server. From the client’s point of view, what it needed to set up the
connection is the server IP address and the corresponding port number. In this
example, the port number of the timeserver is defined at 5113.
/* TCP client that finds the time from a server */
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
if (argc != 2)
{
fprintf(stderr, “usage: %s IPaddr\n”, argv[0]);
exit(1);
}
/* create endpoint */
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror(NULL);
exit(2);
}
/* connect to server */
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(TIME_PORT);
if (connect(sockfd, (struct sockaddr *) &serv_addr,
sizeof(serv_addr)) < 0)
{
perror(NULL);
exit(3);
}
/* transfer data */
nread = read(sockfd, buf, SIZE);
write(1, buf, nread);
close(sockfd);
exit(0);
}
Similar to the client process, the server implementation is listed in Fig.5.2. The
server creates a socket and binds it to port 5113. The binding step is necessary for
the server process; otherwise, the client process cannot connect to it. After the
binding, the server will stop and wait (listen) for the client request. Once a
client request arrives, the server process will keep on going and create a new
socket client_sockfd to handle the communication with the client process.
The server socket sockfd will then be free to accept other new coming requests.
/* TCP time server with the port number 5113 */
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
/* bind address */
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(TIME_PORT);
if (bind(sockfd, &serv_addr, sizeof(serv_addr)) < 0)
{
perror(NULL);
exit(3);
}
/* specify queue */
listen(sockfd, 5);
for (;;)
{
len = sizeof(client_addr);
client_sockfd = accept(sockfd, &client_addr, &len);
if (client_sockfd == -1)
{
perror(NULL);
continue;
}
/* transfer data */
time(&t);
sprintf(buf, “%s”, asctime(localtime(&t)));
len = strlen(buf) + 1;
write(client_sockfd, buf, len);
close(client_sockfd);
}
}
ACTIVITY 5.5
SELF-TEST 5.6
There are two types of socket system call (i.e. listen and accept) used by
the server program in Activity 5.5. Please describe their functions.
Since no connection is established between the client and server processes, there
will not be system calls that are analogue to listen(), accept() and
connect() in the connection-oriented socket programs. The connectionless
server program will also create and assign a name to a socket using socket()
and bind(). After that, the server process waits for the arrival of data from the
client process through the recvfrom() system call. In response to client’s
request, the server process will also send datagram to the clients using the
sendto() system call. The close() system call is also used to prevent any
further read/write from a socket.
sendto() and recvfrom() system calls are described below. Before going
into the details, the following example shows the framework of a typical
connectionless socket program. This may help you get an overall picture of how
the system calls are positioned in a typical connectionless socket program.
Example 5.1
Have you tried using ICQ? You can ‘talk’ with someone via the ICQ server. In
this example, we refer to the program code in Figure 5.1 to design a program for
two clients’ communication. Details of the code are not shown here, but the flow
of the design is listed for your reference.
Both clients run the same program code on two machines, HostA and HostB. The
server HostC needs to know their corresponding IP addresses. In the client
program, it is very similar to the source in Figure 5.1. However, this client
program requires a read function, recvfrom(), to receive the messages from its
partner.
/* Client Process*/
/* close socket */
close(sockid);
}
The function of the server works as a message exchanger. Once the server
receives a message from HostA, it would use sendto() to transmit to the
HostB. The other way round is also true.
/* Server */
/* Binding a socket ID */
bind (...)
...
close(sockid);
}
The sockfd argument is a file descriptor in a socket file. The buff argument
contains the data you want to send or receive, and the addrlen is the number of
bytes in the data. The to argument in the sendto system call contains the
destination address of the message (datagram) sent to the network. You must use
the sendto routine if a datagram transport provider is being used and you want
to send datagrams to several different sockets. Because you specify the destination
address on every message, the sendto routine lets you vary the destination on
every datagram.
The recvfrom routine can be used on both the connection and connectionless
oriented modes. The from argument in the recvfrom system call contains the
source address of the message received from the network. After the recvfrom
routine fills the from argument with the address of the socket that sent the data, it
resets the addrlen argument to contain the number of bytes in the address.
SELF-TEST 5.7
These applications require the user in the client process to input the user’s name.
The user’s name will then be sent to the server process and displayed to the
terminal. The server IP address is 202.40.219.245, which is the host of
plbpc011.ouhk.edu.hk. In order to execute the process, you have to run the server
process first. Then, the server process waits for the client connection and data.
After executing the server process, you can start to run the client process. [Note:
you will be given an account on this new machine when we cover this topic.]
/* Client Process*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define MY_PORT_ID 6059
#define SERVER_PORT_ID 6090
#define SERV_HOST_ADDR "202.40.219.245" /*plbpc011.ouhk.edu.hk host*/
main()
{
int sockid, retcode, length;
struct sockaddr_in my_addr, server_addr;
char yourname[55];
printf("Client: creating socket\n");
if ( (sockid = socket(AF_INET,SOCK_DGRAM, 0)) < 0)
{
printf("Client: socket failed: %d\n",errno);
exit(0);
}
printf("Client: binding my local socket\n");
bzero((char *) &my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
my_addr.sin_port = htons(MY_PORT_ID);
if ( ( bind(sockid, (struct sockaddr *) &my_addr, sizeof(my_addr))
< 0) )
{
printf("Client: bind fail: %d\n",errno);
exit(0);
}
}
retcode = sendto(sockid,yourname,length,0,(struct sockaddr *)
&server_addr, sizeof(server_addr));
if (retcode <= -1)
{
printf("client: sendto failed: %d\n",errno);
exit(0);
}
/* close socket */
close(sockid);
}
/* receiver */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define MY_PORT_ID 6090 /* a number > 5000 */
main()
{
int sockid, nread, addrlen;
struct sockaddr_in my_addr, client_addr;
char msg[55];
printf("Server: creating socket\n");
if ( (sockid = socket (AF_INET,SOCK_DGRAM, 0)) < 0)
{
printf("Server: socket error:%d\n",errno);
exit(0);
}
printf("Server: binding my local socket\n");
bzero((char *) &my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_addr.s_addr = htons(INADDR_ANY);
my_addr.sin_port = htons(MY_PORT_ID);
if ( ( bind (sockid, (struct sockaddr *) &my_addr, sizeof(my_addr))
< 0) )
{
printf("Server: bind fail: %d\n",errno);
exit(0);
}
printf("Server: starting blocking message read\n");
nread = recvfrom(sockid,msg,56,0, (struct sockaddr*)&client_addr,
&addrlen);
if (nread >0) printf("Welcome. Your name is: %.54s\n",msg);
close(sockid);
}
Program 5.2: Server process by using recvfrom (program5.2-server)
The output of programs 5.1 and 5.2 should look like this:
[Note: There is a secure bug in the codes of Program 5.1 and Program 5.2. Despite
this bug, the program is still useful in illustrating the concepts of client and server
proramming. You will learn how to fix the bug in the respective TMA.]
ACTIVITY 5.6
This reading describes the multiple ways to set options that affect a socket,
setsockopt, fcntl and ioctl system calls. It lists all socket options for
those system calls.
ASYNCHRONOUS I/O
Asynchronous I/O allows the process to tell the kernel to notify it when a
specified descriptor is ready for I/O. It is also called signal-driven I/O. The
notification from the kernel to the user process takes place with a signal, the
SIGIO signal.
The process must enable asynchronous I/O using the fcntl system call,
using the F_SETFl command and the FASYNC argument.
A brief summary of the most frequently used system calls for socket
programming can be obtained from this website. This site also provides the
help manual for system calls such as htonl, htons, ntohl, ntohs,
inet_addr, inet_network, gethostbyaddr and
gethostbyname: http://www.scit.wlv.ac.uk/cgi-bin/mansec?3N+htons
Many commonly used system calls of sockets have been introduced to help you
understand how to write client/server applications. The concept of socket
programming is a prerequisite for developing network applications. Detailed
explanations of system calls such as connect, listen, send, recv and so on are
presented in this topic.
As promised earlier, next topic examines remote procedure calls (RPC). RPC is
another mechanism is used for interprocess communications in network
applications.
INTRODUCTION
A remote procedure call (RPC) facility can make it possible to implement client-
server applications without needing to explicitly issue requests for communication
services from within the application program. The idea behind a remote procedure
call facility is that procedure calls are a well-understood mechanism for
transferring control and data from one procedure to another in a computing
application. It is very useful to extend the procedure call mechanism from a set of
procedures in a single-computer environment to a set of procedures in a
distributed, client-server environment. Application programs that use a client-
server approach can then be developed using the same procedure call techniques
that are used in the single-host environment.
time the procedure is actually invoked. With a remote procedure call facility,
binding is even more complex because it involves finding the host computer on
which the desired procedure resides and loading the procedure into memory if
required.
Client-server rendezvous
During actual operation, the RPC facility running in the client procedure’s host
may use a directory service such as DNS to determine on which remote host in the
Internet the called procedure resides. The server host is typically found before the
calling procedure actually invokes the remote procedure.
When the RPC facility in the remote host receives the argument information
generated as a result of the procedure call, it determines whether the requested
server procedure already resides in computer storage there. If it does not, a facility
in the remote host loads the program module containing the requested procedure
and passes control to it, again using a stub unique to that procedure. The called
procedure then passes results back to the calling procedure and passes control
back to it using a process similar to that described for the calling procedure.
Marshalling
The RPC facility uses the services of the TCP/IP networking software to transmit
argument information and results between the RPC facility in the local host and
the RPC facility in the remote host. The process of converting the argument
information in the local host into data units that can be transmitted over the
Internet and perform the same process in the opposite direction for results is
called marshalling.
The following example socket calls show how to implement a procedure to find
the time on a remote machine as a string, using the IP socket calls and using IP
socket calls.
if (sockfd =
socket(AF_INET,
SOCK_STREAM, 0))
< 0)
return 1;
serv_addr.sin_family =
AF_INET;
serv_addr.sin_addr.s_addr =
inet_addr(machine);
serv_addr.sin_port =
htons(13);
if (connect(sockfd,
&serv_addr,
sizeof(serv_addr))
< 0)
return 2;
nread = read(sockfd,
time_buf,
sizeof(time_buf));
time_buf[nread] = '\0';
close(sockfd);
return 0;
}
ACTIVITY 6.1
For some types of procedure, it does not matter whether a procedure is executed
multiple times, as long as it can be determined that the procedure has executed at
least once. Such a procedure is called idempotent. A procedure that simply returns
an uncomplicated result, such as the time of day, or makes a complete
replacement of a data value is idempotent. A procedure that adds a value to a data
element, such as a bank balance, is not idempotent.
The differences between local procedure calling and remote procedure calling
make it necessary to distinguish among three types of remote procedure call
semantics that an RPC facility may be designed to handle: at least once, at most
once, and exactly once.
At least once
If an RPC facility provides at least once semantics, it allows the calling procedure
to determine only that the procedure was executed at least one time. Such
semantics are useful only for idempotent procedures. The RPC facility in the
client host can keep trying the request, and once the called procedure finally
responds, control can be returned to the calling procedure. The calling procedure
then knows that the called procedure was executed one or more times. Many RPC
facilities allow the user to specify that the called procedure is idempotent and that
at least once semantics are all that is required for a particular call.
RPC implementation
Many software subsystems are typically used to provide remote procedure call
facilities in the TCP/IP environment. In this topic, however, we only mention one
— the RPC facility developed by Sun Microsystems — to show the RPC
implementation. This Sun RPC facility provides at most once semantics and
allows the use of at least once semantics for the applications that do not require at
most once semantics.
2 Then run rpcgen data.x to produce three files: a server stub (date_svc.c), a client
stub (date_clnt.c) and a header file (date.h).
3 Create your server procedures (e.g. date_proc.c) and client main function (e.g.
rdate.c)
4 Then compile these files to create the server and client programs:
client: gcc -o rdate rdate.c date_clnt.c –lnsl
server: gcc -o date_svc date_proc.c date_svc.c -lnsl
Figure 6.3 summarizes the process that is used to prepare the client and server
program modules for execution.
Note that the BIN_TIME procedure is called using the name bin_time_1, and
the STR_TIME procedure is called using the name str_time_1.
long *
bin_time_1()
{
static long timeval;
long time();
timeval = time((long *) 0);
return (&timeval);
}
The code for the bin_time_1 procedure is identical to the code that would be
used if the procedure were going to be called locally. It simply obtains a binary
time value using a standard UNIX function and passes control back to the calling
procedure.
char ** str_time_1(bintime)
long *bintime;
{
static char *ptr;
char *ctime();
ptr = ctime(bintime);
return (&ptr);
}
The code for the str_time_1 procedure is also identical to the code that would
be used if it were called locally. It accepts a long integer argument and converts
the time value contained in the argument to a string time value. It then returns a
pointer to the string time value to the called procedure.
Before either of the procedures can be invoked, the client program module must
call a procedure named clnt_create() that is provided with the Sun RPC
facility.
Note that the argument list indicates that the remote procedure has no arguments
and references that the descriptor established previously. The statement invoking
the remote procedure specifies that the long integer result be placed into lresult.
For the details of Sun RPC, you should refer to the following reading. It
describes the actual RPC implementation with the Sun RPC system. A simple
date time RPC example is provided to illustrate the development of RPC
application.
A more detailed description about the concept of RPC and its implementation
is given in the following URL. It illustrates an example of finding the time on
a remote machine as a string. You may take a look if you would like to know
more about RPC: http://pandonia.canberra.edu.au/OS/l14_1.html
SELF-TEST 6.1
ACTIVITY 6.2
1. The following program is run on a single machine. Suppose now
the procedure printmessage() is required to run over the network
(turned to a remote procedure). Modify the program using the RPC
method.
/*
* printmsg.c: print a message on the console
*/
#include <stdio.h>
main(int argc, char **argv)
{
char *message;
if (argc < 2) {
fprintf(stderr, "usage: %s <message>\n",
argv[0]);
exit(1);
}
message = argv[1];
if (!printmessage(message)) {
fprintf(stderr, "%s: couldn’t print your
message\n", argv[0]);
exit(1);
}
printf("Message Delivered!\n");
exit(0);
}
/*
* Print a message to the console. Return a boolean
* indicating whether the message was actually
* printed.
*/
printmessage(char *msg)
{
FILE *f;
f = fopen("/dev/console", "w");
if (f == NULL) {
return(0);
}
fprintf(f, "%s\n", msg);
fclose(f);
return(1);
}
Copyright © Open University Malaysia (OUM)
104 TOPIC 6 REMOTE PROCEDURE CALLS
http://www.scit.wlv.ac.uk/cgi-bin/mansec?3N+htons
http://pandonia.canberra.edu.au/OS/l14_1.html
OR
Thank you.