IPC
IPC (InterProcess Communication)
원칙적으로 프로세스는 다른 프로세스의 공간에 접근할 수 없다. 하지만 요즘은 성능을 높이기 위해 여러 프로세스를 만들어서 동시 실행하기 때문에 프로세스끼리 서로의 상태를 확인하고 데이터를 송수신할 필요가 있다.
fork() 시스템 콜
fork() 함수로 프로세스 자신을 복사해서 새로운 프로세스를 만들 수 있다. 이렇게 복사한 프로세스는 각각의 코어에서 병렬로 실행할 수 있다.
가볍게 생각해보기
- 1 ~ 10000까지 더하기
fork() 함수로 10개 프로세스를 만들어 각각 1~1000, 1001~2000 … 을 더하여 각각의 프로세스의 결과값을 합친다. 이 때 각 프로세스가 더한 값을 수집해야 하므로 프로세스간 통신이 필요하다.
- 웹 서버 만들기
웹 서버란 요청이 오면 HTML과 같은 정적 파일을 클라이언트에 제공한다. 새로운 사용자 요청이 올 때마다 fork() 함수로 새로운 프로세스를 만들고 각 사용자 요청에 즉시 대응할 수 있다. 이 때 프로세스의 제어 및 상태 정보 교환을 위하여 프로세스간 통신이 필요하다.
파일을 사용한 커뮤니케이션
간단히 다른 프로세스에 전달할 내용을 파일에 쓰고 다른 프로세스가 그 파일을 읽는다고 가정해보자. 이러한 방법은 실시간으로 데이터 전달이 어렵다는 문제점이 있다. 파일 입출력과 프로세스 실행을 동시에 할 수 없다. 또한, 파일을 사용한다는 것은 HDD 등의 보조 기억장치를 이용한다는 것인데 이러한 과정에서 오버헤드가 지나치게 크다.
실제 프로세스
프로세스간 공간은 완전히 분리되어 있어 사용자 모드에서는 커널 공간에 접근할 수 없다.
물리 메모리에서는 모든 프로세스가 커널 공간을 공유한다. 대부분의 IPC 기법은 이 커널 공간을 활용하여 이루어 진다.
다양한 IPC 기법
- file 사용
- Message Queue
- Shared Memory
- Pipe
- Signal
- Semaphore
- Socket
2번부터는 모두 커널 공간을 사용
다양한 IPC 기법
Pipe (파이프)
기본 파이프는 단방향 통신으로 fork()로 자식 프로세스를 만들었을 때 부모와 자식간의 통신을 담당한다.
예제
char* msg = "Hello Child Process!";
int main()
{
char buf[255];
int fd[2], pid, nbytes;
if (pipe(fd) < 0) // pipe(fd)로 파이프 생성
exit(1);
pid = fork(); // 이 함수 실행 다음 코드부터 부모/자식 프로세스로 나눠짐
if (pid > 0) { // 부모 프로세스는 pid에 실제 프로세스 ID가 들어감
write(fd[1], msg, MSGSIZE); //fd[1]에 씁니다.
exit(0);
}
else { // 자식 프로세스는 pid가 0이 들어감
nbytes = read(fd[0], buf, MSGSIZE); // fd[0]으로 읽음
printf("%d %s\n", nbytes, buf);
exit(0);
}
return 0;
}
메시지 큐 (message queue)
FIFO 정책으로 데이터를 전송한다. 메시지 큐는 파이프와 달리 부모/자식이 아니라 어느 프로세스 간에도 데이터 송수신이 가능하다.
예제
- A 프로세스
msqid = msgget(key, msgflg) // key는 1234, msgflg는 옵션
msgsnd(msqid, &sbuf, buf_length, IPC_NOWAIT)
- B 프로세스
msqid = msgget(key, msgflg) // key는 동일하게 1234로 해야 해당 큐의 msgid를 얻을 수 있음
msgrcv(msqid, &rbuf, MSGSZ, 1, 0)
공유 메모리 (shared memory)
kernel space에 메모리 공간을 만들어 해당 공간을 변수처럼 쓰는 방식이다. 공유메모리 key를 가지고 여러 프로세스가 접근 가능하다.
예제
- 공유 메모리 생성 및 공유 메모리 주소 얻기
shmid = shmget((key_t)1234, SIZE, IPC_CREAT|0666))
shmaddr = shmat(shmid, (void *)0, 0)
- 공유 메모리에 쓰기
strcpy((char *)shmaddr, "Linux Programming")
- 공유 메모리에서 읽기
printf("%s\n", (char *)shmaddr)
시그널 (signal)
- UNIX에서 30년 이상 사용된 전통적인 기법
- 커널 또는 프로세스에서 다른 프로세스에 어떤 이벤트가 발생되었는지 알려주는 기법
- 프로세스 관련 코드에 관련 시그널 핸들러를 등록해서 해당 시그널 처리를 실행
- 시그널 무시
- 시그널 블록 (블록을 푸는 순간 프로세스에 해당 시그널 전달)
- 등록된 시그널 핸들러로 특정 동작 수행
- 등록된 시그널 핸들러가 없다면 커널에서 기본 동작 수행
주요 시그널
기본 동작
SIGKILL: 프로세스 강제 종료, 슈퍼관리자가 사용하는 시그널
SIGALARM: 알람 발생
SIGSTP: 프로세스 중단
SIGCONT: 멈춰진 프로세스 실행
SIGINT: 프로세스 인터럽트
SIGSEGV: 프로세스가 다른 메모리 영역을 침범
예제
- 시그널 핸들러 등록 및 핸들러 구현
static void signal_handler (int signo) {
printf("Catch SIGINT!\n");
exit (EXIT_SUCCESS);
}
int main (void) {
if (signal (SIGINT, signal_handler) == SIG_ERR) {
printf("Can't catch SIGINT!\n");
exit (EXIT_FAILURE);
}
for (;;)
pause();
return 0;
}
- 시그널 핸들러 무시
int main (void) {
if (signal (SIGINT, SIG_IGN) == SIG_ERR) {
printf("Can't catch SIGINT!\n");
exit (EXIT_FAILURE);
}
for (;;)
pause();
return 0;
}
시그널과 프로세스
PCB에 해당 프로세스가 블록 또는 처리해야 하는 시그널 관련 정보 관리
소켓 (socket)
- 기본적으로는 두 개의 다른 컴퓨터 간의 네트워크 기반 통신을 위한 기술
- 하나의 컴퓨터 안에서 두 개의 프로세스간 통신 기법으로도 사용 가능