[시스템프로그래밍]#2

p식p식·2024년 4월 9일

시스템프로그래밍

목록 보기
2/2

Creating Processes

  • 이번 페이지에서는 fork와 비롯한 parent process와 child process의 관계를 살펴볼 것이다.

fork()

int fork(void)

child proces에 0을 return한다, child의 pid를 parent process에 return 한다.

PID란
PID는 Process ID의 줄임말이다. 운영체제에서 Process를 식별하기 위해서 부여하는 번호를 의미한다.
PPID란
PPID는 Parent Process ID의 줄임말. 즉 부모 프로세스의 PID를 의미한다.

Child는 거의 부모와 동일하다. (= 복사됨)


부모와 자식의 그대로 복사됨. 즉 Process가 2개가 됨.
fork의 중간 단계가 copy된 fork 중간에서부터 2개가 같이 돌아감.
하지만 그 이후에 일어나는 것은 다르게 된다(PID도 다르다.)

fork()는 한 번 호출할 때마다 두 번의 return값을 가진다.

parent와 child process는 structure가 동일하고, 같은 메모리에서 동작하며, context switching을 통해서 번갈아가면서 돌아간다.

  • 실행 상태의 저체 복사본 만들기
    하나는 부모로, 다른 하나는 자식으로 지정
    부모 또는 자식의 실행을 resume한다.
    (optimization:(최적화) 기록 중 복사를 사용하여 RAM 복사를 방지)

(교수님게서 parent와 child가 동일하게 중첩되는 부분이 있으니까 메모리적으로 비효율에 가깝다는 말씀을 하셨다. 이에 따라 virtual memory의 개념이 등장했다고 했다.)

fork example

Example 1

  1. 한 번 호출될 때, return을 두번 한다.
  2. 부모와 자식의 실행 순서는 예측할 수 없다, 동시에 실행이 된다.
  3. Duplicate(중복)되지만 별도의 주소 공간을 갖는다(부모와 자식이), fork가 부모와 자식으로 반환될 때, x의 값은 1이다. x에 대한 후속 변경 사항(subsequent changes to x)는 독립적이다.

우선 pid_t pid; int x;가 선언된 부분은 stack에 저장된다. 나머지는 code에 저장된다. 코드에서 볼 수 있듯이 fork는 두번 return을 하게 되는데 child process에 0을, child's PID를 부모에게 return한다.

pid = Fork();에서 pid == 0이라면 child process라는 뜻이다. 나머지는 else 부분은 parent process부분이다. 위 설명에서 볼 수 있듯이 parent와 child 중 누가 먼저 실행 될지는 모른다. 만약 parent가 먼저 실행된다면, x=1에서 x=0이 되고, print 출력이 된다. 그 후, child가 실행되면 x=2가 된다. 반대의 경우, 순서가 달라지게 된다.

fork 함수를 호출하는 프로세스는 부모 프로세스가 되고 새롭게 생성되는 프로세스는 자식 프로세스가 됩니다.

fork 함수에 의해 생성된 자식 프로세스는 부모 프로세스의 메모리를 그대로 복사하여 가지게 됩니다.

그리고 fork 함수 호출 이후 코드부터 각자의 메모리를 사용하여 실행됩니다

Modeling fork with Process Graphs

Process graph란 이 fork()를 통해 두번 return될 때 헷갈리게 떄문에 ordering을 파악하기 위한 그래프이다.

이 코드는 위와 동일하다. 이를 vertex와 그래프들로 표현을 하면,

이 graph들을 통해서 topological sort를 생각할 수 있다. topological sort는 올바르게 배열된 형태인데,

우선 모든 vertex를 알파벳으로 만들고 순서를 생각해보자

아래의 질문에 답해보자면, vertex를 순서대로 가지 않고 되돌아오는 경우가 있기 때문에 infeasible이라고 할 수 있다.

Example 2

이 코드에서는 fork()가 2번 call되는데, 이 말은 fork()로 인한 process의 개수, 즉 return의 개수가 총 4개라는 것을 알 수 있다.

graph에서 맨 아랫줄은 parent의 경우라고 가정한다.

이 경우 L0 -> L1 -> Bye -> Bye -> L1 -> Bye -> Bye

L0 -> Bye -> L1 -> Bye -> L1 -> Bye -> Bye

Bye -> L1의 부분에서 topological sort를 만족하지 않는다.

Example 3

Reaping Child Processes

Process가 종료되어도 system resource를 계속 사용한다.
UNIX 기반 OS는 자식을 reap하기 위해 부모 프로세스가 필요하다.(wait과 waitpid)를 사용해서.

  • 여기서 말한 reap이란, 뒷처리를 뜻한다. 자식 프로세스가 종료되었을 때, 부모 프로세스는 자식 프로세스가(terminated되었지만) 계속 사용하고 있는 것을 뒷처리를 해줘야 한다. 이 때 뒷처리(reap)을 하기 위해 자식 프로세스가 종료될 때까지 부모 프로세스가 기다리는 것을 wait 함수가 담당한다.

여기서 wait(), waitpid() 함수는 부모 프로세스는 자식 프로세스가 종료될 때까지 아무 일도 하지 않고 기다리게 해준다. waitpid는 특정 프로세스가 종료되길 기다리도록 지정할 수 있다. wait함수는 자식 프로세스의 status 정보를 얻어올 수 있다.

Zombie process

자식 프로세스가 종료되었지만 부모가 wait이나 waitpid()를 통해 자식을 reap하지 않았을 때, 이 종료되었지만 system resource를 계속 소비하는 자식 프로세스를 zombie process라고 한다.

  • '이 system resource를 계속 소비하는 것'과 '프로세스 테이블에 항목이 있다'는 동의어다.

  • 부모가 wait, waitpid를 call하게 되면, child는 보통 제거된다.

  • 모든 프로세스는 일반적으로 잠깐만 좀비 프로세스로 존재할 수 있다.

Orphan process

자 우리는 올바르게 프로세스를 그만두려면, 자식 프로세스가 terminated되고, 부모 프로세스가 wait()이나 waitpid() 함수를 통해서 종료된 자식 프로세스의 뒷처리와 자식 프로세스의 status 정보를 얻어오게끔 하여야 한다. 만약, 부모 프로세스가 먼저 종료되었다면 어떡할까? 이 경우를 orphan process라고 한다.

이때까지 배운 것을 바탕으로 한다면, 위 질문의 답은 자식 프로세스가 올바르게 종료되고, reap될 가능성이 없어보인다. 하지만, 우려와 달리, init process가 이 자식의 부모를 담당하게 되고, 이 init 프로세스는 모든 orphan 프로세스를 주기적으로 reap하게 된다.

그림을 살펴보면, parent의 pid는 6639임을 알 수 있고, child의 pid는 6640임을 알 수 있다.

첫번째 ps 명령어 이후의 결과를 보자면, forks 라는 구문이있다. 이는 6640의 child process가 종료되었지만, 아직 reap되지 않은 상태이다. 또한, 아직까지 process table에 존재하기 때문에 이를 좀비 프로세스라고 일컫는다. 코드를 보면 이 현상의 이유는 parent process가 while의 무한 루프를 돌고 있기 떄문이라고 알 수 있다. 이 경우, parent process를 kill 명령어를 통해서 parent를 없앴다. 따라서, 위 orphan process같이 reap 해갈 process가 없게 되어,init 프로세스가 이를 담당하게 되고 reap해서 좀비프로세스를 없앤다.

이 경우 위의 예시와는 반대로 child process 도중, 무한히 반복되는 while loop에 빠져, child process가 종료되지 않는다. linux 명령어의 결과들을 살펴보면, Parent는 6675, child는 6676이다. parent는 종료되어 첫번째 ps 명령어의 결과물에는 6676의 child process가 계속 존재한다. 이 경우를 orphan process라고 하며, 명시적으로 자식을 kill 하는 구문이 필요하다. 그렇지 않은 경우에는 무한히 계속 실행된다.

orphan process의 경우에는 init프로세스가 자동으로 wait 함수를 호출하며 reap 하는 것이기 떄문에 아무런 액션을 취하지 않아도 해결되지 않을까? 이 경우에는 child process가 while loop에 빠져 계속 실행이 되므로, wait 함수로 cover하기 불가능하다. 따라서, 자식 프로세스를 kill하는 것이 필수적으로 필요하다. 그 외의 경우는 질문을 통해서 알아봐야 할 것 같다.

wait 함수

위에서 설명했듯이 부모 프로세스는 wait 함수를 통해서 child를 reap 할 수 있다.

int wait(int *child_status)

자식 중 하나의 자식이 종료돨 때까지 현재 프로세스를 일시 중단한다.
Process의 PID(종료된 자식 프로세스의)가 return 값이다.

파라미터로 받는 child status가 NULL이 아니면 가리키는 정수는 자식이 종료된 이유와 종료 상태를 나타내는 값으로 설정된다.

자식의 경우 "HC~"를 출력하고 exit을 한다.
부모의 경우, "HP~"를 출력하고, 자식 process를 wait한다, 그 후 자식 프로세스가 종료되었을 때 "CT를 출력하고, Bye를 출력하며 종료한다.

자식이 먼저 시작했을 경우에,
HC -> HP -> exit -> CT -> Bye.
부모가 먼저 시작했을 경우에,
HP -> HC -> exit -> CT -> Bye.

Another wait example

우선 자식 프로세스의 상태들을 나타내는 매크로들을 살펴보면,

  1. WIFEXITED
  2. WEXITSTATUS 첫번째로 WIFEXITED는 자식이 정상적으로 종료되었다면 Non-zero이다. WEXITSTATUS는 exit()를 호출하기 위한 인자나 return 값이 설정되고
    종료된 자식의 반환 코드의 최하위 8비트를 평가한다.

다시 설명하자면 WIFEXITED는 exit, _exit, _Exit 혹은 main에서의 return으로 종료되었는지 확인한다, WEXITSTATUS는 WIFEXITED의 값이 참인 경우에 exit code를 알아낸다.

위 코드에서, pid_t wpid = wait(&child_status),

wait(int * status)의 함수에서 status는 종료 상태를 담을 포인터이다. 이 status를 위 매크로를 통해서 알 수 있다.

execve()

int execve(char * filename, char * argv[], char *envp[])

execve는 filename이 가리키는 파일을 실행한다. 이 파일은 바이너리 실행파일이거나 스크립트 파일이여야 한다.  
argv와 envp는 포인터 배열로 filename의 인자로 들어간다. 마지막에 NULL문자열을 저장해야 한다.

만약

/bin/ls -lt /usr/include

라는 명령어를 자식 프로세스에서 실행했다고 가정하면, argv[]에 공백기준으로 하나씩 삽입된다. argc는 3이다.

envp는 main() 인수 중에 환경 변수값을 가진 인수이다. 프로그램을 실행하기 위한 환경 변수값을 담고 있는 char **envp가 있으면, envp의 각 문자열은 환경변수와 같이 = 문자로 분리되어 있다. shell에서 printenv 명령어를 통해서 확인할 수 있다.

profile
이것저것그것

0개의 댓글