Previous Contents Next

Communication Between Processes

The use of processes in application development allows you to delegate work. Nevertheless, these jobs may not be independent and it may be necessary for the processes to communicate with each other.

We introduce two methods of communication between processes: communication pipes and signals. This chapter does not discuss all possibilities of process communication. It is only a first approach to the applications developed in chapters 19 and 20.

Communication Pipes

It is possible for processes to communicate directly between each other in a file oriented style.

Pipes are something like virtual files from which it is possible to read and to write with the input / output functions read and write. They are of limited size, the exact limit depending from the system. They behave like queues: the first input is also the first output. Whenever data is read from a pipe, it is also removed from it.

This queue behavior is realized by the association of two descriptors with a pipe: one corresponding to the end of the pipe where new entries are written and one for the end where they are read. A pipe is created by the function:

# Unix.pipe ;;
- : unit -> Unix.file_descr * Unix.file_descr = <fun>
The first component of the resulting pair is the exit of the pipe used for reading. The second is the entry of the pipe used for writing. All processes knowing them can close the descriptors.

Reading from a pipe is blocking, unless all processes knowing its input descriptor (and therefore able to write to it) have closed it; in the latter case, the function read returns 0. If a process tries to write to a full pipe, it is suspended until another process has done a read operation. If a process tries to write to a pipe while no other process is available to read from it (all having closed their output descriptors), the process trying to write receives the signal sigpipe, which, if not indicated otherwise, leads to its termination.

The following example shows a use of pipes in which grandchildren tell their process number to their grandparents.
let output, input = Unix.pipe();;

let write_pid input =
try
let m = "(" ^ (string_of_int (Unix.getpid ())) ^ ")"
in ignore (Unix.write input m 0 (String.length m)) ;
Unix.close input
with
Unix.Unix_error(n,f,arg) ->
Printf.printf "%s(%s) : %s\n" f arg (Unix.error_message n) ;;

match Unix.fork () with
0 -> for i=0 to 5 do
match Unix.fork() with
0 -> write_pid input ; exit 0
| _ -> ()
done ;
Unix.close input
| _ -> Unix.close input;
let s = ref "" and buff = String.create 5
in while true do
match Unix.read output buff 0 5 with
0 -> Printf.printf "My grandchildren are %s\n" !s ; exit 0
| n -> s := !s ^ (String.sub buff 0 n) ^ "."
done ;;


We obtain the trace:
My grandchildren are (1067.3).(1067.4).(1067.8).(1067.7).(1067.6).(1067.5).


We have introduced points between each part of the sequence read. This way it is possible to read from the trace the succession of contents of the pipe. Note how the reading is desynchronized: whenever an entry is made, even a partial one, it is consumed.

Named pipes.
Some Unix systems support named pipes, which look as if they were normal files. It is possible then to communicate between two processes without a descendence relation using the name of the pipe. The following function allows you to create such a pipe.

# Unix.mkfifo ;;
- : string -> Unix.file_perm -> unit = <fun>


The file descriptors necessary to use the pipe are obtained by openfile, as for usual files, but their behavior is that of pipes. In particular, the command lseek can not be used, since we have waiting lines.

Warning


mkfifo is not implemented for Windows.


Communication Channels

The Unix module provides a high level function allowing you to start a program associating with it input or output channels of the calling program:

# Unix.open_process ;;
- : string -> in_channel * out_channel = <fun>
The argument is the name of the program, or more precisely the calling path of the program, as we would write it to a command line interpreter. The string may contain arguments for the program to execute. The two output values are file descriptors associated with the standard input / output of the started program. It will be executed in parallel with the calling program.

Warning


The program started by open_process is executed via a call to the Unix command line interpreter /bin/sh.
The use of that function is therefore only possible for systems that have this interpreter.


We can end the execution of a program started by open_process by using:

# Unix.close_process ;;
- : in_channel * out_channel -> Unix.process_status = <fun>
The argument is the pair of channels associated with a process we want to close. The return value is the execution status of the process whose termination we wait.

There are variants of that functions, opening and closing only one input or output channel:

# Unix.open_process_in ;;
- : string -> in_channel = <fun>
# Unix.close_process_in ;;
- : in_channel -> Unix.process_status = <fun>
# Unix.open_process_out ;;
- : string -> out_channel = <fun>
# Unix.close_process_out ;;
- : out_channel -> Unix.process_status = <fun>


Here is a nice small example for the use of open_process: we start ocaml from ocaml!

# let n_print_string s = print_string s ; print_string "(* <-- *)" ;;
val n_print_string : string -> unit = <fun>
# let p () =
let oc_in, oc_out = Unix.open_process "/usr/local/bin/ocaml"
in n_print_string (input_line oc_in) ; print_newline() ;
n_print_string (input_line oc_in) ; print_newline() ;
print_char (input_char oc_in) ;
print_char (input_char oc_in) ;
flush stdout ;
let s = input_line stdin
in output_string oc_out s ;
output_string oc_out "#quit\n" ;
flush oc_out ;
let r = String.create 250 in
let n = input oc_in r 0 250
in n_print_string (String.sub r 0 n) ;
print_string "Thank you for your visit\n" ;
flush stdout ;
Unix.close_process (oc_in, oc_out) ;;
val p : unit -> Unix.process_status = <fun>
The call of the function p starts a toplevel of Objective CAML. We note that it is version 2.03 which is in directory /usr/local/bin. The first four read operations allow us to get the header, which is shown by toplevel. The line let x = 1.2 +. 5.6;; is read from the keybard, then sent to oc_out (the output channel bound to the standard input of the new process). This one evaluates the passed Objective CAML expression and writes the result to the standard output which is bound to the input channel oc_in. This result is read and written to the output by the function input. Also the string "Thank you for your visit" is written to the output. We send the command #quit;; to exit the new process.
# p();;
        Objective Caml version 2.03

# let x = 1.2 +. 5.6;;
val x : float = 6.8
Thank you for your visit
- : Unix.process_status = Unix.WSIGNALED 13
# 

Signals under Unix

One possibility to communicate with a process is to send it a signal. A signal may be received at any moment during the execution of a program. Reception of a signal causes a logical interruption. The execution of a program is interrupted to treat the received signal. Then the execution continues at the point of interruption. The number of signals is quite restricted (32 under Linux). The information carried by a signal is quite rudimentary: it is only the identity (the number) of the signal. The processes have a predefined reaction to each signal. However, the reactions can be redefined for most of the signals.

The data and functions to handle signals are distributed between the modules Sys and Unix. The module Sys contains signals conforming to the POSIX norm (described in [Ste92]) as well as some functions to handle signals. The module Unix defines the function kill to send a signal. The use of signals under Windows is restricted to sigint.

A signal may have several sources: the keyboard, an illegal attempt to access memory, etc. A process may send a signal to another by calling the function

# Unix.kill ;;
- : int -> int -> unit = <fun>
Its first parameter is the PID of the receiver. The second is the signal which we want to send.

Handling Signals

There are three categories of reactions associated with a signal. For each category there is a constructor of type signal_behavior: On reception of a signal, the execution of the receiving process is diverted to the function handling the signal. The function allowing you to redefine the behavior associated with a signal is provided by the module Sys:

# Sys.set_signal;;
- : int -> Sys.signal_behavior -> unit = <fun>
The first argument is the signal to redefine. The second is the associated behavior.

The module Sys provides another modification function to handle signals:

# Sys.signal ;;
- : int -> Sys.signal_behavior -> Sys.signal_behavior = <fun>


It behaves like set_signal, except that it returns in addition the value associated with the signal before the modification. So we can write a function returning the behavioral value associated with a signal. This can be done even without changing this value:

# let signal_behavior s =
let b = Sys.signal s Sys.Signal_default
in Sys.set_signal s b ; b ;;
val signal_behavior : int -> Sys.signal_behavior = <fun>
# signal_behavior Sys.sigint;;
- : Sys.signal_behavior = Sys.Signal_handle <fun>


However, the behavior associated with some signals can not be changed. Therefore our function can not be used for all signals:

# signal_behavior Sys.sigkill ;;
Uncaught exception: Sys_error("Invalid argument")


Some Signals

We illustrate the use of some essential signals.

sigint.
This signal is generally associated with the key combination CTRL-C. In the following small example we modify the reaction to this signal so that the receiving process is not interrupted until the third occurence of the signal.

We create the following file ctrlc.ml:
let sigint_handle =
let n = ref 0
in function _ -> incr n ;
match !n with
1 -> print_string "You just pushed CTRL-C\n"
| 2 -> print_string "You pushed CTRL-C a second time\n"
| 3 -> print_string "If you insist ...\n" ; exit 1
| _ -> () ;;
Sys.set_signal Sys.sigint (Sys.Signal_handle sigint_handle) ;;
match Unix.fork () with
0 -> while true do () done
| pid -> Unix.sleep 1 ; Unix.kill pid Sys.sigint ;
Unix.sleep 1 ; Unix.kill pid Sys.sigint ;
Unix.sleep 1 ; Unix.kill pid Sys.sigint ;;


This program simulates the push of the key combination CTRL-C by sending the signal sigint. We obtain the following execution trace:
$ ocamlc -i -o ctrlc ctrlc.ml
val sigint_handle : int -> unit
$ ctrlc
You just pushed CTRL-C
You pushed CTRL-C a second time
If you insist ...
sigalrm.
Another frequently used signal is sigalrm, which is associated with the system clock. It can be sent by the function

# Unix.alarm ;;
- : int -> int = <fun>
The argument specifies the number of seconds to wait before the sending of the signal sigalrm. The return value indicates the number of remaining seconds before the sending of a second signal, or if there is no alarm set.

We use this function and the associated signal to define the function timeout, which starts the execution of another function and interrupts it if neccessary, when the indicated time is elapsed. More precisely, the function timeout takes as arguments a function f, the argument arg expected by f, the duration (time) of the ``timeout'' and the value (default_value) to be returned when the duration time has elapsed.

A timeout is handled as follows:
  1. We modify the behavior associated with the signal sigalrm so that a Timeout exception is thrown.
  2. We take care to remember the behavior associated originally with sigalrm, so that it can be restored.
  3. We start the clock.
  4. We distinguish two cases:
    1. If everything goes well, we restore the original state of sigalrm and return the value of the calculation.
    2. If not, we restore sigalrm, and if the duration has elapsed, we return the default value.
Here are the corresponding definitions and a small example:

# exception Timeout ;;
exception Timeout
# let sigalrm_handler = Sys.Signal_handle (fun _ -> raise Timeout) ;;
val sigalrm_handler : Sys.signal_behavior = Sys.Signal_handle <fun>
# let timeout f arg time default_value =
let old_behavior = Sys.signal Sys.sigalrm sigalrm_handler in
let reset_sigalrm () = Sys.set_signal Sys.sigalrm old_behavior
in ignore (Unix.alarm time) ;
try let res = f arg in reset_sigalrm () ; res
with exc -> reset_sigalrm () ;
if exc=Timeout then default_value else raise exc ;;
val timeout : ('a -> 'b) -> 'a -> int -> 'b -> 'b = <fun>
# let iterate n = for i = 1 to n do () done ; n ;;
val iterate : int -> int = <fun>

Printf.printf "1st execution : %d\n" (timeout iterate 10 1 (-1));
Printf.printf "2nd execution : %d\n" (timeout iterate 100000000 1 (-1)) ;;
1st execution : 10
2nd execution : -1
- : unit = ()
sigusr1 and sigusr2.
These two signals are provided only for the programer. They are not used by the operating system.

In this example, reception of the signal sigusr1 by the child triggers the output of the content of variable i.
let i = ref 0  ;;
let write_i s = Printf.printf "signal received (%d) -- i=%d\n" s !i ;
flush stdout ;;
Sys.set_signal Sys.sigusr1 (Sys.Signal_handle write_i) ;;

match Unix.fork () with
0 -> while true do incr i done
| pid -> Unix.sleep 0 ; Unix.kill pid Sys.sigusr1 ;
Unix.sleep 3 ; Unix.kill pid Sys.sigusr1 ;
Unix.sleep 1 ; Unix.kill pid Sys.sigkill



Here is the trace of a program execution:
signal received (10) -- i=0
signal received (10) -- i=167722808


When we examine the trace, we can see that after having executed the code associated with signal sigusr1 the first time, the child process continues to execute the loop and to increment i.

sigchld.
This signal is sent to a parent on termination of a process. We will use it to make a parent more attentive to the evolution of its children. Here's how:
  1. We define a function handling the signal sigchld. It handles all terminated children on reception of this signal5 and terminates the parent when he does not have any more children (exception Unix_error). In order not to block the parent if not all his children are dead, we use waitpid instead of wait.
  2. The main program, after having redefined the reaction associated with sigchld, loops to create five children. After this, the parent does something else (loop while true) until his children have terminated.
let rec sigchld_handle s =
try let pid, _ = Unix.waitpid [Unix.WNOHANG] 0
in if pid <> 0
then ( Printf.printf "%d is dead and buried at signal %d\n" pid s ;
flush stdout ;
sigchld_handle s )
with Unix.Unix_error(_, "waitpid", _) -> exit 0 ;;

let i = ref 0
in Sys.set_signal Sys.sigchld (Sys.Signal_handle sigchld_handle) ;
while true do
match Unix.fork() with
0 -> let pid = Unix.getpid ()
in Printf.printf "Creation of %d\n" pid ; flush stdout ;
Unix.sleep (Random.int (5+ !i)) ;
Printf.printf "Termination of %d\n" pid ; flush stdout ;
exit 0
| _ -> incr i ; if !i = 5 then while true do () done
done ;;


We obtain the trace:
Creation of 10658
Creation of 10659
Creation of 10662
Creation of 10661
Creation of 10660
Termination of 10662
10662 is dead and buried at signal 17
Termination of 10658
10658 is dead and buried at signal 17
Termination of 10660
Termination of 10659
10660 is dead and buried at signal 17
10659 is dead and buried at signal 17
Termination of 10661
10661 is dead and buried at signal 17







Previous Contents Next