운영체제 : 프로세스 Forking

msung99·2023년 4월 3일
0
post-thumbnail

본 포스트는 학교 수업 강의내용을 단순 정리본 형태로 만든 내용입니다. 평소 포스트와 달리 다소 설명이 부실할 수 있음을 미리 알려드립니다 🙂

프로세스 생성부터 살펴보자.

프로세스 생성의 주요 원인 4가지는 다음과 같다.

  • New batch job : 디스크나 tape 장치에 있는 bash job 들을 로딩해서 어떤 스크립트나 JCL 와 같은 job 을 제어할 수 있는 sequence 언어에 비해서 job 들을 처리하기 위한 프로세스를 생성할 수 있다는 것이다.

  • Interactive log-on : 사용자가 로그인해서 프로세스를 터미널을 통해서 만들수도있다.

  • Created by OS to provide a service : 때로는 커널이 어떤 서비스를 지원하기위해서 프로세스를 생성할수도 있다.

  • Spawned by existing process : 유저 레벨에서 만든것이다. 존재하는 어떤 프로세스에 의해서 다중 프로세스를 생성해낼 수 있다.

OS 가 프로세스를 만든다는 것은 OS 가 프로세스들을 제어 및 관리 목적을 위한 process context 공간을 만든다는 것이다. 어떤 구성요소들을 생성할까?

  • a.out 파일(프로그램) 과 PCB 를 할당한다.

  • 어떤 디스크상에 있는 데이터와 코드, 텍스트를 memory space 에 단순히 로딩하는 것 뿐만 아니라, 스택을 생성도 한다. 초기 스택은 empty 상태로 초기화된 상태일것이다.

  • PCB 초기화 : PCB 에 대해서 해당 프로세스의 PID, relation 등 상태정보의 기본값들을 초기화해준다.

  • 그 PCB 를 read list (Ready Queue) 에 넣으면 새로운 프로세스가 시작된다.

UNIX 계열에서는 보통은 이렇게 위와 같이 구성요소들을 만들어가는 과정을 직접적으로 프로세스를 생성한다고 표현한다. 이렇게 첫번째 프로세스만 직접적으로 생성하고, 2번째 이후부터는 clone, 즉 forking 시스템을 통해 새로운 프로세스를 기존의 부모 프로세스로부터 복재하는 방식으로 만들어나간다.

=> 부모 프로세스로부터 child 프로세스 만드는것

맨 처음에 직접 생성한 프로세스를 init process (또는 super parent process) 라고부른다.


프로세스 생성을 위한 cloning (복재) 기능에 대해 알아보자.
프로세스 생성을 위해 우리가 직접 정보를 기입하면서 만드는 것이 아니라, 부모 프로새스로 부터 정확한 복사본을 만드는 것을 cloning 또는 forking 또는 process spawning 이라고한다.

=> 부모 프로세스와 모두 똑같이 복재하나, 다른 점은 PID 값, relation 관계등만 다르다.

UNIX 계열에서의 forking 예시를 보면, 위와같이 fork system call 을 요청하면 커널 모드에서 sys_fork 함수가 호출된다.

이렇게 커널에서 sys_fork 함수가 호출되면 새로운 자식 프로세스를 만들고나서 parent 와 child 프로세스에게 2번 리턴하게된다. 이때 리턴값은 parent 에게는 새롭게 자식 프로세스의 PID 값을 리턴해주고, 생성된 그 자식 프로세스에게는 0 을 리턴해준다.

또 프로세스들이 다른 프로그램으로 교채될 수 있도록 execve() system call 을 지원하는데, 여러개의 함수군들이 있다. 이것은 프로세스를 생성하는게 아니라, 생성된 프로세스에서 분리되는 것이다. 즉 PID 가 바뀌는게 아니다. execve() 에 파라미터로 전달된 경로에 위취한 어떤 새로운 프로그램을 로딩해서 엎어치는 방식으로 동작한다.

정리하면, UNIX 리눅스 계열에서는 새로운 프로세스를 forking 해서 만들때 이 sys_fork 함수만 호출해서 만들거나, 또는 fork 하고 exec 하고 프로세스를 생성하는 것이다.


유닉스와 리눅스 계열에서 fork() 와 exec() 을 구분한 이유는 cloning 과정을 줄이고 프로세스 생성을 효과적으로 하려는 역사적인 이유에서 찾아볼 수 있다.

우선 init process 에서 fork() 를 호출해서 3개의 자식 프로세스가 생성되었다.
이때 PID 값이 각자 다르게 생성된 것을 볼 수 있다.

그리고 exec() 을 호출하는데, PID 값이 동일하다. PID 값이 동일하기 때문에 replacing 만 된것이다. 즉, 터미널 관리의 어떤 프로그램으로 프로세스가 만들어진 것을 볼 수 있다.

getty 로 부터 어떤 로그인 프로그램이 수행된다고하면 exec() 만 된것이고,

또 shell 프로그램을 exec 만 해서 새로운 프로세스를 만들지 않고 효과적으로 시스템을 운용할 수 있는 모습을 볼 수있다.


리눅스에서 fork 시스템이 어떻게 되는것인지 예시로 알아보자.
fork 는 앞서 말했듯이 2번 리턴한다고 했었고, 부모 프로세스에서 1번, 자식 프로세스에서 1번 리턴하는 것이였다.

이 리턴되는 순서는 스캐줄러에 의해 결정되기 때문에 순서는 보장되지 않는다.

위 조건은 child proess 에 헤당되는 경우다. 리턴값이 0이라 했으므로!

  • getpid() : 현재 프로세스의 PID 를 얻어내는 함수
  • getppid() : 부모 프로세스의 PID 를 얻어내는 함수

또 g_variable, l_variable 이라는 전역변수와 지역변수가 존재한다. 각 프로세스에 대해 현재 부모 프로세스에 저장된 g_variable 와 l_variable 에 저장된 값도 자식 프로세스에 복제된다.

복제된 이후는 이 변수들은 모든 프로세스가 함께 공유하지 않는다. 각 프로세스마다 독립된 고유한 스택, data, code 영역등을 가지고있기 떄문이다.

Hello 는 1번, Bye 는 2번 출력된 모습을 볼 수 있습니다. 왜냐하면 child process 의 시작은 "pid = fork()" 부분부터 시작하기 떄문이다. 보통 fork() 를 호출했을때 stack frame 으로 초기화되서 출발하기 때문에 child process 는 여기서부터 출발하게 된다.

=> 이 예제를 통해 확인할 수 있는점들을 정리해보면, data 와 stack 영역은 각 프로세스별로 별도로 유지되고있다. 또 이 예제에선 볼 수 없지만, text 영역은 보통 모든 프로세스가 공유해서 사용한다.


이번에는 프로세스 생성 속도를 높이기위한 방법으로 COW 기법에 대해 알아보자.

프로세스를 생성할때 미리 모든 복사본을 만드는것이 아니라, 처음에는 shared 로 마킹하고 데이터의 수정이 있을때만 필요한 시점에 페이지 단위로 복사하는 방법이다. 즉 shared 된 page 어떤 데이터 변경이 있을때만 page 를 복사하는 방법이다.

그래서 보통 fork 를 하게되면 현대 OS 는 프로그램 조각인 page 단위에 대해 관리하기 위해 page table 를 사용하고있다. (자세한 내용은 virtual memory 시간때 배운다!)

우선 프로세스의 pid 가 각각 11, 12 이다. pid = 11 인 프로세스가 부모 프로세스이고, pid = 12 인 놈은 자식 프로세스이다.

처음에는 이 page 들을 미리 할당, 복재하지 않고 부모 프로세스의 page 를 공유하고 있다. (sharing 하고있다. shared page 가 있다!)

보듯이 이 과정은 복사가 없기 때문에 프로세스 fork 과장이 굉장히 빠르다.
이렇듯이, 만약에 read 만 있다면 그대로 그냥 부모 프로세스의 데이터를 사용하면 되기 때문에 효율적으로 동작할 수 있다.

반면, 만약에 write 연산이 이루어지게 되면 (데이터 변경이 이루어지게 되면), 변경이 이루어진 해당 page, 즉 위 예제에서는 page A 에 어떤 데이터 접근이 이루어져서 write 가 진행되었다면, 해당 page 만 복사하고 유지.관리하는 방법으로 동작할 수 있기 때문에 프로세스 생성 시간을 줄일 수 있다.
또 새롭게 할당되는 page 수를 최소화 할 수 있는 COW 기법으로 진화하게 되었다.


execl() 함수는 아까 말했듯이 이 자체만으로 새로운 프로세스를 만드는게 아니라, 보통 이렇게 fork 를 통해서 프로세스를 만든 이후에 교채하는 것이다.

execl() 이 호출되면 인자로 주어진 다양한 함수군들이 있는데, 이를 exec system call 이라고한다.

위는 bin 디랙토리 밑에 ls 프로그램을 새롭게 child process 로 실행시키려는 것이다.

따라서 새로운 코드 영역으로 바껴서 실행되기 때문에 ls 명령어가 실행된 결과를 위처럼 볼 수 있다.

그렇다면 child process 는 이 프로그램 코드의 전범위였다. 그런데 프로세스가 교채되었기 때문에 위와 같은 execel() 이후의 코드는 실행할 수 없다. ls 로 바뀔테니깐!

Bye 는 1번 출력된 모습을 볼 수 있다. 부모 프로세스 3579 에 의해 1번 실행된 것을 볼 수 있다.


일반적으로 process 의 termination (프로세스의 종료) 는 자발적인 종료(voluntary termination) 과 비자발적인 종료(involuntary termination) 으로 구분된다.

voluntary termination

자발적으로 종료하는 방법은, 우리가 명시적으로 exit system call 을 호출하는 방법이다. 즉, 더 이상 어떤 프로세스의 수행이 남아있지 않고 다 완료된 상황에서 명시적으로 user program 에서 exit 시스템콜을 사용하는 것이다. 이 exit 시스템 콜을 호출하게 되면, 시스템의 현재 프로세스를 커널에 요청하는 것이다. 그러면 커널이 해당 프로세스를 종료하는데, 해당 프로세스의 자원들이 deallocated 되고 OS 에 의해서 회수당한다.

involuntary termination

또는 이 exit 시스템 콜을 명시적으로 호출하지 않더라도 UNIX 계열은 main 의 end 블럭을 만나면 기본적으로 이 exit 함수가 호출 및 실행되는 방식으로 종료된다.

  • 다른 프로세스 (부모 프로세스 등) 이나 OS 에 의해서 종료된다.
  • kill(pid, signal) : 시스템 콜 kill 은 다른 프로세스의 식별자(pid) 나 또는 그룹에 signal 을 보내서 종료할 수도있고,
  • abort() : 또는 abort 함수를 통해 종료할 수도 있다.

zombie state (좀비 상태) 에 있는 프로세스를 zombie 라고한다.

  • 정의 : 프로세스는 더 이상 실행될 수 없는, 죽은 프로세스인데 커널이 바로 그 프로세스의 PCB 이라던지 일부 레코드가 지울 수 없고 남아있는 어정쩡한 상태이다.
    • exit status : 종료 상태값 -> 명령어가 수행에 설공했으면 0을, 아니면 0이 아닌값을 출력한다.

exit status, 즉 종료상태 값은 parent process 에게 전달되어야 하는데, 그 값이 일반적으로 PCB 의 어떤 일부나, 또는 어떤 레코드에 연결되있을 것이다. 또는 parent process 가 그 값을 가져갈 수 있는 것이다.
만약 parent process 가 그 값을 안가져가면 얘를 무작정 child process 가 종료했다고 막 삭제하게 되면 해당 정보가 손실될것이다. 그래서 그 parent process 가 값을 가져갈 수 없다.

보통 UNIX 계열에서는 fork 를 하면, child process 가 하나 생길것이고, child process 가 exit 를 하면 보통 signal 이 parent process 에게 간다. 그 다음 parent process 는 해당 signal 값을 어떻게 가져가나면 wait 시스템 콜을 활용한다.
wait 시스템 콜 함수의 파라미터로 아까 봤듯이 wait(&pos) 이렇게 정수값 주소를 설정하면 해당 주소의 프로그램에 대한 리턴값이 저장되게된다.

child process 가 종료된 후에 이 부모 process 에 의해서 이렇게 wait 시스템 콜에 의해 전달될 때 까지 이 어정쩌한 상태를 zombie state 라고한다.

Reaping

OS 적으로 waiting 시스템 콜을 호출하면 child 프로세스의 exit status 값을 parent process 에 전달하는 그 과정을 reaping 이라고 한다. 즉, 그 exit status 값이 전달되는 과정을 말하는것이다.

즉 프로세스가 terminated state, 즉 종료된 상태인데 부모에 의해서 그 종료상태값이 전달(reape) 될 때까지의 그 시간을 좀비 상태라고 하는것이다.



wait 시스템콜은 parent 프로세스가 fork 를 하고 child 프로세스가 실행하면 exec() 시스템 콜을 하면 child process 의 이 종료상태값이 wait(&status) 이렇게 인자로 전달되게 하는것이다.
그래서 parent procerss 는 기다리는 상태, 즉 blocked 상태가 된다. 이 child process 가 종료됐을때 wake up 해서 값을 즉각적으로 얻어올 수 있고, 좀비 프로세스도 만들지 않게된다.

즉, 유저래밸에서 multi processing 환경에서 parent process 가 child process 를 기다리고 종료될 때 까지 동기화돼서 되는 실행흐름을 가져올 수 있도록 지원하는 모델로써 사용될 수 있는 기능이다.


부모 프로세스가 자식 프로세스보다 먼저 종료하게 될때 그 종료된 프로세스의 저식 프로세스를 Orphan 프로세스라고 하고, 보통 그 first process (init process) 를 양육하는 과정을 진행하게 된다.

profile
블로그 이전했습니다 🙂 : https://haon.blog

0개의 댓글