본 벨로그의 내용 및 이미지는 다음의 강의를 참고해서 작성이 되었으며, 틀린 내용이 있을 수도 있으니 너그럽게 봐주시면 감사하겠습니다:)
OS는 프로세스가 독립적인 공간에서 작동되도록 하는 것을 보장하며 외부로 부터의 접근을 차단한다. 하지만 한 프로세스가 다른 프로세스와 데이터를 주고 받으면서 통신해야할 때도 있는데 프로세스 간 통신의 종류는 크게 3가지로 구분지을 수 있다.
프로세스 간 통신 방식은 데이터를 주거나 (send) 받는 (receive) 행위로 크게 나누어질 수 있다. 하지만, 통신하려는 프로세스는 어떻게 찾을지, 데이터의 크기는 얼마로 할지, 데이터의 도착 여부는 어떻게 확인할지 (동기화)등 많은 문제를 정의해야 원활한 통신이 가능해진다.
동기화(synchronization) 수신하는 프로세스에 데이터가 도착했음을 알려주는 기능이다. 동기화 기능이 없으면 데이터를 받는 프로세스는 바쁜 대기를 사용하여 데이터가 도착했는지 여부를 직접 확인해야 한다.
전역 변수, 파일, 파이프, 소켓을 이용한 프로세스 간 통신은 다음과 같이 구분지을 수 있다.
int GV
int main()
{ int pid;
pid=fork();
...
전역 변수를 이용한 통신은 공동으로 관리하는 메모리를 사용하여 데이터를 주고받는다. 데이터를 보내는 쪽에서 전역변수나 파일에 값을 쓰고, 데이터를 받는 쪽에서는 전역 변수의 값을 읽는다. 위의 코드를 보면 main() 이전에 정의된 전역 변수 GV는 부모 프로세스와 자식 프로세스가 공유하는 메모리 영역으로, GV에 데이터를 쓰거나 읽는 방법으로 두 프로세스가 통신한다.
전역 변수를 2개 만들면 프로세스는 양방향으로 데이터를 보낼 수 있게 된다. (A 변수는 > 방향 & B 변수는 < 방향)
2차 메모리인 파일을 이용해 데이터를 주고받는 통신이다.
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{ int fd;
char buf[5];
fd = open("com.txt", O_RDWR);
write(fd, "Test", 5);
read(fd, buf, 5);
close(fd);
exit(0);
}
위의 코드는 open 함수를 이용하여 어떤 파일에 접근할 수 있는 권한인 file descriptor를 변수fd에 할당함으로써 2차 저장장치인 파일 시스템과 통신한다. 프로세스가 입출력 관리 프로세스에 쓰기를 요구하면 데이터가 저장되고, 읽기를 요구하면 입출력 관리 프로세스로부터 데이터를 가져온다. 쓰기 연산은 하드디스크에 데이터를 전송하는 명령, 읽기 연산은 데이터를 가져오는 명령이라고 볼 수 있다.
OS가 제공하는 동기화 통신 방식으로, 파일 입출력과 같이 open() 함수로 기술자를 얻고 작업을 한후 close() 함수로 마무리한다. 데이터를 보내는 프로세스가 쓰기 연산을 완료하면 받는 프로세스의 대기 상태가 자동으로 풀리기 때문에 동기화 문제로부터 자유롭다는 장점이 있다.
여러 컴퓨터에 있는 프로세스 간 통신을 네트워킹이라고 하는데, 이때 이용하는 것이 소켓이다. 소켓은 프로세스 동기화를 지원하기 때문에 데이터를 받는 프로세스가 바쁜 대기를 하지 않아도 되며, 양방향 통신을 위해 전역변수나 파이프처럼 2개의 객체가 필요하지 않다.
공유자원은 여러 프로세스가 공동으로 이용하는 변수, 메모리, 파일, 등을 말한다. 공유 자원은 공동으로 이용되기 때문에 누가 언제 데이터를 읽거나 쓰느냐에 따라 그 결과가 달라질 수 있다.
2개 이상의 프로세스가 공유 자원을 병행적으로 읽거나 쓰는 상황을 '경쟁 조건 (race condition)'이라고 한다. 경쟁 조건이 발생하면, 공유 자원 접근 순서에 따라 실행 결과가 달라 질 수 있다.
공유 자원 접근 순서에 따라 실행 결과가 달라지는 프로그램의 영역을 임계구역이라고 한다.
위와 같이 서로 독립적인 producer()와 consumer()가 sum이라는 전역변수에 동시에 접근하는 코드가 있다고 할 때, sum을 읽어오고 덮어쓰는 부분이 임계구역이다.
sum=3 인 상황에서 (1) sum = sum + 1 과 (2) sum = sum - 1 이 미세한 시간 차이로 인해 온전히 실행되지 않아 원자성을 보장 받지 않을 경우 sum = 2 or 4인 결과가 도출될 수 있다. 따라서 이러한 문제를 해결하기 위해서는 프로세스들이 임계구역에서 동시에 작업을 하지 못하도록 해야 한다.
임계구역 문제를 해결하기 위해서는 다음의 3가지 조건을 만족해야 한다.
임계구역을 해결하는 방법은 기본적으로 임계구역을 최소화하는 것이다. 하지만 강의에 따르면 임계구역을 최소화하는 것은 상황 by 상황에 따라 다른 접근법을 사용해야 된다고 한다.
하지만 나는 OS 잘 모르기 때문에 .. ㅎㅎ
임계구역 문제를 해결하는 단순한 방법인 lock이 있는데 이 방법과 이 방법이 갖는 한계를 하나씩 써내려가면서 나의 이해도를 재고해보도록 하겠다.
위의 코드 동작 원리는 간단하다. 프로세스 P1는 lock==true이면 임계구역에 진입하지 못한다 (다른 프로세스가 임계구역을 점유하고 있으면). 만약 lock==false이면 프로세스 P1은 임계구역에 대한 lock을 걸고 작업을 처리한다. 해당 방식은 상호 배제의 원칙을 완전히 만족시켜주는 것처럼 보이지만, 실제로는 그렇지 않다. 만약 lock==false인 상황에서 프로세스 P1인 상태에서 while(lock==true);까지만 실행되고 context switching이 일어나 프로세스 P2가 실행 될 경우 P1, P2가 모두 임계구역에 들어갈 수 있기 때문이다. 또한 while(lock==true);는 작업을 할 필요가 없을 때도 계속 무한 루프를 돌면서 시스템 자원을 낭비한다는 단점이 존재한다.
상호 배제의 원칙을 만족시키기 위해 2개의 공유변수를 도입하는 방식이 있다. 위의 그림처럼 2개의 공유 변수를 도입하면 같은 맥락에서의 상호 배제의 원칙을 만족시킬 수 있다. 하지만, lock1==true;나 lock2==true;가 실행된 이후 context switching이 일어날 경우 P1과 P2 모두 양쪽 lock을 걸어 놓았기 때문에 임계구역으로 진입할 수 없는 무한정 대기 상태인 deadlock 상태에 빠지게 된다는 한계가 존재한다.
상호 배제 및 한정 대기의 원칙을 모두 만족시키는 lock 방법은 공유변수를 정수형 변수로 선언하는 것이다. 하지만 이 방법의 문제는 한 프로세스가 연달아 임계구역에 진입할 수 없다는 한계가 존재한다 (P1 > P2). 즉 한 프로세스의 진행이 다른 프로세스로 인해 방해 받는 현상이 발생하며, 이를 '경직된 동기화'리고 한다.
하드웨어적인 해결방법은 그림에서 보이는 것처럼 while(lock=true);문과 lock=true;문을 하드워에적으로 동시에 실행해 그 사이에 context switching이 발생하는 것을 방지하는 것이다. 하지만 이 역시 시스템 자원을 낭비한다는 문제를 근본적으로 해결하지는 못한다 (while문 사용).
세마포어는 임계구역에 진입하기 전에 스위치를 사용 중으로 놓고 임계구역으로 들어간다. 이후에 도착하는 프로세스는 앞의 프로세스가 작업을 마칠때까지 기다리다가 이후에 작업을 마치면 다음 프로세스에 임계구역을 사용하라는 동기화 신호를 보낸다. 이때 대기하는 프로세스는 '큐' 에서 자기 순서를 기다리고 있기 때문에 직접 임계구역이 잠겼는지 점검하면서 바쁜대기를 할 필요가 없다.
위의 코드에 나오는 용어들을 정리해보면
세마포어가 안정적으로 작동되도록 하기 위해서는 P()와 V()의 원자성을 보장해 상호 배제 조건을 만족할 수 있도록 해주어야 한다.
세마포어 알고리즘이 안정적으로 작동하기 위해서는 매 임계구역마다 P()와 V()를 정확하게 지정해주어야 한다. 따라서 공유 자원을 사용할 때 모든 프로세스가 세마포어 알고리즘을 따른다면 굳이 P()와 V()를 사용할 필요 없이 자동으로 처리한 게 바로 '모니터'이다. 모니터는 공유 자원을 내부적으로 숨기고 공유 자원에 접근하기 위한 인터페이스만 제공함으로써 자원을 보호하고 프로세스 간에 동기화를 시킨다.
책에서는 모니터를 시스템 호출과 같은 개념으로 설명하는데, 사용자 입장에서는 임계구역에서 작업할 수 있는 인터페이스만 제공받는 것이다. 따라서 P()나 V()를 사용하지 않고도 프로세스는 모니터 큐에 들어가서 자기 순서를 기다리다가 자기 순서가 되면 모니터에서 처리가 된 후 동기화 신호를 통해 끝났음을 모니터 큐에 알려준다.
monitor shared_balance {
private:
int balance=10;
boolean busy=false;
condition mon;
public:
increase(int amount){
if(busy==true) mon.wait(); /* waiting in queue */
busy=true;
balance=balance+amount;
mon.signal(); /* wake up next waiting process */
}
2개 이상의 프로세스가 다른 프로세스의 작업이 끝나기만 기다리며 작업을 더 이상 진행하지 못하는 상태를 교착 상태라고 한다. 컴퓨터 시스템에서 교착 상태는 시스템 자원 (다른 프로세스와 공유할 수 없는 자원(P1는 프린터를 할당받고 스피커를 기다림. P2는 스피터를 할당받고 프린터를 기다림)을 사용할 때), 공유 변수 (위의 예시에서 서로 다른 프로세스 P1 & P2가 lock1 & lock2를 걸고 무한 루프에 걸릴때), 응용 프로그램 (데이터베이스에서 일관성, 원자성을 위해서 lock을 사용할 때)등을 사용할 때 발생할 수 있다.
교착 상태를 보다 잘 설명해주는 용어는 자원 할당 그래프이다. 위 그림에서 원은 프로세스, 사각형은 자원이다. 실선은 프로세스가 자원을 할당받은 상태를 의미하며, 점선은 프로세스가 자원을 기다리는 것을 의미한다. 위처럼 각 프로세스가 할당받은 자원이 다른 프로세스 입장에서 할당 받기 위해 기다리는 자원이라면 교착 상태가 발생한다.
교착 상태가 발생하기 위해서는 다음의 4가지 조건을 모두 만족시켜야 한다.