원본 논문

초기와 현대의 UNIX Process Control 방식 차이

  • File system 의 경우에는 핵심적인 구조는 이전이나 지금이나 크게 바뀌지 않았지만, Process control 의 경우에는 많은 것이 바뀌었다.
  • 지금의 UNIX 운영체제는 다음과 같이 프로세스들을 실행한다:
    1. Shell 은 terminal 로부터 command line 을 파일로 읽어들인다.
    2. Shell 이 fork 을 해 child process 를 만든다.
    3. Child process 는 파일에 있던 command 를 exec 한다.
    4. 그동안은 parent process (shell) 은 child process 가 exit 을 하여 종료될 때까지 wait 한다.
    5. 1번 과정을 반복한다.
  • 하지만 초기의 UNIX 운영체제에는 Process 의 개념은 있었지만 fork, wait, exec 은 없었고 exit 도 지금과는 살짝 달랐다.
  • 초기의 UNIX 운영체제는 다음과 같이 작동하였다.
    1. Shell 은 기존에 open 되어 있던 파일들을 전부 close 하고, stdin 과 stdout 두개의 파일을 연다.
    2. Shell 은 terminal 로부터 command line 을 파일로 읽어들인다.
    3. Shell 은 command 파일을 열고, 메모리 상단에 자그마한 bootstrap program 을 적재한 후, bootstrap program 으로 점프한다. 이 boottrap program 은 command 파일의 내용을 shell code 위에 overwrite 하고, 그쪽으로 jump 를 해 command 를 실행한다.
    4. Command 가 완료된 다음 exit 을 호출하면, os 가 command code 위에 새로운 shell code 를 overwrite 하고, 실행시킨다.
  • 위의 초기 UNIX 구현에서 짚고 넘어가야 할 것은 추후에 추가되어야 할 기능들이 어느정도 예측되어서 구현에 반영되었다는 것이다.
    • 백그라운드 프로세스나 파이프 등이 구현되어 있지는 않았지만,
    • IO redirection 은 stdout 과 stdin 을 그냥 다른 파일로 대체하면 된다는 점에서 어렵지 않게 구현될 수 있었고
    • Shell 을 kernel 의 일부가 아닌 user level program 으로 구현하는 것도 가능하게 했다.
  • 따라서 parent/child process 같은 개념이 없었기 때문에, terminal 당 process 는 1개였다.
    • 그리고 초기의 UNIX 는 terminal 을 2개 지원했기에, 총 두개의 프로세스가 작동된다고 할 수 있다.
    • 이것은 UNIX 이전의 Multics 나 CTSS 같은 운영체제의 영향을 받은 것이었다.
  • Terminal 당 process 가 하나인 것은 여러모로 문제를 일으켰다.
    • Shell 이 매번 실행될 때마다 모든 파일을 닫았기에, stdin 과 stdout 은 매번 다시 열어줘야 했다.
    • 매번 fresh shell process 가 실행되었기에, command 실행 간에 메모리 상태를 유지하는 것도 불가능 했다.
    • 또한 각 디렉토리에 tty 라는 특별한 파일을 가지는 것이 요구되었다.
      • 이 파일은 디렉토리를 연 터미널을 참조하도록 되어 있고 만일 이 파일이 없다면 shell 은 무한루프에 빠지게 된다… 왜그런지는 모르겠다.

초기 UNIX 에서의 개선 과정

forkexec 구현하기

  • 놀라운 점은 기존의 Process control 지금의 그것 (fork 를 활용하는) 것으로 개선하는 것은 설계와 구현까지 단 며칠밖에 걸리지 않았다는 점이다.
  • 이것은 아래의 세가지 사실을 적절히 활용한 것이라 생각할 수 있을 것이다:
    1. 앞에서도 언급했다시피, 하나의 process 에서 code 만 바꿔 프로그램을 exec 하는 것은 당시에는 흔한 방법이었다.
    2. Thompson 은 이미 기존의 Berkeley Time-sharing System 에서 fork 와 exec 을 분리하여 프로세스를 관리한다는 것을 잘 알고 있었다.
    3. 기존의 UNIX 에서는 이미 두개의 터미널에서 각각 프로세스를 돌리고 있었기 때문에 프로세스를 여러개 돌리는 것에 대비가 되어 있었던 상황이었다.
  • 따라서 이것은 이렇게 활용될 수 있었다:
    • (2) 번을 이용해 Process 관리에 fork 와 exec 을 분리하는 아이디어를 얻고
    • (3) 번에서 2개의 프로세스를 실행시키는 것을 그보다 더 많은 프로세스를 실행시키는 것으로 확장하여 fork 를 구현했으며
    • fork 를 통해 parent process 와 동일한 process 를 생성한 뒤에 (1) 번에서 했던 것처럼 code 만 바꿔 프로그램을 실행하는 식으로 exec 을 구현했다.
    • 즉, fork 를 구현할 때에는 다음의 두 가지만 요구되었었다.
      1. 기존의 Process table 을 확장하고
      2. 기존의 swap IO 방식 (두 개의 process 를 와리가리하기 위해 swapping 하던 것) 을 그대로 사용해 현재의 process 를 disk swap area 에 복사하고 process table 을 수정하는 방식으로 fork 함수를 구현하는 것
  • 위와 같은 방법으로 단 27줄의 assembly code 만을 추가해 fork-exec 기능을 구현해 냈다.

exit 구현하기

  • 이전에는 새 shell code 를 현재 process code 에 overwrite 하는 방식이었다면,
  • 새로운 exit 을 구현하는 것은 보다 더 단순했다:
    • 그냥 Process table entry 를 비워버리고,
    • CPU 점유를 포기해 버리면 되었기 때문

wait 구현하기

  • 초기 UNIX 에는 다음의 두 함수가 있었다:
    • smes(pid, message): 이것은 특정 pidmessage 를 보내고 수신자가 메세지를 읽을 때까지 기다린다.
    • (pid, message) = rmes(): 메세지가 올때가지 기다리고, 메세지가 오면 해당 메세지의 송신자 (pid) 와 내용 (message) 를 반환한다.
  • 따라서 smes 을 이용하면 wait 은 다음과 같은 방식으로 구현될 수 있었다:
    1. Parent process 가 child process 를 fork 한다.
    2. Parent process 에서 실행한 fork 함수는 child process 의 pid 를 반환하고, parent process 는 이 pid 와 smes 를 이용해 child process 에게 메세지를 보낸다.
    3. Child process 는 이 메세지를 읽지 않고 command 를 실행한 뒤, 종료된다.
    4. 그럼 parent process 의 입장에서는 child process 가 실행되던 중에는 계속 대기하다가, child process 가 종료되면 process 를 찾을 수 없다는 에러를 반환한다.
    5. Parent process 가 기다리게 하기 위해 child process 에서 의도적으로 메세지를 읽지 않은 것이기 때문에, 위의 에러는 무시한다.
  • rmes 를 이용하면 init process 의 wait 과정도 구현할 수 있다:
    1. Init process 는 터미널 당 두개의 shell process 를 생성한다.
    2. 그리고 rmes 를 걸어 놓는다.
    3. 각 shell 은 명령어 file 을 실행한 뒤에, init process 에게 I am terminating 이라는 메세지를 smes 로 보낸다.
      • Init process 의 pid 는 무조건 1 이기 때문에, shell 은 init process 의 pid 를 알아내야할 필요가 없다.
    4. Init process 는 메세지를 받은 후 대기가 풀리고, 다시 해당 터미널에 대해 shell 프로세스를 실행하고 2번 과정을 반복한다.
  • smesrmes 가 더 범용적인 기능을 제공하기는 하지만, 지금은 wait 만 있고 이 두 기능은 사라졌다.
    • 왜냐면 smesrmes 가 이 wait 기능을 구현하는 것 이외에로는 사용되지 않았기에, 덜 범용적이지만 목적이 확실한 wait 로 인터페이스를 축소시키는 것이 좋았고
      • 아마 wait 을 구현하는 데에는 사용되지만 process 에서 smes, rmes 를 syscall 같은 것으로 호출하지는 못한다 정도의 의미인 것 같다.
    • Shell 이 smes 로 child process 를 기다리고 있었는데, child process 에서 어떤 이유로든 rmes 를 사용하면 smes 로 보낸 메세지가 읽히기 때문에 shell 이 다른 command 를 읽어 들여 child process 가 비정상적으로 종료되는 문제가 있기 때문이다.
  • 이런 wait 구현 방식은 background process 등의 기능도 아주 손쉽게 구현할 수 있도록 해주었다고 한다.

Aftereffects: 구현 이후의 부작용들

chdir 문제

  • Process control 방식을 바꾼 뒤 발견한 첫번째 문제는 chdir 명령어를 실행해도 현재 디렉토리가 바뀌지 않는다는 것이었다.
  • 이것은 좀만 생각해 보면 당연한 일이다:
    • chdir 을 실행할 때 child process 가 fork 될 것이고
    • 해당 프로세스는 현재 문맥에서 디렉토리를 변경하기 때문에
    • chdir 을 실행한 parent process 에서는 디렉토리가 변경되지 않는다.
  • 따라서 이와 비슷한 현상을 보이는 명령어들은 새로운 프로세스가 fork 되지 않고 shell 내부에서 처리되도록 변경되었다고 한다.

I/O redirection 문제

  • 두번째로 발견한 문제는, 여러개의 command 들을 파일에 작성해서 실행시키고, 그 결과를 redirect 할 때 stdout 이 씹히는 것이었다.
  • 가령, 다음과 같은 script (파일 이름: comfile) 를 작성했다고 해보자.
ls
who
  • 이때, 다음과 같이 실행할 경우 ls 의 결과가 output 파일에 저장되지 않는 문제가 있었다.
sh comfile > output
  • 문제의 원인은 파일을 열었을 때 파일에 대한 read/write pointer 가 파일을 연 프로세스에 귀속된다는 것이었다.
  • 문제의 원인은 다음과 같았다:
    1. 메인 shell 은 child process (1) 을 생성한다.
      • Child process (1) 은 comfile 이 실행되는 shell process 이다.
    2. Child process (1) 는 stdout 을 리다이렉트하기 위해 outfile 을 open 한다.
      • 즉, outfile 에 대한 r/w pointer 는 child process (1) 에 귀속된다.
    3. Child process (1) 은 새로운 child process (2) 를 fork 한다.
      • Child process (2) 는 ls process 이다.
    4. Child process (2) 는 child process (1) 로 부터 r/w pointer 를 상속받아 outfile 에 결과를 입력한 뒤, 종료된다.
    5. Child process (1) 은 새로운 child process (3) 을 fork 한다.
      • Child process (3) 은 who process 이다.
    6. 마찬가지로 child process (3) 도 child process (1) 로 부터 r/w pointer 를 상속받아 outfile 에 결과를 입력해야 하는데 이런 시상에! child process (3) 의 결과는 child process (2) 의 결과를 덮어쓰게 된다.
      • 이것은 child process (2) 가 outfile 에 입력하며 write pointer 를 옮겼지만, child process (1) 한테는 반영이 안되기 때문에, child process (3) 은 옮겨지지 않은 write pointer 를 상속받았기 때문이다.
  • 따라서 위와 같은 문제를 해결하기 위해 r/w pointer 를 파일을 open 한 프로세스에 귀속시키지 않고 system level 에서 관리하기 위해 IO pointer 들을 담은 system table 을 추가했다고 한다.