web-dev-qa-db-ja.com

Linuxソケットによるファイル記述子の送信

Linuxソケットでファイル記述子を送信しようとしていますが、機能しません。何が悪いのですか?このようなものをデバッグするにはどうすればよいですか?可能な限りすべての場所にperror()を入れてみましたが、すべて問題ないと主張しました。これが私が書いたものです:

#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 第15章でプロセス間でファイル記述子を転送するプロセスについて説明しますUnix Domain Protocols、特に§15.7記述子の受け渡し完全に説明するのは面倒ですが、Unixドメインソケット(_AF_UNIX_または_AF_LOCAL_)で行う必要があり、送信者プロセスは sendmsg() while受信者は recvmsg() を使用します。

私はこの問題のコードを少し修正して(そしてインストルメント化して)入手し、Mac OS X 10.10.1 Yosemiteと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;
}
_

元のコードのインストルメント済みで修正されていないバージョンからの出力は次のとおりです。

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

親が子の前に終了したため、出力の中央にプロンプ​​トが表示されたことに注意してください。

「修正済み」コードからの出力は次のとおりです。

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

主な重要な変更は、両方の関数の_struct iovec_のデータに_struct msghdr_を追加することと、制御メッセージ用の受信関数(odbierz())にスペースを提供することでした。デバッグの中間ステップとして、_struct iovec_を親に提供し、親の「メッセージが長すぎる」エラーが削除されたことを報告しました。それが機能していることを証明するために(ファイル記述子が渡された)、渡されたファイル記述子からファイルを読み取って出力するコードを追加しました。元のコードにはsleep(0.5)がありましたが、sleep()は符号なし整数を取るため、これはスリープしないことと同じでした。 C99複合リテラルを使用して、子供を0.5秒間寝かせました。親が1.5秒間スリープするため、親が終了する前に子からの出力が完了します。 wait()またはwaitpid()も使用できましたが、面倒すぎて使用できませんでした。

私は戻って、すべての追加が必要であることを確認していません。

_"stderr.h"_ヘッダーは、err_*()関数を宣言します。エラーを簡潔に報告するために私が書いたコード(1987年より前の最初のバージョン)です。 err_setlogopts(ERR_PID)呼び出しは、すべてのメッセージの前にPIDを付けます。タイムスタンプについても、err_setlogopts(ERR_PID|ERR_STAMP)が役立ちます。

アライメントの問題

名目上の動物 が示唆する コメント

データに直接アクセスするのではなく、memcpy()を使用して記述子intをコピーするようにコードを変更することをお勧めしますか?これは必ずしも正しく整列されているとは限りません。これがmanページの例でもmemcpy()を使用している理由です。また、整列されていないintアクセスが問題を引き起こす多くのLinuxアーキテクチャがあります(最大でSIGBUSシグナルがプロセスを強制終了します)。

Linuxアーキテクチャだけでなく、両方ともSPARCとPowerは、データを整列させる必要があり、多くの場合、それぞれSolarisとAIXを実行します。むかしむかし、DEC Alphaもそれを必要としましたが、フィールドではほとんど見られません最近。

これに関連するマニュアルページのコード cmsg(3) は次のとおりです。

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

fdptrへの割り当ては、CMSG_DATA(cmsg)が_int *_に変換されるのに十分な位置合わせであり、memcpy()が_NUM_FD_が1.そうは言っても、それは配列bufを指しているはずであり、それはNominal Animalが示唆するように十分に整列されていない可能性があるため、fdptrは単に侵入者であり、例の場合はより良いでしょう中古:

_memcpy(CMSG_DATA(cmsg), myfds, NUM_FD * sizeof(int));
_

そして、受信側の逆のプロセスが適切です。このプログラムは単一のファイル記述子のみを渡すため、コードは次のように変更可能です。

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

私はまた、さまざまなOSの歴史的な問題を思い出しているようです。通常のペイロードデータのない補助データ。少なくとも1つのダミーバイトも送信することで回避できますが、確認する参照が見つからないため、間違っている可能性があります。

Mac OS X(Darwin/BSDベース)には少なくとも1つの_struct iovec_が必要であることを考えると、たとえそれが長さゼロのメッセージを説明しているとしても、上記のコードには3バイトのメッセージは、正しい一般的な方向への良いステップです。メッセージはおそらく3文字ではなく単一のnullバイトである必要があります。

コードを以下のように修正しました。 memmove()を使用して、cmsgバッファーとの間でファイル記述子をコピーします。ヌルバイトである単一のメッセージバイトを転送します。

また、ファイル記述子を子に渡す前に、親プロセスにファイルの(最大)32バイトを読み取らせます。子供は親が中断したところから読み続けます。これは、転送されたファイル記述子にファイルオフセットが含まれていることを示しています。

受信者は、ファイル記述子を渡すメッセージとして処理する前に、cmsgをさらに検証する必要があります。

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

そしてサンプル実行:

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