Inter-Process Communication

On Unix-like operating systems, several means of inter-process communication (IPC) are available, for instance:

In modern Fortran, we have access to further IPC mechanisms, such as:

Signals

On POSIX-compliant operating systems, signals are used to interrupt, terminate, or control running processes. A process can react to specific signals by implementing signal handlers for them. For example, an application may catch SIGINT to terminate when the user presses CTRL + C. Common POSIX signals are listed in table 1. See signal(3) for a more comprehensive overview.

# Name Default Action Description
1 SIGHUP Terminate process. Terminal line hang-up.
2 SIGINT Terminate process. Interrupt program.
3 SIGQUIT Create core image. Quit program.
9 SIGKILL Terminate process. Kill program.
Table 1: Selection of POSIX signals on FreeBSD

GNU Fortran

GNU Fortran provides a compiler-dependent interface to POSIX signals. A signal handler is registered by calling the signal() routine. The signal number and the handler function are passed as arguments:

! signal.f90
module handlers
    implicit none
contains
    subroutine sigint_handler()
       print '("Process interrupted (SIGINT), exiting ...")'
    end subroutine sigint_handler
end module handlers

program main
    use :: handlers
    implicit none

    ! Register signal handler.
    call signal(2, sigint_handler)
    print '("Press CTRL + C to send SIGINT.")'

    do
        print '("zzz ...")'
        call sleep(1)
    end do
end program main

Press CTRL + C to send SIGINT to the Fortran program. The signal handler must be an external or module routine.

POSIX

The library fortran-unix provides an ISO C binding interface to signal(3) for Fortran 2008, without depending on non-standard compiler extensions:

! signal.f90
program main
    use :: unix
    implicit none
    integer        :: rc
    type(c_funptr) :: ptr

    ! Register signal handler.
    ptr = c_signal(SIGINT, c_funloc(sigint_handler))
    print '("Press CTRL + C to send SIGINT.")'

    do
        print '("zzz ...")'
        rc = c_usleep(10**6)
    end do
contains
    subroutine sigint_handler(signum) bind(c)
        !! Signal handler for SIGINT.
        integer(kind=c_int), intent(in), value :: signum

        print '("Received SIGINT (", i0, "). Terminating ...")', signum
        stop
    end subroutine sigint_handler
end program main

Link the example with libfortran-unix.a:

$ gfortran13 -o signal signal.f90 libfortran-unix.a
$ ./signal

We can run kill -s PID, with s being the signal number and PID the process id, to send an arbitrary signal. The Unix command-line tool ps(1) returns the ids of all processes. The PID of process signal is piped to kill, using signal 1 (SIGHUB):

$ ps -A | grep signal | awk '{ print $1 }' | xargs kill -1 $1

The Fortran program has to register a SIGHUB handler to catch the signal.

Anonymous Pipes

Unix pipes allow processes to communicate with other processes without having been designed explicitly for this task. The input and output streams of processes can be chained together, providing a one-way flow of data. Anonymous or unnamed pipes are a way of inter-process communication that let the output of one program to become the input of another. The output can be redirected by using the pipe symbol | on the command-line.

The following example program reads given values in degrees Fahrenheit (°F) from stdin and writes the converted values in degrees Celsius (°C) to stdout:

! f2c.f90
program main
    implicit none
    integer :: rc
    real    :: f

    do
        read (*, *, iostat=rc) f                ! Read degrees Fahrenheit (without unit) from stdin.
        if (rc /= 0) exit                       ! Exit on input error.
        print '(f0.2, " °C")', (f - 32) * 5 / 9 ! Write degrees Celsius (with unit) to stdout.
    end do
end program main

After compilation, just pipe the value in degrees Fahrenheit to the Fortran program:

$ gfortran13 -o f2c f2c.f90
$ echo "60.45" | ./f2c
15.81 °C

The < symbol allows to redirect the input to come from a file, while > writes the output to a file:

$ cat fahrenheit.txt
24.6
74.3
79.8
30.0
$ ./f2c < fahrenheit.txt
-4.11 °C
23.50 °C
26.56 °C
-1.11 °C

Furthermore, we can read the input values from file fahrenheit.txt and write the output to file celsius.txt:

$ ./f2c < fahrenheit.txt > celsius.txt
$ cat celsius.txt
-4.11 °C
23.50 °C
26.56 °C
-1.11 °C

The output of a Fortran program can be piped to other applications as well, for instance, to format the current time:

! time.f90
program main
    implicit none
    integer :: dt(8)

    call date_and_time(values=dt)
    print '(3(i0, ":"), i0)', dt(5), dt(6), dt(7), dt(8)
end program main

Pipe the output of program time to FIGlet:

$ gfortran13 -o time time.f90
$ ./time | figlet -f mini
        _  _  _  _     _ __
/||_|_o|_ / \o_)|_ o/||_  /
 |  | o _)\_/o_) _)o ||_)/

Named Pipes

A further kind of pipes are named pipes, also knows as FIFO (first in, first out). The pipe name is simply an arbitrary file name on the file system. On Unix, named pipes can be created with mkfifo, and removed with rm or unlink.

$ mkfifo ./pipe
$ echo "Hello, World!" > ./pipe

Another process may then read from the named pipe:

$ tail -f ./pipe
Hello, World!

In Fortran, the read and write statements can be utilised to interchange data through named pipes.

! example.f90
program main
    implicit none
    character(len=*), parameter :: FIFO_PATH = '/tmp/fifo.tmp'

    character(len=32) :: buf
    integer           :: fu, rc
    logical           :: file_exists

    inquire (file=FIFO_PATH, exist=file_exists)

    if (.not. file_exists) then
        print '("Error: File ", a, " not found")', FIFO_PATH
        stop
    end if

    do
        ! Open named pipe as formatted stream.
        open (access='stream', &
              action='read', &
              file=FIFO_PATH, &
              form='formatted', &
              iostat=rc, &
              newunit=fu, &
              status='old')

        if (rc /= 0) then
            print '("Error: Opening ", a, " failed")', FIFO_PATH
            exit
        end if

        do
            read (fu, '(a)', iostat=rc) buf

            if (is_iostat_end(rc)) then
                ! End of file.
                exit
            else if (rc /= 0) then
                ! Input error.
                cycle
            end if

            print '(a)', trim(buf)
        end do

        close (fu)
    end do
end program main

The Fortran 2003 function is_iostat_end(i) returns whether the given I/O status variable has the value of end of file. The function is equivalent to comparing the variable with the IOSTAT_END parameter of the intrinsic Fortran module iso_fortran_env.

Compile the source code, create a new named pipe with mkfifo, and then run the example:

$ gfortran13 -o example example.f90 libfortran-unix.a
$ mkfifo /tmp/fifo.tmp
$ ./example

The Fortran program will output any data it receives trough the named pipe from another process:

$ echo "Hello" > /tmp/fifo.tmp

Be aware that the input data is read in record-based in this example (form='formatted'), i. e., spaces and commas separate input values. Make sure to remove the named pipe when done:

$ unlink /tmp/fifo.tmp

References