2007년 8월 27일 월요일

소켓을 통해 다른 프로세스에 FD를 넘겨보자!

Windows에서 되는지 실행 안 해봤고, 단순히 UNIX Network Programming(이하 UNP)에 나온 걸 정리해보겠다. MSDN에는 WSASendMsg라는 녀석을 준비하였는데 대충 비슷하게 보인다. 다만 Overlapped I/O를 Windows용으로 써야하기때문에 API가 다른 것 같다.

socket에는 sendmsg/recvmsg라는 녀석이 있다. 이 녀석 형태를 보면 아래와 같다.
ssize_t sendmsg(int s, const struct msghdr *msg, int flags);
ssize_t recvmsg(int s, struct msghdr *msg, int flags);

send/recv와 달리 버퍼가 안 보이고, msghdr 라는 구조체를 쓰는데 - 물론 이 녀석이 버퍼겠지 - 이걸 우선 까보자.
struct msghdr {
    void         * msg_name;     // 접속할 주소
    socklen_t    msg_namelen;    // 접속할 주소 크기
    struct iovec * msg_iov;      // IO 버퍼
    size_t       msg_iovlen;     // IO 버퍼 개수
    void         * msg_control;  // 제어 정보 버퍼
    socklen_t    msg_controllen; // 제어 정보 버퍼 크기
    int          msg_flags;      // 플래그
};
읔, 뭔가 많다. ㅡ_-) 난 이런게 젤 싫더라...

sendmsg/recvmsg는 접속/비접속형 소켓을 모두 지원한다. msg_name과 msg_namelen은 비접속형 소켓(예:UDP)에서 받을 주소 구조체(예:struct sockaddr_in)을 알맞게 만들어서 넣어주는 것이다.

또한 I/O vector를 지원하여, msg_iov에는 struct iovec 배열의 주소, msg_iovlen은 배열 원소 개수를 예쁘게 넣는다. 이때 msg_iov는 NULL이 아니며, msg_iovlen은 0이 될 수 없다. 무조건 뭔가라도 하나 보내야한다. 만약 FD외에도 다른 데이터를 함께 넘기고 싶다면 이것을 애용(?)하면 좋겠다.

자, 드디어 FD에 대한 중요한 내용. 제어 정보 - 즉, FD 같은 걸 넘기는 방법이다. 이것을 위해 준비된 구조체가 있다. 바로 cmsghdr라는 구조체인데, 역시나 한 번 까보자.

struct cmsghdr {
    socklen_t   cmsg_len;   // 제어 정보 전체 길이 (헤더크기 포함)
    int         cmsg_level; // 제어 정보 레벨
    int         cmsg_type;  // 제어 정보 타입
    /*
       unsigned char cmsg_data[???];   // 제어 정보
    */
};
요놈은 제어 정보 헤더이다. 넘기고 싶은게 있다면, 이 헤더를 이용해서 새로운 구조체를 만들어야한다. 각 인자를 설명하는 것보다 대충 먼저 예제를 만들어보자. 우리가 넘기고자 하는 것은 file-descriptor. int형이고, 32비트 운영체제에서 4바이트를 차지하는 어여쁜 녀석이다. 이 녀석을 넘길 데이터 구조체를 만들어보자. UNP에선 union을 썼는데, 귀찮으니 대충 보고 따라해보자.

참고로 FD를 넘기려면 Protocol은 UNIX domain, level은 SOL_SOCKET, type은 SCM_RIGHTS로 설정하라고 위대하신 UNP님께서 말씀하신다. (지랄)

typedef union _cmsg_fd
{
    struct cmsghdr cmsg;
    char cmsg[CMSG_SPACE(sizeof(int))];
} cmsg_fd;
CMSG_SPACE라는 매크로는 (제어 정보 + sizeof(cmsghdr))이다. 결국 cmsg_fd = [ cmsghdr ][ int ] 로 이뤄진 것이다. 우리가 넘길 건 int 하나이니 대충 이렇게 잡고 넘어가자. 나중에 데이터부분을 접근하려면 CMSG_DATA(cmsg_fd)하면 데이터부분만 포인터로 접근할 수 있다.


자, 이제 예제 소스이다. _-_
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/param.h>
#include <sys/wait.h>
#include <iostream>
#include <cstdlib>
#include <errno.h>
using namespace std;
// 헉! 헤더만 해도 어마어마하게 많구나!!

//! @brief Control Message for sending/receiving FD.
typedef
union _cmsg_fd
{
    struct cmsghdr cmsg;
    char data[CMSG_SPACE(sizeof(int))];
} cmsg_fd;

bool
recvFD(int sockfd, int& fd, char* buf, size_t buflen)
{
    struct msghdr msg;
    struct iovec iov;
    cmsg_fd cmsg;
    fd = -1;

    // 뭔가 받아올 데이터가 있나부지?
    iov.iov_base = buf;
    iov.iov_len = buflen;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    msg.msg_control = &cmsg;
    msg.msg_controllen = sizeof(cmsg);

    msg.msg_flags = 0;

    if ( -1 == recvmsg(sockfd, &msg, 0) )
    {
        cerr << __func__ << '(' << getpid() << "): "
            << strerror(errno) << '(' << errno << ')' << endl;
        return -1;
    }

    const struct cmsghdr* cptr(CMSG_FIRSTHDR(&msg));
    if ( !cptr )
    {
        cerr << __func__ << '
(' << getpid() << "): "
            << "no cmsg 1st header" << endl;
        return false;
    }

    if ( SOL_SOCKET != cptr->cmsg_level || SCM_RIGHTS != cptr->cmsg_type )
    {
        cerr << __func__ << '(' << getpid() << "): "
            << "invalid control message" << endl;
        return false;
    }

    memcpy(&fd, CMSG_DATA(cptr), sizeof(fd));
    return true;
}

bool
sendFD(int sockfd, int fd, const char* buf, size_t buflen)
{
    struct msghdr msg;
    struct iovec iov;
    cmsg_fd cmsg;

    // 뭐 따로 보낼 데이터가 있다면 함께 알콩달콩 보내보자.
    iov.iov_base = (void*)buf;
    iov.iov_len = buflen;

    msg.msg_name = NULL; // TCP가 아니라면 sockaddr_in 같은 걸 이용하자. -_-;
    msg.msg_namelen = 0;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    // 컨트롤 메시지를 지정해준다.
    msg.msg_control = &cmsg;
    msg.msg_controllen = sizeof(cmsg);

    msg.msg_flags = 0;

    // CMSG를 세팅하자!!
    struct cmsghdr* cptr(CMSG_FIRSTHDR(&msg));
    // 이럴 리 없겠지?
    // if ( NULL == cptr ) { 에러처리; }

    cptr->cmsg_len = CMSG_LEN(sizeof(int));
    cptr->cmsg_level = SOL_SOCKET;
    cptr->cmsg_type = SCM_RIGHTS;

    memcpy(CMSG_DATA(cptr), &fd, sizeof(int));

    // 전송이닷!
    if ( -1 == sendmsg (sockfd, &msg, 0) )
    {
        cerr << __func__ << '(' << getpid() << "): "
            << strerror(errno) << '(' << errno << ')' << endl;
        return false;
    }

    return true;
}

int
main(int argc, char* argv[])
{
    int sockfd[2];
    if ( -1 == socketpair(AF_UNIX, SOCK_STREAM, AF_LOCAL, sockfd) )
    {
        cerr << __func__ << '(' << getpid() << "): "
            << strerror(errno) << '(' << errno << ')' << endl;
        return EXIT_FAILURE;
    }

    if ( fork() )
    {
        // 부모라면...
        // 표준 출력을 자식에게 보낸다.
        int fd(1);
        sendFD(sockfd[0], fd, (char*)&fd, sizeof(fd));
        close(1);
        cout << "standard output is closed" << endl; // 출력 안 될 것이다.
        int res;
        wait(&res);
    }
    else
    {
        // 자식이라면...
        // 표준 출력 FD를 받는다. 1이 아닐 것이다.
       
close(1);
        int fd, ofd;
        recvFD(sockfd[1], fd, (char*)&ofd, sizeof(ofd));
        cerr << "received fd: " << fd << ", extra data: " << ofd << endl;
        if ( fd > -1 )
        {
            cout << "ah, ah standard output is testing..." << endl; // 출력 안 될 것이다.
            dup2(fd, 1);   // 받은 fd를 자식의 표준출력에 복사한다.
            cout << "hello, world!" << endl; // 이것만 출력될 것이다.
        }
    }

    return EXIT_SUCCESS;
}


결과
received fd: 6, extra data: 1
hello, world!


좀 길긴 하지만... -_- 대충 컴파일하고 실행하면, 부모의 표준 출력을 자식에게 잘 넘겨서 "hello, world!"만을 잘 찍는 것을 확인할 수 있다.

참고로 sendmsg로 fd를 전송하고, recvmsg로 fd를 받기 전에 fd를 close하더라도 sendmsg 시점에 fd에 대한 reference count를 증가하기 때문에, 받기도 전에 닫히는 불상사는 없을 것이다.

뱀발: 간만에 기술 관련 장문을 올리려니 삭신이 다 쑤신다. 쿨럭쿨럭.
뱀발2: 저~ 위에선 sendFD의 buf에 원래 fd를 넣었는데, NULL을 넣으면 데이터가 없거나 끊긴 것을 판단하지 못해서인지 recvFD에서 블럭된다. 귀찮더라도 1바이트라도 예쁘게 넣어서 보내주자.