Process Management

뚝딱이·2023년 10월 29일
0

운영체제

목록 보기
7/15
post-thumbnail

프로세스가 어떻게 만들어지는가에 대해 얘기해보자

프로세스 생성

프로세스의 생성자

누가 프로세스를 만드느냐 ?

부모 프로세스가 자식 프로세스를 생성한다.
프로세스가 하나 있으면 그 해당 프로세스가 다른 프로세스를 생성하는데, 이때 생성형식이 복제이다.
사람 처럼 아버지와 어머니가 있는게 아닌, 프로세스는 부모가 하나 있으며 부모 프로세스는 여러개의 자식 프로세스를 생성할 수 있다.
따라서 트리 형태의 족보가 생긴다.

프로세스는 자원을 필요로 하며 운영체제로부터 받게 된다. 자원은 부모 프로세스와 공유할 수도 있고, 공유하지 않을 수 있는데 원칙적으로는 공유하지 않는다. 왜냐면 자식 프로세스가 생성되면 별개의 프로세스이기 때문에 CPU, 메모리를 더 많이 얻으려고 경쟁하려 하기 때문이다.

그렇다면 자원을 공유한다는 것은 무엇일까?
자식 프로세스가 부모 프로세스를 그대로 copy하기 때문에 메모리엔 2개의 copy가 올라간다. 부모과 자식의 내용이 똑같다면 당장 copy를 만들 필요는 없을 것이다. 메모리 낭비가 있을 수 있기 때문이다. 따라서 PC 하나만 복사해 똑같은 위치를 가리키도록 한다. 이렇게 사용하다가 부모와 자식이 달라질 때 (변수의 내용이 달라지거나 stack에 쌓이는 내용이 달라질 때), 부모와 공유하던 memory 공간의 일부를 copy해서 사용한다. 이러한 기법은 copy-on-write라고 하며 줄여서 COW라고 한다. 직역한다면 write가 발생했을 때 copy를 하겠다는 뜻이다. write란 내용이 바뀐다는 뜻이다.
code, data, stack은 물리적인 메모리에 통째로 올라가는 것이 아니다. 잘개 쪼개져서 필요한 부분만 올라가기 때문에 잘게 쪼개진 부분에 대해 write가 일어나면 그 부분만 copy를 한다. 따라서 공유를 할 수 있으면 공유를 하는게 효율적이나, 독립적인 개체이므로 공유를 안하는게 원칙이다.

프로세스가 실행될 때 부모와 자식이 공존하며 실행하는 모델도 있고, 자식이 종료될 때 까지 부모가 기다릴 수도 있다.

부모 프로세스의 자식 프로세스 생성 방식

그렇다면 부모 프로세스는 자식 프로세스를 어떻게 생성할까?
위에서 우리는 자식 프로세스가 부모 프로세스의 복제 형식으로 생성된다는 것을 알았다.
부모 프로세스는 주소 공간을 가지고 있고, 자식은 부모의 주소 공간(code, data, stack)과 운영체제의 데이터(PCB, 자원 등)을 복사하는 것이다.
이렇게 복제 생성을 한다고 생각하면 똑같은 프로세스가 생긴다는 느낌을 준다. 따라서 컴퓨터에 있는 프로세스는 다 같은 프로그램을 실행해야 하는 것 아닌가? 라는 생각이 들 수 있다.
하지만 프로세스는 그렇게 실행하지 않는다. 일단 복제를 해놓고, 복사한 주소공간에 새로운 프로그램을 덮어 씌운다. 이러한 방식으로 서로 다른 프로그램들이 컴퓨터내에 존재할 수 있는 것이다.

유닉스에서 프로세스의 생성

유닉스의 예를 들어 보자
fork() 시스템 콜이 새로운 프로세스를 생성하게 된다. 따라서 프로세스를 그대로 복사해 주소 공간을 할당한다. 이때 exec() 시스템 콜을 사용해 할당된 주소 공간에 새로운 프로그램을 올린다.
이 두가지 명령어는 독립적이기 때문에 복제하고 새로운 프로그램을 올리지 않을 수도, 복제를 안하고 새로운 프로그램으로 교체만 할 수도 있는 것이다.

또한 위에서 fork와 exec 시스템 콜이라고 지칭했기 때문에 사용자가 이를 수행하는 것이 아닌, 운영체제가 이를 수행한다.

  • fork () : 부모 프로세스의 복제본인 자식 프로세스를 생성
  • exec () : 새로운 프로그램을 메모리에 올림

따라서 생성 단계는 두단계라고 할 수 있다. 부모 프로세스를 먼저 복사하고, 다른 프로그램을 덮어씌우는 단계로 이루어진다.

프로세스 종료

exit

프로세스가 마지막 명령을 수행한 후 운영체제에게 exit 시스템 콜을 통해 종료를 알린다.

C언어로 main 함수안에 프로그램을 작성하고 마지막에 중괄호를 닫으면 프로그래머가 명시적으로 exit 시스템 콜을 하지 않더라도, 컴파일러가 중괄호가 닫히는 시점에 exit 시스템 콜을 자동으로 넣어준다.

프로세스가 종료될 땐 자식이 부모에게 어떠한 데이터를 보낸다. 대게 사람은 부모가 더 일찍 죽고 자식이 더 오래살아 남게 되는데 프로세스는 그렇지 않다. 부모 프로세스가 자식 프로세스를 생성했다면 자식 프로세스가 먼저 죽고 부모 프로세스가 그에 대한 처리를 하는 것이 원칙이다.

따라서 자식 프로세스가 종료될 때는 wait 시스템 콜을 통해 부모 프로세스에게 데이터가 전달 된다. 따라서 자발적으로 프로세스가 종료될 때는 exit 시스템 콜을 사용하면 되고, 비자발적으로 종료시킬 때는 abort를 사용해야한다.

abort

abort는 부모 프로세스가 자식의 수행을 종료시키는 것이다.

그렇다면, abort를 하는 경우는 어떤게 있을까?

  • 자식이 할당 자원의 한계치를 넘어설 때
    • 자식이 너무 큰 자원을 요청 할 때
  • 자식에게 할당된 태스크가 더이상 필요하지 않을 때
    • 애초에 자식을 생성한 이유는 자식에게 시킬 일이 있어서이기 때문에, 더이상 자식에게 시킬 일이 없을 때 부모프로세스는 자식 프로세스를 죽인다.
  • 부모 프로세스가 종료하는 경우
    • 원칙적으로 자식 프로세스가 종료된 다음 부모 프로세스가 종료되어야 한다. (운영체제는 부모 프로세스가 종료하는 경우 자식이 더 이상 수행되도록 두지 않는다.)
    • 그런데 어떠한 상황에 의해 부모 프로세스가 종료되어야 한다면, 부모가 생성한 모든 자식 프로세스들을 죽이고 자신이 죽는다.
    • 이때 자식이 또 자식 프로세스를 생성했다면 이 프로세스들이 먼저 죽어야한다. 따라서 부모프로세스는 자식의 자식 프로세스가 다 죽고 자식 프로세스가 죽으면 죽을 수 있는 것이다.
    • 따라서 단계적인 종료가 이루어진다.

위의 내용을 읽으면 알겠지만 컴퓨터의 세계는 사람 세계의 시각으로 바라본다면 굉장히 잔인하다

fork() 시스템 콜

int main()
{ int pid;
  pid = fork();
  if (pid == 0) /* this is child */
    printf("\n Hello, I am child! \n");
    
  else if (pid > 0) /* this is parent */
    printf("\n Hello, I am parent! \n");

처음엔 자식 프로세스를 생성하지 않았으므로 부모 프로세스 하나이다. 그러다가 명령어 fork()를 만나게 되면 자식 프로세스를 하나 만들게 된다. 자식 프로세스는 fork() 다음부터 수행하게 된다. 부모프로세스의 PC는 fork()를 가리키고 있고, 이때 생성된 자식 프로세스는 부모 프로세스의 PC를 복사하게 되므로 똑같이 fork()를 가리켜 이 이후부터 수행하는 것이다. 따라서 main함수의 맨 위에서부터 다시 수행하는 것이 아니다.

문제는 자식 프로세스가 자신이 복제본이 아닌 원본이라고 주장할 경우이다. 또한 fork를 하면 부모와 같은게 만들어지므로 프로그램에서 똑같은 제어흐름을 가져야할 것 같은 문제가 있다. 따라서 이를 해결하기 위해 복제본을 만들면 자식과 부모를 구분한다. fork는 부모와 자식에게 각각 다른 값을 반환한다.

fork의 반환 값

  • 부모 : 자식의 주민번호 -> 자식의 pid (따라서 양수)
  • 자식 : 0

따라서 위를 통해 자식과 부모를 분리할 수 있는 것이다. 이렇게 분리가 일어나므로 각자 다른일을 할 수 있는 것이다.

int main()
{ int pid;
  printf("\n Hello! \n");
  pid = fork();
  if (pid == 0) /* this is child */
    printf("\n Hello, I am child! \n");
  else if (pid > 0) /* this is parent */
    printf("\n Hello, I am parent! \n");

만약 이와 같이 fork이전에 print문이 하나 더 있다면, 부모 프로세스만 이를 실행할 수 있다. 즉 한번만 출력된다.

위에서 살펴본 코드에선 생성된 프로세스에 새로운 일이 할당되지 않아 부모와 자식이 같은 제어흐름을 가진다. 이제 새로운 프로그램을 덮어씌우는 것을 살펴보자.

exec() 시스템 콜

int main()
{ int pid;
  pid = fork();
  if (pid == 0) /* this is child */
  {
    printf("\n Hello, I am child! Now I'll run date \n");
    execlp("/bin/date", "/bin/date", (char*)0);
  }
  else if (pid > 0) /* this is parent */
    printf("\n Hello, I am parent! \n");

execlp를 만나면 이전의 기억은 잊어버리고 새로 소개되는 프로그램으로 덮어씌워진다. 위의 /bin/date는 리눅스 명령어로 현재 날짜를 실행해준다.
이제 자식 프로세스는 /bin/date 프로그램을 처음부터 실행한다. 한번 exec 하게 되면 다시 돌아올 수 없다.

exec은 꼭 자식을 만들어서 해야하는 것은 아니다. 따라서 fork를 빼고 다음과 같이 사용할 수도 있다.

int main()
{ 
  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");
}

이렇게 되면 부모 프로세스는 /bin/date로 덮어씌워지게 된다. 따라서 exec 뒤에 있는 printf는 실행되지 않는다. 즉, print는 Hello, I am child! Now I'll run date 만 되는 것이다. /bin/date가 익숙치 않다면 다음과 같이 작성후 실행해보자. echo는 넘어온 argument를 출력해주는 명령어이다.

int main()
{ 
  printf("1");
  execlp("echo", "echo", 3, (char*)0);
  printf("2");
}

따라서 위의 main을 실행한다면 출력은 1,3 뿐일 것 이다. argument를 여러개 하고 싶다면, 다음과 같이 적으면 된다. execlp("echo", "echo", arg1, arg2, (char*)0);

wait() 시스템 콜

이 프로세스를 잠들게 하는 것이다. block 상태로 보내는 것이다. block상태라는 것은 오래걸리는 이벤트를 기다리고 이벤트가 만족되면 CPU를 얻을 수 있는 ready 상태로 돌아온다. 예) IO
자식 프로세스를 만든 다음 wait 시스템 콜을 하게 된다. 자식 프로세스가 종료되기를 기다리면서 block 상태가 되는 것이다. 그래서 자식이 종료되면 부모 프로세스가 ready 상태로 돌아가 CPU를 얻길 기다린다.

부모 프로세스가 실행하는 코드에 wait 시스템콜을 넣게 되면 block 상태가 되어 CPU를 얻지 못하고 기다린다. 자식 프로세스가 할당 받은 일을 모두 수행하고 종료되면 wait() 시스템콜을 빠져나가 다음 코드를 실행할 수 있다.

리눅스에서는 prompt에 명령어를 작성하면 해당 명령어를 수행하고 다시 명령어를 받는다. 이러한 것도 wait 시스템콜을 이용한 모델이라고 볼 수 있다. 왜냐하면 다시 명령어를 받는 것 또한 하나의 프로그램이기 때문에 부모프로세스가 명령어를 받기를 기다리고 명령어를 작성하면 자식 프로세스를 만들어 해당 명령어를 수행한다(부모 프로세스는 wait()). 자식프로세스가 해당 명령어를 수행하고 나서 종료되면 부모프로세스는 기다리다가 다시 명령어를 받는다.

부모와 자식이 병렬적으로 수행되는 경우도 있지만, 자식이 종료될 때 까지 부모가 기다리는 모델도 있다.

exit() 시스템 콜

프로세스를 종료시킬 때 호출하는 시크템 콜이다.

int main()
{ 
  printf("1");
  exit();
  execlp("echo", "echo", 3, (char*)0);
  printf("2");
}

위와 같이 eixt을 중간에 작성하면 프로세스는 1만 출력하고 종료된다. 이와 같이 exit을 명시적으로 작성하는 방법도 있고, 컴파일러가 필요한 시점에 넣어 프로세스가 종료되도록 한다.

자발적 종료

마지막 문장 수행 후 exit() 시스템 콜을 통해 이루어진다.
프로그램에 명시적으로 적어주지 ㅇ낳아도 main 함수가 리턴되는 위치에 컴파일러가 넣어준다.

비자발적 종료

프로그램 본인은 열심히 하고 있는데 외부의 누군가가 죽이는 것
부모 프로세스가 자식 프로세스를 강제 종료시킨다.

  • 자식 프로세스가 한계치를 넘어서는 자원 요청
  • 자식에게 할당된 태스크가 더 이상 필요하지 않음
    사람이 키보드로 Ctrl+C, kill, break등을 친 경우
    부모가 종료하는 경우 (자식이 먼저 죽고 본인이 죽어야함 -> 계층 구조)
  • 부모 프로세스가 종료하기 전에 자식들이 먼저 종료된다.

프로세스 간 협력

독립적 프로세스(Independent process)

원칙적으로 프로세스는 대단히 독립적이다. 자식과 부모가 서로 결쟁하는 독립적인 생태계이다.
프로세스는 각자의 주소 공간을 가지고 수행되므로 원칙적으로 하나의 프로세스는 다른 프로세스의 수행에 영향을 미치지 못한다.

협력 프로세스

서로 정보를 주고 받으면서 실행하는 것이다.
프로세스 협력 메커니즘을 통해 하나의 프로세스가 다른 프로세스의 수행에 영향을 미칠 수 있다.

프로세스 간 협력 메커니즘 (IPC: Interprocess Communication)

프로세스간 정보를 주고 받을 수 있는 방법

메시지를 전달하는 방법

message passing: 커널을 통해 메시지 전달
process A가 process B에게 메시지를 전달 하고 메시지에 따라 B가 프로세스를 실행, B가 실행하다가 필요한 경우 A에게 또 메시지 passing
하지만 프로세스는 원칙적으로 독립적이기 때문에 직접적으로 메시지를 전달할 수단이 없다; 따라사ㅓ 커널을 통해 메시지를 전달한다.

Message system

프로세스 사이에 공유 변수를 일체 사용하지 않고 통신하는 시스템이다.

communication의 방식을 두가지로 나뉘지만 communication이 커널을 통해서 이루어진다는 것은 똑같다.

Direct Communication

통신하려는 프로세스의 이름을 명시적으로 표시하는 경우
Send(받을 사람, 메세지), Receive (준 사람, 메세지)

Indirect Communication

mailbox(또는 port)를 통해 메시지를 간접 전달
Send(넣을 우체통, 메세지), Receive (가져온 우체통, 메세지)
누가 받을지 명시를 안하기 때문에 아무나 가져가라를 구현할 수 있다.

주소 공간을 공유하는 방법

shared memory: 서로 다른 프로세스 간에도 일부 주소 공간을 공유하게 하는 shared memory 메커니즘
일부 영역이 공유되도록 매핑해놓은 것으로 A가 수정하면 B도 인지가 가능하다. 프로세스끼리 당장 공유할 수 있는 것이 아니라 커널에 시스템콜을 통해 매핑을 하고 공유하게 된다.
커널이 한번 공유하게 해주면 커널의 추후 개입없이 프로세스 끼리 사용하는 것이기 때문에 예상치 못한 결과가 나올 수 있으므로 주의해야한다. 따라서 두 프로세스는 상당히 신뢰하는 관계여야한다.

thread: thread는 사실상 하나의 프로세스(하나의 프로세스안에 여러 CPU 수행단위)이므로 프로세스 간 협력으로 보기 어려우나 동일한 process를 구성하는 thread들 간에는 주소 공간을 공유하므로 협력이 가능하다.
프로세스 하나에 여러 스레드가 활동하는 것이므로 당연히 스레드 끼리는 완전한 협력이 가능하다.

출처

Operating System Concepts 10th
KOCW 강의 - [운영체제] 이화여자대학교 반효경 교수

profile
백엔드 개발자 지망생

0개의 댓글