We'll be focusing on three subjects for this recitation and the next, having to do with interprocess communication. The first, locks, are used as one way of controlling concurrent access to a resource, either directly (locking a file that is in use) or indirectly (using a special lock file to signify that some other resource is currently in use). Next, signals are a way to send simple communications between processes. Lastly, pipes are one way to communicate arbitrary data between two processes.
fcntlfcntl is used to place or release a lock on a file, to control
concurrent access. This is extremely common in distributed systems, where you
have many clients all wanting to access and modify a lot of shared data. To
prevent total chaos, we have locks, which let processes enforce varying levels
of file access.
int fcntl(int fil_des, int cmd, struct flock lock_struct);
fcntl takes a file descriptor, a command (you'll mostly be using
F_SETLK), and usually an flock struct (technically the
last argument is optional, so it's not shown in the Monkey book's formal prototype
of fcntl) which looks like this:
struct flock {
short l_type; /* lock type: read/write, etc. */
short l_whence; /* type of l_start */
off_t l_start; /* starting offset */
off_t l_len; /* len = 0 means until end of file */
pid_t l_pid; /* lock owner */
};
There are a couple other attributes (see pg 95) but these are the ones we care about. l_type is used to tell what kind of lock you want to set or remove. The next three control the granularity of the locking, so you can lock small pieces of a file if you want to. For your purposes, just set them all to 0 (i.e. lock the whole file) and don't worry about it. The lock owner is the process who set the lock. A couple of notes:
fcntl's cmd
argument, and the flock struct's l_type member.
Unfortunately, the tables for both constants are on the same page (95)
and start with 'F_', so people sometimes confuse them. The tables
are labeled so you know which constants go where, so pay close attention
to that. The way to remove a lock is (somewhat unintuitively) with
F_SETLK as the cmd and F_UNLCK as the l_type
(and not with F_UNLCK
as the cmd).F_SETLK than with
F_GETLK. When setting lock information, it contains instructions
on how to set the lock, so you need to fill in l_type, l_whence, l_start,
and l_len. When getting lock information, it's both a command and a return
value; You fill in the information as for setting a lock, and fcntl finds
information about a lock that interferes with the lock you describe. If no
such lock is found, it sets the flock's l_type to F_UNLCK, meaning
the coast is clear. If there is an interfering lock, the entire flock
structure is filled in with that lock's information, so you can find out
what process already has a lock (l_pid) and what type (l_type).See page 97 for a full example of how you go about setting a lock.
signal and killSignals are widely used in Unix. Any time you Cntl-C out of a program, or kill it from the command line, or any number of other things, you are sending a signal. Programs have default responses to many signals, but you can change them, which combined with the ability to send signals from within a process allows a basic form of inter-process communication.
There's quite a bit of info about signals in the Monkey book, but we are only
going to scratch the surface. Definitely read section 4.4 to understand what
signals are, but don't worry too much about most of the calls in 4.5. The most
important calls to understand are signal and kill.
Using the signal (pg 105) (or the slightly improved sigset
function, you can set up a specific response to a signal. Although the function
prototype looks pretty much indecipherable, it's actually not that hard to use.
By passing it the name of a signal, and a response (SIG_DFL,
SIG_IGN, or a function pointer), you control your program's reaction
to that signal. For example, to ignore the SIGUSR1 signal, you could use:
signal(SIGUSR1, SIG_IGN);
Pages 109-110 and 112 have good examples of signal and sigset,
and show how to declare and use a function pointer.
Now that we can catch signals, we just need to know how to send them. We already know how
to do it from the command line (kill [signal] process_id),
and it's almost the same from within your program, using the kill (pg
103) system call. The main difference is that the order of the parameters is
reversed, so it's kill(pid_t pid, int signal);
Next week we'll cover the details of how pipes are used, but for now I want to give the idea of what pipes are, and why they are useful.
Pipes come in a variety of forms, but the essence is that data goes in one end, and comes out the other; unlike normal files, where data stays after it's written, data in a pipe is destroyed when it's read. The Monkey book makes the analogy that a pipe is like a conveyor belt; you put things on at one end, it's transported to the other end, then it falls off. This makes it a very useful method of communication, somewhat like a telephone: there's no permanent storage, but it gets information from on place to another. The ends of a pipe can be any number of things, but commonly they are two different processes. We've already seen this in the shell when we do something like ps -ef | grep sbm5. The pipe symbol (|) takes data from the output of the ps command to the input of grep. Next week, we'll cover how to do the same thing in our processes.
Stuart Morgan, 2003