오픈 소스/LXC

[LXC] lxc-monitor.c 분석

benjykim 2019. 5. 9. 13:28
반응형

lxc-monitor.c

regexp

  • lxc_monitor.c 중간에 regexp라는 변수가 있다. 만일 사용자가 lxc-monitor -n c1 으로 명령어를 입력한다면 regexp : ^c1$이다. lxc-monitor -n 'c1|c2'로 한다면 regexp : ^c1|c2$이다.

my_args.lxcpath[i]

  • 중간에 nfds의 수만큼 for문을 도는 문장이 있다. 여기서 my_args.lxcpath[i]를 가지고 lxc_tool_monitord_spawn, lxc_monitor_open과 같은 함수를 사용한다.
  • lxc-monitor -n c1을 입력했을 때 my_args.lxcpath[i] => /usr/local/var/lib/lxc이다.

lxc_tool_monitord_spawn

  • 설명 : Used to spawn a monitord either on startup of a daemon container, or when lxc-monitor starts.

  • 한글 : 데몬 컨테이너 시작시 또는 lxc-monitor가 시작될 때 monitord를 생성하는 데 사용됩니다.

/* double fork to avoid zombies when monitord exits */
    pid1 = fork();
    if (pid1 < 0) {
        SYSERROR("Failed to fork()");
        return -1;
    }

    if (pid1) {
        if (waitpid(pid1, NULL, 0) != pid1)
            return -1;

        return 0;
    }

    if (pipe(pipefd) < 0) {
        SYSERROR("Failed to create pipe");
        _exit(EXIT_FAILURE);
    }

    pid2 = fork();
    if (pid2 < 0) {
        SYSERROR("Failed to fork()");
        _exit(EXIT_FAILURE);
    }

    if (pid2) {
        char c;

        /* Wait for daemon to create socket. */
        close(pipefd[1]);

        /* Sync with child, we're ignoring the return from read
         * because regardless if it works or not, either way we've
         * synced with the child process. the if-empty-statement
         * construct is to quiet the warn-unused-result warning.
         */
        if (lxc_read_nointr(pipefd[0], &c, 1))
            ;

        close(pipefd[0]);

        _exit(EXIT_SUCCESS);
    }

(1). pid1 = fork();를 통해 부모 프로세스(A)는 자식 프로세스 B를 생성한다.
(2). 프로세스 A는 waitpid()를 통해 프로세스 B가 끝나기를 기다린다.
(3). 그 다음 pipe()를 통해 파이프를 생성한다.
(4). pid2 = fork();를 통해 프로세스 B는 프로세스 C를 생성한다.

  • 프로세스 B는 if (pid2) {...} 으로 들어가 프로세스 C와 싱크를 맞춘다. close(pipefd[1])을 통해 write을 막고, lxc_read_nointr(pipefd[0], &c, 1)을 통해 커널에 의해 버퍼된 모든 내용을 읽어들인다. 그리고 pipefd[0]을 통해 read를 막고 exit한다. (double fork 함으로써 프로세스 B는 프로세스 C를 생성한 뒤 파이프로 커널에 버퍼된 내용을 다 읽어들이고 종료한다. 프로세스 C는 부모 프로세스(B)가 죽었기 때문에 부모가 init 으로 변경된다. A 프로세스는 자식 프로세스 B를 waitpid()를 통해 정상적으로 종료시킨다)
if (setsid() < 0) {;
    SYSERROR("Failed to setsid()");
    _exit(EXIT_FAILURE);
}

lxc_tool_check_inherited(true, &pipefd[1], 1);
if (null_stdfds() < 0) {
    ERROR("Failed to dup2() standard file descriptors to /dev/null");
    _exit(EXIT_FAILURE);
}

close(pipefd[0]);

ret = snprintf(pipefd_str, sizeof(pipefd_str), "%d", pipefd[1]);
if (ret < 0 || ret >= sizeof(pipefd_str)) {
    ERROR("Failed to create pid argument to pass to monitord");
    _exit(EXIT_FAILURE);
}

execvp(args[0], args);
SYSERROR("Failed to exec lxc-monitord");

_exit(EXIT_FAILURE);
  • setsid()함수를 통해 프로세스 C를 현재 세션과 무관하게 동작시킨다.
  • 일련의 과정을 거쳐 args의 값을 채운 뒤 execvp를 통해 lxc-monitord를 실행시킨다.

lxc_monitor_open

  • 설명 : Open the monitoring mechanism for a specific container. The function will return an fd corresponding to the events. Returns a file descriptor on success, < 0 otherwise.
  • 한글 : 특정 컨테이너에 대한 모니터링 매커니즘을 오픈한다. 함수는 이벤트에 해당하는 fd를 반환한다. 성공 시, 파일 디스크립터를 반환한다. 실패 시, 0 미만의 숫자를 리턴한다.
int lxc_monitor_open(const char *lxcpath)
{
    struct sockaddr_un addr;
    int fd;
    size_t retry;
    int backoff_ms[] = {10, 50, 100};

    if (lxc_monitor_sock_name(lxcpath, &addr) < 0)
        return -1;

    DEBUG("Opening monitor socket %s with len %zu", &addr.sun_path[1], strlen(&addr.sun_path[1]));

    for (retry = 0; retry < sizeof(backoff_ms) / sizeof(backoff_ms[0]); retry++) {
        fd = lxc_abstract_unix_connect(addr.sun_path);
        if (fd != -1 || errno != ECONNREFUSED)
            break;

        SYSERROR("Failed to connect to monitor socket. Retrying in %d ms", backoff_ms[retry]);
        usleep(backoff_ms[retry] * 1000);
    }

    if (fd < 0) {
        SYSERROR("Failed to connect to monitor socket");
        return -1;
    }

    return fd;
  • lxc_monitor_open명령어를 통해 사용자가 입력한 컨테이너에 대한 fd를 얻어온다. 그리고 아래의 과정을 통해 struct pollfd *fds를 초기화한다.

    fds[i].fd = fd;
    fds[i].events = POLLIN;
    fds[i].revents = 0;

monitor msg 출력하는 부분

for (;;) {
        if (lxc_monitor_read_fdset(fds, nfds, &msg, -1) < 0)
            goto close_and_clean;

        msg.name[sizeof(msg.name)-1] = '\0';
        if (regexec(&preg, msg.name, 0, NULL, 0))
            continue;

        switch (msg.type) {
        case lxc_msg_state:
            printf("'%s' changed state to [%s]\n",
                   msg.name, lxc_state2str(msg.value));
            break;
        case lxc_msg_exit_code:
            printf("'%s' exited with status [%d]\n",
                   msg.name, WEXITSTATUS(msg.value));
            break;
        default:
            /* ignore garbage */
            break;
        }
    }
  • lxc_monitor_read_fdset 에서 poll()를 사용하여 이벤트(fd[i].events = POLLIN)를 감지하고 recv()를 통해 메시지를 읽어들인다.
  • msg의 값을 수정한 뒤 switch문을 통해 알맞은 메시지를 출력한다.

잘 몰랐던 용어

  • daemon(데몬) : 멀티태스킹 OS에서 데몬은 사용자가 직접적으로 제어하지 않고, 백그라운드에서 돌면서 여러 작업을 하는 프로그램을 말한다.

    • 윈도우의 서비스 같은 개념이라 볼 수 있는데 일반적으로 프로세스의 형식으로 실행되며 데몬이라는 표시를 위해 뒤에 d가 붙는다(syslogd등).

    • 유닉스 계열에서는 데몬을 시스템의 기능을 제공하거나 백그라운드에서 항시 실행되는 프로그램을 뜻하며 다른 OS에서는 '시스템 프로세스'라 불린다. 대부분 시스템의 시작과 끝을 함께 한다고 한다. 그렇기에 대개 관리자 권한으로 실행되어 네트워크 요청이나 하드웨어 동작 등 여러 기능을 담당하며 다양한 목적으로 사용된다.

    • 데몬의 특징으로는 부모 프로세스가 없다는 것인데 이에 따라 PPID(부모프로세스 ID)는 1이며 시스템의 첫 프로세스인 init의 바로 하위에 위치하게 된다. (init은 Java의 Object 클래스처럼 모든 프로세스의 직간접적 부모인 데몬이며 이것이 없다면 커널 패닉이 발생한다)

    • 데몬은 일반적으로 자식 프로세스를 fork하여 자기 사진을 복사, 생성한 후 자기 자신은 삭제하여 해당 프로세스를 고아 프로세스로 만든 후 이를 init이 자신의 자식 프로세스로 받아들이도록 하는 과정을 통하여 만들어지며 이를 fork off and die라고 한다.

    • 데몬의 동작방식은 두가지인데 첫째는 standalone, 둘째는 super daemon이다. 메일 서버, 웹 서버처럼 혼자 요청을 받아 처리하는 데몬(httpd등)을 standalone이라고 한다. 요청이 언제 들어올지 모르는 상태에서 항상 서비스가 가능하고 다른 매커니즘 없이 바로 데몬이 클라이언트의 요청을 처리할 수 있다. 다만 항상 동작하며 메모리를 잡기 때문에 요청이 많지 않은 경우 비효율적이다. (standalone은 /etc/init.d나 /etc/rc.d/init.d에 있는 스크립트 파일로 실행된다. 대표적으로 SSH 서비스를 찾아볼 수 있으며 'service 서비스이름 + start/stop/restart'로 데몬을 관리할 수 있다.)

    • super daemon은 자기 자신이 직접 서비스를 수행하지 않고 클라이언트 등에서 들어온 요청에 따라 해당 데몬을 실행시키는 방식이다. inetd라는 데몬(현재는 보안상의 이유로 xinetd)이 받은 메시지에 따라 해당되는 데몬을 메모리에 적재함으로써 실행하여 서비스를 처리한다. standalone에 비해 처리가 늦다.(/etc/xinetd.d에 스크립트 파일이 있다)]


  • setsid() : 이 함수를 호출하는 이유는 fork로 생성된 자식 프로세스를 현재 세션과 무관하게 동작시키기 위함이다.

    • session : 프로세스 그룹들의 모임, 프로세스 그룹을 일종의 작업이라 하고, 세션은 일종의 작업 공간이다.
    • setsid()를 호출한 프로세스는 새로운 하나의 세션을 만들고, 세션 리더가 된다. 또한 새로운 세션 그룹을 만들고 그룹의 리더가 된다.
    • setsid()를 호출하여 새롭게 만든 세션은 제어 터미널을 가지고 있지 않다.
    • 이미 프로세스의 그룹 리더인 프로세스는 호출할 수 없다.

  • poll

    • pollselect와 마찬가지로 다중입출력을 구현하기 위한 방법으로 사용되며, 동시에 여러개의 클라이언트를 다루는 서버를 제작하기 위한 방법으로 흔히 사용된다.
    • select의 경우 입출력 이벤트가 발생했을 대 넘겨주는 정보가 너무 적어 신경쓸 것이 많은데, poll을 이용하면 보다 수월하게 코드를 작성할 수 있다.
    int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
    • poll이 여러개의 파일을 다루는 방법은 select와 마찬가지로 파일 디스크립터의 이벤트를 기다리다가 이벤트가 발생하면, poll에서의 block이 해제되고, 다음 루틴에서 어떤 파일 디스크립터에 이벤트가 발생했는지 검사하는 방식을 사용한다.

    • 우선 poll함수의 첫번째 인자인 pollfd구조체에 대해서 알아보자.

    struct pollfd 
    {
        int fd;            // 관심있어하는 파일 디스크립터
        short events;    // 발생된 이벤트
        short revents;     // 돌려받은 이벤트
    }
    • pollfd구조체는 3개의 멤버변수가 있는데, 이 구조체에 우리가 관심있어하는 파일 디스크립터를 셋팅하고(fd), 관심있어하는 파일 디스크립터가 어떤 이벤트가 발생하는걸 기다릴 것인지(events)를 지정하게 된다. 그럼 poll은 해당 fd 에 해당 events 가 발생하는지를 검사하게 되고, 해당 events 가 발생하면 revents 를 채워서 돌려준다.
    • revents 는 events 가 발생했을 때 커널에서 이 events 에 어떻게 반응했는지에 대한 반응값이다. 후에 revent 값을 조사함으로써, 해당 파일 디스크립터에 어떠한 event 가 취해지고 커널에서 그 event 를 어떻게 처리했는지(입력/출력이 제대로 이루어졌는지, 에러가 발생했는지)를 알아내서 적절한 조치(읽을 데이터가 있으면 읽는 일등)를 취할 수 있게 된다.
    • events에 셋팅할 수 있는 값. 이 값들은 <sys/poll.h>에 정의되어 있다.
    #define POLLIN      0x0001  // 읽을 데이타가 있다.
    #define POLLPRI     0x0002  // 긴급한 읽을 데이타가 있다.
    #define POLLOUT     0x0004  // 쓰기가 봉쇄(block)가 아니다. 
    #define POLLERR     0x0008  // 에러발생
    #define POLLHUP     0x0010  // 연결이 끊겼음
    #define POLLNVAL    0x0020  // 파일지시자가 열리지 않은것같은
                                // Invalid request (잘못된 요청)
    • 2번째 인자인 nfds 는 pollfd 배열의 크기 즉 우리가 조사할 파일 디스크립터의 크기(네트워크 프로그래밍에서 보자면, 받아들일 수 있는 클라이언트의 크기)로, 보통 프로그래밍 할 때 그 크기를 지정해준다.
    • 마지막 argument timeout는 select의 time과 같은 역할을 한다.
      • 값을 지정하지 않을 경우, 이벤트가 발생하기 전까지 영원히 기다린다.
      • 0일 경우, 기다리지 않고 곧바로 다음 루틴을 진행한다.
      • 0보다 큰 양의 정수일 경우, 해당 시간만큼 기다린다. 해당 시간 내에 어떤 이벤트가 발생하면 즉시 되돌려주며, 시간을 초과하게 되면 0을 리턴한다.
    • 위의 3가지 argument 를 채워넣어야 poll을 사용할 수 있다. poll함수의 리턴값은 int 형인데, 에러일 경우 -1이 리턴되고, 그렇지 않을 경우 revent가 발생한 pollfd 구조체의 숫자를 돌려주게 된다.
반응형