Important Unix Fundamentals
Process Control:<stdlib.h>,<unistd.h>
A process is basically a single running program. It may be a “system” program (e.g login, update, csh) or program initiated by the user (textedit, dbxtool or a user written one).
When UNIX runs a process it gives each process a unique number – a process ID, pid.
The UNIX command ps will list all current processes running on your machine and will list the pid.
The C function int getpid() will return the pid of process that called this function.
A program usually runs as a single process. However later we will see how we can make programs run as several separate communicating processes.
Running UNIX Commands from C
We can run commands from a C program just as if they were from the UNIX command line by using thesystem() function. NOTE: this can save us a lot of time and hassle as we can run other (proven) programs, scripts etc. to do set tasks.
int system(char *string) — where string can be the name of a unix utility, an executable shell script or a user program. System returns the exit status of the shell. System is prototyped in<stdlib.h>
Example: Call ls from a program
main()
{ printf(``Files in Directory are:n'');system(``ls -l'');
}
system is a call that is made up of 3 other system calls: execl(), wait() and fork() (which are prototyed in <unistd>)
execl()
execl has 5 other related functions — see man pages.
execl stands for execute and leave which means that a process will get executed and then terminated by execl.
It is defined by:
execl(char *path, char *arg0,...,char *argn, 0);
The last parameter must always be 0. It is a NULL terminator. Since the argument list is variable we must have some way of telling C when it is to end. The NULL terminator does this job.
where path points to the name of a file holding a command that is to be executed, argo points to a string that is the same as path (or at least its last component.
arg1 ... argn are pointers to arguments for the command and 0 simply marks the end of the (variable) list of arguments.
So our above example could look like this also:
main()
{ printf(``Files in Directory are:n'');execl(`/bin/ls'',``ls'', ``-l'',0);
}
fork()
int fork() turns a single process into 2 identical processes, known as the parent and the child. On success, fork() returns 0 to the child process and returns the process ID of the child process to the parent process. On failure, fork() returns -1 to the parent process, sets errno to indicate the error, and no child process is created.
NOTE: The child process will have its own unique PID.
The following program illustrates a simple use of fork, where two copies are made and run together (multitasking)
main()
{ int return_value;printf(``Forking processn'');
fork();
printf(``The process id is %d
and return value is %dn",
getpid(), return_value);
execl(``/bin/ls/'',``ls'',``-l'',0);
printf(``This line is not printedn'');
}
The Output of this would be:
Forking process
The process id is 6753 and return value is 0
The process id is 6754 and return value is 0
two lists of files in current directory
NOTE: The processes have unique ID's which will be different at each run.
It also impossible to tell in advance which process will get to CPU's time -- so one run may differ from the next.
When we spawn 2 processes we can easily detect (in each process) whether it is the child or parent since fork returns 0 to the child. We can trap any errors if fork returns a -1. i.e.:
int pid; /* process identifier */
pid = fork();
if ( pid < 0 )
{ printf(``Cannot fork!!n'');exit(1);
}
if ( pid == 0 )
{ /* Child process */ ...... } else
{ /* Parent process pid is child's pid */.... }
wait()
int wait (int *status_location) — will force a parent process to wait for a child process to stop or terminate. wait() return the pid of the child or -1 for an error. The exit status of the child is returned to status_location.
exit()
void exit(int status) — terminates the process which calls this function and returns the exitstatus value. Both UNIX and C (forked) programs can read the status value.
By convention, a status of 0 means normal termination any other value indicates an error or unusual occurrence. Many standard library calls have errors defined in the sys/stat.h header file. We can easily derive our own conventions.
Interprocess Communication (IPC), Pipes
We have now began to see how multiple processes may be running on a machine and maybe be controlled (spawned by fork() by one of our programs.
In numerous applications there is clearly a need for these processes to communicate with each exchanging data or control information. There are a few methods which can accomplish this task. We will consider:
- Pipes
- Signals
- Message Queues
- Semaphores
- Shared Memory
- Sockets
In this chapter, we will study the piping of two processes. We will study the others in turn in subsequent chapters.
Piping in a C program: <stdio.h>
Piping is a process where the input of one process is made the input of another. We have seen examples of this from the UNIX command line using .
We will now see how we do this from C programs.
We will have two (or more) forked processes and will communicate between them.
We must first open a pipe
UNIX allows two ways of opening a pipe.
popen() — Formatted Piping
FILE *popen(char *command, char *type) — opens a pipe for I/O where the command is the process that will be connected to the calling process thus creating the pipe. The type is either “r” – for reading, or “w” for writing.
popen() returns is a stream pointer or NULL for any errors.
A pipe opened by popen() should always be closed by pclose(FILE *stream).
We use fprintf() and fscanf() to communicate with the pipe’s stream.
pipe() — Low level Piping
int pipe(int fd[2]) — creates a pipe and returns two file descriptors, fd[0], fd[1]. fd[0] is opened for reading, fd[1] for writing.
pipe() returns 0 on success, -1 on failure and sets errno accordingly.
The standard programming model is that after the pipe has been set up, two (or more) cooperative processes will be created by a fork and data will be passed using read() and write().
Pipes opened with pipe() should be closed with close(int fd).
Example: Parent writes to a child
int pdes[2];
pipe(pdes);
if ( fork() == 0 )
{ /* child */close(pdes[1]); /* not required */
read( pdes[0]); /* read from parent */
.....
}
else
{ close(pdes[0]); /* not required */write( pdes[1]); /* write to child */
.....
}
An futher example of piping in a C program is plot.c and subroutines and it performs as follows:
- The program has two modules plot.c (main) and plotter.c.
- The program relies on you having installed the freely gnuplot graph drawing program in the directory /usr/local/bin/ (in the listing below at least) -- this path could easily be changed.
- The program plot.c calls gnuplot
- Two Data Stream is generated from Plot
- y = sin(x)
- y = sin(1/x)
- 2 Pipes created -- 1 per Data Stream.
- °Gnuplot produces ``live'' drawing of output.
The code listing for plot.c is:
/* plot.c - example of unix pipe. Calls gnuplot graph drawing package to draw
graphs from within a C program. Info is piped to gnuplot */
/* Creates 2 pipes one will draw graphs of y=0.5 and y = random 0-1.0 */
/* the other graphs of y = sin (1/x) and y = sin x */
/* Also user a plotter.c module */
/* compile: cc -o plot plot.c plotter.c */
#include "externals.h"
#include <signal.h>
#define DEG_TO_RAD(x) (x*180/M_PI)
double drand48();
void quit();
FILE *fp1, *fp2, *fp3, *fp4, *fopen();
main()
{ float i;float y1,y2,y3,y4;
/* open files which will store plot data */
if ( ((fp1 = fopen("plot11.dat","w")) == NULL) || ((fp2 = fopen("plot12.dat","w")) == NULL) || ((fp3 = fopen("plot21.dat","w")) == NULL) || ((fp4 = fopen("plot22.dat","w")) == NULL) ) { printf("Error can't open one or more data files\n");exit(1);
}
signal(SIGINT,quit); /* trap ctrl-c call quit fn */
StartPlot();
y1 = 0.5;
srand48(1); /* set seed */
for (i=0;;i+=0.01) /* increment i forever use ctrl-c to quit prog */
{ y2 = (float) drand48();if (i == 0.0)
y3 = 0.0;
else
y3 = sin(DEG_TO_RAD(1.0/i));
y4 = sin(DEG_TO_RAD(i));
/* load files */
fprintf(fp1,"%f %f\n",i,y1);
fprintf(fp2,"%f %f\n",i,y2);
fprintf(fp3,"%f %f\n",i,y3);
fprintf(fp4,"%f %f\n",i,y4);
/* make sure buffers flushed so that gnuplot */
/* reads up to data file */
fflush(fp1);
fflush(fp2);
fflush(fp3);
fflush(fp4);
/* plot graph */
PlotOne();
usleep(250); /* sleep for short time */
}
}
void quit()
{ printf("\nctrl-c caught:\n Shutting down pipes\n");StopPlot();
printf("closing data files\n");fclose(fp1);
fclose(fp2);
fclose(fp3);
fclose(fp4);
printf("deleting data files\n");RemoveDat();
}
The plotter.c module is as follows:
/* plotter.c module */
/* contains routines to plot a data file produced by another program */
/* 2d data plotted in this version */
/**********************************************************************/
#include "externals.h"
static FILE *plot1,
*plot2,
*ashell;
static char *startplot1 = "plot [] [0:1.1]'plot11.dat' with lines,
'plot12.dat' with lines\n";
static char *startplot2 = "plot 'plot21.dat' with lines,
'plot22.dat' with lines\n";
static char *replot = "replot\n";
static char *command1= "/usr/local/bin/gnuplot> dump1";
static char *command2= "/usr/local/bin/gnuplot> dump2";
static char *deletefiles = "rm plot11.dat plot12.dat plot21.dat plot22.dat";
static char *set_term = "set terminal x11\n";
void
StartPlot(void)
{ plot1 = popen(command1, "w");fprintf(plot1, "%s", set_term);
fflush(plot1);
if (plot1 == NULL)
exit(2);
plot2 = popen(command2, "w");
fprintf(plot2, "%s", set_term);
fflush(plot2);
if (plot2 == NULL)
exit(2);
}
void
RemoveDat(void)
{ ashell = popen(deletefiles, "w");exit(0);
}
void
StopPlot(void)
{ pclose(plot1);pclose(plot2);
}
void
PlotOne(void)
{ fprintf(plot1, "%s", startplot1);fflush(plot1);
fprintf(plot2, "%s", startplot2);
fflush(plot2);
}
void
RePlot(void)
{ fprintf(plot1, "%s", replot);fflush(plot1);
}
The header file externals.h contains the following:
/* externals.h */
#ifndef EXTERNALS
#define EXTERNALS
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
/* prototypes */
void StartPlot(void);
void RemoveDat(void);
void StopPlot(void);
void PlotOne(void);
void RePlot(void);
#endif
IPC:Interrupts and Signals: <signal.h>
In this section will look at ways in which two processes can communicate. When a process terminates abnormally it usually tries to send a signal indicating what went wrong. C programs (and UNIX) can trap these for diagnostics. Also user specified communication can take place in this way.
Signals are software generated interrupts that are sent to a process when a event happens. Signals can be synchronously generated by an error in an application, such as SIGFPE and SIGSEGV, but most signals are asynchronous. Signals can be posted to a process when the system detects a software event, such as a user entering an interrupt or stop or a kill request from another process. Signals can also be come directly from the OS kernel when a hardware event such as a bus error or an illegal instruction is encountered. The system defines a set of signals that can be posted to a process. Signal delivery is analogous to hardware interrupts in that a signal can be blocked from being delivered in the future. Most signals cause termination of the receiving process if no action is taken by the process in response to the signal. Some signals stop the receiving process and other signals can be ignored. Each signal has a default action which is one of the following:
- The signal is discarded after being received
- The process is terminated after the signal is received
- A core file is written, then the process is terminated
- Stop the process after the signal is received
Each signal defined by the system falls into one of five classes:
- Hardware conditions
- Software conditions
- Input/output notification
- Process control
- Resource control
Macros are defined in <signal.h> header file for common signals.
These include:
| SIGHUP 1 /* hangup */ | SIGINT 2 /* interrupt */ |
| SIGQUIT 3 /* quit */ | SIGILL 4 /* illegal instruction */ |
| SIGABRT 6 /* used by abort */ | SIGKILL 9 /* hard kill */ |
| SIGALRM 14 /* alarm clock */ | |
| SIGCONT 19 /* continue a stopped process */ | |
| SIGCHLD 20 /* to parent on child stop or exit */ |
Signals can be numbered from 0 to 31.
Sending Signals — kill(), raise()
There are two common functions used to send signals
int kill(int pid, int signal) – a system call that send a signal to a process, pid. If pid is greater than zero, the signal is sent to the process whose process ID is equal to pid. If pid is 0, the signal is sent to all processes, except system processes.
kill() returns 0 for a successful call, -1 otherwise and sets errno accordingly.
int raise(int sig) sends the signal sig to the executing program. raise() actually uses kill() to send the signal to the executing program:
kill(getpid(), sig);
There is also a UNIX command called kill that can be used to send signals from the command line – seeman pages.
NOTE: that unless caught or ignored, the kill signal terminates the process. Therefore protection is built into the system.
Only processes with certain access privileges can be killed off.
Basic rule: only processes that have the same user can send/receive messages.
The SIGKILL signal cannot be caught or ignored and will always terminate a process.
For examplekill(getpid(),SIGINT); would send the interrupt signal to the id of the calling process.
This would have a similar effect to exit() command. Also ctrl-c typed from the command sends aSIGINT to the process currently being.
unsigned int alarm(unsigned int seconds) — sends the signal SIGALRM to the invoking process after seconds seconds.
Signal Handling — signal()
An application program can specify a function called a signal handler to be invoked when a specific signal is received. When a signal handler is invoked on receipt of a signal, it is said to catch the signal. A process can deal with a signal in one of the following ways:
- The process can let the default action happen
- The process can block the signal (some signals cannot be ignored)
- the process can catch the signal with a handler.
Signal handlers usually execute on the current stack of the process. This lets the signal handler return to the point that execution was interrupted in the process. This can be changed on a per-signal basis so that a signal handler executes on a special stack. If a process must resume in a different context than the interrupted one, it must restore the previous context itself
Receiving signals is straighforward with the function:
int (*signal(int sig, void (*func)()))() — that is to say the function signal() will call thefunc functions if the process receives a signal sig. Signal returns a pointer to function func if successful or it returns an error to errno and -1 otherwise.
func() can have three values:
SIG_DFL
– a pointer to a system default function SID_DFL(), which will terminate the process upon receipt ofsig.
SIG_IGN
– a pointer to system ignore function SIG_IGN() which will disregard the sig action (UNLESS it isSIGKILL).
A function address
– a user specified function.
SIG_DFL and SIG_IGN are defined in signal.h (standard library) header file.
Thus to ignore a ctrl-c command from the command line. we could do:
signal(SIGINT, SIG_IGN);
TO reset system so that SIGINT causes a termination at any place in our program, we would do:
signal(SIGINT, SIG_DFL);
So lets write a program to trap a ctrl-c but not quit on this signal. We have a function sigproc() that is executed when we trap a ctrl-c. We will also set another function to quit the program if it traps theSIGQUIT signal so we can terminate our program:
#include <stdio.h>
void sigproc(void);
void quitproc(void);
main()
{ signal(SIGINT, sigproc);signal(SIGQUIT, quitproc);
printf(``ctrl-c disabled use ctrl- to quitn'');
for(;;); /* infinite loop */}
void sigproc()
{ signal(SIGINT, sigproc); /* *//* NOTE some versions of UNIX will reset signal to default
after each call. So for portability reset signal each time */
printf(``you have pressed ctrl-c n'');
}
void quitproc()
{ printf(``ctrl- pressed to quitn'');exit(0); /* normal exit status */
}
sig_talk.c — complete example program
Let us now write a program that communicates between child and parent processes using kill() and signal().
fork() creates the child process from the parent. The pid can be checked to decide whether it is the child (== 0) or the parent (pid = child process id).
The parent can then send messages to child using the pid and kill().
The child picks up these signals with signal() and calls appropriate functions.
An example of communicating process using signals is sig_talk.c:
/* sig_talk.c --- Example of how 2 processes can talk */
/* to each other using kill() and signal() */
/* We will fork() 2 process and let the parent send a few */
/* signals to it`s child */
/* cc sig_talk.c -o sig_talk */
#include <stdio.h>
#include <signal.h>
void sighup(); /* routines child will call upon sigtrap */
void sigint();
void sigquit();
main()
{ int pid;/* get child process */
if ((pid = fork()) < 0) { perror("fork");exit(1);
}
if (pid == 0)
{ /* child */signal(SIGHUP,sighup); /* set function calls */
signal(SIGINT,sigint);
signal(SIGQUIT, sigquit);
for(;;); /* loop for ever */
}
else /* parent */
{ /* pid hold id of child */ printf("\nPARENT: sending SIGHUP\n\n");kill(pid,SIGHUP);
sleep(3); /* pause for 3 secs */
printf("\nPARENT: sending SIGINT\n\n");kill(pid,SIGINT);
sleep(3); /* pause for 3 secs */
printf("\nPARENT: sending SIGQUIT\n\n");kill(pid,SIGQUIT);
sleep(3);
}
}
void sighup()
{ signal(SIGHUP,sighup); /* reset signal */ printf("CHILD: I have received a SIGHUP\n");}
void sigint()
{ signal(SIGINT,sigint); /* reset signal */ printf("CHILD: I have received a SIGINT\n");}
void sigquit()
{ printf("My DADDY has Killed me!!!\n");exit(0);
}
Other signal functions
There are a few other functions defined in signal.h:
int sighold(int sig) — adds sig to the calling process’s signal mask
int sigrelse(int sig) — removes sig from the calling process’s signal mask
int sigignore(int sig) — sets the disposition of sig to SIG_IGN
int sigpause(int sig) — removes sig from the calling process’s signal mask and suspends the calling process until a signal is received
IPC:Message Queues:<sys/msg.h>
The basic idea of a message queue is a simple one.
Two (or more) processes can exchange information via access to a common system message queue. The sending process places via some (OS) message-passing module a message onto a queue which can be read by another process (Figure 24.1). Each message is given an identification or type so that processes can select the appropriate message. Process must share a common key in order to gain access to the queue in the first place (subject to other permissions — see below).
Fig. 24.1 Basic Message Passing IPC messaging lets processes send and receive messages, and queue messages for processing in an arbitrary order. Unlike the file byte-stream data flow of pipes, each IPC message has an explicit length. Messages can be assigned a specific type. Because of this, a server process can direct message traffic between clients on its queue by using the client process PID as the message type. For single-message transactions, multiple server processes can work in parallel on transactions sent to a shared message queue.
Before a process can send or receive a message, the queue must be initialized (through the msggetfunction see below) Operations to send and receive messages are performed by the msgsnd() andmsgrcv() functions, respectively.
When a message is sent, its text is copied to the message queue. The msgsnd() and msgrcv() functions can be performed as either blocking or non-blocking operations. Non-blocking operations allow for asynchronous message transfer — the process is not suspended as a result of sending or receiving a message. In blocking or synchronous message passing the sending process cannot continue until the message has been transferred or has even been acknowledged by a receiver. IPC signal and other mechanisms can be employed to implement such transfer. A blocked message operation remains suspended until one of the following three conditions occurs:
- The call succeeds.
- The process receives a signal.
- The queue is removed.
Initialising the Message Queue
The msgget() function initializes a new message queue:
int msgget(key_t key, int msgflg)
It can also return the message queue ID (msqid) of the queue corresponding to the key argument. The value passed as the msgflg argument must be an octal integer with settings for the queue’s permissions and control flags.
The following code illustrates the msgget() function.
#include <sys/ipc.h>;
#include <sys/msg.h>;
...
key_t key; /* key to be passed to msgget() */
int msgflg /* msgflg to be passed to msgget() */
int msqid; /* return value from msgget() */
...
key = ...
msgflg = ...
if ((msqid = msgget(key, msgflg)) == –1)
{ perror("msgget: msgget failed");exit(1);
} else
(void) fprintf(stderr, “msgget succeeded");
...
IPC Functions, Key Arguments, and Creation Flags: <sys/ipc.h>
Processes requesting access to an IPC facility must be able to identify it. To do this, functions that initialize or provide access to an IPC facility use a key_t key argument. (key_t is essentially an inttype defined in <sys/types.h>
The key is an arbitrary value or one that can be derived from a common seed at run time. One way is with ftok() , which converts a filename to a key value that is unique within the system. Functions that initialize or get access to messages (also semaphores or shared memory see later) return an ID number of type int. IPC functions that perform read, write, and control operations use this ID. If the key argument is specified as IPC_PRIVATE, the call initializes a new instance of an IPC facility that is private to the creating process. When the IPC_CREAT flag is supplied in the flags argument appropriate to the call, the function tries to create the facility if it does not exist already. When called with both theIPC_CREAT and IPC_EXCL flags, the function fails if the facility already exists. This can be useful when more than one process might attempt to initialize the facility. One such case might involve several server processes having access to the same facility. If they all attempt to create the facility withIPC_EXCL in effect, only the first attempt succeeds. If neither of these flags is given and the facility already exists, the functions to get access simply return the ID of the facility. If IPC_CREAT is omitted and the facility is not already initialized, the calls fail. These control flags are combined, using logical (bitwise) OR, with the octal permission modes to form the flags argument. For example, the statement below initializes a new message queue if the queue does not exist.
msqid = msgget(ftok("/tmp",key), (IPC_CREAT | IPC_EXCL | 0400));
The first argument evaluates to a key based on the string (“/tmp”). The second argument evaluates to the combined permissions and control flags.
Controlling message queues
The msgctl() function alters the permissions and other characteristics of a message queue. The owner or creator of a queue can change its ownership or permissions using msgctl() Also, any process with permission to do so can use msgctl() for control operations.
The msgctl() function is prototypes as follows:
int msgctl(int msqid, int cmd, struct msqid_ds *buf )
The msqid argument must be the ID of an existing message queue. The cmd argument is one of:
IPC_STAT
– Place information about the status of the queue in the data structure pointed to by buf. The process must have read permission for this call to succeed.
IPC_SET
– Set the owner’s user and group ID, the permissions, and the size (in number of bytes) of the message queue. A process must have the effective user ID of the owner, creator, or superuser for this call to succeed.
IPC_RMID
– Remove the message queue specified by the msqid argument.
IPC:Semaphores
Semaphores are a programming construct designed by E. W. Dijkstra in the late 1960s. Dijkstra’s model was the operation of railroads: consider a stretch of railroad in which there is a single track over which only one train at a time is allowed. Guarding this track is a semaphore. A train must wait before entering the single track until the semaphore is in a state that permits travel. When the train enters the track, the semaphore changes state to prevent other trains from entering the track. A train that is leaving this section of track must again change the state of the semaphore to allow another train to enter. In the computer version, a semaphore appears to be a simple integer. A process (or a thread) waits for permission to proceed by waiting for the integer to become 0. The signal if it proceeds signals that this by performing incrementing the integer by 1. When it is finished, the process changes the semaphore’s value by subtracting one from it.
Semaphores let processes query or alter status information. They are often used to monitor and control the availability of system resources such as shared memory segments.
Semaphores can be operated on as individual units or as elements in a set. Because System V IPC semaphores can be in a large array, they are extremely heavy weight. Much lighter weight semaphores are available in the threads library (see man semaphore and also Chapter 30.3) and POSIX semaphores (see below briefly). Threads library semaphores must be used with mapped memory . A semaphore set consists of a control structure and an array of individual semaphores. A set of semaphores can contain up to 25 elements.
In a similar fashion to message queues, the semaphore set must be initialized using semget(); the semaphore creator can change its ownership or permissions using semctl(); and semaphore operations are performed via the semop() function. These are now discussed below:
Initializing a Semaphore Set
The function semget() initializes or gains access to a semaphore. It is prototyped by:
int semget(key_t key, int nsems, int semflg);
When the call succeeds, it returns the semaphore ID (semid).
The key argument is a access value associated with the semaphore ID.
The nsems argument specifies the number of elements in a semaphore array. The call fails when nsemsis greater than the number of elements in an existing array; when the correct count is not known, supplying 0 for this argument ensures that it will succeed.
The semflg argument specifies the initial access permissions and creation control flags.
The following code illustrates the semget() function.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
...
key_t key; /* key to pass to semget() */
int semflg; /* semflg to pass tosemget() */
int nsems; /* nsems to pass to semget() */
int semid; /* return value from semget() */
...
key = ...
nsems = ...
semflg = ... ...
if ((semid = semget(key, nsems, semflg)) == -1) { perror("semget: semget failed");exit(1); }
else
...
Controlling Semaphores
semctl() changes permissions and other characteristics of a semaphore set. It is prototyped as follows:
int semctl(int semid, int semnum, int cmd, union semun arg);
It must be called with a valid semaphore ID, semid. The semnum value selects a semaphore within an array by its index. The cmd argument is one of the following control flags:
GETVAL
– Return the value of a single semaphore.
SETVAL
– Set the value of a single semaphore. In this case, arg is taken as arg.val, an int.
GETPID
– Return the PID of the process that performed the last operation on the semaphore or array.
GETNCNT
– Return the number of processes waiting for the value of a semaphore to increase.
GETZCNT
– Return the number of processes waiting for the value of a particular semaphore to reach zero.
GETALL
– Return the values for all semaphores in a set. In this case, arg is taken as arg.array, a pointer to an array of unsigned shorts (see below).
SETALL
– Set values for all semaphores in a set. In this case, arg is taken as arg.array, a pointer to an array of unsigned shorts.
IPC_STAT
– Return the status information from the control structure for the semaphore set and place it in the data structure pointed to by arg.buf, a pointer to a buffer of type semid_ds.
IPC_SET
– Set the effective user and group identification and permissions. In this case, arg is taken as arg.buf.
IPC_RMID
– Remove the specified semaphore set.
A process must have an effective user identification of owner, creator, or superuser to perform anIPC_SET or IPC_RMID command. Read and write permission is required as for the other control commands. The following code illustrates semctl ().
The fourth argument union semun arg is optional, depending upon the operation requested. If required it is of type union semun, which must be explicitly declared by the application program as:
union semun {int val;
struct semid_ds *buf;
ushort *array;
} arg;
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun {int val;
struct semid_ds *buf;
ushort *array;
} arg;
int i;
int semnum = ....;
int cmd = GETALL; /* get value */
...
i = semctl(semid, semnum, cmd, arg);
if (i == -1) { perror("semctl: semctl failed");exit(1);
}
else
...
Semaphore Operations
semop() performs operations on a semaphore set. It is prototyped by:
int semop(int semid, struct sembuf *sops, size_t nsops);
The semid argument is the semaphore ID returned by a previous semget() call. The sops argument is a pointer to an array of structures, each containing the following information about a semaphore operation:
- The semaphore number
- The operation to be performed
- Control flags, if any.
The sembuf structure specifies a semaphore operation, as defined in <sys/sem.h>.
struct sembuf {ushort_t sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
The nsops argument specifies the length of the array, the maximum size of which is determined by theSEMOPM configuration option; this is the maximum number of operations allowed by a single semop() call, and is set to 10 by default. The operation to be performed is determined as follows:
- A positive integer increments the semaphore value by that amount.
- A negative integer decrements the semaphore value by that amount. An attempt to set a semaphore to a value less than zero fails or blocks, depending on whether IPC_NOWAIT is in effect.
- A value of zero means to wait for the semaphore value to reach zero.
There are two control flags that can be used with semop():
IPC_NOWAIT
– Can be set for any operations in the array. Makes the function return without changing any semaphore value if any operation for which IPC_NOWAIT is set cannot be performed. The function fails if it tries to decrement a semaphore more than its current value, or tests a nonzero semaphore to be equal to zero.
SEM_UNDO
– Allows individual operations in the array to be undone when the process exits.
This function takes a pointer, sops, to an array of semaphore operation structures. Each structure in the array contains data about an operation to perform on a semaphore. Any process with read permission can test whether a semaphore has a zero value. To increment or decrement a semaphore requires write permission. When an operation fails, none of the semaphores is altered.
The process blocks (unless the IPC_NOWAIT flag is set), and remains blocked until:
- the semaphore operations can all finish, so the call succeeds,
- the process receives a signal, or
- the semaphore set is removed.
Only one process at a time can update a semaphore. Simultaneous requests by different processes are performed in an arbitrary order. When an array of operations is given by a semop() call, no updates are done until all operations on the array can finish successfully.
If a process with exclusive use of a semaphore terminates abnormally and fails to undo the operation or free the semaphore, the semaphore stays locked in memory in the state the process left it. To prevent this, the SEM_UNDO control flag makes semop() allocate an undo structure for each semaphore operation, which contains the operation that returns the semaphore to its previous state. If the process dies, the system applies the operations in the undo structures. This prevents an aborted process from leaving a semaphore set in an inconsistent state. If processes share access to a resource controlled by a semaphore, operations on the semaphore should not be made with SEM_UNDO in effect. If the process that currently has control of the resource terminates abnormally, the resource is presumed to be inconsistent. Another process must be able to recognize this to restore the resource to a consistent state. When performing a semaphore operation with SEM_UNDO in effect, you must also have it in effect for the call that will perform the reversing operation. When the process runs normally, the reversing operation updates the undo structure with a complementary value. This ensures that, unless the process is aborted, the values applied to the undo structure are cancel to zero. When the undo structure reaches zero, it is removed.
NOTE:Using SEM_UNDO inconsistently can lead to excessive resource consumption because allocated undo structures might not be freed until the system is rebooted.
IPC:Shared Memory
Shared Memory is an efficeint means of passing data between programs. One program will create a memory portion which other processes (if permitted) can access.
In the Solaris 2.x operating system, the most efficient way to implement shared memory applications is to rely on the mmap() function and on the system’s native virtual memory facility. Solaris 2.x also supports System V shared memory, which is another way to let multiple processes attach a segment of physical memory to their virtual address spaces. When write access is allowed for more than one process, an outside protocol or mechanism such as a semaphore can be used to prevent inconsistencies and collisions.
A process creates a shared memory segment using shmget()|. The original owner of a shared memory segment can assign ownership to another user with shmctl(). It can also revoke this assignment. Other processes with proper permission can perform various control functions on the shared memory segment using shmctl(). Once created, a shared segment can be attached to a process address space using shmat(). It can be detached using shmdt() (see shmop()). The attaching process must have the appropriate permissions for shmat(). Once attached, the process can read or write to the segment, as allowed by the permission requested in the attach operation. A shared segment can be attached multiple times by the same process. A shared memory segment is described by a control structure with a unique ID that points to an area of physical memory. The identifier of the segment is called the shmid. The structure definition for the shared memory segment control structures and prototypews can be found in <sys/shm.h>.
Accessing a Shared Memory Segment
shmget() is used to obtain access to a shared memory segment. It is prottyped by:
int shmget(key_t key, size_t size, int shmflg);
The key argument is a access value associated with the semaphore ID. The size argument is the size in bytes of the requested shared memory. The shmflg argument specifies the initial access permissions and creation control flags.
When the call succeeds, it returns the shared memory segment ID. This call is also used to get the ID of an existing shared segment (from a process requesting sharing of some existing memory portion).
The following code illustrates shmget():
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
...
key_t key; /* key to be passed to shmget() */
int shmflg; /* shmflg to be passed to shmget() */
int shmid; /* return value from shmget() */
int size; /* size to be passed to shmget() */
...
key = ...
size = ...
shmflg) = ...
if ((shmid = shmget (key, size, shmflg)) == -1) { perror("shmget: shmget failed"); exit(1); } else {(void) fprintf(stderr, "shmget: shmget returned %d\n", shmid);
exit(0);
}
...
CONTROLLING A SHARED MEMORY SEGMENT
shmctl() is used to alter the permissions and other characteristics of a shared memory segment. It is prototyped as follows:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
The process must have an effective shmid of owner, creator or superuser to perform this command. The cmd argument is one of following control commands:
SHM_LOCK
– Lock the specified shared memory segment in memory. The process must have the effective ID of superuser to perform this command.
SHM_UNLOCK
– Unlock the shared memory segment. The process must have the effective ID of superuser to perform this command.
IPC_STAT
– Return the status information contained in the control structure and place it in the buffer pointed to by buf. The process must have read permission on the segment to perform this command.
IPC_SET
– Set the effective user and group identification and access permissions. The process must have an effective ID of owner, creator or superuser to perform this command.
IPC_RMID
– Remove the shared memory segment.
The buf is a sructure of type struct shmid_ds which is defined in <sys/shm.h>
Threads: Basic Theory and Libraries
This chapter examines aspects of threads and multiprocessing (and multithreading). We will firts study a little theory of threads and also look at how threading can be effectively used to make programs more efficient. The C thread libraries will then be introduced. The following chapters will look at further thead issues sucj a synchronisation and practical examples.
Processes and Threads
We can think of a thread as basically a lightweight process. In order to understand this let us consider the two main characteristics of a process:
Unit of resource ownership
– A process is allocated:
· a virtual address space to hold the process image
· control of some resources (files, I/O devices…)
Unit of dispatching
- A process is an execution path through one or more programs:
· execution may be interleaved with other processes
· the process has an execution state and a dispatching priority
If we treat these two characteristics as being independent (as does modern OS theory):
- The unit of resource ownership is usually referred to as a process or task. This Processes have:
- a virtual address space which holds the process image.
- protected access to processors, other processes, files, and I/O resources.
- The unit of dispatching is usually referred to a thread or a lightweight process. Thus a thread:
- Has an execution state (running, ready, etc.)
- Saves thread context when not running
- Has an execution stack and some per-thread static storage for local variables
- Has access to the memory address space and resources of its process
- all threads of a process share this when one thread alters a (non-private) memory item, all other threads (of the process) sees that a file open with one thread, is available to others
BENEFITS OF THREADS VS PROCESSES
If implemented correctly then threads have some advantages of (multi) processes, They take:
- Less time to create a new thread than a process, because the newly created thread uses the current process address space.
- Less time to terminate a thread than a process.
- Less time to switch between two threads within the same process, partly because the newly created thread uses the current process address space.
- Less communication overheads — communicating between the threads of one process is simple because the threads share everything: address space, in particular. So, data produced by one thread is immediately available to all the other threads.
MULTITHREADING VS. SINGLE THREADING
Just a we can multiple processes running on some systems we can have multiple threads running:
Single threading
– when the OS does not recognize the concept of thread
Multithreading
– when the OS supports multiple threads of execution within a single process
Figure 28.1 shows a variety of models for threads and processes.
Fig. 28.1 Threads and Processes Some example popular OSs and their thread support is:
MS-DOS
– support a single user process and a single thread
UNIX
– supports multiple user processes but only supports one thread per process
Solaris
– supports multiple threads
Multithreading your code can have many benefits:
- Improve application responsiveness — Any program in which many activities are not dependent upon each other can be redesigned so that each activity is defined as a thread. For example, the user of a multithreaded GUI does not have to wait for one activity to complete before starting another.
- Use multiprocessors more efficiently — Typically, applications that express concurrency requirements with threads need not take into account the number of available processors. The performance of the application improves transparently with additional processors. Numerical algorithms and applications with a high degree of parallelism, such as matrix multiplications, can run much faster when implemented with threads on a multiprocessor.
- Improve program structure — Many programs are more efficiently structured as multiple independent or semi-independent units of execution instead of as a single, monolithic thread. Multithreaded programs can be more adaptive to variations in user demands than single threaded programs.
- Use fewer system resources — Programs that use two or more processes that access common data through shared memory are applying more than one thread of control. However, each process has a full address space and operating systems state. The cost of creating and maintaining this large amount of state information makes each process much more expensive than a thread in both time and space. In addition, the inherent separation between processes can require a major effort by the programmer to communicate between the threads in different processes, or to synchronize their actions.
Figure 28.2 illustrates different process models and thread control in a single thread and multithreaded application.
Fig. 28.2 Single and Multi- Thread Applicatiions
SOME EXAMPLE APPLICATIONS OF THREADS
:
Example : A file server on a LAN
- It needs to handle several file requests over a short period
- Hence more efficient to create (and destroy) a single thread for each request
- Multiple threads can possibly be executing simultaneously on different processors
Example 2: Matrix Multiplication
Matrix Multilication essentially involves taking the rows of one matrix and multiplying and adding corresponding columns in a second matrix i.e:
Fig. 28.3 Matrix Multiplication (3×3 example) Note that each element of the resultant matrix can be computed independently, that is to say by a different thread.
We will develop a C++ example program for matrix multiplication later (see Chapter ).
Thread Levels
There are two broad categories of thread implementation:
- User-Level Threads — Thread Libraries.
- Kernel-level Threads — System Calls.
There are merits to both, in fact some OSs allow access to both levels (e.g. Solaris).
USER-LEVEL THREADS (ULT)
In this level, the kernel is not aware of the existence of threads — All thread management is done by the application by using a thread library. Thread switching does not require kernel mode privileges (no mode switch) and scheduling is application specific
Kernel activity for ULTs:
- The kernel is not aware of thread activity but it is still managing process activity
- When a thread makes a system call, the whole process will be blocked but for the thread library that thread is still in the running state
- So thread states are independent of process states
Advantages and inconveniences of ULT
Advantages:
- Thread switching does not involve the kernel — no mode switching
- Scheduling can be application specific — choose the best algorithm.
- ULTs can run on any OS — Only needs a thread library
Disadvantages:
- Most system calls are blocking and the kernel blocks processes — So all threads within the process will be blocked
- The kernel can only assign processes to processors — Two threads within the same process cannot run simultaneously on two processors
KERNEL-LEVEL THREADS (KLT)
In this level, All thread management is done by kernel No thread library but an API (system calls) to the kernel thread facility exists. The kernel maintains context information for the process and the threads, switching between threads requires the kernel Scheduling is performed on a thread basis.
Advantages and inconveniences of KLT
Advantages
- the kernel can simultaneously schedule many threads of the same process on many processors blocking is done on a thread level
- kernel routines can be multithreaded
Disadvantages:
- thread switching within the same process involves the kernel, e.g if we have 2 mode switches per thread switch this results in a significant slow down.
COMBINED ULT/KLT APPROACHES
Idea is to combine the best of both approaches
Solaris is an example of an OS that combines both ULT and KLT (Figure 28.4:
- Thread creation done in the user space
- Bulk of scheduling and synchronization of threads done in the user space
- The programmer may adjust the number of KLTs
- Process includes the user’s address space, stack, and process control block
- User-level threads (threads library) invisible to the OS are the interface for application parallelism
- Kernel threads the unit that can be dispatched on a processor
- Lightweight processes (LWP) each LWP supports one or more ULTs and maps to exactly one KLT
