blog · git · desktop · images · contact & privacy · gopher


IPC: POSIX Message Queues

Beim Booten von systemd kann man kurz das hier aufblitzen sehen:

[  OK  ] Mounted POSIX Message Queue File System.

Schauen wir uns das mal an.

Hintergrund

Es gibt eine Menge von IPC-Mechanismen, System V IPC genannt, die man von Userland-Prozessen aus benutzen kann. Folgende Tools sind verfügbar:

Unter Linux kann man in der Manpage „man svipc“ mehr darüber lesen.

Diese API ist recht alt, auch wenn ich genaue Datümer hier nicht kenne. Da es „SystemV …“ heißt, kann man wohl davon ausgehen, dass sie in den 1980ern oder vielleicht 1990ern eingeführt wurde.

Einige Jahre später wurde das Interface verfeinert (sozusagen). POSIX hat dieselben Mechanismen spezifiziert, was dann folglich POSIX IPC genannt wurde. Oracle sagt, dass diese API in Solaris 7 hinzukam. Das wiederum wurde 1998 veröffentlicht, also ist auch POSIX IPC nicht gerade brandneu.

Details zu POSIX IPC in den folgenden Manpages:

Da wir mittlerweile 2016 haben und beim täglichen Programmieren an ein hohes Level an Abstraktion gewöhnt sind, finde ich es wichtig zu erwähnen, dass die meisten dieser API-Calls Kernel-Features sind (unter Linux) – es geht also nicht um Bibliotheken von Drittanbietern. Man schaue sich „man 2 syscalls“ an. Was dort allerdings fehlt, sind POSIX Semaphoren und POSIX Shared Memory, da diese als Dateisystemobjekte unter Linux implementiert sind.

Das größte Problem ist, dass zwar System V IPC auf vielen Betriebssystemen unterstützt wird, die POSIX API aber nicht. Philip Semanchuk verwaltet eine Liste mit Kompatibilitäten. Insbesondere OpenBSD ist nicht aufgeführt. Da sich dieser Blogpost auf Message Queues konzentriert, kann man an dieser Stelle vielleicht erwähnen, dass OpenBSD sein eigenes Nachrichtensystem hat: imsg. Dazu vielleicht in einem weiteren Blogpost mehr.

Beispielcode

Okay, wie benutzt man POSIX Message Queues? Hier ist ein einfacher Sender:

#include <fcntl.h>
#include <limits.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

int
main(int argc, char **argv)
{
    mqd_t queue;
    struct mq_attr attrs;
    size_t msg_len;

    if (argc < 3)
    {
        fprintf(stderr, "Usage: %s <queuename> <message>\n", argv[0]);
        return 1;
    }

    queue = mq_open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR, NULL);
    if (queue == (mqd_t)-1)
    {
        perror("mq_open");
        return 1;
    }

    if (mq_getattr(queue, &attrs) == -1)
    {
        perror("mq_getattr");
        mq_close(queue);
        return 1;
    }

    msg_len = strlen(argv[2]);
    if (msg_len > LONG_MAX || (long)msg_len > attrs.mq_msgsize)
    {
        fprintf(stderr, "Your message is too long for the queue.\n");
        mq_close(queue);
        return 1;
    }

    if (mq_send(queue, argv[2], strlen(argv[2]), 0) == -1)
    {
        perror("mq_send");
        mq_close(queue);
        return 1;
    }

    return 0;
}

Kompilieren wir das und lassen es laufen – Queuenamen müssen mit einem Slash beginnen:

$ gcc -Wall -Wextra -o send send.c -lrt
$ ./send /mytest 'hello, world!'
$

Man beachte, dass das Programm sofort zuende ist. Was ist mit unserer Nachricht passiert? Ist die verloren, weil keiner zugehört hat? Schauen wir uns die Queue an.

Unter Linux (mit systemd) sind POSIX Message Queues im Dateisystem unter „/dev/mqueue“ zu sehen:

$ ls -al /dev/mqueue/
total 0
drwxrwxrwt  2 root root     60 May 16 09:40 .
drwxr-xr-x 18 root root  3,060 May 16 09:11 ..
-rw-------  1 void users    80 May 16 10:24 mytest

Was steht da drin?

$ cat /dev/mqueue/mytest 
QSIZE:13         NOTIFY:0     SIGNO:0     NOTIFY_PID:0     
$

Das ist nicht unsere Nachricht sondern Informationen über die Queue. „QSIZE:13“ zeigt an, dass 13 Bytes in der Queue warten. Genau das ist unser „hello, world!“. Das heißt, der Kernel hat unsere Nachricht empfangen und speichert sie, bis sie denn jemand abholt. Machen wir das:

#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

int
main(int argc, char **argv)
{
    mqd_t queue;
    struct mq_attr attrs;
    char *msg_ptr;
    ssize_t recvd;
    size_t i;

    if (argc < 2)
    {
        fprintf(stderr, "Usage: %s <queuename>\n", argv[0]);
        return 1;
    }

    queue = mq_open(argv[1], O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR, NULL);
    if (queue == (mqd_t)-1)
    {
        perror("mq_open");
        return 1;
    }

    if (mq_getattr(queue, &attrs) == -1)
    {
        perror("mq_getattr");
        mq_close(queue);
        return 1;
    }

    msg_ptr = calloc(1, attrs.mq_msgsize);
    if (msg_ptr == NULL)
    {
        perror("calloc for msg_ptr");
        mq_close(queue);
        return 1;
    }

    recvd = mq_receive(queue, msg_ptr, attrs.mq_msgsize, NULL);
    if (recvd == -1)
    {
        perror("mq_receive");
        return 1;
    }

    printf("Message: '");
    for (i = 0; i < (size_t)recvd; i++)
        putchar(msg_ptr[i]);
    printf("'\n");
}

Übersetzen und ausführen:

$ gcc -Wall -Wextra -o recv recv.c -lrt
$ ./recv /mytest
Message: 'hello, world!'
$

Da ist die Nachricht.

Es gibt auch die Option, sich vom Kernel über die Ankunft neuer Nachrichten informieren zu lassen. Das geht entweder über ein Signal oder über einen neuen Thread (!), den der Kernel in unserem Prozess starten kann.

Wie man sehen kann, enthält der Code keine Calls zu „mq_unlink“, um die Queue wieder zu zerstören. Das ist für diese kleine Demo nicht so problematisch, da man das einfach mit „rm“ erledigen kann:

$ rm /dev/mqueue/mytest
$ ls -al /dev/mqueue/
total 0
drwxrwxrwt  2 root root    40 May 16 10:51 .
drwxr-xr-x 18 root root 3,060 May 16 09:11 ..
$

Bewertung

Wann und wieso würde man POSIX Message Queues verwenden wollen?

Zuallererst finde ich das Interface wirklich gut benutzbar. Es ist einfach und ziemlich unmissverständlich. Man vergleiche das mal mit Code für UNIX Domain Sockets.

Auch schön: Die Kommunikation ist asynchron. Man wirft eine Nachricht ab und geht dann weiter. Man muss nicht darauf warten, dass da wirklich ein Leser ist oder dass ein Handshake stattgefunden hat. (Natürlich kann man das auch als Nachteil bewerten, denn man weiß nie so recht, ob die Nachricht auch wirklich von jemandem empfangen wurde.) Zu guter Letzt werden Nachrichten als Ganzes übertragen – man braucht keine Schleife, um einzelne Chunks zu lesen.

Aber wie oben schon erwähnt, unterstützen nicht alle Systeme diese API. Man muss sich auch wirklich sicher sein, dass man nicht doch irgendwie nochmal Netzwerk-Support braucht. POSIX Message Queues sind rein lokal.

IPC-Mechanismen sind ein interessantes Thema. Demnächst: OpenBSDs imsg, DBus (da kommt man nicht drumherum) und vielleicht kqueue der BSDs. Und falls es mal übernommen wird: kdbus.