IPC(Inter-Process Communication) | 프로세스 간 통신
프로세스간의 통신은 운영체제에서 반드시 필요한 기능이지만, 실제 프로세스들은 가상메모리를 이용해 완전히 독립되므로 일반적으로 불가능하다. 그래서 통신을 위한 기능들이 추가된다.
1. 프로세스 통신 분류
- 단방향 통신 ( Simplex ) : A -> B 만 가능한 통신
- 반양방향 통신 ( 반이중, Half-duplex ) : A -> B, B -> A 모두 가능하지만, 한 번에 한 통신만 가능함.
- 양방향 통신 ( 전이중, Full-duplex ) : A -> B, B -> A 가 동시에 가능함.
- 대기가 있는 통신 : 동기화를 지원하는 통신 방식 ( like Interrupt )
- 대기가 없는 통신 : 동기화를 지원하지 않는 통신 방식 ( like Polling )
2. 프로세스 데이터 통신 방법 분류
- 내부 데이터 통신 ( Thread ) : 하나의 프로세스 내의 2개 이상의 스레드가 전역변수나 파일을 이용해 데이터 통신.
- 프로세스간 통신 : 같은 컴퓨터의 여러 프로세스끼리 공용 파일 or OS가 제공하는 PIPE를 사용해 데이터 통신.
- 네트워크간 통신 : 여러 컴퓨터가 네트워크로 연결되었을 때, 소켓을 이용해 데이터 통신
3. 프로세스 통신 방법
다양한 방법이 있으나, 우열이 존재하지 않고 상황마다 적절한 방법을 이용하는 것이 좋다.
- PIPE
- 이름 없는 파이프 (PIPE), 명명 파이프(FIFO) 의 두 개로 분류된다.
- PIPE는 서로 다른 프로세스간에서는 데이터 통신이 불가능하며, 파일 시스템에 이미지를 생성하지 않는다.
- FIFO는 서로 다른 프로세스간에서 데이터 통신이 가능하며, 파일 시스템에 이미지를 갖는다.
- 파이프에서 사용하는 temp.txt같은 파일은 일반적으로 cat과 같은 명령어로 읽을 수 없다.
- PIPE는 부모 자식간 단방향 통신에, FIFO는 다른 프로세스와의 단방향 통신에 사용된다.
- Memory Queue
- 파이프나 커널 메모리 공간을 활용해 Queue를 만들어 데이터를 처리한다.
- EnQueue를 수행하는 생성자와 Dequeue하는 소비자가 따로 존재한다.
- 구조체를 기반으로 동작한다.
- 다른 프로세스와의 단방향 통신에 사용된다.
- Shared Memory
- 커널이 관리하는 일정한 크기의 공유 메모리 공간을 통해 통신한다.
- 사용하려는 프로세스간 할당된 크기가 동일해요 사용할 수 있는 방법이다.
- 다른 프로세스와의 양방향 통신에 사용된다.
- Memory Map
- 파일을 프로세스 메모리에 밀정 부분 맵핑해 사용한다. -> read/write syscall에서의 불필요한 복사가 방지된다.
- 파일로 대용량 데이터를 공유할 때 유리하다.
- 단, 메모리 내용이 파일에 Swap될 때, Page 단위로 작동하므로, 실제 맵핑 공간의 크기에 따라 불필요한 작업이 발생할 수 있음.
- 다른 프로세스와의 양방향 통신에 사용된다.
- Socket
- 네트워크간에서 서로 다른 컴퓨터가 자신의 소켓과 상대의 소켓을 연결해 통신한다. (내부 시스템 통신도 가능함)
- socket() : 소켓을 생성하는 함수
- bind() : socket()로 생성한 소켓을 server socket 으로 등록한다.
- listen() : server socket을 통해 클라이언트가 서버에 접속 요청을 수행했는지 확인하도록 한다.
- accept() : 클라이언트가 접속 요청 및 대기를 한것을 수락한다. (Client Socket을 생성한다)
- read()/write() : Client Socket에 데이터를 송수신한다.
- close() : Client Socket을 소멸시킨다.
- 다른 시스템 간 양방향 통신에 주로 사용된다.
예제코드 1. 파일을 이용한 통신
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd;
char buf[124]={0,};
fd = open("temp.txt", O_RDWR); // temp.txt 파일을 읽기/쓰기 모두 가능하게 연다.
write(fd, "Test", 5); // temp.txt파일에 Test를 작성한다.
lseek(fd,0,SEEK_SET); // 파일을 읽기 시작할 위치를 설정하는 함수. 0번지로 돌려보냄.
read(fd, buf, 5); // temp.txt파일을 읽어서 buf에 저장한다.
printf(buf);
close(fd);
return 0;
}
write로 파일을 작성하면, Test\0 까지 작성 후, buffer가 5로 이동해있는데, 이상태로 read시 5부터 5byte를 읽는다.
이를 방지하기 위해, lseek함수를 이용해 0부터 읽게 한것.
예제코드 2. 무명 파이프를 이용한 통신
int main() {
int pipefd[2]; // [0] for read, [1] for write
pid_t pid;
char buf[128];
if(pipe(pipefd) < 0){ // int pipe(int pipefd[2]);
printf("pipe error\n");
return 1;
}
pid = fork();
if(pid > 0){ //parent process
close(pipefd[0]);
strcpy(buf, "Hello?\n");
write(pipefd[1], buf, strlen(buf));
}else if(pid == 0) { //child process
close(pipefd[1]);
read(pipefd[0], buf, 128);
printf(">> I am child! I can hear the parent!!: %s\n",buf);
} else {
printf("fork error\n");
}
return 0;
}
pipefd에 read용과 write용 파이프를 둔 뒤 이용함
예제코드 3. 명명 파이프를 이용한 통신
// Receiver
int main(void)
{
int counter = 0;
int fd;
char buff[128];
if ( mkfifo("temp.txt", 0666) < 0){ // temp.txt를 Named PIPE용 파일로 생성함. exists시 에러
perror( "mkfifo() error");
return 1;
}
// Readonly로 사용하면, Write를 할 프로세스가 하나도 없을 때,
// 즉시 Return 0를 계속 반환하게됨. -> 쓰레기 값이 계속 입력된다.
fd = open("temp.txt", O_RDWR);
while(1){
memset( buff, 0, 128); // buff의 값을 0으로 초기화.
read(fd, buff, 128); // temp.txt를 읽어서 buff에 입력
printf( "%d: %s\n", counter++, buff);
}
close(fd);
}
// Sender
int main(void)
{
int fd;
char *str = "I am the sender!";
fd = open("temp.txt", O_WRONLY);
write(fd, str, strlen(str));
close(fd);
}
4. 공유 자원
- 서로 다른 프로세스/스레드가 공동으로 이용하는 변수, 메모리이다.
- 공용으로 이용하는 데이터이기 때문에, 읽고 쓰는 순서에 따라 결과가 달라질 수 있다.
- 경쟁 상태 ( Race Condition ) : 멀티 스레드 환경에서 자주 발생하며, 데이터가 오염(Corrupted)될 수 있다.
5. 동기화 ( Synchronization )
- 공유 자원에 대해 접근하는 다수가 있을 때, 데이터 오염을 막기 위한 방법
- 한 프로세스/스레드가 공유 자원을 배타적 독점하여 상호 배제를 수행한다.
- 임계 구역 ( Critical Section ) : 공유 자원 접근 순서에 따라 결과가 달라질 수 있는 부분
- 임계 자원 ( Critical Data ) : 결과가 달라질 수 있는 공유 자원
- 상호 배제 ( Mutual Exclusion )
- 임계 구역이 오직 한 프로세스만 배타 독점적으로 사용할 수 있게 하는 기술
- 입구에 열쇠를 만들어 Lock이 걸려 있으면 진입이 불가능하게 한다.
- enterCS() : 접근 가능한지 검사, 불가능할 시 대기한다.
- exitCS() : 임계 구역에서 작업을 수행하던 프로세스가 작업이 종료되었음을 알리는 후처리
- 일반 코드 -> Entrty -> Critical -> Exit 순서로 진행된다.
- Lock을 만드는 방법
- 하드웨어적 방법 : Interrupt Service 금지, Atomic Instruction 사용
- 소프트웨어적 방법 : Dekker, Peterson, Lamport 알고리즘 등..
- Lock의 조건
- 상호 배제가 수행되어야 한다.
- 한정된 시간만 대기해야 한다. ( 무한정 대기하는 프로세스가 생기면 안됨 )
- 진행에 융통성이 있어야 한다. ( 다른 프로세스의 진행을 방해해서는 안됨 )
6. Lock을 만드는 방법
- 인터럽트 서비스 금지
- 임계구역 Entry Code에서 인터럽트 서비스를 금지해, Context switching을 차단한다.
- 다른 프로세스가 진행되지 않으므로, Exit Code가 수행될 때 까지 해당 프로세스만 공유 자원에 접근 가능할 것임
- 그러나, 모든 인터럽트가 무시되서 시스템의 효율적인 운영을 방해한다.
- 멀티코어, 다중 CPU에서는 특히, 모든 CPU의 인터럽트가 멈추므로, 진행의 융통성 조건을 어긴다.
- 전역변수로 lock을 선언
- lock 전역변수도 공유데이터가 되므로 문제가 해결될 수 없다.
- lock 전역변수에 사용 가능한 스레드의 정보를 담는 방식이 제안되었으나, 순서를 할당받은 스레드가 공유 자원을 사용하지 않으면, 순서가 돌지 않으므로 한정 대기, 진행의 융통성을 불충족한다.
- 지역변수로 상대 스레드의 lock 변수를 설정
- 공유 데이터는 아니지만, Context switching이 진행되면서 무한루프에 빠질 수 있다.
- 원자 명령 ( Atomic Instruction )
- 위의 lock이 올바르게 동작하지 않는 이유는 Load와 Set이 두 개의 Instruction으로 구성되어 있어서, 그 사이에 Context switching이 진행되면 문제가 발생한다.
- 즉, Load와 Set을 동시에 수행하는 Instruction을 구현하면 해결 가능하다.
- 이를 구현한 것이다.
7. 동기화 방식
- Locks 방식
- 상호 배제를 수행하는 Lock을 사용함.
- 동기화 대상이 1개일 때 사용하며, Lock를 소유한 프로세스만 임계 구역에 진입할 수 있다.
- Spinlock는 Busy-wait를 수행하며 Lock이 오는 것을 기다린다.
- -> 단일코어에서는 비효율적이나, 임계 구역의 실행시간이 짧은 경우 멀티코어에서 효과적이다. 그러나, Busy-wait를 통해 대기하므로, 운이 안 좋으면 기아 현상이 발생할 수 있다.
- Mutex는 Sleep-wait를 수행하며, Queue에 대기 프로세스를 넣어둔다.
- Wait-Signal 방식
- Semaphore는 N개의 공유 자원에 대해 다수의 프로세스가 공유하여 사용하도록 돕는 자원 관리 기법이다.
- 동시에 여러 개의 프로세스가 임계 구역에 접근할 수 있도록 입구에 cnt변수를 가지며, cnt==0일때 대기하게 된다.
- P연산 (wait, =enterCS) : 자원 요청 시 실행
- V연산 (signal, =exitCS) : 자원 반환 시 실행
- n개의 자원과 대기 Queue, Count변수 로 구성된다.
- Binary Semaphore는 공유 자원이 1개이며, 접근 가능한 스레드도 1개인 세마포이다. ( Mutex와 유사함 )
- 단, 프로세스가 세마포를 사용하지 않고 임계구역에 진입하거나, P연산과 V연산이 반대로 사용될 시 공유 자원을 보호할 수 없다.
- 또한, P를 두번 사용하면 wake_up가 발생하지 않고, 동기화가 되지 않아 큐가 무한대기하게 된다.
- 이런 공유 자원 문제를 해결하기 위해 Monitor를 도입한다.
- Mutex와 Binary Semaphore의 차이
- Mutex는 자원을 소유하며 이에 대한 책임을 갖는다. 또한, 자원을 소유한 스레드만 Mutex를 해제 가능하다.
- Semaphore는 자원을 소유할 수 없으며, 자원을 소유하지 않은 스레드가 Semaphore를 해제할 수 있다.
8. 동기화 문제의 해결법
- Monitor
- 공유 자원을 내부적으로 숨기고, 자원에 접근하기 위한 인터페이스(Method)만 제공해준다. (추상화)
- Monitor는 요청받은 작업을 Monitor Queue에 저장 후 순서대로 처리하며, 처리 결과만 전달한다.
- 하나의 클래스 개념으로 볼 수 있음. ( c.f 스마트 포인터 )
- 우선순위 역전
- 스레드 동기화로 인해, 높은 순위 스레드가 늦게 스케줄링 될 수 있음. 이는 시스템의 근본이 붕괴되는 일이며, 높은 순위의 스레드가 늦게 실행되고, 낮은 스레드가 길어질수록 더 문제가 된다.
- ex) Pn에서 n이 높을 수록 우선순위가 높은 스레드일 때, P1이 작업을 수행하는 중에 P3이 공유자원에 접근해 대기를 하게 된다고 해보자. 이 때, 공유자원에 접근하지않는 P2가 실행되면 우선순위에 따라 P1에서 P2로 context switching된다. 그리고 P2가 종료되고 P1로 돌아오고, P1의 공유자원 접근이 종료되면 P3이 진행된다.
- 이를 해결하기 위해, 공유 자원을 소유한 스레드의 우선 순위를 일시적으로 올려서 바른 순서대로 실행되게 한다.
9. 생산자-소비자 문제
- 생산자는 꽉찬 Queue에 더 집어넣으면 안되고, 소비자는 빈 Queue에서 꺼내려고 하면 안된다는 것.
- 버퍼 큐에 읽기 가능한 버퍼 갯수를 cnt로 두는 Read Semaphore와, 버퍼 큐에 쓰기 가능한 버퍼 갯수를 cnt로 두는 Write Semaphore를 둔다. 그러면 해당 작업이 진행 불가능할 시, Semaphore가 wait을 돌려주므로 해결됨.
10. 교착상태 (Deadlock)
- 서로 자원을 요구하기만 하고 처리하지 못하는 상태.
- 환형 요청/대기 (Circular Wait)에 의해 발생함. 이는 프로그램이 스스로 인식하거나 해체가 불가능하므로, 원형상태의 요청이 발생하지 않도록 디자인해야한다.
- 교착상태를 직접적으로 막는것은 높은 비용이 들어가므로, 교착상태의 방지가 매우 중요한 경우가 아니라면 적극적인 방지를 준비하지 않는다. ( 발생후 System reboot 등으로 대응한다. )
- 자원 할당 그래프로 모델링하며, Vertex는 프로세스,스레드,자원 등을 의미, Edge는 소유/요청/할당 등을 의미한다.
11. 교착상태의 원인
- 멀티스레드가 자원을 동시에 사용하려고 하는 경우
- 한 스레드가 여러 자원을 동시에 필요로 하는 경우
- 한번에 하나씩 자원을 할당하는 OS정책에 의해 발생하는 경우
-> 필요한 자원을 프로세스 실행때 한번에 모두 요청할경우 발생하지 않는다. - 할당된 자원을 OS가 강제적으로 뺏어올 수 없는 경우.
-> 뺏어올 수 있다면 발생하지 않는다.
12. 기아상태와 교착상태의 차이
- 기아(Starvation) : OS의 정책의 잘못으로 인해 특정 프로세스의 작업이 지연되는 현상
- 교착(Deadlock) : 여러 프로세스가 작업을 진행하다가 자연적으로 발생하는 현상
'3-1공부 > 운영체제' 카테고리의 다른 글
기말3. 캐시, 가상메모리 (0) | 2022.05.31 |
---|---|
기말2. 메모리관리 (0) | 2022.05.21 |
7. 스케줄링 알고리즘 (0) | 2022.04.19 |
6. 스케줄링 (0) | 2022.04.19 |
5. 스레드 (0) | 2022.04.19 |