Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
중앙대학교 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

Embedded System/Embedded Linux

[Embedded Linux] 프로세스간 통신 IPC - 파일 기반의 IPC

sm_amoled 2025. 11. 29. 15:56

일반적으로 프로세스 끼리는 서로 독립된 메모리 공간을 사용한다. 따라서 프로세스끼리는 서로의 영역을 침범할 수 없다.

ㅤㅤ

프로그램을 설계하다보면 여러 프로세스가 서로에게 정보나 신호를 주고받아야하는 상황이 온다. A 에서 처리한 결과가 B 에게 전달되어야 한다거나, 혹은 A와 B가 동시에 작업을 시작하거나 작업의 우선순위가 있을 수 있다. 이러한 상황에서 프로세스 간의 통신 (IPC, Inter-Process Communication)이 필요하다.

ㅤㅤ

이번 아티클에서는 기본으로 사용하는 파일 기반의 IPC, 산업적인 니즈에 따라 나온 System V IPC, 그리고 여기에 대해 개선된 POSIX 기반의 IPC에 대해서 차례대로 소개해보려 한다.

ㅤㅤ

우선 파일을 거쳐 데이터를 주고받는 기본적인 형태의 IPC 에 대해 살펴보자. 프로세스끼리 가장 쉽게 서로 데이터를 주고받는 방법은 임시 버퍼파일에 서로 전달할 데이터를 읽고 쓰면서 서로의 작업에 대해 파악하는 것이다. 개념 자체는 간단하다!

ㅤㅤ

1. 파일 기반의 IPC

1-1 파이프

특징

  • 단방향 통신만 가능하다
  • 부모-자식 관계의 프로세스 끼리만 통신이 가능하다
  • 파이프의 용량은 4KB ~ 최대 64KB. 한 번에 쓸 수 있는 데이터의 크기는 보통 4KB. 파이프는 커널이 관리하는 공간의 버퍼공간을 사용하기 때문에, 따로 파일 시스템에서 확인하지 못한다.

ㅤㅤ

장점

  • 구현이 매우 간단하고 사용 방법이 직관적임!

ㅤㅤ

단점

  • 부모-자식 관계에서만 사용할 수 있다. 관련 없는 프로세스와는 연결되지 않는다.
  • 단방향으로 구성되어 있다 (부모 → 자식 or 자식 → 부모 만 가능)
  • 4KB가 한 번에 읽거나 쓸 수 있는 데이터 크기. 이 크기를 뛰어넘는 큰 데이터를 한 번에 담으려고 한다면 원자성이 보장되지 않는다.

ㅤㅤ

어디에 쓰나?

  • 주로 셸 명령어 연결 파이프로 사용한다.

ㅤㅤ

파이프 실습

#include <stdio.h>
#include <stdlib.h>     // exit() 함수를 위해 사용
#include <unistd.h>     // fork(), pipe(), sleep() 함수를 위해 사용
#include <sys/wait.h>   // waitpid() 함수를 위해 사용
#include <string.h>     // strlen() 함수를 위해 사용

#define BUF_SIZE 256

int main(int argc, char **argv)
{
    pid_t pid;
    int parent_to_child[2]; // 부모 -> 자식 통신을 위한 파이프 (pfd[0]: 읽기, pfd[1]: 쓰기)
    int child_to_parent[2]; // 자식 -> 부모 통신을 위한 파이프 (사용하지 않을 수 있지만, 구조를 위해 정의)
    char send_msg[BUF_SIZE];
    char recv_msg[BUF_SIZE];
    int status;
    int i;

    // 1. 부모 -> 자식 파이프 생성
    if (pipe(parent_to_child) < 0) {
        perror("pipe(parent_to_child)");
        return -1;
    }

    // 2. 자식 -> 부모 파이프 생성 
    if (pipe(child_to_parent) < 0) {
        perror("pipe(child_to_parent)");
        return -1;
    }

    if ((pid = fork()) < 0) {
        perror("fork()");
        return -1;
    } else if (pid == 0) {
        // == 자식 프로세스 (메시지 대기 및 수신) ==

        // 3. 자식은 부모로부터 읽고, 부모에게 쓰지 않음
        close(parent_to_child[1]); // 부모에게 쓰는 파이프 닫기
        close(child_to_parent[0]); // 자식에게 쓰는 파이프 닫기

        printf("자식: 부모의 메시지를 기다리는 중...\n");

        // 4. 부모로부터 메시지 읽기
        while (1) {
            int read_bytes = read(parent_to_child[0], recv_msg, BUF_SIZE);
            if (read_bytes > 0) {
                recv_msg[read_bytes] = '\0'; // 문자열 종료 처리
                printf("자식 : [%s] -> 응답 [ 네 엄마~ ]\n", recv_msg);

                // 종료 메시지를 받으면 루프 탈출
                if (strcmp(recv_msg, "QUIT") == 0) {
                    break;
                }
            } else if (read_bytes == 0) {
                // 파이프의 쓰기 쪽이 닫히면 read()는 0을 반환합니다.
                printf("자식: 부모 파이프 닫힘. 종료.\n");
                break;
            } else {
                perror("child read()");
                break;
            }
        }

        close(parent_to_child[0]);
        close(child_to_parent[1]);
        _exit(0);

    } else { 
        // == 부모 프로세스 (1초마다 메시지 전송) ==

        // 5. 부모는 자식에게 쓰고, 자식으로부터 읽지 않음
        close(parent_to_child[0]); // 자식으로부터 읽는 파이프 닫기
        close(child_to_parent[1]); // 자식에게 읽는 파이프 닫기

        // 6. 1초마다 메시지 전송
        for (i = 0; i < 5; i++) {
            sprintf(send_msg, "맛있게 먹자~");
            printf("부모: 메시지 전송 -> [%s]\n", send_msg);

            if (write(parent_to_child[1], send_msg, strlen(send_msg) + 1) < 0) {
                perror("parent write()");
                break;
            }

            sleep(1); // 1초 대기
        }

        // 7. 자식에게 종료 메시지 전송
        const char *quit_msg = "QUIT";
        write(parent_to_child[1], quit_msg, strlen(quit_msg) + 1);
        printf("부모: 종료 메시지 전송.\n");

        close(parent_to_child[1]); // 쓰기 파이프 닫기 (자식이 read()에서 0을 받게 됨)

        // 8. 자식 프로세스의 종료를 기다림
        waitpid(pid, &status, 0);
    }

    return 0;
}

ㅤㅤ

1-2 FIFO

특징

  • 파이프의 확장 버전. 특수한 파일 형태가 파일 시스템에 만들어진다.
  • 부모-자식 관계가 아닌 프로세스끼리도 FIFO를 사용해 통신할 수 있다.
  • 일반 파일처럼 open(), read(), write() 로 접근 및 사용할 수 있다.
  • 이건 파이프랑 동일한데 파일 시스템을 사용한다는 점에서 ‘네임드 파이프’라고도 불린다.

ㅤㅤ

장점

  • 파이프처럼 단순한 스트림 형태로 작동

ㅤㅤ

단점

  • 여전히 단방향 통신만 가능하다
  • 파일 시스템에 통신의 흔적이 남는다.

ㅤㅤ

어디에 쓰나?

  • 독립된 두 프로그램이 파일 시스템을 통해 데이터를 주고받는 상황에서 사용
  • 서비스 데몬 끼리 로그 데이터, 명령 전달하기

ㅤㅤ

FIFO 실습

코드를 통해 fifo 라는 이름의 FIFO 구조를 생성해주면, 이렇게 파일로 fifo 가 생성된다. 가장 앞에 보면 파일의 형식이 p (네임드 파이프) 로 되어있는 것을 볼 수 있다.

ㅤㅤ

이 FIFO로 데이터를 보내는 코드를 통해 입력 > FIFO에 담기 를 해보면 fifo 에 입력한 내용이 담기는 것을 볼 수 있다.

‘cat’ 명령어를 통해서 실제로 이 파일 안에 내용들이 잘 찍히고 있는지를 확인하고싶었다. 확실히 일반 파일이 아니라 파이프 파일이라 그런지, cat으로 내용 출력만 해보려했는데 그대로 Ctrl+C 를 입력할 때 까지 fifo가 열려있고, 위쪽 Client 코드에서 값을 넣어줄 때마다 한 줄씩 추가되는 모습을 보여주었다.

ㅤㅤ
그래서 nano로 열어서 확인해보려고 했는데, 이 파일은 텍스트가 아니라서 nano가 인식하지는 못했다. cat 명령어는 되는걸 보니, FIFO를 쓰면 프로세스끼리 통신하는 내용을 몰래 염탐할 수 있을 것 같다. (버그가 아니라 기능일지도)

ㅤㅤ

실습 코드를 실행해보면, 전혀 상관없는 두 프로세스끼리 데이터를 주고받는 모습을 볼 수 있다.

// server.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>             /* read(), close(), unlink() 등의 시스템 콜을 위한 헤더 파일 */
#include <sys/stat.h>

#define FIFOFILE "fifo"

int main(int argc, char **argv)
{
    int n, fd;
    char buf[BUFSIZ];
    unlink(FIFOFILE);             /* 기존의 FIFO 파일을 삭제한다. */

    if(mkfifo(FIFOFILE, 0666) < 0) {     /* 새로운 FIFO 파일을 생성한다. */
        perror("mkfifo()");
        return -1;
    }

    if((fd = open(FIFOFILE, O_RDONLY)) < 0) {         /* FIFO를 연다. */
        perror("open()");
        return -1;
    }

    while((n = read(fd, buf, sizeof(buf))) > 0)     /* FIFO로부터 데이터를 받아온다. */
        printf("%s", buf);         /* 읽어온 데이터를 화면에 출력한다. */

    close(fd);

    return 0;
}
// client.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define FIFOFILE "fifo"

int main(int argc, char **argv)
{
    int n, fd;
    char buf[BUFSIZ];

    if((fd = open(FIFOFILE, O_WRONLY)) < 0) {         /* FIFO를 연다. */
        perror("open()");
        return -1;
    }

    while ((n = read(0, buf, sizeof(buf))) > 0)     /* 키보드로부터 데이터를 입력받는다 */
        write(fd, buf, n);                 /* FIFO로 데이터를 보낸다. */

    close(fd);

    return 0;
}

ㅤㅤ

1-3 SOCKET

특징

  • 양방향 통신이 가능하다.
  • 통신의 주체가 명확 (서버 - 클라이언트)
  • 파일 디스크립터를 이용하나, 파일 시스템 외부에 존재한다.
  • TCP 소켓, UDP 소켓, 유닉스 도메인 소켓 (로컬 IPC 전용) 등이 있다.

ㅤㅤ

장점

  • 로컬 통신 / 네트워크 통신을 아울러 사용이 가능하다
  • 양방향 데이터 통신 + 복잡한 통신 구조에 적합하다

ㅤㅤ

단점

  • 설정 및 구현이 복잡함

ㅤㅤ

어디에 쓰나?

  • 네트워크 통신, 클라이언트-서버 구조로 동작하는 로컬 프로그램
  • 양방향으로 움직이는 복잡하고 안정적인 데이터 교환이 필요할 때 사용

ㅤㅤ

SOCKET 실습

소켓에 대한 예시로, 위 FIFO와 동일하게 client 의 입력을 server에서 받아서 출력하는 동작을 확인해보았다. 코드는 제미나이의 도움을 받았다.

ㅤㅤ

소켓 서버에서 echo_socket으로부터 데이터를 듣겠다고 socket을 열어주면 아래에서 볼 수 있듯이 파일이 하나 생성된다. 이때 생성되는 파일의 종류는 s (socket 파일) 이다.

요 파일은 cat 명령어로 열리지 않는 파일이였다. 통신중에 nano로 엿보려고 했는데, 이것도 열리지 않았음.

ㅤㅤ

이때 Client 코드를 실행시켜서 접속을 시도하면 Server 에서도 이를 인지하고 서로 연결이 된다.

클라이언트와 서버끼리 서로 소켓 파일을 이용해 데이터를 잘 주고받는 모습도 확인할 수 있었다.

 

// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

// 서버가 사용할 소켓 파일 경로
#define SOCKET_PATH "./echo_socket"
#define BUF_SIZE 256

int main() {
    int server_fd, client_fd;
    struct sockaddr_un server_addr;
    char buffer[BUF_SIZE];
    ssize_t bytes_read;

    // 1. 기존 소켓 파일 제거 (이전 실행 잔여물 방지)
    unlink(SOCKET_PATH);

    // 2. 소켓 생성 (AF_UNIX: 유닉스 도메인 소켓)
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 3. 서버 주소 구조체 설정
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    // 4. 소켓에 주소(파일 경로) 바인딩
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 5. 연결 대기 (최대 5개의 대기열)
    if (listen(server_fd, 5) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on %s...\n", SOCKET_PATH);

    // 6. 클라이언트 연결 수락
    client_fd = accept(server_fd, NULL, NULL);
    if (client_fd == -1) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    printf("Client connected.\n");

    // 7. 데이터 수신 및 에코
    while ((bytes_read = recv(client_fd, buffer, BUF_SIZE - 1, 0)) > 0) {
        buffer[bytes_read] = '\0'; // 문자열 종료
        printf("Received: %s", buffer);

        // Echo 메시지 구성
        char response[BUF_SIZE + 8];
        snprintf(response, sizeof(response), "Echo: %s", buffer);

        // 클라이언트에게 Echo 메시지 전송
        if (send(client_fd, response, strlen(response), 0) == -1) {
            perror("send");
            break;
        }
    }

    // 8. 정리
    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH);
    printf("Server shut down.\n");
    return 0;
}
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

// 서버가 사용할 소켓 파일 경로
#define SOCKET_PATH "./echo_socket"
#define BUF_SIZE 256

int main() {
    int client_fd;
    struct sockaddr_un server_addr;
    char send_buffer[BUF_SIZE];
    char recv_buffer[BUF_SIZE];
    ssize_t bytes_read;

    // 1. 소켓 생성
    client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (client_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 서버 주소 구조체 설정
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    // 3. 서버에 연결
    if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect");
        close(client_fd);
        exit(EXIT_FAILURE);
    }

    printf("Connected to server. Enter messages (Ctrl+D to exit):\n");

    // 4. 사용자 입력 -> 서버 전송 -> 응답 수신
    while (fgets(send_buffer, BUF_SIZE, stdin) != NULL) {
        // 데이터 전송
        if (send(client_fd, send_buffer, strlen(send_buffer), 0) == -1) {
            perror("send");
            break;
        }

        // 서버로부터 응답 수신
        bytes_read = recv(client_fd, recv_buffer, BUF_SIZE - 1, 0);
        if (bytes_read > 0) {
            recv_buffer[bytes_read] = '\0';
            printf("Server Reply: %s", recv_buffer);
        } else if (bytes_read == 0) {
            printf("Server closed connection.\n");
            break;
        } else {
            perror("recv");
            break;
        }
    }

    // 5. 정리
    close(client_fd);
    return 0;
}

ㅤㅤ

320x100