Inter-Process Communication

Modern Unix-like operating systems provide several means of inter-process communication (IPC), for instance:

The Fortran 2003 ISO C interoperability features allow us to write interfaces to selected IPC mechanisms.

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 example, an application may catch SIGINT to terminate when the user presses CTRL + C. Some signal numbers for FreeBSD are shown in table 1. See the man pages for a list of all signals. The GNU Fortran provides a compiler-dependent interface to POSIX signals (not a Fortran standard).

No. 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 signals supported by FreeBSD

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
contains
    subroutine sigint_handler
       print *, 'Process interrupted (SIGINT), exiting ...'
    end subroutine sigint_handler
end module handlers

program main
    use :: handlers

    call signal(2, sigint_handler)
    print *, 'Sleeping for 30 seconds ...'
    call sleep(30)
end program main

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

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 f2c
    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 f2c

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

$ gfortran9 -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

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 time
    implicit none
    integer :: dt(8)

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

Pipe the output of time to figlet:

$ gfortran9 -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 within 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 can then read from the named pipe:

$ tail -f ./pipe
Hello, World!

In Fortran, we use the read and write statements to interchange data through named pipes.

SysV Message Queues

Threads and processes can utilise Unix System V message queues to send and receive data in an arbitrary order. Message queues were later adopted by a POSIX standard.

C Function Description
msgget() Creates a new message queue or connects to an existing one.
msgsnd() Sends a message to the message queue.
msgrcv() Receives messages of all types or only messages of the given type from the message queue. The access is blocking by default, unless flag IPC_NOWAIT is set.
msgctl() Changes permissions or closes the message queue.
Table 2: SysV message queue functions in C

Only a few routine calls are necessary to interact with SysV message queues from Fortran (table 2). The Fortran module sysv in sysv.f90 provides implementations of the required interface bindings. A custom derived type holds the message text:

integer, parameter :: MESSAGE_LEN = 256

type, bind(c) :: message_type
    integer(kind=c_long)   :: type
    character(kind=c_char) :: text(MESSAGE_LEN)
end type message_type

Instead of the particular message type, any derived type is allowed. The maximum size of a message is limited, usually 4056 bytes.

Code Name Description
2 ENOENT No message queue exists for key and msgflg did not specify IPC_CREAT.
12 ENOMEM The system does not have enough memory for the new data structure.
13 EACCES The calling process does not have permission to access the queue.
17 EEXIST A message queue already exists for key.
28 ENOSPC The system limit for the maximum number of message queues (MSGMNI) is reached.
Table 3: Error codes written to errno by msgget()

For debugging, we can output the numeric error code that gives further information regarding the type of error (table 3). The C integer errno in errno.h is defined as a macro and cannot be accessed directly from Fortran. Therefore, we first have to write a wrapper function in C, like error_number() in errno.c, that simply returns errno. Then, we are able to implement a Fortran interface, as in module posix in posix.f90.

Compiling the C and Fortran sources with gcc and gfortran creates the object files sysv.o, posix.o, and errno.o for linking:

$ gcc9 -c errno.c
$ gfortran9 -c posix.f90
$ gfortran9 -c sysv.f90

Example

The example opens a new SysV message queue with c_msgget(), sends a message of type 1 with c_msgsnd(), and then waits until c_msgrcv() returns the received message. The message queue access has been abstracted with the Fortran function ipc_send() and ipc_receive(). The example combines sender and receiver in a single process. In general, a message queue would rather be used to interconnect separate threads or processes instead.

! example.f90
program main
    use, intrinsic :: iso_fortran_env, only: stderr => output_unit, stdin => input_unit
    use, intrinsic :: iso_c_binding,   only: c_char, c_loc, c_null_char, c_null_ptr
    use :: posix
    use :: sysv
    implicit none
    integer,         parameter :: IPC_PERM = 0666 ! Access permissions.
    integer(kind=8), parameter :: IPC_TYPE = 1    ! Message type.
    character(len=MESSAGE_LEN) :: string          ! Received message.
    integer                    :: msqid           ! Message queue id.

    ! Create message queue.
    msqid = c_msgget(IPC_PRIVATE, ior(IPC_CREAT, IPC_PERM))

    if (msqid < 0) then
        write (stderr, '(a, i0)') 'msgget() failed: ', c_errno()
        stop
    end if

    print '(a, i0, /)', 'Message Queue ID: ', msqid

    ! Send message to message queue.
    print '(a)', 'Sending message ...'

    if (ipc_send(type=IPC_TYPE, text='Hello, World!', flag=IPC_NOWAIT) > -1) then
        print '(a)', 'Done.'
    else
        write (stderr, '(a, i0)') 'msgsnd() failed: ', c_errno()
    end if

    ! Receive message from message queue (blocking I/O). Set `flag` to
    ! `IPC_NOWAIT` for non-blocking I/O.
    print '(a)', 'Waiting for message ...'

    if (ipc_receive(type=IPC_TYPE, text=string, flag=0) > 0) then
        print '(2a)', 'Received: ', trim(string)
    else
        write (stderr, '(a, i0)') 'msgsnd() failed: ', c_errno()
    end if

    ! Wait for user input.
    print '(/, a)', 'Press Enter to quit.'
    read (*, '(a)')

    ! Remove message queue.
    print '(a)', 'Closing message queue ...'

    if (c_msgctl(msqid, IPC_RMID, c_null_ptr) > 0) then
        write (stderr, '(a, i0)') 'msgctl() failed: ', c_errno()
    end if
contains
    function ipc_receive(type, text, flag)
        !! Waits for message of given type and returns message text. Calling the
        !! function is blocking, unless `flag` is set to `IPC_NOWAIT`.
        integer(kind=8),            intent(in)  :: type
        character(len=MESSAGE_LEN), intent(out) :: text
        integer,                    intent(in)  :: flag
        integer(kind=8)                         :: ipc_receive
        type(message_type), target              :: message

        ipc_receive = c_msgrcv(msqid, c_loc(message), MESSAGE_LEN, type, flag)
        ! Convert C char array to Fortran string.
        call c_f_string_chars(message%text, text)
    end function ipc_receive

    function ipc_send(type, text, flag)
        !! Converts Fortran string to C char array, and then sends message of
        !! given type by calling `c_msgsnd()`.
        integer(kind=8),  intent(in) :: type
        character(len=*), intent(in) :: text
        integer,          intent(in) :: flag
        integer                      :: ipc_send
        type(message_type), target   :: message
        integer                      :: i

        message%type = type
        ! Convert Fortran string to C char array.
        call f_c_string_chars(text, message%text, int(MESSAGE_LEN, kind=4))
        ipc_send = c_msgsnd(msqid, c_loc(message), MESSAGE_LEN, flag)
    end function ipc_send

    subroutine c_f_string_chars(c_string, f_string)
        !! Copies a C string, passed as a char array pointer, to a Fortran string.
        character(len=1, kind=c_char), intent(in)  :: c_string(*)
        character(len=*),              intent(out) :: f_string
        integer                                    :: i

        i = 1

        do while (c_string(i) /= c_null_char .and. i <= len(f_string))
            f_string(i:i) = c_string(i)
            i = i + 1
        end do

        if (i < len(f_string)) &
            f_string(i:) = ' '
    end subroutine c_f_string_chars

    subroutine f_c_string_chars(f_string, c_string, n)
        !! Copies a Fortran string to a C char array.
        character(len=*),              intent(in)  :: f_string
        integer,                       intent(in)  :: n
        character(len=1, kind=c_char), intent(out) :: c_string(n)
        integer                                    :: i

        i = 1

        do while (f_string(i:i) /= c_null_char .and. i <= size(c_string))
            c_string(i) = f_string(i:i)
            i = i + 1
        end do

        if (i < size(c_string)) &
            c_string(i:) = c_null_char
    end subroutine f_c_string_chars
end program main

The example workspace directory should contain the following files:

We can then compile and run the message queue example with:

$ gfortran9 -o example example.f90 sysv.o posix.o errno.o
$ ./example
Message Queue ID: 393227

Sending message ...
Done.
Waiting for message ...
Received: Hello, World!

Press Enter to quit.

Another process can open the created message queue for inter-process communication by calling c_msgget(393227_8, 0666). It is crucial to always remove opened message queues, as no more queues can be created after the system limit is reached (usually 40). The command-line tool ipcs shows all open SysV message queues:

$ ipcs -q
Message Queues:
T           ID          KEY MODE        OWNER    GROUP
q       393227            0 --rw--wa-w- user     user

Zombie message queues can be removed manually with ipcrm:

$ ipcrm -q <msqid>