본문 바로가기
CS 지식/시스템 프로그래밍

[C언어] 프로세스 간 통신 (IPC)

by 코딩하는 동현 2024. 10. 18.

프로세스 간 통신 (IPC)

1. 파이프 (Pipe)

  • 스트림 지향: 데이터를 순차적으로 단방향으로 전송하는 방식입니다.
  • 시스템 호출: pipe(), read(), write()를 사용하여 데이터 전송을 처리합니다.
  • 동작 방식: 파이프의 쓰기 끝에 데이터를 작성하면, 커널이 해당 데이터를 버퍼에 저장하고 읽기 끝에서 읽힐 때까지 유지합니다.
  • FIFO 동작: First In First Out 방식으로 데이터를 처리합니다.

2. 메시지 큐 (Message Queue)

  • 메시지 지향: 프로세스가 메시지를 쓰고 다른 프로세스가 읽는 형태입니다.
  • 특징: 메시지 큐는 메시지 식별자로 관리되며, 커널 내에서 메시지의 연결 리스트로 구현됩니다.
  • 메시지 순서: 메시지는 FIFO 방식으로 읽히지만, 비순차적으로도 메시지를 검색할 수 있습니다.

3. 공유 메모리 (Shared Memory)

  • 가상 메모리 영역: 여러 프로세스가 공유하는 메모리 영역입니다.
  • 정보 공유: 프로세스가 메모리의 특정 영역을 공유하여 정보를 전달합니다.
  • 동기화 필요: 세마포어를 사용하여 공유 메모리에 대한 접근을 동기화해야 합니다.

4. 세마포어 (Semaphore)

  • 동기화 기법: unsigned 전역 정수 변수로, 프로세스 간의 동기화를 도와줍니다.
  • P와 V 연산:
    • P(s): s가 0이 아니면 1 감소하고 즉시 반환합니다. 이 작업은 원자적으로 수행됩니다.
    • V(s): s를 1 증가시킵니다. 이 작업도 원자적으로 수행됩니다.
  • SysV와 POSIX: 각각 semget(), semctl(), semop() 및 sem_open(), sem_wait(), sem_post() 등의 시스템 호출을 사용합니다.

5. 소켓 (Sockets)

  • 통신 끝점: 컴퓨터 네트워크를 통한 IPC 흐름의 끝점입니다.
  • TCP/IP 지원: 다양한 네트워크 프로토콜을 사용하여 통신합니다.

POSIX (Portable Operating System Interface)

  • 표준화: IEEE에서 운영 체제 간의 호환성을 유지하기 위해 제정된 표준입니다.
  • 지원하는 IPC:
    • Pipe: POSIX.1에서 지원
    • Message Queue: POSIX.1b에서 지원
    • Shared Memory: POSIX.1b에서 지원
    • Semaphore: POSIX.1b에서 지원

파이프 (Pipes) 및 명령어 연결

  • 명령어 연결: cmd1 | cmd2
    • 파이프 |는 첫 번째 명령어(cmd1)의 출력을 두 번째 명령어(cmd2)의 입력으로 전달합니다.
  • 입출력 흐름:
    • cmd1의 표준 입력은 터미널 키보드에서 들어옵니다.
    • cmd1의 표준 출력은 cmd2의 표준 입력으로 전달됩니다.
    • cmd2의 표준 출력은 터미널 화면에 연결되어 표시됩니다.

로그 파일 처리 예시

다음은 로그 파일을 다루는 여러 명령어 예시입니다:

  1. 로그 파일 페이지 나누기 more:
    1. /var/log/auth.log와 /var/log/syslog의 내용을 합쳐서 페이지 단위로 보여줍니다. 
    2. cat /var/log/auth.log /var/log/syslog | more
  2. 특정 사용자 관련 로그 검색:
    • 두 로그 파일에서 'user konkuk'과 관련된 로그 라인을 출력합니다.
    • cat /var/log/auth.log /var/log/syslog | grep 'user konkuk'
  3. 정렬된 로그 출력:
    • 특정 사용자의 로그를 정렬하여 출력합니다.
    • cat /var/log/auth.log /var/log/syslog | grep 'user konkuk' | sort
  4. 결과를 파일에 저장:
    • 특정 사용자의 로그를 정렬한 후, 결과를 test라는 파일에 저장합니다.
    • cat /var/log/auth.log /var/log/syslog | grep 'user konkuk' | sort > test

이와 같이 파이프를 활용하면 명령어 간에 출력을 효과적으로 연결하여 데이터를 처리할 수 있습니다.

 


pipe 코드 예시

int pipe(int fd[2])

fd[0] = read

fd[1] = write

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h> // for strcpy

int main(){
    int pipefd[2];
    char buf[1];
    // fd[0] = read
    // fd[1] = write

    //파이프 만들고 예외처리
    if (pipe(pipefd) == -1)
    {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    

    // fork() 하고 예외 처리
    pid_t childpid = fork();
    if (childpid == -1)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    

    // 부모가 자식에게 파이프로 메세지 보냄
    if (childpid == 0)
    {
        close(pipefd[1]);
        while (read(pipefd[0], &buf, 1) > 0)
        {
            write(STDOUT_FILENO, "\n", 1);
        }
        close(pipefd[0]);
        exit(EXIT_SUCCESS);
        
    } else  {
        // 부모는 write만 할것이므로 read 닫음
        close(pipefd[0]);
        char str[] = "1234567890";
        write(pipefd[1], str, strlen(str));
        close(pipefd[1]);
        wait(NULL);
        exit(EXIT_SUCCESS);
    }
    
    
}

 


Message Queue

Message Oriented

MQ는 데이터가 연속적인 스트림이 아니라, 메시지 단위로 처리됩니다. 각 메시지는 독립된 단위로 송수신되며, 특정 메시지를 선택적으로 읽거나 처리할 수 있습니다.

 

프로세스 간 통신 (IPC)

여러 프로세스가 메시지 큐에 메시지를 쓰거나 읽을 수 있습니다. 하나의 프로세스가 작성한 메시지는 다른 프로세스에서 읽을 수 있는 형태로 구현되어 있습니다.

 

식별자 사용

각 메시지 큐는 고유한 식별자(Message Queue Identifier)를 통해 관리됩니다.

 

커널에서의 구현

메시지 큐는 커널 내부에서 연결 리스트(linked list) 형태로 구현되어 있으며, 큐에 저장된 메시지는 커널 메모리에 위치합니다. 사용자 공간에서 직접 접근할 수 없습니다. 메시지는 기본적으로 FIFO(First-In-First-Out) 방식으로 처리되며, 특정 조건에 따라 우선순위가 높은 메시지를 먼저 읽을 수도 있습니다.

 

활용 예시

메시지 큐는 독립적인 프로세스 간 데이터를 주고받기 위한 IPC(Inter-Process Communication) 수단으로 사용됩니다. 특히 생산자-소비자 패턴에 적합합니다.

 

MQ 삭제

메시지 큐는 삭제될 때까지 계속 존재합니다. 메시지 큐는 한 번 생성되면, 큐를 사용한 모든 프로세스가 종료되더라도 큐 자체는 사라지지 않습니다. 따라서 수동으로 삭제되기 전까지 시스템에 남아있습니다.

 

메시지 큐 삭제 방법

메시지 큐를 삭제하려면 msgctl() 시스템 콜을 사용해야 하며, IPC_RMID 명령을 통해 큐를 제거할 수 있습니다. msgctl()은 메시지 큐의 상태를 변경하거나 삭제하는 데 사용됩니다.

 

메시지 큐 상태 확인

ipcs 명령어를 사용하면 시스템에 존재하는 메시지 큐들을 확인할 수 있습니다. 사용하지 않는 메시지 큐가 있다면 이 명령어를 통해 목록을 확인할 수 있습니다.

 

메시지 큐 삭제

사용하지 않는 메시지 큐를 삭제하려면 ipcrm 명령어를 사용합니다. 이 명령어는 메시지 큐, 세마포어, 공유 메모리 등을 삭제하는 데 사용됩니다.


SysV Message Queue

 

Message Format

 

SysV 메시지 큐에서의 메시지 포맷은 msgbuf 구조체로 정의되며, 메시지에는 mtypemtext가 포함됩니다.

struct msgbuf{
    long mtype;
    char mtext[N]; // N byte
}

 

mtype은 필수적이며 양수여야 하며, mtext는 문자 배열로 저장할 수 있습니다.

 

Message Queue 생성

 

새로운 메시지 큐를 생성하거나 기존 큐에 접근하려면 msgget() 함수를 사용합니다.

int msgget(key_t key, int flag)

key: 시스템 전역적으로 고유한 메시지 큐의 구분자

flag: IPC_CREAT, IPC_EXCL 등 다양한 옵션이 존재

 

메시지 송신

 

메시지를 큐에 전달하려면 msgsnd() 함수를 사용합니다.

int msgsnd(int msgid, const void *ptr, size_t nbytes, int flags)

메시지 수신

 

메시지를 큐에서 받으려면 msgrcv() 함수를 사용합니다.

int msgrcv(int msgid, void *ptr, size_t nbytes, long type, int flag)

 

type: 메시지 유형을 지정

flag: 메시지가 너무 클 경우의 처리 방법

 

메시지 큐 삭제

 

메시지 큐를 삭제하려면 msgctl()을 사용합니다.

msgctl(msgid, IPC_RMID, NULL)

SysV Message Queue 예제 코드

MQ 송신자

int main() {
    key_t ipckey;
    int mqdes, i;
    size_t buf_len;
    struct {
        long id;
        int value;
    } mymsg;
    buf_len = sizeof(mymsg.value);
    ipckey = ftok("./tmp/foo", 1946);
    mqdes = msgget(ipckey, 0600 | IPC_CREAT);
    if (mqdes == -1) {
        perror("msgget");
        return 1;
    }
    for (i = 0; i < MAX_ID; i++) {
        mymsg.id = i+1;
        mymsg.value = i*3;
        printf("sending %ld %d\n", mymsg.id, mymsg.value);
        int ret = msgsnd(mqdes, &mymsg, buf_len, 0);
        if (ret == -1) {
            perror("msgsnd");
            return 1;
        }
    }
}

 

MQ 수신자:

int main() {
    key_t ipckey;
    int mqdes, i;
    size_t buf_len;
    struct {
        long id;
        int value;
    } mymsg;
    buf_len = sizeof(mymsg.value);
    ipckey = ftok("./tmp/foo", 1946);
    mqdes = msgget(ipckey, 0600 | IPC_CREAT);
    if (mqdes == -1) {
        perror("msgget");
        return 1;
    }
    for (i = 0; i < MAX_ID; i++) {
        int ret = msgrcv(mqdes, &mymsg, buf_len, i+1, 0);
        if (ret == -1) {
            perror("msgrcv");
            return 1;   
        }
        printf("received %ld %d\n", mymsg.id, mymsg.value);
    }
}

 

지정한 id(mtype)에 맞는것을 뽑아서 온다


POSIX Message Queue

Message Queue 생성

POSIX 메시지 큐는 mq_open() 함수로 생성합니다.

mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr)
  • name: 큐 이름
  • oflag: 읽기/쓰기 모드
  • mode: 큐 권한
  • attr: 큐 속성

메시지 송신

메시지를 큐에 보내려면 mq_send() 함수를 사용합니다.

int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned prio)
 
  • msg_ptr: 메시지 내용
  • msg_len: 메시지 길이
  • prio: 메시지 우선순위

메시지 수신

메시지를 큐에서 받으려면 mq_receive() 함수를 사용합니다.

int mq_receive(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned *msg_prio)
 
  • msg_ptr: 수신할 메시지 버퍼
  • msg_len: 메시지의 길이
  • msg_prio: 메시지 우선순위

메시지 큐 삭제

큐를 닫으려면 mq_close()를 사용하고, 큐를 시스템에서 제거하려면 mq_unlink()를 사용합니다.

mq_close(mqd_t mqdes) mq_unlink(const char *name)

 


POSIX Message Queue 예제 코드

MQ 송신자:

#include <mqueue.h>
#include <stdio.h>

int main() {
    struct mq_attr attr;
    int value = 0;
    unsigned int prio;
    mqd_t mqdes;

    attr.mq_maxmsg = 10;
    attr.mq_msgsize = MSG_SIZE;

    mqdes = mq_open("/m_queue", O_CREAT | O_RDWR, 0600, &attr);
    if (mqdes == -1) {
        perror("mq_open");
        return 1;
    }
    for (prio = 0; prio < MAX_PRIO; prio++) {
        printf("sending value %d with priority %u\n", value, prio);
        int ret = mq_send(mqdes, (char *)&value, MSG_SIZE, prio);
        if (ret == -1) {
            perror("mq_send");
            return 1;
        }
        else {
            value+=3;
        }
    }
    mq_close(mqdes);
    return 0;
}

 

MQ 수신자:

#include <stdio.h>
#include <mqueue.h>

int main() {
    struct mq_attr attr;
    int value;
    unsigned int prio;
    mqd_t mqdes;

    attr.mq_maxmsg = 10;
    attr.mq_msgsize = MSG_SIZE;

    mqdes = mq_open("/m_queue", O_RDONLY|O_CREAT, 0600, &attr);
    if (mqdes == -1) {
        perror("mq_open");
        return 1;
    }

    for (int i = 0; i < MAX_PRIO; i++) {
        int ret = mq_receive(mqdes, (char *)&value, MSG_SIZE, &prio);
        if (ret == -1) {
            perror("mq_receive");
            return 1;
        }
        printf("received value %d with priority %u\n", value, prio);
    }
    mq_close(mqdes);
    mq_unlink("/m_queue");
    return 0;
}

 

우선순위 순서대로 받아온다.

 


공유메모리 코드 예시

공유 메모리(Shared Memory)는 여러 프로세스가 같은 물리적 메모리 공간을 직접 접근할 수 있게 해주는 방법으로, 빠른 데이터 공유와 통신을 가능하게 합니다. 아래는 C언어를 사용한 공유 메모리 예시입니다.

 

1. 공유 메모리 생성 및 사용 예시 (POSIX)

 

Sender (공유 메모리 데이터를 작성하는 프로세스)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h> 
#include <sys/stat.h> 

#define SHM_NAME "/my_shared_memory"  // 공유 메모리 이름
#define SIZE 4096                    // 공유 메모리 크기

int main() {
    // 공유 메모리 객체 생성
    int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (shm_fd == -1) {
        perror("shm_open");
        exit(1);
    }

    // 공유 메모리 크기 설정
    if (ftruncate(shm_fd, SIZE) == -1) {
        perror("ftruncate");
        exit(1);
    }

    // 공유 메모리 맵핑
    void *shm_ptr = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (shm_ptr == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }

    // 데이터를 공유 메모리에 쓰기
    int *data = (int *)shm_ptr;
    for (int i = 0; i < 10; i++) {
        data[i] = i * 2;
        printf("Writing value: %d\n", data[i]);
    }

    // 공유 메모리 파일 디스크립터 닫기
    close(shm_fd);
    return 0;
}

 

 

Receiver (공유 메모리에서 데이터를 읽는 프로세스)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h> 
#include <sys/stat.h> 

#define SHM_NAME "/my_shared_memory"  // 공유 메모리 이름
#define SIZE 4096                    // 공유 메모리 크기

int main() {
    // 공유 메모리 객체 열기
    int shm_fd = shm_open(SHM_NAME, O_RDONLY, 0666);
    if (shm_fd == -1) {
        perror("shm_open");
        exit(1);
    }

    // 공유 메모리 맵핑
    void *shm_ptr = mmap(0, SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);
    if (shm_ptr == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }

    // 데이터를 공유 메모리에서 읽기
    int *data = (int *)shm_ptr;
    for (int i = 0; i < 10; i++) {
        printf("Reading value: %d\n", data[i]);
    }

    // 공유 메모리 파일 디스크립터 닫기
    close(shm_fd);

    // 공유 메모리 unlink
    shm_unlink(SHM_NAME);
    return 0;
}

 

 

1. 공유 메모리 생성 (Sender):

  • shm_open()을 사용하여 공유 메모리 객체를 생성하고, ftruncate()로 메모리 크기를 지정합니다.
  • mmap()을 사용하여 해당 메모리를 프로세스의 주소 공간에 맵핑합니다.
  • 데이터를 shm_ptr을 통해 공유 메모리에 작성합니다.

2. 공유 메모리 읽기 (Receiver):

  • shm_open()으로 이미 생성된 공유 메모리 객체를 엽니다.
  • mmap()으로 공유 메모리를 프로세스의 주소 공간에 맵핑합니다.
  • 데이터를 읽어서 출력합니다.

 

공유 메모리 종료 및 삭제

사용이 끝난 후 shm_unlink()를 사용하여 공유 메모리 객체를 삭제합니다.


세마포어 코드 예시

1. 이진 세마포어 (Binary Semaphore): 0과 1의 값만을 가질 수 있는 세마포어.

2. 카운팅 세마포어 (Counting Semaphore): 여러 개의 자원을 관리할 수 있는 세마포어로, 특정 자원의 개수를 세는 방식으로 동작합니다.

 

Sender (세마포어를 사용하여 메시지를 보내는 프로세스)

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

sem_t semaphore;  // 세마포어 선언

void* send_message(void* arg) {
    sem_wait(&semaphore);  // 세마포어 획득 (세마포어 값이 0이면 대기)
    
    printf("Sending message...\n");
    sleep(1);  // 메시지 전송 시뮬레이션
    
    printf("Message sent!\n");
    
    sem_post(&semaphore);  // 세마포어 반납 (세마포어 값 증가)
    
    return NULL;
}

int main() {
    pthread_t sender1, sender2;

    // 세마포어 초기화 (초기값 1)
    if (sem_init(&semaphore, 0, 1) != 0) {
        perror("sem_init");
        exit(1);
    }

    // 두 프로세스가 동시에 메시지를 보내려고 함
    pthread_create(&sender1, NULL, send_message, NULL);
    pthread_create(&sender2, NULL, send_message, NULL);

    // 스레드 종료 대기
    pthread_join(sender1, NULL);
    pthread_join(sender2, NULL);

    // 세마포어 해제
    sem_destroy(&semaphore);

    return 0;
}

 

 

Receiver (세마포어를 사용하여 메시지를 수신하는 프로세스)

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

sem_t semaphore;  // 세마포어 선언

void* receive_message(void* arg) {
    sem_wait(&semaphore);  // 세마포어 획득 (세마포어 값이 0이면 대기)
    
    printf("Receiving message...\n");
    sleep(1);  // 메시지 수신 시뮬레이션
    
    printf("Message received!\n");
    
    sem_post(&semaphore);  // 세마포어 반납 (세마포어 값 증가)
    
    return NULL;
}

int main() {
    pthread_t receiver1, receiver2;

    // 세마포어 초기화 (초기값 1)
    if (sem_init(&semaphore, 0, 1) != 0) {
        perror("sem_init");
        exit(1);
    }

    // 두 프로세스가 동시에 메시지를 수신하려고 함
    pthread_create(&receiver1, NULL, receive_message, NULL);
    pthread_create(&receiver2, NULL, receive_message, NULL);

    // 스레드 종료 대기
    pthread_join(receiver1, NULL);
    pthread_join(receiver2, NULL);

    // 세마포어 해제
    sem_destroy(&semaphore);

    return 0;
}

설명

  • Sender와 Receiver 모두 세마포어를 사용하여 자원을 요청하고 반납하는 방식으로 동기화가 이루어집니다.
  • send_message와 receive_message는 메시지를 처리하는 함수로, 세마포어의 상태를 제어하여 동기화를 맞추고 있습니다.
  • 이 예시에서는 메시지 송신과 수신이 단순화되어 있지만, 실제 메시지 송수신의 구현에서는 세마포어가 자원에 대한 접근을 제어하는 데 활용됩니다.

세마포어 자체는 자원 접근의 동기화만 담당하며, 메시지를 전달하는 데 필요한 구체적인 내용은 다른 메커니즘(예: 메시지 큐, 공유 메모리 등)을 통해 처리해야 합니다.


예시 명령어

  • cat: 파일을 연결하여 표준 출력에 출력합니다.
    • 예: cat /etc/hostname → 시스템의 호스트 이름 출력
  • more: 텍스트를 한 화면씩 페이지 나누어 보여줍니다.
    • 예: more /etc/hostname
  • grep: 특정 패턴이 포함된 줄을 출력합니다.
    • 예: grep 'authentication failure' /var/log/auth.log
  • sort: 텍스트 파일의 줄을 정렬합니다.
    • 예: sort /etc/passwd

로그 파일 연결 및 검색

  • 특정 사용자에 대한 로그 라인을 찾아 정렬 후 파일에 저장하는 명령어 예시:bash 
  • 코드 복사
    cat /var/log/auth.log /var/log/syslog | grep 'user konkuk' | sort > test

요약

IPC는 다양한 방법을 통해 프로세스 간의 데이터를 교환하고 동기화하는 데 필수적인 요소입니다. 각 기법은 고유한 특성과 장단점을 가지고 있어, 상황에 맞는 방법을 선택하여 사용해야 합니다.

반응형

댓글