본문 바로가기
개인공부/윤성우의 열혈 TCP&IP 소켓 프로그래밍

Chapter 11 - 프로세스간 통신(Inter Process Communication)

by 하고싶은건많은놈 2023. 2. 24.

11-1 : 프로세스간 통신의 기본 개념

프로세스들은 서로 별개의 메모리 공간을 점유하기 때문에 프로세스간 통신은 별도로 마련된 방법을 통해서만 이뤄질 수 있음

두 프로세스간 파이프(Pipe)를 생성하여 통신 가능
파이프는 소켓과 마찬가지로 운영체제에 속하는 자원으로 fork() 함수로 복사되지 않음

#include <unistd.h>

int pipe(int filedes[2]);
// 성공시 0, 실패시 -1 반환
  • 배열에 담기는 두개의 파일 디스크립터는 각각 파이프의 입구와 출구로 사용
  • fork() 함수로 입구 또는 출구에 해당하는 파일 디스크립터를 자식 프로세스에 전달
  • 단, 이 때 파이프가 복사된 것이 아닌 파이프의 입출력에 사용되는 파일 디스크립터가 복사된 것에 유의

pipe() 함수 사용 예제

// pipe1.cpp

#include <iostream>
#include <unistd.h>

using namespace std;

#define BUF_SIZE 30

int     main(int argc, char *argv[])
{
    int        fds[2];
    char    str[] = "Who are you?";
    char    buf[BUF_SIZE];
    pid_t    pid;

    pipe(fds);
    pid = fork();
    if (pid == 0)
        write(fds[1], str, sizeof(str));
    else
    {
        read(fds[0], buf, BUF_SIZE);
        cout << buf << endl;
    }
    return 0;
}

파이프를 사용해 양방향으로 통신할 경우에는 주의가 필요함

// pipe2.cpp

#include <iostream>
#include <unistd.h>

using namespace std;

#define BUF_SIZE 30

int     main(int argc, char *argv[])
{
    int        fds[2];
    char    str1[] = "Who are you?";
    char    str2[] = "Thank you for your message";
    char    buf[BUF_SIZE];
    pid_t    pid;

    pipe(fds);
    pid = fork();
    if (pid == 0)
    {
        write(fds[1], str1, sizeof(str1));
        //sleep(2);
        read(fds[0], buf, BUF_SIZE);
        cout << "Child proc output : " << buf << endl;
    }
    else
    {
        read(fds[0], buf, BUF_SIZE);
        cout << "Parent proc output : " << buf << endl;
        write(fds[1], str2, sizeof(str2));
        sleep(3);
    }
    return 0;
}

위 코드가 주석이 있는 채로 실행될 경우 정상적으로 작동하지 않음
이는 파이프에 들어간 데이터가 데이터를 먼저 읽어들이는 프로세스에게 전달되기 때문
따라서 이를 컨트롤하기 위해서는 프로그램의 실행흐름을 예측해야하지만 이는 사실상 불가능
따라서 대신 파이프를 두개 생성하여 서로 다른 데이터의 흐름을 담당하게 해야함

pipe() 함수를 활용한 올바른 양방향 통신 예시

// pipe3.cpp

#include <iostream>
#include <unistd.h>

using namespace std;

#define BUF_SIZE 30

int     main(int argc, char *argv[])
{
    int        fds1[2], fds2[2];
    char    str1[] = "Who are you?";
    char    str2[] = "Thank you for your message";
    char    buf[BUF_SIZE];
    pid_t    pid;

    pipe(fds1), pipe(fds2);
    pid = fork();
    if (pid == 0)
    {
        write(fds1[1], str1, sizeof(str1));
        read(fds2[0], buf, BUF_SIZE);
        cout << "Child proc output : " << buf << endl;
    }
    else
    {
        read(fds1[0], buf, BUF_SIZE);
        cout << "Parent proc output : " << buf << endl;
        write(fds2[1], str2, sizeof(str2));
    }
    return 0;
}

11-2 : 프로세스간 통신의 적용

이전의 에코서버에 클라이언트가 전송하는 문자열을 서버에 전달되는 순서대로 파일에 저장하는 기능을 추가
이 때 해당 기능을 별도의 프로세스가 담당하게끔 파이프를 활용

// echo_storeserver.cpp
// 클라이언트는 이전의 echo_mpclient.cpp를 사용

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 100
void    error_handling(char *message);
void    read_childproc(int sig);

int        main(int argc, char *argv[])
{
    int                    serv_sock, clnt_sock;
    struct sockaddr_in    serv_adr, clnt_adr;

    int                    fds[2];
    pid_t                pid;
    struct sigaction    act;
    socklen_t            adr_sz;
    int                    str_len, state;
    char                buf[BUF_SIZE];

    if(argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        exit(1);
    }

    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, 0);

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (::bind(serv_sock, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");

    if (::listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    pipe(fds);
    pid = fork();
    if (pid == 0)
    {
        FILE    *fp = fopen("echomsg.txt", "wt");
        char    msgbuf[BUF_SIZE];

        for (int i = 0; i < 10; i++)
        {
            int len = read(fds[0], msgbuf, BUF_SIZE);
            fwrite((void*)msgbuf, 1, len, fp);
        }
        fclose(fp);
        return 0;
    }

    while (1)
    {
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
        if (clnt_sock == -1)
            continue;
        else
            cout << "new client connected..." << endl;

        pid = fork();
        if (pid == -1)
        {
            close(clnt_sock);
            continue;
        }
        else if (pid == 0)
        {
            close(serv_sock);
            while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
            {
                write(clnt_sock, buf, str_len);
                write(fds[1], buf, str_len);
            }

            close(clnt_sock);
            cout << "client disconnected..." << endl;
            return 0;
        }
        else
            close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}

void    read_childproc(int sig)
{
    int        status;
    pid_t    pid = waitpid(-1, &status, WNOHANG);

    if (WIFEXITED(status))
        cout << "Removed proc id : " << pid << endl;
}

void    error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

내용 확인문제

  1. 프로세스간 통신이 의미하는 바는 무엇인가? 이를 개념적으로, 그리고 메모리의 관점에서 각각 설명해보자.

서로 다른 두 프로세스가 데이터를 주고 받는 것이며, 두 프로세스가 동시에 접근 가능한 메모리 공간이 존재해야함


  1. 프로세스간 통신에는 IPC라는 별도의 메커니즘이 요구된다. 그리고 이는 운영체제에 의해서 지원되는 별도의 기능이다. 그렇다면 프로세스간 통신에 있어서 이렇듯 운영체제의 도움이 필요한 이유는 무엇인가?

프로세스들은 서로 별개의 메모리 공간을 점유하기 때문


  1. 대표적인 IPC 기법으로 '파이프(pipe)'라는 것이 있다. 파이프의 IPC 기법과 관련해서 다음 질문에 답해보자.

파이프는 프로세스간에 데이터를 송수신하는 경로를 의미한다. 그렇다면 이 경로는 어떻게해서 생성되며, 누구에 의해서 만들어지는가?

  • pipe() 함수 호출시 운영체제에 의해 생성

프로세스간 통신을 위해서는 통신의 주체가 되는 두 프로세스 모두 파이프에 접근이 가능해야한다. 그렇다면 하나의 파이프에 두 프로세스는 어떻게 해서 모두 접근이 가능한가?

  • 파이프의 입출력에 해당하는 파일 디스크립터를 fork() 함수 호출로 복사

파이프는 두 프로세스간에 양방향 통신이 가능하게한다. 그렇다면 양방향 통신을 진행하는데 있어서 특히 주의해야 할 사항은 무엇인가?

  • 파이프는 데이터 흐름을 읽지 않기 때문에 파이프에 들어간 데이터는 먼저 읽어들이는 쪽에 전달됨에 유의

  1. IPC 기법을 확인하는 차원에서, 두 프로세스 사이에서 총 3회에 걸쳐서 문자열을 한번씩 주고받는 예제를 작성해보자. 물론 두 프로세스는 부모, 자식의 관계로 형성이되며, 주고받을 문자열의 종류는 프로그램상에서 여러분이 임의로 결정하기 바란다.
// three_conversation.cpp

#include <iostream>
#include <unistd.h>

using namespace std;

#define BUF_SIZE 30

int     main(int argc, char *argv[])
{
    int        fds1[2], fds2[2];
    char    buf[BUF_SIZE];
    pid_t    pid;

    pipe(fds1), pipe(fds2);
    pid = fork();
    if (pid == 0)
    {
        for (int i = 0; i < 3; i++)
        {
            cout << "Child : ";
            cin.getline(buf, BUF_SIZE);
            write(fds1[1], buf, strlen(buf));
            memset(buf, 0, sizeof(buf));
            read(fds2[0], buf, BUF_SIZE);
            cout << "from Parent : " << buf << endl;
        }
    }
    else
    {
        for (int i = 0; i < 3; i++)
        {
            memset(buf, 0, sizeof(buf));
            read(fds1[0], buf, BUF_SIZE);
            cout << "from Child : " << buf << endl;
            cout << "Parent : ";
            cin.getline(buf, BUF_SIZE);
            write(fds2[1], buf, strlen(buf));
        }
    }
    return 0;
}

댓글