Enviando descritor de arquivo pelo soquete Linux



sockets file-descriptor (1)

Stevens (et al) UNIX® Network Programming, Vol. 1: A API Sockets Networking descreve o processo de transferência de descritores de arquivo entre processos no Capítulo 15 Protocolos de Domínio Unix e, especificamente, em § 15.7 Descritores passantes. É complicado descrever na íntegra, mas isso deve ser feito em um soquete de domínio Unix ( AF_UNIX ou AF_LOCAL ), e o processo do remetente usa sendmsg() enquanto o receptor usa recvmsg() .

Consegui esta versão do código levemente modificada (e instrumentada) da pergunta para trabalhar para mim no Mac OS X 10.10.1 Yosemite com 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;
}

A saída da versão instrumentada mas não corrigida do código original foi:

$ ./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

Observe que o pai terminou antes do filho; portanto, o prompt apareceu no meio da saída.

A saída do código 'fixo' foi:

$ ./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
$

As principais mudanças significativas foram adicionar o struct iovec aos dados no struct msghdr em ambas as funções e fornecer espaço na função de recebimento ( odbierz() ) para a mensagem de controle. Relatei uma etapa intermediária na depuração, em que forneci a struct iovec ao pai e o erro "mensagem muito longa" do pai foi removido. Para provar que estava funcionando (um descritor de arquivo foi passado), adicionei código para ler e imprimir o arquivo a partir do descritor de arquivo passado. O código original tinha sleep(0.5) mas como a sleep() recebe um número inteiro não assinado, isso foi equivalente a não sleep() . Eu usei literais compostos C99 para que a criança durma por 0,5 segundos. O pai dorme por 1,5 segundos para que a saída do filho seja concluída antes que o pai saia. Eu poderia usar wait() ou waitpid() também, mas estava com preguiça de fazê-lo.

Não voltei e verifiquei se todas as adições eram necessárias.

O cabeçalho "stderr.h" declara as funções err_*() . É o código que escrevi (primeira versão antes de 1987) para relatar erros de forma sucinta. A err_setlogopts(ERR_PID) prefixa todas as mensagens com o PID. Também para timestamps, err_setlogopts(ERR_PID|ERR_STAMP) faria o trabalho.

Problemas de alinhamento

Animal Nominal sugere em um comment :

Posso sugerir que você modifique o código para copiar o descritor int usando memcpy() vez de acessar os dados diretamente? Não é necessariamente alinhado corretamente - e é por isso que o exemplo da página de manual também usa memcpy() - e há muitas arquiteturas Linux em que o acesso int desalinhado causa problemas (até o sinal SIGBUS interromper o processo).

E não apenas as arquiteturas Linux: o SPARC e o Power exigem dados alinhados e geralmente executam o Solaris e o AIX, respectivamente. Era uma vez, o DEC Alpha exigia isso também, mas eles raramente são vistos em campo atualmente.

O código na página de manual cmsg(3) relacionado a isso é:

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);

A atribuição a fdptr parece assumir que CMSG_DATA(cmsg) está suficientemente bem alinhado para ser convertido em um int * e o memcpy() é usado na suposição de que NUM_FD não seja apenas 1. Dito isso, ele deve estar apontando no array buf , e isso pode não estar suficientemente bem alinhado, como sugere o Animal Nominal, então parece-me que o fdptr é apenas um intruso e seria melhor se o exemplo usado:

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

E o processo inverso no lado receptor seria então apropriado. Este programa passa apenas um descritor de arquivo único, portanto, o código pode ser modificado para:

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

Também pareço relembrar problemas históricos em vários sistemas operacionais, com dados auxiliares sem dados normais de carga útil, evitados pelo envio de pelo menos um byte fictício também, mas não consigo encontrar nenhuma referência a ser verificada, para que eu possa me lembrar errado.

Dado que o Mac OS X (que é baseado em Darwin / BSD) requer pelo menos uma struct iovec , mesmo que isso descreva uma mensagem de tamanho zero, estou disposto a acreditar que o código mostrado acima, que inclui uma mensagem de 3 bytes , é um bom passo na direção geral certa. A mensagem talvez deva ser um único byte nulo em vez de três letras.

Revisei o código para ler como mostrado abaixo. Ele usa o memmove() para copiar o descritor de arquivo para e do buffer cmsg . Ele transfere um único byte de mensagem, que é um byte nulo.

Ele também faz com que o processo pai leia (até) 32 bytes do arquivo antes de passar o descritor de arquivo para o filho. A criança continua lendo de onde o pai parou. Isso demonstra que o descritor de arquivo transferido inclui o deslocamento do arquivo.

O receptor deve fazer mais validação no cmsg antes de tratá-lo como uma mensagem de passagem de descritor de arquivo.

#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;
}

E uma amostra:

$ ./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
$

https://src-bin.com

Estou tentando enviar algum descritor de arquivo pelo soquete do linux, mas ele não funciona. O que estou fazendo errado? Como se deve depurar algo assim? Tentei colocar o perror () em todos os lugares possíveis, mas eles alegaram que está tudo bem. Aqui está o que eu escrevi:

#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);
    }

}




file-descriptor