1. 프로세스의 생성
프로세스의 생성은 부모 프로세스가 본인과 같은 자식 프로세스를 복제하여 생성됩니다. 여기서 복제라는 것은 프로세스의 문맥을 모두 복사하는 것입니다. 즉, 부모 프로세스의 주소 공간인 code, data, stack을 그대로 복사하여 자식 프로세스를 하나 만들고, 부모 프로세스의 CPU 문맥인 프로그램 카운터(CPU에서 인스트럭션을 어디까지 수행했는지 나타내는 레지스터)도 부모 프로세스를 복제하게 됩니다.
이렇게 복제된 프로세스는 독립적인 프로세스이기 때문에, 부모 프로세스와 자원을 공유하는 것이 아니라 경쟁적으로 사용하는 것이 원칙적으로는 맞습니다.
하지만 자식 프로세스가 부모 프로세스를 복제하면 메모리에 똑같은 내용이 두 번 올라가게 되어 메모리가 낭비됩니다. 그래서 일부 운영체제(리눅스 등)에서는 자식이 부모의 주소 공간을 공유하고 있습니다.
자식 프로세스가 부모의 모든 내용을 복제하는 대신에 프로그램 카운터만 하나 복제하여 똑같은 위치를 가리키고 있다가, 실행을 하다 보면 부모 프로세스와 자식 프로세스의 내용이 당연히 달라지게 됩니다. 주소 공간의 데이터가 달라질 수 있고 함수 호출이 달라져 스택에 쌓이는 내용도 달라지며 부모 프로세스와 자식 프로세스는 각자의 길을 가게 됩니다. 그러면 그제서야 부모가 공유하던 메모리 공간의 일부를 자식이 복제하게 됩니다.
이런 방식을 Copy on Write라고 부릅니다. 말 그대로 Write가 발생했을 때 Copy 하겠다고 하는 것인데 Write가 발생하기 전까지는 공유하다가, Write가 발생하면 해당 부분만을 카피하여 각자의 공간으로 관리하는 것입니다.
즉, 원칙적으로는 각 프로세스는 독립적으로 실행되고 자원을 사용하는 것이 맞지만, 자원을 공유할 수 있는 경우에는 자원을 공유해 운영체제의 효율성과 성능을 높일 수 있는 것입니다.
이 프로세스를 만드는 과정은 보통의 프로세스가 할 수 있는 일이 아닙니다. 그래서 부모 프로세스가 직접 하는 것이 아니라, 운영체제에게 자식 프로세스를 만들어달라고 시스템 콜을 통해 요청하게 됩니다. 이렇게 새로운 프로세스를 만드는 시스템 콜이 fork() 시스템 콜입니다. fork() 시스템 콜과 함께 프로세스와 관련된 시스템 콜들을 알아보겠습니다.
2. 프로세스와 관련한 시스템 콜
엄밀히 따지면, 보통의 프로세스 생성은 부모를 그대로 복사하는 fork() 시스템 콜과 주소 공간을 할당하는 exec() 시스템 콜의 2단계를 거칩니다.
다음의 시스템 콜 별 설명을 통해 자세히 알아보겠습니다.
(1) fork() 시스템 콜
int main()
{
int pid;
pid = fork();
if(pid == 0) //자식 프로세스인 경우 (자식은 리턴 값이 0)
printf("\n Hello, I am child!\n");
else if(pid > 0) //부모 프로세스인 경우 (부모는 리턴값이 양수. 정확히는 자식의 PID)
printf("\n Hello, I am parent!\n");
}
운영체제에 새로운 프로세스를 만들어 달라는 요청을 하기 위한 시스템 콜이 fork() 입니다.
fork() 시스템 콜을 통해 운영체제가 프로세스 복제를 하면, 자식과 부모를 구분하기 위해서 return 값을 다르게 반환합니다. 부모 프로세스는 양수로, 자식 프로세스는 0으로 부모와 자식 프로세스를 구분하게 됩니다.
fork() 시스템 함수가 기존의 순차적인 함수 호출과 다른 것은, 문맥까지 복사를 하기 때문에 fork() 함수가 실행되는 순간, 자식 프로세스도 fork() 다음 코드부터 실행된다는 것입니다. 부모 프로세스는 fork()를 통해 아래 코드를 순차적으로 실행하지만, 자식 프로세스는 부모 프로세스의 Program Counter가 fork()가 끝난 시점을 가리키는 문맥도 복제되어 가지고 있기 때문에, 그 다음부터 실행하게 됩니다.
(2) exec() 시스템 콜
fork()를 통해 자식 프로세스가 생성되면 같은 코드를 가지는 프로세스만 존재하는 경우가 생깁니다. 그래서 다양한 프로그램을 실행하기 위해서, exec() 시스템 콜을 통해 부모 프로세스의 코드를 잊고, 프로그램을 새로 덮어쓰게 해줍니다.
int main()
{
int pid;
pid =fork();
if(pid==0) // 자식 프로세스
{
printf("\n Hello, I am child! Now I'll run date \n");
execlp("/bin/date", "/bin/date", (char*)0);// 새로운 프로그램으로 덮어 씌운다
printf("\n Hello, I am parent!\n"); // 영원히 실행 불가
}
else if(pid>0) // 부모 프로세스
printf("\n Hello, I am parent!\n");
}
코드를 통해 설명하자면, 먼저 fork()를 통해 자식 프로세스를 생성하고 부모 프로세스는 일부 내용을 출력한 뒤 종료됩니다. 그리고 자식 프로세스는 새로운 프로그램을 실행하기 위해 exec() 시스템 콜을 호출합니다. exec() 시스템 콜을 만나면 자식 프로세스는 완전히 새로운 프로그램으로 탄생하고 부모의 코드를 완전히 잊어버리게 됩니다.
그래서 위 코드에서 exec() 시스템 콜 함수를 호출해 프로그램을 /bin/date라는 새로운 프로그램으로 덮어버렸기 때문에 exec 뒤의 prinf() 함수는 영원히 실행이 불가능합니다.
int main()
{
printf("1");
execlp("echo", "echo", "3", (char*)0);
printf("2"):
}
비슷한 코드 예시입니다. 여기서 echo는 뒤에 나오는 argument를 화면에 출력하는 command입니다. 위에서의 설명과 같은 이유로 2는 영원히 출력되지 않습니다.
(3) wait() 시스템 콜
wait() 시스템 콜은 프로세스를 일시적으로 Blocked 상태로 만들어 해당 프로세스가 특정 이벤트를 기다릴 수 있도록 하는 것입니다. 주로 자식 프로세스를 생성한 후 wait() 시스템 콜을 사용해 부모 프로세스가 자식 프로세스의 종료를 기다리게 됩니다. 자식 프로세스가 종료되면 부모 프로세스는 Blocked 상태에서 벗어나 Ready 상태로 만들어 실행을 재개합니다.
코드를 보면 부모 프로세스에서 우선 fork()를 통해 자식 프로세스가 만들어집니다. 부모 프로세스가 실행되는 코드 부분(pid = 0이 아닌 부분)에 wait() 시스템 콜을 실행시키면 부모 프로세스는 blocked 상태가 되어 CPU를 얻지 못하다가, 자식 프로세스가 종료가 되면 그제야 wait() 함수의 다음 코드를 실행하게 됩니다.
(4) exit() 시스템 콜
exit() 시스템 콜은 현재 실행 중인 프로세스를 종료시킵니다.
프로세스를 종료시키는 과정은 자발적 종료와 비자발적 종료 두 가지로 나뉩니다.
1. 자발적 종료
int main() {
// 프로그램의 작업 수행
// ...
// 프로세스 종료
exit(0);
}
위 코드처럼 마지막 문장 수행 후 exit() 시스템 콜을 통해 이루어지게 됩니다.
프로그램에 명시적으로 적어주지 않아도 main 함수가 리턴되는 위치에 컴파일러가 exit()시스템 콜을 넣어줍니다.
2. 비자발적 종료
프로세스가 외부 요인에 의해 종료되는 경우를 비자발적 종료라고 합니다.
1. 부모 프로세스가 자식 프로세스를 강제로 종료하는 경우
부모 프로세스가 자식 프로세스에게 종료 시그널을 보내어 강제로 종료시키는 경우를 말합니다.
자식 프로세스가 한계치를 넘어서는 자원을 요청하거나 할당된 태스크가 더 이상 필요하지 않을 때 발생합니다.
2. 사용자가 키보드 입력으로 강제 종료하는 경우
Ctrl+C 또는 Ctrl+Break 등의 command를 누르는 경우 프로세스가 종료됩니다.
3. 부모 프로세스가 종료되어 자식 프로세스가 먼저 종료되는 경우
프로세스 간 계층 구조에서는 부모 프로세스가 종료되기 전에 자식 프로세스가 먼저 종료됩니다. 부모 프로세스가 종료되면 해당 부모 프로세스가 생성한 자식 프로세스들은 고아 프로세스로 간주되어 종료됩니다.
3. 프로세스 간 협력
프로세스 간의 협력은 프로세스가 독립적으로 실행되는 것과는 달리, 상호작용하고 정보를 주고받는 메커니즘을 의미합니다.
프로세스는 기본적으로 독립적인 주소 공간을 가지며 서로 영향을 미치지 않는데, 협력이 필요한 경우 이를 위한 메커니즘으로 IPC (Inter-Process Communication)을 사용합니다.
프로세스 간 협력 매커니즘(IPC : Interprocess Communication)
IPC는 프로세스 간에 정보를 주고받는 방법을 말하며, 주로 message passing과 shared memory의 두 가지 방식으로 나뉩니다
(1) Message Passing
프로세스 간에 메시지를 전달하는 방법입니다. 프로세스는 독립적이기 때문에 자기 자신의 메모리 주소 공간만 볼 수 있고, 원칙적으로는 프로세스가 메시지를 직접 전달할 수 있는 방법이 없습니다. 이 역할을 운영체제 커널이 메신저 역할을 맡아 메시지를 전달해 줍니다.
다이렉트 커뮤니케이션 (Direct Communication)
메시지를 보내는 프로세스가 메시지를 받을 프로세스를 명시적으로 지정합니다. 이때 커널을 통해 메시지를 전달합니다.
인다이렉트 커뮤니케이션 (Indirect Communication)
메시지를 받을 프로세스를 명시하지 않고 운영체제가 제공하는 mailbox(또는 port)와 같은 공유 매커니즘을 통해 메시지를 간접 전달합니다.
(2) Shared memory
프로세스 간에 일부 메모리 주소 공간을 공유하는 방식입니다. 프로세스는 원칙상 자기 자신의 주소 공간에만 접근할 수 있지만, Shared memory는 일부 주소 공간을 공유합니다. 커널을 통해 물리적인 메모리에 프로세스 간의 공유 영역을 만들고, 해당 영역을 두 프로세스가 공유합니다.
커널에게 Shared memory를 한다는 시스템 콜을 통해 매핑을 해주고, 커널이 매핑을 한 번만 진행해 주면 이후는 사용자 프로세스가 작업을 하게 됩니다.
+ 추가로 thread 와 같은 경우는 프로세스 하나 안에 CPU 수행 단위가 여러 개이기에 프로세스 간의 협력은 아니지만, thread 끼리는 주소 공간을 공유하고 있기 때문에, thread 끼리는 완전한 협력이 가능합니다.
참고자료
[KOCW 이화여대 반효경 교수님 - Process Management 2]
https://core.ewha.ac.kr/publicview/C0101020140325134428879622?vmode=f
[ Chapter3 Operating System Concepts - Abraham Silberschatz ]
https://www.yes24.com/Product/Goods/89496122