web-dev-qa-db-fra.com

Envoi du descripteur de fichier par socket Linux

J'essaie d'envoyer un descripteur de fichier par socket linux, mais cela ne fonctionne pas. Qu'est-ce que je fais mal? Comment est-on censé déboguer quelque chose comme ça? J'ai essayé de mettre perror () partout où c'est possible, mais ils ont affirmé que tout allait bien. Voici ce que j'ai écrit:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>

void wyslij(int socket, int fd)  // send fd by socket
{
    struct msghdr msg = {0};

    char buf[CMSG_SPACE(sizeof fd)];

    msg.msg_control = buf;
    msg.msg_controllen = sizeof buf;

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof fd);

    *((int *) CMSG_DATA(cmsg)) = fd;

    msg.msg_controllen = cmsg->cmsg_len;  // why does example from man need it? isn't it redundant?

    sendmsg(socket, &msg, 0);
}


int odbierz(int socket)  // receive fd from socket
{
    struct msghdr msg = {0};
    recvmsg(socket, &msg, 0);

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);

    unsigned char * data = CMSG_DATA(cmsg);

    int fd = *((int*) data);  // here program stops, probably with segfault

    return fd;
}


int main()
{
    int sv[2];
    socketpair(AF_UNIX, SOCK_DGRAM, 0, sv);

    int pid = fork();
    if (pid > 0)  // in parent
    {
        close(sv[1]);
        int sock = sv[0];

        int fd = open("./z7.c", O_RDONLY);

        wyslij(sock, fd);

        close(fd);
    }
    else  // in child
    {
        close(sv[0]);
        int sock = sv[1];

        sleep(0.5);
        int fd = odbierz(sock);
    }

}
17
Dekakaruk

Stevens (et al) NIX® Network Programming, Vol 1: The Sockets Networking API décrit le processus de transfert de descripteurs de fichiers entre processus au chapitre 15 Protocoles de domaine Unix et spécifiquement §15.7 Passing Descriptors . C'est compliqué à décrire en détail, mais cela doit être fait sur un socket de domaine Unix (AF_UNIX Ou AF_LOCAL), Et le processus de l'expéditeur utilise sendmsg() = pendant que le récepteur utilise recvmsg() .

J'ai obtenu cette version légèrement modifiée (et instrumentée) du code de la question pour travailler pour moi sur Mac OS X 10.10.1 Yosemite avec GCC 4.9.1:

#include "stderr.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

static
void wyslij(int socket, int fd)  // send fd by socket
{
    struct msghdr msg = { 0 };
    char buf[CMSG_SPACE(sizeof(fd))];
    memset(buf, '\0', sizeof(buf));
    struct iovec io = { .iov_base = "ABC", .iov_len = 3 };

    msg.msg_iov = &io;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof(fd));

    *((int *) CMSG_DATA(cmsg)) = fd;

    msg.msg_controllen = CMSG_SPACE(sizeof(fd));

    if (sendmsg(socket, &msg, 0) < 0)
        err_syserr("Failed to send message\n");
}

static
int odbierz(int socket)  // receive fd from socket
{
    struct msghdr msg = {0};

    char m_buffer[256];
    struct iovec io = { .iov_base = m_buffer, .iov_len = sizeof(m_buffer) };
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    char c_buffer[256];
    msg.msg_control = c_buffer;
    msg.msg_controllen = sizeof(c_buffer);

    if (recvmsg(socket, &msg, 0) < 0)
        err_syserr("Failed to receive message\n");

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);

    unsigned char * data = CMSG_DATA(cmsg);

    err_remark("About to extract fd\n");
    int fd = *((int*) data);
    err_remark("Extracted fd %d\n", fd);

    return fd;
}

int main(int argc, char **argv)
{
    const char *filename = "./z7.c";

    err_setarg0(argv[0]);
    err_setlogopts(ERR_PID);
    if (argc > 1)
        filename = argv[1];
    int sv[2];
    if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) != 0)
        err_syserr("Failed to create Unix-domain socket pair\n");

    int pid = fork();
    if (pid > 0)  // in parent
    {
        err_remark("Parent at work\n");
        close(sv[1]);
        int sock = sv[0];

        int fd = open(filename, O_RDONLY);
        if (fd < 0)
            err_syserr("Failed to open file %s for reading\n", filename);

        wyslij(sock, fd);

        close(fd);
        nanosleep(&(struct timespec){ .tv_sec = 1, .tv_nsec = 500000000}, 0);
        err_remark("Parent exits\n");
    }
    else  // in child
    {
        err_remark("Child at play\n");
        close(sv[0]);
        int sock = sv[1];

        nanosleep(&(struct timespec){ .tv_sec = 0, .tv_nsec = 500000000}, 0);

        int fd = odbierz(sock);
        printf("Read %d!\n", fd);
        char buffer[256];
        ssize_t nbytes;
        while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
            write(1, buffer, nbytes);
        printf("Done!\n");
        close(fd);
    }
    return 0;
}

La sortie de la version instrumentée mais non fixée du code d'origine était:

$ ./fd-passing
fd-passing: pid=1391: Parent at work
fd-passing: pid=1391: Failed to send message
error (40) Message too long
fd-passing: pid=1392: Child at play
$ fd-passing: pid=1392: Failed to receive message
error (40) Message too long

Notez que le parent a terminé avant l'enfant, donc l'invite est apparue au milieu de la sortie.

La sortie du code "fixe" était:

$ ./fd-passing
fd-passing: pid=1046: Parent at work
fd-passing: pid=1048: Child at play
fd-passing: pid=1048: About to extract fd
fd-passing: pid=1048: Extracted fd 3
Read 3!
This is the file z7.c.
It isn't very interesting.
It isn't even C code.
But it is used by the fd-passing program to demonstrate that file
descriptors can indeed be passed between sockets on occasion.
Done!
fd-passing: pid=1046: Parent exits
$

Les principales modifications importantes consistaient à ajouter le struct iovec Aux données du struct msghdr Dans les deux fonctions et à fournir de l'espace dans la fonction de réception (odbierz()) pour le message de contrôle. J'ai signalé une étape intermédiaire dans le débogage où j'ai fourni le struct iovec Au parent et l'erreur "message trop long" du parent a été supprimée. Pour prouver qu'il fonctionnait (un descripteur de fichier a été transmis), j'ai ajouté du code pour lire et imprimer le fichier à partir du descripteur de fichier transmis. Le code d'origine avait sleep(0.5) mais comme sleep() prend un entier non signé, cela équivalait à ne pas dormir. J'ai utilisé des littéraux composés C99 pour faire dormir l'enfant pendant 0,5 seconde. Le parent dort pendant 1,5 seconde afin que la sortie de l'enfant soit terminée avant la sortie du parent. Je pouvais aussi utiliser wait() ou waitpid(), mais j'étais trop paresseux pour le faire.

Je ne suis pas retourné et vérifié que tous les ajouts étaient nécessaires.

L'en-tête "stderr.h" Déclare les fonctions err_*(). C'est du code que j'ai écrit (première version avant 1987) pour signaler succinctement les erreurs. L'appel err_setlogopts(ERR_PID) préfixe tous les messages avec le PID. Pour les horodatages également, err_setlogopts(ERR_PID|ERR_STAMP) ferait l'affaire.

Problèmes d'alignement

Animal nominal suggère dans un commentaire :

Puis-je vous suggérer de modifier le code pour copier le descripteur int en utilisant memcpy() au lieu d'accéder directement aux données? Il n'est pas nécessairement correctement aligné - c'est pourquoi l'exemple de page de manuel utilise également memcpy() - et il existe de nombreuses architectures Linux où l'accès non aligné int cause des problèmes (jusqu'au signal SIGBUS tuant le processus).

Et pas seulement les architectures Linux: les deux SPARC et Power nécessitent des données alignées et exécutent souvent Solaris et AIX respectivement. Il était une fois, DEC Alpha l'exigeait aussi, mais ils sont rarement vus sur le terrain ces jours-ci.

Le code dans la page de manuel cmsg(3) en rapport avec ceci est:

struct msghdr msg = {0};
struct cmsghdr *cmsg;
int myfds[NUM_FD]; /* Contains the file descriptors to pass. */
char buf[CMSG_SPACE(sizeof myfds)];  /* ancillary data buffer */
int *fdptr;

msg.msg_control = buf;
msg.msg_controllen = sizeof buf;
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int) * NUM_FD);
/* Initialize the payload: */
fdptr = (int *) CMSG_DATA(cmsg);
memcpy(fdptr, myfds, NUM_FD * sizeof(int));
/* Sum of the length of all control messages in the buffer: */
msg.msg_controllen = CMSG_SPACE(sizeof(int) * NUM_FD);

L'affectation à fdptr semble supposer que CMSG_DATA(cmsg) est suffisamment bien aligné pour être converti en un int * Et que memcpy() est utilisé en supposant que NUM_FD N'est pas seulement 1. Cela dit, il est censé pointer vers le tableau buf, et cela pourrait ne pas être suffisamment bien aligné comme le suggère Animal nominal, il me semble donc que le fdptr est juste un intrus et il serait préférable que l'exemple utilisé:

memcpy(CMSG_DATA(cmsg), myfds, NUM_FD * sizeof(int));

Et le processus inverse à la réception serait alors approprié. Ce programme ne transmet qu'un seul descripteur de fichier, le code est donc modifiable pour:

memmove(CMSG_DATA(cmsg), &fd, sizeof(fd));  // Send
memmove(&fd, CMSG_DATA(cmsg), sizeof(fd));  // Receive

Il me semble également que je me souviens de problèmes historiques sur divers systèmes d'exploitation sans fil. données auxiliaires sans données de charge utile normales, évitées en envoyant au moins un octet fictif aussi, mais je ne trouve aucune référence à vérifier, donc je me souviens peut-être mal.

Étant donné que Mac OS X (qui a une base Darwin/BSD) nécessite au moins un struct iovec, Même si cela décrit un message de longueur nulle, je suis prêt à croire que le code ci-dessus, qui comprend un Le message sur 3 octets est un bon pas dans la bonne direction générale. Le message devrait peut-être être un seul octet nul au lieu de 3 lettres.

J'ai révisé le code pour lire comme indiqué ci-dessous. Il utilise memmove() pour copier le descripteur de fichier vers et depuis le tampon cmsg. Il transfère un seul octet de message, qui est un octet nul.

Le processus parent lit également (jusqu'à) 32 octets du fichier avant de transmettre le descripteur de fichier à l'enfant. L'enfant continue de lire là où le parent s'est arrêté. Cela démontre que le descripteur de fichier transféré inclut le décalage de fichier.

Le récepteur doit faire plus de validation sur le cmsg avant de le traiter comme un message de passage de descripteur de fichier.

#include "stderr.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

static
void wyslij(int socket, int fd)  // send fd by socket
{
    struct msghdr msg = { 0 };
    char buf[CMSG_SPACE(sizeof(fd))];
    memset(buf, '\0', sizeof(buf));

    /* On Mac OS X, the struct iovec is needed, even if it points to minimal data */
    struct iovec io = { .iov_base = "", .iov_len = 1 };

    msg.msg_iov = &io;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof(fd));

    memmove(CMSG_DATA(cmsg), &fd, sizeof(fd));

    msg.msg_controllen = CMSG_SPACE(sizeof(fd));

    if (sendmsg(socket, &msg, 0) < 0)
        err_syserr("Failed to send message\n");
}

static
int odbierz(int socket)  // receive fd from socket
{
    struct msghdr msg = {0};

    /* On Mac OS X, the struct iovec is needed, even if it points to minimal data */
    char m_buffer[1];
    struct iovec io = { .iov_base = m_buffer, .iov_len = sizeof(m_buffer) };
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    char c_buffer[256];
    msg.msg_control = c_buffer;
    msg.msg_controllen = sizeof(c_buffer);

    if (recvmsg(socket, &msg, 0) < 0)
        err_syserr("Failed to receive message\n");

    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);

    err_remark("About to extract fd\n");
    int fd;
    memmove(&fd, CMSG_DATA(cmsg), sizeof(fd));
    err_remark("Extracted fd %d\n", fd);

    return fd;
}

int main(int argc, char **argv)
{
    const char *filename = "./z7.c";

    err_setarg0(argv[0]);
    err_setlogopts(ERR_PID);
    if (argc > 1)
        filename = argv[1];
    int sv[2];
    if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) != 0)
        err_syserr("Failed to create Unix-domain socket pair\n");

    int pid = fork();
    if (pid > 0)  // in parent
    {
        err_remark("Parent at work\n");
        close(sv[1]);
        int sock = sv[0];

        int fd = open(filename, O_RDONLY);
        if (fd < 0)
            err_syserr("Failed to open file %s for reading\n", filename);

        /* Read some data to demonstrate that file offset is passed */
        char buffer[32];
        int nbytes = read(fd, buffer, sizeof(buffer));
        if (nbytes > 0)
            err_remark("Parent read: [[%.*s]]\n", nbytes, buffer);

        wyslij(sock, fd);

        close(fd);
        nanosleep(&(struct timespec){ .tv_sec = 1, .tv_nsec = 500000000}, 0);
        err_remark("Parent exits\n");
    }
    else  // in child
    {
        err_remark("Child at play\n");
        close(sv[0]);
        int sock = sv[1];

        nanosleep(&(struct timespec){ .tv_sec = 0, .tv_nsec = 500000000}, 0);

        int fd = odbierz(sock);
        printf("Read %d!\n", fd);
        char buffer[256];
        ssize_t nbytes;
        while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
            write(1, buffer, nbytes);
        printf("Done!\n");
        close(fd);
    }
    return 0;
}

Et un exemple d'exécution:

$ ./fd-passing
fd-passing: pid=8000: Parent at work
fd-passing: pid=8000: Parent read: [[This is the file z7.c.
It isn't ]]
fd-passing: pid=8001: Child at play
fd-passing: pid=8001: About to extract fd
fd-passing: pid=8001: Extracted fd 3
Read 3!
very interesting.
It isn't even C code.
But it is used by the fd-passing program to demonstrate that file
descriptors can indeed be passed between sockets on occasion.
And, with the fully working code, it does indeed seem to work.
Extended testing would have the parent code read part of the file, and
then demonstrate that the child codecontinues where the parent left off.
That has not been coded, though.
Done!
fd-passing: pid=8000: Parent exits
$
38
Jonathan Leffler