InterProcess Communication - IPC
원칙적으로는 프로세스 간 커뮤니케이션을 할 수 없도록 되어있는데, 특별한 경우 IPC를 이용하여 커뮤니케이션 할 수 있다.
그럼 왜 프로세스끼리는 애초에 커뮤니케이션을 하지 못할까?
프로세스가 다른 프로세스 메모리에 접근할 수 있다면 다른 프로세스의 코드와 데이터를 수정할 수 있다.
그리고 그 자체야말로 해킹이다.
But, 프로세스간 통신이 필요한 경우가 있다.
요즘 제조되는 CPU는 기본적으로 멀티코어이기 때문에,
성능을 높이기 위해 여러 프로세스를 만들어서 각각의 코어에서 동시에 수행하도록 만들 수 있다.
그리고 이 과정에서 프로세스 실행 과정을 서로 공유하고 정리해야 할 필요가 있기 때문에, 프로세스 간 통신이 필요한 것이다.
예를 들어, fork() 라는 시스템 콜은 ‘프로세스 복제’를 위한 시스템 콜이고, 이 함수로 인해 부모-자식 프로세스가 만들어진다.
이 함수를 이용하면, 특정 프로세스를 멀티코어 CPU에서 동시에 실행하도록 만들 수 있기 때문에
프로세스 수행시간이 (원래 수행시간) / 코어 개수 n개 만큼 줄어들게 된다.
웹, WAS서버의 경우에도 request가 들어올 때 CPU 병렬처리를 통해 더 나은 성능의 응답을 보낼 수 있다!
새로운 요청이 올 때마다 fork() 함수로 자식 프로세스를 만들어, 자신이 직접 대응하지 않고 복제한 자식 프로세스가 대응하도록 만드는 것이다.
서버 관리자들은 웹서버의 현재 프로세스 처리 상태를 모니터링 함으로써 웹서버를 유지해야한다.
어쨌든, 프로세스 간 통신을 가능케하는 IPC 기법에는 어떤 것들이 있을까?
가장 쉬운 방법은 file을 사용하여 프로세스 정보를 기록하는 것이다.
file은 프로세스마다 시스템콜을 이용하여 하드웨어에 접근하여 file read를 수행하면 되므로, 가장 쉬운 방법이긴 하다.
그러나! 이 경우 파일에 기록된 변동사항을 실시간으로 확인할 수 없으며, file read를 계속 수행하는 것 또한 expansive하다.
파일 읽기보다 더 나은 다양한 IPC 방법이 존재한다.
빈번한 I/O 처리는 엄청난 overhead를 발생시킨다. 왜? 빈번한 시스템 콜 및 빈번한 인터럽트가 발생하게 되며, 보조기억장치에서 데이터를 읽고 쓰는 과정은 상대적으로 많은 시간이 걸리기 때문이다.
따라서 시스템 프로그래밍의 I/O처리를 할 때 주기억장치에서 필요한 처리를 할 수 있도록 만드는 것이 성능에 많은 영향을 준다.
Ex) 빅데이터 분석 플랫폼 ‘Spark’는 데이터의 처리를 왠만하면 메모리에서 하도록 설계되어있다(In Memory Engine).
Tip : 리눅스의 프로세스 메모리 용량은 4GB이고, 이 중 1GB 공간은 운영체제가 쓰는 공간(커널 공간), 나머지 3GB는 프로그램이 쓰는 공간이다(사용자 공간).
사용자 공간에서는 커널공간에 접근할 수 없다.
단, 프로세스가 만들어 질 때 마다 1GB의 커널 공간을 잡아버리면 낭비가 너무 심하므로, 커널 공간은 프로세스 사이에서 공유된다 (물리 메모리에서 일정 공간만 커널공간으로 잡히고, 그 공간은 공유된다는 뜻) - 보다 자세한 내용은 가상 메모리 시간에 다룸.
IPC의 방법에는
등의 방법이 있고, IPC 기법의 핵심은 커널 공간을 공유하는 것이다.(단, file 사용시에는 커널 공간 사용하지 않음).
커널공간 내부에 부모- 자식 프로세스 간 공유되는 버퍼 공간을 마련하여 통신하는 방식
파이프 생성의 시스템콜은 pipe() 이다. –> 파이프가 정상적으로 생성이되면 특정 주소값을 return해준다.
아래 코드는 파이프 방식으로 부모 process로부터 자식 process에 msg를 넘기는 과정을 나타낸다.
char* msg = "Hello Child Process";
int main(){
char buf[255];
int fd[2], pid, nbytes; // 파이프 변수, process id, 읽을 bytes 변수 생성
if (pipe(fd) < 0) // pipe(fd)로 파이프 생성 --> fd 변수에 pipe()함수의 리턴값이 담김.
exit(1);
pid = fork(); // fork()를 통해 parent-child 프로세스로 나뉘어짐
if (pid > 0){
write(fd[1], mgs, MSGSIZE); // fd[1]에 값을 쓴다.
}else{
nbytes = read(fd[0], buf, MSGSIZE); // fd[0]으로 읽는다.
printf("%d %s\n", nbytes, buf);
exit(0);
}
return 0;
}
결국 위 코드는, parent 프로세스에서 write한 결과(fd[1]의 주소가 가리키는 곳)를 자식 프로세스가 read한 결과를 보여준다.
즉, 부모 - 자식 프로세스 간 통신이 일어나고 있다.이처럼 부모-자식 프로세스 간의 통신을 지원하는 것이 파이프 방식이다.
기본 파이프는 단방향 통신 (부모 -> 자식 방향만 가능)이다(읽기 or 쓰기만 가능). –> 반이중 통신
따라서 읽기와 쓰기를 모두 하기 원한다면(전이중 통신), 파이프를 2개 만들어야 한다.
메시지가 FIFO방식으로 담긴 큐 자료구조를 커널 공간에서 보관함으로서,
다른 프로세스가 한 프로세스가 담은 큐 내부의 데이터를 꺼낼 수 있다.
단, key값을 알고 있어야 선택하고자 하는 메시지 큐를 get할 수 있다.
msqid = msgget(key, msgflg); // key값 , 옵션 을 파리미터로 받아서 queue를 생성한다.
msgsnd(msqid, $sbuf, buf_length, IPC_NOWAIT); // 메시지를 send한다.
msqid = msgget(key, msgflg); //동일 key값으로 해당 큐의 msqid를 get한다.
msgrcv(msqid, $rbuf, MSGSZ, 1, 0); // 메시지를 receive한다.
이렇게 큐를 통해서 A, B, 프로세스 사이에 데이터를 주고받을 수 있으며,
큐를 2개 생성할 시 양방향 전송이 가능하다.
노골적으로 커널 공간에 메모리 공간을 만들고, 해당 공간을 변수처럼 쓰는 방식이다.
Message queue처럼 FIFO 방식이 아니라, 해당 메모리 주소를 마치 변수처럼 접근하는 방식이다.
shmget(key값)이라는 시스템 콜을 통해 kernel space에 공유 공간을 만들고,
shmaddr(key값) 함수를 통해 공유 메모리에 데이터를 읽거나 쓸 수 있다.
유닉스에서 30년 이상 사용된 전통적인 기법으로,
커널 또는 프로세스에서 다른 프로레스에 어떤 이벤트가 발생되었는지를 알려주는 기법.
Signal은 ‘미리 정의되어있는’ 기본 동작들로, 그 예시는 다음과 같다.
PCB에 해당 프로세스가 블록 또는 처리해야 하는 시그널 관련 정보를 관리한다.
(5) Socket (소켓)
소켓은 원래 네트워크 통신을 위한 기술로, 기본적으로는 클라이언트 - 서버 간 통신을 위한 기술이다.
socket() 이라는 시스템 콜을 활용한다.
소켓은 네트워크 기기를 활용할 수 있는 시스템 콜이라고 볼 수도 있다.
그런데, 이 기술을 다른 컴퓨터 말고, 자기 컴퓨터 내부에서도 데이터를 네트워크로 주고받을 수 있으므로
IPC에 활용될 수 있다.
즉 소켓을 통해 한 프로세스에서 네트워크 장비로 데이터를 송신하고,
다른 프로세스에서 네트워크 장비를 통해 데이터를 수신할 수 있다.
(자기 자신에서 자기 자신으로 데이터를 송-수신하는 것)
Pipe, Message queue, Shared memory 는 IPC를 위해 나온 기술이다
반면 Signal이나 Socket은 IPC를 위해 나온 기술은 아니지만, IPC에도 활용될 수 있는 기술로 이해하면 된다.
위 내용은 ‘패스트캠퍼스’의 컴퓨터공학 강좌 내용을 요약 정리한 것임을 밝힙니다. (https://www.fastcampus.co.kr/)