Sqix

CS - OS - 05. Inter Process Communication(IPC) 본문

CS/OS

CS - OS - 05. Inter Process Communication(IPC)

Sqix_ow 2021. 11. 19. 22:54

Inter-Process Communication

프로세스간의 접근권한이 낮은 경우, 예를 들어 A 프로세스에서 B 프로세스의 데이터 / 코드를 바꿀 수 있다면 이는 보안 상 매우 위험하므로, 이러한 커뮤니케이션 방식은 제공되지 않는다. 다만, 프로세스 간 커뮤니케이션이 필요한 경우 제공되는 기법이 IPC이다.

 

프로세스 간 통신은 성능을 높이기 위해 동시에 여러 프로세스를 만들어 실행하는 경우 프로세스 상태 확인, 결과를 통해 만들어진 데이터 송수신을 위해 필요할 수 있다.


동시 작업을 위해 하나의 프로세스를 여러 개의 프로세스로 쪼개서 사용하는 fork의 경우

  • sys_fork()를 통해 프로세스 자신을 복사하여 자식 프로세스 생성
  • 동시 작업을 위해 여러 프로세스를 fork하여 동시 수행
    과 같은 병렬 처리를 수행할 것이고 이를 위해 프로세스 상태 확인 및 여기서 발생하는 데이터 처리를 위해 IPC가 필요할 것이다.

이러한 IPC 작업을 위해서는 커널 공간을 사용한다.


프로세스가 메모리에 올라갈 때는 일련의 변환 과정을 거쳐 가상 메모리를 사용하게 되고, 크게 Kernel Space와 User Space로 나뉘는데 실제 물리 메모리에는 Kernel space가 올라가고 동일한 부모 프로세스를 가진 자식 프로세스들에게는 Kernel space가 공유된다.

 

IPC 기법

 

공유 가능한 저장매체(파일)를 활용 (커널 공간 사용 X)

  • 실시간으로 직접 원하는 프로세스에 데이터를 전달할 수 없음
  • 저장매체에 대한 접근이 필요하므로 오버헤드가 매우 큼

pipe 기법


unistd.h 헤더 파일에 존재하는 pipe()함수를 통해서 사용할 수 있으며 반이중 방식(두 개의 스트림이 각각 양방향으로 통신을 할 수 있으나, 하나의 통신을 할 때 한 방향으로만 진행이 가능)으로 통신을 하고, 부모 프로세스 - 자식 프로세스 관계의 프로세스들끼리 통신이 가능하다.


함수의 원형은

int pipe(int pipefd[2]);

로, 인자로 크기가 2인 정수형 배열이 들어간다.


파이프는 커널 영역에 생성되고, 파이프를 생성한 프로세스는 fd만을 가지고 있게 된다. 여기서 fd[1]은 write-only, fd[2]는 read-only 권한이다. 즉, 우리가 fd[1]으로 데이터를 쓴다면 fd[0]으로 그 데이터를 읽어들일 수 있다.


한 쌍의 부모 프로세스와 자식 프로세스의 통신을 위해서는 2개의 파이프가 필요하다. 파이프를 1개만 사용한다면 통상적인 상황에서는 문제가 없으나, 부모 프로세스가 파이프에 데이터를 쓰자마자 이를 읽으면 파이프에 있는 데이터는 바로 없어질 것이다. 이 때, 자식 프로세스는 파이프의 데이터를 읽기 위해 계속 listen을 하고 있으므로 프로그램이 동작하지 않을 것이다.

 


이러한 이유로 파이프를 2개 사용한다. fd_A[2] array와 fd_B[2] array를 사용한다고 가정하고

  • 파이프 A : 부모 프로세스가 데이터를 쓰고 자식 프로세스가 데이터를 읽음
  • 파이프 B : 자식 프로세스가 데이터를 쓰고 부모 프로세스가 데이터를 읽음

이렇게 파이프를 만들면 부모 프로세스는 자식에게로부터 쓰여지는 fd_B[1]과 자식의 데이터를 읽는 fd_A[0]은 필요 없으니 이를 닫아 주면 되고 자식 프로세스는 반대로 fd_B[0]과 fd_A[1]을 닫아 주면 됩니다.

 

메시지 큐

 

프로세스 간 쓰레드와 큐를 활용하여 정보를 교환하는 IPC 기법 중 하나

  • 임의의 프로세스에서 메시지 큐 생성 (msgget() 시스템 함수 호출)
  • 이외의 프로세스는 해당 메시지 큐 open(msgget() 시스템 함수 호출)
  • 수신 프로세스 : 메시지큐로부터 메시지 수신(msgrcv() 시스템 함수 호출)
  • 송신 프로세스 : 메시지큐에게 메시지 송신(msgsnd() 시스템 함수 호출)

예시)

pthread_create(&msgqueue_rx_thread, NULL, Msgqueue_RX_Thread, NULL) < 0);

pthread 함수를 통해 메시지를 수신할 스레드 생성 (Msgqueue_RX_Thread 함수 수행)

struct Msg_type_1
{
    long type;
    uint32_t data;
};

static void *Msgqueue_RX_Thread(void *arg)
{
    int result;
    struct Msg_type_1 msg;  
    
    while (1)
    {
        result = msgrcv(MSG_QUEUE_ID, &msg, sizeof(msg) - sizeof(long), 1, 0);
        ...
    }
    ...
    return NULL;
}

수신 프로세스 : msgrcv() 함수를 통해 지정된 타입에 맞는 메세지 수신 및 수신된 메세지 처리

msgqueue_id = msgget((key_t)MSG_QUEUE_ID, IPC_CREAT | 0666);
struct Msg_type_1 = msg;

while(1)
{
    memset(&msg, 0, sizeof(msg));
    msg.type = 1;
    msg.data = 1;
    if(msgsnd(msgqueue_id, &msg, sizeof(msg) - sizeof(long), IPC_NOWAIT) < 0)
    {
    	perror("msgsnd()");
    }
}

...

송신 프로세스 : 메시지큐를 열고(msgget() ) 메시지큐에 메시지를 전송한다(msgsnd() )

 

커널 공간에 메시지 큐가 생성되어 작동하며, 메시지 큐 ID를 맞춰줄 수 있다면 상호 간 어떠한 프로세스이더라도 통신이 가능하고, 큐 형태이므로 FIFO 순서로 데이터가 처리된다.

 

공유 메모리(Shared Memory)

 

Kernel Space에 메모리 공간을 만들고 해당 공간을 변수처럼 사용하는 방식으로, 해당 메모리에 대해서 변수를 활용하듯 접근을 할 수 있고 공유메모리 Key를 가지고 있다면 여러 프로세스가 접근하여 사용할 수 있다.

 

공유 메모리를 처음 생성할 때만 System Call을 하기 때문에 속도가 빠르다는 장점이 있으나, 동기화에 대한 관리가 필요하다.

 

공유메모리 관련 함수

int shmget(key_t key, size_t size, int shmflg);
  • shmget 함수
    • key : 공유 메모리 설정 시 사용되는 고유 key로, 해당 key로 접근
    • size : 메모리의 최소 size 설정
    • shmflg -> 생성 옵션 / IPC_CREAT : 새 메모리 세그먼트 생성. / IPC_EXCL : 메모리 세그먼트 존재 여부 확인
void *shmat(int shmid, const void* shmaddr, int shmflg);
  • shmat 함수
    • shmid : shmget 함수를 호출할 때 받는 리턴 id값. 접근하고자 하는 공유 메모리 id
    • shmaddr : 접근하고자 하는 공유 메모리 주소로, 0인 경우 커널에서 주소 할당
    • 접근한 공유 메모리 주소를 리턴
int shmdt(const void* shmaddr);
  • shmdt 함수
    • 프로세스에서 공유메모리를 접근 해제할 때 사용하는 함수
    • shmaddr : 접근 해제하고자 하는 주소
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmctl 함수
    • 공유 메모리 컨트롤 함수, 공유 메모리 정보를 가져오거나 공유메모리를 삭제할 수 있음
    • cmd : 실행할 명령어
    • buf : 컨트롤하고자 하는 버퍼

SIGNAL

 

커널 및 프로세스에서 다른 프로세스에 대한 이벤트 발생을 알려주는 방법으로, 프로세스는 시그널 관련 코드에 핸들러를 등록하여 시그널에 대한 처리를 진행한다. 시그널에 대한 처리는 커널 모드에서 유저 모드로 돌아갈 때 처리되며, 처리 방식은 다음과 같이 나뉠 수 있다.

  • 시그널 무시
  • 시그널 블록(블록을 풀면 프로세스에 시그널이 전달됨)
  • 등록된 시그널 핸들러를 통해 시그널 처리
  • 시그널에 대해 커널에 등록된 기본 동작 수행

시그널은 PCB 안에 있는 시그널 관련 정보를 관리하는 구조체에 의해 관리된다. 이전 글인 interrupt에서 다뤘던 task_struct라는 구조체 내부에 다음과 같은 구조체가 있다.

 

struct signal_struct *signal;
struct sighand_struct *sighand;

 

주요 시그널은 다음과 같다.

번호 이름 설명 기본 처리
1 SIGHUP
(HUP)
HangUP의 약어로 터미널에서 접속이 끊겼을 때 보내지는 시그널.
데몬 관련 환경 설정 파일을 변경시키고 변화된 내용을 적용하기 위해 재시작할 때 이 시그널이 사용된다.
종료
2 SIGINT 
(INT)
키보드로부터 오는 인터럽트 시그널로 실행을 중지. 
[CTRL]+[c] 입력 시에 보내지는 시그널.
종료
3 SIGQUIT 
(QUIT)
키보드로부터 오는 실행 중지 시그널.
[CTRL] + [\] 입력 시에 보내지는 시그널.
기본적으로 프로세스를 종료시킨 뒤 코어를 덤프.
코어 덤프
4 SIGILL 
(ILL)
illegal instruction의 약자. 잘못된 명령을 사용했을 때 발생. 코어 덤프
5 SIGTRAP 
(TRAP)
trace(추적), breakpoint(중지점)에서 TRAP 발생할 때  코어 덤프
6 SIGABRT (ABRT) abort의 약자로 비정상종료 함수에 의해 발생. 
(즉 abort 시스템 호출을 하였을 때 발생)
코어 덤프
7 SIGBUS 메모리 접근 에러시 발생하는 시그널. 코어 덤프
9 SIGKILL (KILL) KILL! 무조건 종료, 즉 프로세스를 강제로 종료시키는 시그널 종료
11 SIGSEGV invalid memory reference 종료 +
코어덤프
15 SIGTERM (TERM) Terminate의 약자로 가능한 정상 종료시키는 시그널.
kill 명령의 기본 시그널.
종료
17 SIGCHLD
(child)
자식 프로세스가 stop 되거나 종료되었을 때 부모에게 전달되는 신호. 무시
18 SIGCONT (CONT) Continue의 약자로 STOP 시그널에 의해 정지된 프로세스를 다시 실행시킬 때 사용. 재시작
19 SIGSTOP (STOP) 터미널에서 입력된 정지 시그널. SIGCONT로 재실행시킬 수 있다. 중지
20 SIGTSTP (TSTP) 실행 정지 후 다시 실행을 계속하기 위해 대기시키는 시그널이다.
[CTRL] + [z]를 입력했을 때 보내지는 시그널이다.
SIGCONT로 역시 다시 실행시킬 수 있다.
중지
29 SIGIO 비동기 입출력이 발생했을 경우(I/O now possible!) 종료

이 외에도 다양한 시그널이 있고, 터미널에서 kill -l을 입력하면 확인할 수 있다.

 

시그널을 핸들링하기 위해서는 signal 함수를 이용한다.

static void sig_handler(int signo)
{
    ...
    exit(EXIT_SUCCESS);
}

int main(void)
{
    if (signal(SIGINT, signal_handler) == SIG_ERR)
    {
        printf("Can't catch SIGINT\n");
        exit(EXIT_FAILURE);
    }
    ...
    return 0;
}

 SOCKET

 

프로그램이 네트워크를 통해서 데이터를 통신할 수 있도록 하는 연결부로, 통신할 두 프로그램(Client - Server)에 모두 소켓이 생성되어야 한다.

 

 

 

1. socket() 생성

 

-> 다른 노드들과 통신하기 위한 소켓 생성

 

2. bind()

 

-> 자신이 이용하고자 하는 ip와 port를 socket에 할당

 

3. listen()

 

-> 다른 노드에서의 요청을 받아들이기 위한 대기상태로 만들어주는 함수

 

4. accept()

 

-> 다른 노드(클라이언트)에서 연결 요청이 왔을 때 이에 대한 응답(수락)

 

5. read() / write()

 

-> 다른 노드와의 연결 이후 데이터 송/수신

 

6. close()

 

-> 다른 노드와의 상호작용이 끝나면 소켓을 닫고 종료

 

 

 

 

통신에 사용되는 주요 함수는 다음과 같다.

 

int socket(int domain, int type, int protocol);

 

  • socket() 함수
    • domain : 어떤 영역에서 통신할 것인지를 지정(ex : AF_UNIX, AF_INET, AF_INET6 등)
    • type : 어떤 서비스 타입의 소켓을 생성할 것인지(ex : SOCK_STREAM(TCP), SOCK_DGRAM(UDP) 등)
    • protocol : 소켓에서 사용할 프로토콜이 어떤 것인지(ex : IPPROTO_TCP, IPPROTO_UDP, 0(type에서 지정) 등)
    • 리턴값은 정상 생성 시 0 이상의 값(fd)을, 생성 실패 시 -1을 반환
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

 

  • bind() 함수
    • sockfd : 파일 디스크립터로, socket() 함수의 리턴값이 들어가는 자리
    • sockaddr : 서버의 주소
    • addrlen : 주소의 길이
    • 리턴값은 성공 시 0, 실패 시 -1을 반환
int listen(int sockfd, int backlog);
  • listen() 함수
    • sockfd : 디스크립터로, socket()함수의 리턴값이 들어가는 자리
    • backlog : listen을 하기 위한 대기열의 크기 지정
    • 리턴값은 성공 시 0, 실패 시 -1을 반환
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • accpet() 함수
    • sockfd : 디스크립터로, socket()함수의 리턴값이 들어가는 자리
    • sockaddr : 클라이언트의 주소 
    • addrlen : sockaddr 주소의 길이

'CS > OS' 카테고리의 다른 글

CS - OS - 07. 가상 메모리  (0) 2021.11.24
CS - OS - 06. Thread  (0) 2021.11.23
CS - OS - 04. 프로세스와 컨텍스트 스위칭  (0) 2021.11.19
CS - OS - 03. 인터럽트  (0) 2021.11.18
CS - OS - 02. 프로세스 스케쥴링  (0) 2021.11.16
Comments