Process는 프로그램을 실행하는 작업 단위입니다. 하나의 프로그램을 여러 번 실행하면, 여러 개의 process가 실행됩니다.
개념
프로세스 context(문맥) : CPU 수행 상태를 나타내는 하드웨어 문맥
프로세스 주소 공간 : code,data,stack
프로세스 관련 커널 자료 구조 : PCB(Process Control Block), Kernel stack
Process : program code, execution state, process state, process ID 등의 정보 + 자신의 virtual memory
Process는 반드시 다음의 다섯 가지 중 하나의 상태를 가지게 됩니다.
종합 상태도
PCB는 각 process마다 따로따로 존재하며, 해당 process에 대한 정보를 담고 있는 자료 구조입니다. PCB들은 Kernel 내부에 보관되며, 다음과 같은 정보들을 포함하고 있습니다.
이러한 PCB에 저장되어있는 process들의 상태를 Context라고 부릅니다. CPU에서는 process를 실행할 때 register set에 이 PCB들을 불러와서 사용하게 됩니다.
CPU에서 실행하는 process를 바꿀 때 (기존에 실행되던 process가 Running 상태에서 Ready 상태나 Terminated 상태로 옮겨가고 Ready 상태에 있던 새로운 process가 Running 상태로 들어갈 때) 원래의 process의 context를 저장하고 새로운 process의 context를 불러오게 됩니다.
이를 Context Switch라고 부릅니다.
System call이나 Interrupt가 발생할 때 context switch가 일어날 수 있습니다. 하지만 반드시 context switch가 일어나는건 아닙니다. 단순 system call만 호출해서 mode bit만 바뀌는 상황도 있고, timer interrupt나 I/O 요청같이 프로세스 자체가 바뀌는 요청에서 context switch가 일어납니다.
Context Switch가 자주 발생할수록 프로그램이 느려질 수 있습니다.
실제로 CPU가 add 같은 단순 연산을 실행하는 데 1 정도의 힘이 든다고 가정한다면, Context Switch에는 1만에서 10만 정도의 힘이 들게 됩니다.
그래서 최대한 Context Switch를 줄이는 것이 프로그램의 성능에 큰 역할을 하게 됩니다.
모든 process는 자신만의 정수 값으로 표현되는 number를 가집니다. 이를 Process Identifier, 줄여서 PID라고 부릅니다
process는 자기 자신을 복제하여 새로운 process를 만들 수 있습니다. 이 과정에 나타나는 프로세스를 부모 process, 자식 process라고 부릅니다.
이렇게 자기 복제를 통해 process들을 만들고 나면 아래와 같이 tree 모양의 자료 구조가 탄생하게 됩니다.
Process는 종료를 위해 exit() system call을 호출합니다. exit() system call이 호출되게 되면 process가 가지고 있던 (메모리와 같은) resource들을 풀어주게 되고 부모 process에게 자신의 상태를 전달합니다.
부모 process 자식 process를 종료시킬 수 있는 특수 상황
자식 process가 과도하게 많은 resource를 사용할 때.
자식 process의 작업이 더 이상 필요 없을 때.
부모 process가 작업을 종료했고, 해당 운영체재가 orphan process를 허용하지 않을 때.
Orphan Process
Zombie Process
Process들은 운영체제 내에서 병렬적으로 수행되기 때문에 서로 독립적이면서도 상호 협력적인 관계에 있습니다. 이러한 Process들 간의 협력을 통해 우리는 몇 가지 이득을 취할 수 있습니다.
정보 공유 (Information Sharing): 여러 응용 프로그램들이 동일한 정보에 흥미를 가질 수 있기 때문에 병행적으로 접근이 가능한 것이 좋다.
계산 가속화 (Computation Speedup): 작업을 빨리 끝마치기 위해서는 작업을 서브 작업들로 분할하여 여러 process들에서 병렬적으로 수행하는 것이 좋다.
모듈성 (Modularity): 시스템의 기능들을 프로세스들로 나누어 모듈화하여 시스템을 구성할 수 있다.
하지만 어쨋든 IPC에 대해서 한 문장으로 말하자면 IPC는 'Process들 간에 데이터와 정보를 교환할 수 있게 해 준다.'라고 할 수 있습니다.
IPC는 대표적으로 두 가지 방법을 통해 실행됩니다.
Shared Memory는 쉽게 말해서 메모리에 서로 공유하고 있는 영역이 있어서 그 부분에 자신이 원하는 데이터를 작성하는 방법입니다.
물론 그 공유하는 영역을 만드는 작업은 운영체제의 도움을 받아야 가능합니다. Process들은 단순히 메모리에 데이터를 읽고 쓸 수 있으며, 어떤 업데이트가 일어나더라도 곧바로 모든 process들이 그 내용을 확인할 수 있습니다.
그리고 가장 중요한 장점은 일단 공유 메모리 영역을 만들어 놓기만 하면, 그 뒤에는 Kernel의 어떤 도움도 필요로 하지 않는다는 것입니다.
Shared memory 방식을 사용하기 위해 우선 process들은 자신의 주소 공간에 공유 공간으로 사용할 부분을 준비합니다. 그리고 이 shared memory를 이용해 통신하고자 하는 다른 process들은 이 세그먼트를 자신의 주소 공간에 추가하여야 합니다. 일반적으로 운영체제는 한 process가 다른 process의 memory에 접근하는 것을 금지하기 때문에 Kernel의 도움을 받아서 이 제약 조건을 제거하는 과정이 필요합니다. 이런 과정을 거친 뒤에 process들은 공유 영역에 메세지를 읽고 쓸 수 있습니다.
Message Passing 방법은 데이터를 메세지 처럼 전달하는 방법입니다. Message passing은 메세지를 보내는 send와 메세지를 받는 receive 두 가지 연산으로 실행됩니다. 그리고 어떤 방식으로 메세지를 주고받는지에 따라 또 두 가지 방법으로 분류할 수 있습니다.
1. Direct Communication
Direct Commnuication은 말 그대로 point-to-point로 직접 전달하는 방식입니다. send 연산은 send(수신자 PID, message)로, receive 연산은 receive(송신자 PID, message)의 형태로 구성됩니다. 통신을 위해 상대의 ID만 알고 있다면 두 process 간에 자동으로 연결이 구축됩니다. 연산 자체에 수신자 혹은 송신자의 ID를 적어줘야 하기 때문에 혹시 ID가 수정되게 되면 아예 코드 전체를 수정해줘야 하는 단점이 있습니다. 또한 1대 1 통신만 가능하고 Broadcast 기능은 사용할 수 없다는 것도 한 단점입니다.
2. Indirect Communication
Indirect Communication은 직접 메세지를 전달하지 않고 mailbox를 이용하는 형태입니다. 다시 말해서, 수신자는 mailbox에 원하는 메세지를 담고, 송신자는 mailbox에서 원하는 메세지를 가져가는 형식입니다. 어떤 process라도 mailbox에 정보를 넣을 수 있고 빼 갈 수 있습니다. 연산은 send(mailbox 이름, message), receive(mailbox 이름, message)의 형태를 가집니다.
Indirect Communication 구조
Message Passing 방법에서는 Blocking과 Nonblocking 방식을 통해 메세지를 주고받습니다.
Blocking Send: 송신하는 process는 메세지가 수신자에 의해 수신될 때까지 아무것도 하지 못하고 blocking 된다.
Nonblocking Send: 송신하는 process가 메세지를 보내고 그 수신의 여부와 관계없이 다음 작업을 수행한다.
Blocking Receive: 수신하는 process는 메세지가 이용 가능할 때까지 아무것도 하지 못하고 blocking 된다.
Nonblocking Receive: 수신하는 process는 유효한 메세지인지 Null인지 계속 검색한다.
두 process 간의 교류가 꼭 쌍방향일 필요는 없을 때가 있고 이 때 pipe를 유용하게 사용할 수 있다.
상황에 따라서는 생산자와 소비자의 관계로 한 process는 보내기만 하고 한 process는 받기만 하는 경우가 생길 수도 있습니다.
pipe() system call을 통해 pipe를 생성하면
그리고 pipe는 오로지 자신을 생성한 process로부터만 접근이 가능합니다.
하지만 부모 process가 자식 process를 생성할 때 pipe에 관한 정보도 상속하기 때문에 부모 process가 생성한 pipe로 자식 process와 소통하는 것은 가능합니다. (pipe()를 먼저 호출한 뒤 fork()를 호출하면 부모와 자식 간에 소통이 가능합니다.)
특별한 케이스로 Named pipe라는 것이 존재합니다(FIFO).
pipe임에도 불구하고 양방향 통신이 가능합니다. 또한 꼭 부모-자식 관계일 필요도 없으며 일단 생성되면 같은 하드웨어 안에 있는 여러 process가 사용할 수도 있습니다.
게다가 생성한 process가 죽는다고 해서 이 pipe가 사라지지도 않습니다. mkfifo() system call을 이용해서 named pipe를 만들 수 있으며, open(), read(), write(), close()와 같은 system call을 이용해 제어할 수 있습니다.
두 process들이 네트워크를 이용해 통신을 하는 방법으로 소켓을 사용할 수 있습니다.
소켓들은 IP 주소와 Port Number를 통해 서로를 구별하며 일반적으로 서버-클라이언트의 구조를 가집니다.
한국말로는 '원격 프로시져 호출'이라고 불리는데 함수 호출을 네트워크를 이용해 할 수 있는 방법을 뜻합니다.
즉, RPC 서버에 함수가 실제로 구현이 되어있고 클라이언트의 코드에서 함수를 호출하면 서버에 있는 함수가 실행되는 형태를 가집니다.
서버와 클라이언트 간에는 Stub라는 것을 이용해 서로 네트워크를 연결합니다.