하나의 프로세스는 여러 자식을 생성할 수 있다. 이때 일반적으로 복제 생성 방식으로 자식이 생성된다. 복제 생성 방식이란 부모 프로세스의 주소공간을 자식이 그대로 복사한다는 것을 의미한다. 이런 구조로 인해 프로세스는 트리 구조를 형성하게 된다.
일반적으로 프로세스가 직접 자식을 생성하는 것이 아니라 시스템 콜을 통해 운영체제에게 요청해 운영체제가 자식을 생성하게 된다.
프로세스 생성에 사용되는 시스템 콜은 fork 시스템 콜과 exac 시스템 콜이 있다.
fork : 프로세스를 복제 생성하는 시스템 콜
프로세스를 생성하게 되면 부모의 프로세스의 context를 그대로 복사해서 생성된다.
int main()
{
int pid;
pid = fork();
if (pid == 0 )
printf("\n Hello, I am child!\n");
else if (pid > 0)
printf("\n Hello, I am parent!\n");
}
다음과 같은 코드가 있을 때 부모 프로세스로 부터 생성된 자식 프로세스는 fork()이후의 코드부터 실행이 된다. 이는 복제 생성되어 부모 프로세스가 어디까지 실행되었는지 알고 있기 때문이다. 이로 인해 자식 프로세스는 fork()이전의 코드는 실행이 불가능하다.
fork를 통해 생성된 자식과 부모는 fork의 결과값으로 구분할 수 있다.
부모 프로세스의 경우 fork의 결과값으로 자식 프로세스의 pid값인 양수값을 얻게 되는 반면 자식 프로세스는 fork의 결과값으로 0을 얻게 된다. 이를 통해 부모와 자식 프로세스를 구분 가능하며 서로 다른 일을 시키는 것이 가능해진다.
exec : 어떤 프로세스를 완전히 새로운 프로세스로 태어나게 해주는 시스템 콜
프로세스를 아예 다른 작업을 하는 프로세스로 변경하고 싶을 때 exec 시스템 콜을 사용한다.
int main()
{
int pid;
pid = fork();
if (pid == 0 )
printf("\n Hello, I am child!\n");
execlp("/bin/date", "/bin/data", (char*) 0);
else if (pid > 0)
printf("\n Hello, I am parent!\n");
}
exec는 아예 다른 작업을 하는 프로세스로 변경을 해주는데 이 때 해당 작업의 가장 첫부분부터 시작하게 된다. 한번 exec을 해서 변경된 프로세스는 이전 프로세스로 되돌아올 수 없기에 exec 이후 코드들에 대해서는 실행이 불가하다.
일반적으로 프로세스를 생성하는 것은 프로세스를 복제 후 새로우 프로그램으로 덮어씌우는 작업까지를 의미한다.
exec은 반드시 새로운 프로세스 생성 후에만 사용가능한 것은 아니며 어떤 동작을 하고 있는 기존 프로세스에서도 작업을 변경하기 위해서 사용 가능하다.
프로세스의 모델은 여러가지가 있지만 그중에 자식 프로세스를 생성했을 때 종료될 때까지 부모 프로세스가 기다리는 모델도 존재한다. 이 때 부모 프로세스는 자식 프로세스가 종료되기를 기다리면서 blocked 상태가 되고 자식 프로세스가 종료되면 부모 프로세스는 ready 상태로 바뀌어서 cpu를 다시 할당받을 수 있게 된다.
대표적인 예시로 리눅스 shell에 해당된다. 리눅스 shell은 그 자체로 이미 하나의 프로그램이 떠 있는 상태이다. 그 상태에서 어떤 명령어 입력시 일반적으로 해당 명령어가 종료될때까지 다시 명령어 입력을 할 수 없고 해당 명령어가 종료되면 다시 shell 명령을 할 수 있는 상태가 된다.
프로세스 종료는 자발적인 종료와 비자발적 종료 2가지로 구분된다.
먼저, 자발적인 종료는 프로세스에 종료를 명시해서 그 부분에 도달했을 때 종료되는 방식을 의미한다. 자발적인 종료를 위해 사용되는 시스템 콜은 exit이 있다.
비자발적인 종료는 외부 요인에 의해 종료되는 방식을 의미하고 부모 프로세스가 자식 프로세스를 종료하는 경우, 사람이 키보드로 강제 종료를 할 경우, 부모 프로세스가 죽게 되는 경우를 의미한다. 일반적으로 현식 세계와는 다르게 프로세스 세계에서는 부모 프로세스보다 자식 프로세스가 먼저 종료되어야 한다는 원칙이 있기 때문에 부모 프로세스가 죽게되면 하위의 자식과 후손 프로세스들이 모두 종료된 이후에 부모가 종료되게 된다. 비자발적인 종료를 위한 시스템 콜은 abort가 있다.
int main()
{
int pid;
exit();
pid = fork();
if (pid == 0 )
printf("\n Hello, I am child!\n");
else if (pid > 0)
printf("\n Hello, I am parent!\n");
}
exit 시스템 콜 : 프로세스를 종료시키기 위해 호출하는 시스템 콜
exit 시스템 콜을 통해 프로세스를 종료시키며 exit 이후 코드들은 실행 불가하다. 컴파일러는 exit을 명시적으로 시정하지 않았더라도 코드이 마지막 부분을 실행하면 자동으로 exit을 호출해 종료하게 된다.
원칙적으로 프로세스는 독립적으로 동작해 하나의 프로세스가 다른 프로세스의 실행에 영향을 주지는 않는다. 하지만 경우에 따라서 프로세스가 협력을 해야 효율적으로 실행이 되는 경우가 있는데 운영체제는 이를 위해 몇가지 협력 메커니즘(interprocess communication - IPC)을 제공한다.
message passing : 메시지를 통해 프로세스 간에 정보를 주고받는 것

message passing은 메시지를 통해 프로세스 간 협력하는 방식으로 프로세스가 직접 메시지를 주고받는 방법은 없기 때문에 커널을 통해서 메시지를 주고 받아야 한다.
이 때 통신하려는 프로세스의 이름을 명시해서 보내고 커널이 이를 통해 받는 쪽으로 메시지를 보내주는 방식을 direct communication이라고 하고 프로세스가 통신하려는 대상 프로세스에 대한 명시 없이 메시지를 커널 내부에 있는 Mailbox에 전송하고 그 Mailbox에 있는 메시지를 받는 쪽에서 가져가게 하는 방식이 indirect communication이라고 한다.
shared memory : 프로세스 간 일부 공간을 공유하는 방식

두 프로세스가 일부 공간을 공유하게 함으로써 한 프로세스가 공유 공간에 작성한 내용을 다른 프로세스에서 확인 가능하도록 하는 방식이다. 프로세스끼리 직접 공유하지는 못하고 shared memory를 사용하겠다는 시스템 콜을 통해 커널에 요청을 해서 커널이 shared memory를 매핑해주면 공유가 가능해진다.
스레드는 애초에 프로세스 내 공간을 공유하기 때문에 스레드 간 협력이 이루어질 수 있다.