원본 논문
- 이 글은 Dennis M. Ritchie - The Evolution of the Unix Time-sharing System 논문을 읽고 정리한 글입니다.
- 별도의 명시가 없으면, 본 논문에서 그림을 가져왔습니다.
초기와 현대의 UNIX Process Control 방식 차이
- File system 의 경우에는 핵심적인 구조는 이전이나 지금이나 크게 바뀌지 않았지만, Process control 의 경우에는 많은 것이 바뀌었다.
- 지금의 UNIX 운영체제는 다음과 같이 프로세스들을 실행한다:
- Shell 은 terminal 로부터 command line 을 파일로 읽어들인다.
- Shell 이 fork 을 해 child process 를 만든다.
- Child process 는 파일에 있던 command 를 exec 한다.
- 그동안은 parent process (shell) 은 child process 가 exit 을 하여 종료될 때까지 wait 한다.
- 1번 과정을 반복한다.
- 하지만 초기의 UNIX 운영체제에는 Process 의 개념은 있었지만 fork, wait, exec 은 없었고 exit 도 지금과는 살짝 달랐다.
- 초기의 UNIX 운영체제는 다음과 같이 작동하였다.
- Shell 은 기존에 open 되어 있던 파일들을 전부 close 하고, stdin 과 stdout 두개의 파일을 연다.
- Shell 은 terminal 로부터 command line 을 파일로 읽어들인다.
- Shell 은 command 파일을 열고, 메모리 상단에 자그마한 bootstrap program 을 적재한 후, bootstrap program 으로 점프한다. 이 boottrap program 은 command 파일의 내용을 shell code 위에 overwrite 하고, 그쪽으로 jump 를 해 command 를 실행한다.
- 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 에서의 개선 과정
fork
와 exec
구현하기
- 놀라운 점은 기존의 Process control 지금의 그것 (fork 를 활용하는) 것으로 개선하는 것은 설계와 구현까지 단 며칠밖에 걸리지 않았다는 점이다.
- 이것은 아래의 세가지 사실을 적절히 활용한 것이라 생각할 수 있을 것이다:
- 앞에서도 언급했다시피, 하나의 process 에서 code 만 바꿔 프로그램을 exec 하는 것은 당시에는 흔한 방법이었다.
- Thompson 은 이미 기존의 Berkeley Time-sharing System 에서 fork 와 exec 을 분리하여 프로세스를 관리한다는 것을 잘 알고 있었다.
- 기존의 UNIX 에서는 이미 두개의 터미널에서 각각 프로세스를 돌리고 있었기 때문에 프로세스를 여러개 돌리는 것에 대비가 되어 있었던 상황이었다.
- 따라서 이것은 이렇게 활용될 수 있었다:
- (2) 번을 이용해 Process 관리에 fork 와 exec 을 분리하는 아이디어를 얻고
- (3) 번에서 2개의 프로세스를 실행시키는 것을 그보다 더 많은 프로세스를 실행시키는 것으로 확장하여 fork 를 구현했으며
- fork 를 통해 parent process 와 동일한 process 를 생성한 뒤에 (1) 번에서 했던 것처럼 code 만 바꿔 프로그램을 실행하는 식으로 exec 을 구현했다.
- 즉, fork 를 구현할 때에는 다음의 두 가지만 요구되었었다.
- 기존의 Process table 을 확장하고
- 기존의 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)
: 이것은 특정pid
에message
를 보내고 수신자가 메세지를 읽을 때까지 기다린다.(pid, message) = rmes()
: 메세지가 올때가지 기다리고, 메세지가 오면 해당 메세지의 송신자 (pid
) 와 내용 (message
) 를 반환한다.
- 따라서
smes
을 이용하면 wait 은 다음과 같은 방식으로 구현될 수 있었다:- Parent process 가 child process 를 fork 한다.
- Parent process 에서 실행한 fork 함수는 child process 의 pid 를 반환하고, parent process 는 이 pid 와
smes
를 이용해 child process 에게 메세지를 보낸다. - Child process 는 이 메세지를 읽지 않고 command 를 실행한 뒤, 종료된다.
- 그럼 parent process 의 입장에서는 child process 가 실행되던 중에는 계속 대기하다가, child process 가 종료되면 process 를 찾을 수 없다는 에러를 반환한다.
- Parent process 가 기다리게 하기 위해 child process 에서 의도적으로 메세지를 읽지 않은 것이기 때문에, 위의 에러는 무시한다.
rmes
를 이용하면 init process 의 wait 과정도 구현할 수 있다:- Init process 는 터미널 당 두개의 shell process 를 생성한다.
- 그리고
rmes
를 걸어 놓는다. - 각 shell 은 명령어 file 을 실행한 뒤에, init process 에게
I am terminating
이라는 메세지를smes
로 보낸다.- Init process 의 pid 는 무조건
1
이기 때문에, shell 은 init process 의 pid 를 알아내야할 필요가 없다.
- Init process 의 pid 는 무조건
- Init process 는 메세지를 받은 후 대기가 풀리고, 다시 해당 터미널에 대해 shell 프로세스를 실행하고 2번 과정을 반복한다.
smes
와rmes
가 더 범용적인 기능을 제공하기는 하지만, 지금은 wait 만 있고 이 두 기능은 사라졌다.- 왜냐면
smes
와rmes
가 이 wait 기능을 구현하는 것 이외에로는 사용되지 않았기에, 덜 범용적이지만 목적이 확실한 wait 로 인터페이스를 축소시키는 것이 좋았고- 아마 wait 을 구현하는 데에는 사용되지만 process 에서
smes
,rmes
를 syscall 같은 것으로 호출하지는 못한다 정도의 의미인 것 같다.
- 아마 wait 을 구현하는 데에는 사용되지만 process 에서
- 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 가 파일을 연 프로세스에 귀속된다는 것이었다.
- 문제의 원인은 다음과 같았다:
- 메인 shell 은 child process (1) 을 생성한다.
- Child process (1) 은
comfile
이 실행되는 shell process 이다.
- Child process (1) 은
- Child process (1) 는 stdout 을 리다이렉트하기 위해
outfile
을 open 한다.- 즉,
outfile
에 대한 r/w pointer 는 child process (1) 에 귀속된다.
- 즉,
- Child process (1) 은 새로운 child process (2) 를 fork 한다.
- Child process (2) 는
ls
process 이다.
- Child process (2) 는
- Child process (2) 는 child process (1) 로 부터 r/w pointer 를 상속받아
outfile
에 결과를 입력한 뒤, 종료된다. - Child process (1) 은 새로운 child process (3) 을 fork 한다.
- Child process (3) 은
who
process 이다.
- Child process (3) 은
- 마찬가지로 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 를 상속받았기 때문이다.
- 이것은 child process (2) 가
- 메인 shell 은 child process (1) 을 생성한다.
- 따라서 위와 같은 문제를 해결하기 위해 r/w pointer 를 파일을 open 한 프로세스에 귀속시키지 않고 system level 에서 관리하기 위해 IO pointer 들을 담은 system table 을 추가했다고 한다.