기본 콘텐츠로 건너뛰기

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

댓글

이 블로그의 인기 게시물

버즈 라이브 배터리 교체

나는 버즈 라이브(SM-R180)가 좋은데, 평가가 별루였는지, 해당 스타일로 버즈를 더 이상 만들지 않고 있다. 아무튼, 오래 쓴 버즈 라이브 배터리가 슬슬 맛이 가기 시작해서, 블로그 를 참조하면서 분해 및 교체를 하였다. (진짜 쉬움) 요로코롬 위아래를 살짝 눌러주면 뚜껑이 벌어진다. 안쪽 플라스틱은 오른쪽은 분홍색, 왼쪽은 회색이다. 리본 케이블 살짝 들어내고, 기판을 떼어내면, 작은 나사가 있다. 나사를 풀고, 플라스틱을 걷어내면, 검은 양면 테이프로 고정된 CR1254 배터리가 보인다. 잘 쑤셔서(?) 꺼낸다. 새로운 CR1254 배터리를 넣는다. 음극이 아래로 가도록 하고, 분해의 역순으로 조립하면 된다. 조립할 때, 아까 풀었던 나사는 잊지 말고 꼭 조여준다. (까먹고 조립해서 다시 뜯고 조립함) 충전도 잘 되고, 소리도 잘 나는거 보면, 조립도 잘 된 것 같다. 이렇게 버즈 라이브의 수명을 강제로 늘렸다. 나중에 본체 배터리도 갈아야겠다.

Windows 에서 절전을 깨우는 장치 찾기

참조:  https://www.reddit.com/r/computer/comments/wquswv/windows_11_pc_wakes_up_every_time_i_move_usb/ powercfg /devicequery wake_armed powercfg /deviceenablewake "[DEVICE]" # $PROFILE function Get-WakeArmedDevices { $devices = powercfg -devicequery wake_armed if ($devices) { $devices | ForEach-Object { $_.Trim() } } else { Write-Host "No devices are currently armed for wake events." } } function Set-EnableWakeOnDevice { param( [string]$deviceName ) sudo powercfg -deviceenablewake $deviceName } function Set-DisableWakeOnDevice { param( [string]$deviceName ) sudo powercfg -devicedisablewake $deviceName }