기본 콘텐츠로 건너뛰기

괜찮으십니까

소켓을 통해 다른 프로세스에 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바이트라도 예쁘게 넣어서 보내주자.

댓글

이 블로그의 인기 게시물

Bash Array, Map 정리

Bash에서 Array, Map에 대한 정리. (매번 찾기 귀찮) 찾아보진 않았지만, Bash에서 Array든 Map이든 동일하게 Map(C++에서 Unordered Map)으로 동작하는 것 같다. 왜냐하면, Array의 Index가 연속하지 않아도 동작한다. 그저 Key가 0 이상의 정수인 Map이랑 비슷하게 동작한다. 예) 1, 2, 3, 9, 10 Array # 생성 declare -a empty_array declare -a ar=(haha hoho baba "long string haha hoho") # 접근 echo "ar[0]=${ar[0]}" echo "all as array=${ar[@]}" # 큰따옴표 안에서 각 원소를 따로따로 전달한다. echo "all as one=${ar[*]}" # 큰따옴표 안에서 각 원소를 문자열 하나로 합쳐 전달한다. echo "indexes=${!ar[@]}" echo "indexes=${!ar[*]}" echo "length=${#ar[@]}" echo "length=${#ar[*]}" echo "last=${ar[-1]}" echo "last=${ar[@]: -1}" # 콜론 뒤에 빈 칸이 꼭 필요하다. 옛 방식 # 현재 상황 declare -p ar #(출력) declare -a ar=([0]="haha" [1]="hoho" [2]="baba" [3]="long string haha hoho") ar[100]=hello # 인덱스를 건너 뛰어도 동작한다. declare -p ar #(출력) declare -a ar=([0]="haha" [1]="hoho" [2]="baba" [3]=&

SQLite에서 파일 크기 줄이기

간단한 개인 프로젝트를 하고 있는데, SQLite DB파일 크기가 매우 커져서 테이블에 필요 없는 레코드를 날렸다. 그런데 날리고도 파일크기가 그대로라서 여기저기 뒤져보니 VACUUM 커맨드를 사용하란다. 사용법은 매우 간단하다. 그저 "VACUUM;"이라고 날려주면 동작한다. (참조: http://sqlite.org/lang_vacuum.html ) 다만, 동작이 매우 느려서 자주 쓸만한 것은 아니다. 실제로 100MB짜리 파일을 7KB로 줄이는데 수 분이 걸렸다. 소스를 봐야겠지만, DB를 EXPORT한 뒤에, 파일을 지우고 다시 IMPORT하는게 아닐까 하는 의구심이 든다. 매번 하기 귀찮으면 "PRAGMA auto_vacuum=1;"를 하면, 새로운 빈 페이지(DELETE나 DROP TABLE 같은...)가 생길 때마다, VACUUM을 실행한다. 다만, SQLite구조 문제로 테이블을 생성하기 전에 미리 날려야하는 안타까움이 있다. (참조: http://sqlite.org/pragma.html#pragma_auto_vacuum )

std::thread에서 클래스 메소드 호출하기

C++11이 나온지 어언 3년... 그동안 GCC도 많이 발전하고 해서, 귀찮은 pthread_* 함수를 쓰는 것보다, 잘 감싼 std::thread를 써보고 있다. 딱히 커다란 장단점은 찾아볼 수 없지만, 콜백 함수 형태에 구애받지 않는다. C++11이 갖는 꽃별천지 언어특성과 표준라이브러리가 만나 그렇게 할 수 있는 것이지만, 자세한 설명은 생략한다... (응?) 보통 일반 함수를 쓸 경우, 아래와 같이 쓸 수 있다. void cb_func1(void) { /* ... */ } void cb_func2(int i) { /* ... */ } void init(void) { std::thread t1(cb_func1); std::thread t2(cb_func1, 10); t1.join(); t2.join(); } 그러면 메소드를 쓰다면...? 메소드 포인터 다음에 객체 포인터를 넣어서 해결할 수 있다. class MyThread { public: MyThread() { m_th = std::thread(&MyThread::proc, this); m_th.detach(); } private: void proc(void) { /* ... */ } private: std::thread m_th; }; 요로코롬 할 수 있다. 물론 밖에서 호출할 때는 "this" 대신에 해당 객체 포인터를 넘겨야 제대로 동작한다.