-
운영체제 6: 프로세스 간 커뮤니케이션 - 가상 메모리와 IPC에 대해CS기초/OS,HW 2021. 12. 30. 02:12728x90반응형
프로세스는 프로그램이 실행된 상태인데, 앞서 설명했던 대로 한 프로그램은 여러 프로세스로 나누어질 수 있다. 그리고 기본적으로 각 프로세스는 독립적인 코드와 데이터 공간을 사용하며 다른 프로세스가 데이터 및 코드를 변경할 수 있는 가능성을 제한하기 위해 서로 직접적으로 통신(코드 및 데이터 영역에 접근)할 수 없다. 하지만 여러 CPU 코어를 사용하여 한 가지 작업을 하기 위해 프로세스를 나눌 때 등의 경우 프로세스 간 커뮤니케이션이 필요하다. 이를 도와주는 것이 IPC(InterProcess Communication)기술이다.
(하나의 작업을 수행하기 위해 여러 프로세스를 사용하는 경우의 대표적인 예로 리눅스의 fork() 시스템 콜을 들 수 있다. fork() 함수는 프로세스를 복사해서 새로운 프로세스를 만드는 기능을 하며, 이 때 원본 프로세스를 부모 프로세스, 복제된 프로세스를 자식 프로세스라고 한다. 자세한 내용은 이 후에 다룰 예정이다. 간단히 소개하면 1부터 10000까지 더하는 작업을 수행할 때 각 작업을 100단위로 나눠서 100개의 프로세스를 병렬적으로 수행하여 작업 처리 속도를 높이는 경우가 있다.)
가상 메모리
IPC를 이해하기 위해서는 먼저 가상 메모리에 대한 이해가 필요하다. 프로세스 간의 공간은 완전히 분리되어 있다는 것을 이해하기 위해 리눅스의 프로세스 공간을 살펴보자.
하나의 프로세스는 항상 4GB의 공간을 가진다. 이 때 얘기하는 저장 공간은 물리적인 저장 공간인 메모리가 아닌 가상 공간이다. 즉, 내 컴퓨터의 메모리가 16GB라고 해서 네 개의 프로세스만 돌릴 수 있는 것은 아니라는 의미이다. 가상 공간에는 데이터의 가상 주소가 저장되고, 내부적으로 이를 메모리에 있는 물리 주소로 변환하는 과정을 거침으로써 데이터를 찾는다.
위의 가상 공간 그림에서 4GB부터 3GB까지의 공간, 즉 1GB는 운영체제의 코드가 저장되는 공간으로, 커널 공간이다 = 사용자 모드에서 접근할 수 없다. 나머지 0GB부터 3GB까지 3GB의 공간이 사용자가 작성하는 코드와 관련된 공간이다.
프로그램이 실행되는데 운영체제의 코드가 계속 새로 저장될 필요는 없으므로 커널 공간에 저장되는 운영체제 코드는 실제 물리 메모리에는 모든 프로세스에서 동일한 공간을 참조하여 공유할 수 있도록 한다.
IPC를 구현한 대표적인 기법
저장 매체 이용 (File 사용)
저장 매체는 모든 프로세스가 접근 가능하기 때문에 각 프로세스의 결과값을 파일에 쓰고 파일에 쓰여진 각 결과값을 취합하는 방법. 이 방법은 각 프로세스의 작업이 완료되었는지 알기 위해서는 파일을 읽어야만 하는데 파일은 실시간으로 계속 읽을 수 없기 때문에 직접적으로 원하는 프로세스에 데이터를 전달하는 것이 어렵고 속도가 느리다는 한계가 있다.
다음에 소개되는 모든 기술들은 커널 공간을 공유함으로써 가상 메모리의 커널 공간을 활용다는 특징이 있다.
Message Queue
메세지 큐는 큐 자료구조를 사용하기 때문에 FIFO 정책을 따른다. A 프로세스에서 데이터를 넣은 순서대로 B 프로세스에서 데이터를 사용한다. 큐에 저장되는 데이터 역시 커널 공간에 저장된다.
- A 프로세스 코드 (생략된 위의 코드에서 msqid 생성 및 key 설정)
msqid = msgget(key, msgflg) // 메세지 키를 통해 메세지 아이디를 부여받음 msgsnd(msgid, &sbuf, buf_length, IPC_NOWAIT) // 메세지 큐에 메세지 아이디와 데이터를 보냄
- B 프로세스 코드
msqid = msgget(key, msgflg) // key를 동일하게 설정해야 A에서 사용하는 큐의 msgid를 얻을 수 있다. msgrcv(msgid, &rbuf, MSGSZ, 1, 0) // 메세지 큐에서 데이터를 받아옴 (A에서 보낸 &sbuf가 B에서 받아온 &rbuf)
Pipe
파이프는 단방향 통신을 지원하며, fork()로 자식 프로세스를 만들었을 때, 부모 프로세스와 자식 프로세스 간의 통신을 지원한다.
fork()함수는 위에서부터 코드를 실행하다가 fork()함수를 만나게 되면 기존 프로세스의 코드 공간, stack, heap등을 모두 복사하여 자식 프로세스를 생성한다. 자식 프로세스의 PC는 fork()함수의 바로 다음 코드를 가리킨다. fork()함수는 주로 if문과 함께 실행되어 부모 프로세스의 pid는 실제 프로세스의 id가 되고 if문을 실행하며, 자식 프로세스의 pid는 0으로 다르게 부여되어 else문을 실행한다.
pipe는 pipe(fd)로 생성이 되며, pipe가 정상적으로 실행되지 않았을 경우 pipe(fd)는 0을 리턴하고, 정상적으로 실행되었을 경우 fd는 정수 배열을 갖는다 (fd[1]이 부모 프로세스).
char* msg = "Hello world" int main() { char buf[255] //여기까지는 관련 없는 코드 int fd[2], pid, nbytes; // fd를 3개의 정수배열, pid와 nbytes를 정수로 정의해줌 if (pipe(fd) < 0) // pipe(fd)로 파이프가 제대로 생성되지 않으면 exit(1); // 코드 종료 pid = fork(); // fork()함수를 통해 부모-자식 프로세스 생성 if (pid > 0) { // 부모 프로세스의 pid는 실제 프로세스의 ID (0 초과) write(fd[1], msg, MSGSIZE); // fd[1](부모프로세스)에 msg를 MSGSIZE 사이즈로 써라 exit(0) // 코드 종료 } else { // 자식 프로세스의 pid는 0 nbytes = read(fd[0], buf, MSGSIZE); // fd[0]으로 읽어라 printf("%d %s\n", nbytes, buf); // 출력 명령어 exit(0) } return 0; // main 함수의 리턴값 }
위의 코드는 아래의 그림처럼 실행된다. 단방향 통신이라는 것은 부모 프로세스에서 fd[0]으로 읽을(read) 수 없고, 자식 프로세스에서 fd[1]로 쓸 수 없다는 뜻이다. 따라서 부모 프로세스에서 자식 프로세스로 갈 수는 있어도, 자식 프로세스에서 부모 프로세스로 갈 수는 없다.
여기서 파이프를 통해 부모 프로세스에서 자식 프로세스로 전달되는 데이터는 가상 공간의 커널 영역 어딘가에 저장된다.
Message Queue와 Pipe의 차이
- Message Queue는 부모-자식 프로세스 관계가 아니라 어느 프로세스 간에라도 데이터 송수신이 가능하다. 즉, Message Queue를 여러 개 만들면 양방향 통신이 가능하다 (하나의 Queue를 사용해도 되지만 데이터가 덮어써지는 오류가 발생할 수 있기 때문에 보통 통신 방향 별 Queue를 별도로 만드는 경우가 더 일반적이다).
- Message Queue는 먼저 넣은 데이터가 먼저 읽혀진다.
아래 두 가지는 IPC 기법을 위해 만들어진 기술들은 아니지만 IPC 기법을 위해서도 많이 사용되는 기술들이다.
Shared Memory
공유 메모리는 별도로 커널 공간에 메모리를 만들어 해당 공간을 변수처럼 사용하는 방법이다.
shmid = shmget((key_t)1234, SIZE, IPC_CREAT|0666)) // 공유 메모리를 SIZE만큼 생성 shmaddr = shmat(shmid, (void *)0, 0) // 프로세스마다 해당 코드를 작성하여 공유메모리 주소값을 얻음 strcpy((char *)shmaddr, "Hello world") // 공유 메모리에 쓰기 printf("%s\n", (char *)shmaddr) // 공유 메모리에서 데이터 읽어오기
Signal
시그널은 Unix 운영체제에서 커널 또는 프로세스에서 다른 프로세스에 어떤 이벤트가 발생되었는지 알려주는 기법이다. 미리 정의가 되어있는 이벤트로 대표적으로 다음과 같은 동작들이 있다 (SIGUSR를 통해서 특정한 동작(이벤트)를 정의할 수도 있다).
- SIGKILL: 프로세스를 죽여라
- SIGALARM: 알람 발생
- SIGSTP: 프로세스 멈춤 (=Ctrl+z)
- SIGCONT: 멈춘 프로세스를 실행
- SIGINT: 프로세스에 인터럽트를 보내서 프로세스를 죽여라 (=Ctrl + c (복사 아님))
- SIGSEGV: 프로세스가 다른 메모리 영역을 침범함
프로세스 A와 프로세스 B가 서로 시그널을 보냄으로써 통신을 할 수 있도록 IPC 를 가능하게 한다. 주로 다음과 같은 기능을 수행하기 위해 사용된다.
- 시그널 무시
- 시그널 멈춤 (멈춤을 푸는 순간 프로세스에 원래 전달되기로 했던 시그널 전달)
- 등록된 시그널 핸들러로 특정 동작 수행
- 등록된 시그널 핸들러가 없다면 커널에서 기본 동작 수행
프로세스에 전달된 시그널들은 PCB에 저장된다. 컨텍스트 스위칭을 하면서 사용자 모드와 커널 모드를 왔다갔다하는데, 커널모드에서 처리가 끝나고 다시 사용자모드로 넘어가는 시점에서 PCB를 확인하여 처리가 필요한 시그널이 있다면 해당 시그널을 처리하기 위해 커널 함수를 호출하는 형식으로 진행된다.
static void signal_handler (int signo) { printf("Catch SIGINT\n"); exit (EXIT_SUCCESS); } int main (void) { if (signal (SIGINT, signal_handler) == SIG_ERR) { // SIGINT를 받았을 때 원래 SIGINT로 정의되어있는 동작 말고 signal_handler함수를 동작시키고, 그 결과가 SIG_ERR일 경우 아래 코드 실행 printf("Can't catch SIGINT\n"); exit (EXIT_FAILURE); } for (;;) pause(); return 0; }
Socket
소켓은 클라이언트-서버 등 두 개의 다른 컴퓨터 간의 통신을 위한, 즉, 네트워크 통신을 위한 기술이다. 이 기술을 두 개의 컴퓨터 간이 아니라 프로세스 간의 통신에 사용함으로써 IPC를 구현한다. 자세한 과정은 다음 그림에서 볼 수 있다.
728x90반응형'CS기초 > OS,HW' 카테고리의 다른 글
도커를 공부하다보면 마주치게 되는 가상화란, 가상화의 종류 (0) 2022.12.18 운영체제 7: 코드를 통한 프로세스 작동 방식의 이해 (0) 2022.05.30 운영체제 5: 컨텍스트 스위칭 (Context Switching) (0) 2021.12.28 운영체제 4: 프로세스 (Process)의 구조 (0) 2021.12.28 운영체제 3: 인터럽트 (Interrupt) (0) 2021.12.28