출처 서울대학교 홍성수 교수님, [K-MOOC] 운영체제의 기초, 4주차 Processes and Threads
프로세스 생성 방식
윈도우는 OS가 프로세스의 상태를 하나하나 만든다. 그러면 새로 생성된 프로세스도 Context Switching을 할 수 있는 상태들을 가지게 된다.
반면 유닉스 시스템은 기존의 프로세스를 복제하는 클론, fork를 하는 방식으로 프로세스를 생성한다.
초기 프로세스 생성 방식
유닉스 시스템도 초기에 운영체제를 부팅할 때 처음 생성되는 프로세스(process id가 0이다) 한 번은 직접 만든다.
1. 코드와 데이터를 executable file로부터 메인 메모리에 로드한다.
2. 빈 런타임 스택을 생성한다.
3. PCB를 초기화한다.
4. 새로운 프로세스를 ready list에 넣는다.
그 이후의 프로세스 생성 방식
부모 프로세스를 클론하여 자식 프로세스를 만든다.
- 현대 프로세스를 중지시키고 그것의 상태를 저장한다.
- 기존 프로세스(부모 프로세스)의 코드, 데이터, 스택, PCB를 복사함으로써 새로운 프로세스(자식 프로세스)를 생성한다.
- 물론 자식 프로세스는 자신만의 고유한 process id를 부여받는다. - 새로운 프로세스를 ready list에 넣는다.
fork()
clone을 시작하는 시점은 부모 프로세스 안에서 시스템 콜 fork()를 호출했을 때이다. 이 fork() 함수 수행이 끝나고 부모 프로세스로 다시 돌아오면 자식 프로세스가 생성되는 것이다.
exec()
fork() 시스템 콜로 프로세스를 생성하면 모든 프로세스는 모두 하나의 실행 파일만 수행하게 되니 문제가 된다. exec() 시스템 콜도 수행해한다. exec()은 자식 프로세스 안에서 호출되는 시스템 콜이다. exec()은 자식 프로세스를 위한 새로운 실행 파일을 찾아서, 부모 프로세스로부터 복사한 데이터 세그먼트에 오버라이드를 해서 새로운 프로그램 코드로 수행하게 만들어준다.
wait()
자식 프로세스가 수행을 종료할 때까지 부모 프로세스는 block시키는 것이다. 자식 프로세스가 exit()할 때까지 기다린다.
Unix 운영체제가 운영체제 부팅을 할 때 형성하는 프로세스의 가계도 (Family Tree)
초기에는 프로세스 0이 핸드크래핑 방식으로 생성된다. 프로세스 0은 여러 가지 일을 하다가 운영체제 시스템이 완전히 부팅을 하게 되면 memory management를 담당하는 swapper라는 프로세스의 역할을 수행한다. 이 프로세스 0은 두 개의 프로세스를 fork한다. 하나는 Init (프로세스 1)이고 다른 하나는 PageDaemon (프로세스 2)이다.
Init 프로세스
Init 프로세스는 컴퓨터 시스템이 해야 하는 서버로서의 기능을 설정한다. 또한 운영체제가 관리하는 터미널 라인을 초기화시키고 터미널 라인에 유저가 로그인을 하고 컴퓨터 시스템을 사용할 수 있게 준비시킨다.
ttys라는 터미널 라인 초기화 작업을 수행하는데 이것은 getty라는 프로세스를 fork한다. 키보드 스트로크가 발생하면 로그인할 수 있게 해주는 것이다. 로그인 아이디가 유효하면 터미널을 오픈시켜준다. 사용자가 명령어를 입력하면 이것을 해석해서 운영체제에게 제공해주는 중간 매개체가 필요한데, 제3의 소프트웨어인 Shell이 그것이다. 가장 널리 쓰이는 shell은 BASH (Born Again Shell)이다. shell은 다른 말로 하면 command line interpreter이다.
다음은 Shell 코드이다.
Shell은 무한 루프를 돌면서 유저의 커맨드라인을 입력받고 해석해서 운영체제의 서비스를 받는다.
무한 루프에서 빠져나오는 것은 로그아웃이다.
부모 프로세스와 자식 프로세스의 코드 데이터는 동일하다. 부모 프로세스가 pid = fork () 수행이 종료된 이후부터 수행을 재개하듯이 자식 프로세스도 (자신은 fork() 함수를 수행한 적이 없지만 마치 fork()를 호출한 것처럼) 여기에서부터 수행을 재개한다. 여기가 자식 프로세스가 수행시키는 첫 번째 명령이다. 자식 프로세스는 return value로 0이 전달된다. exec()을 수행하게 된다. exec의 파라미터는 입력받은 커맨드 라인이다.
왜 fork ()로 프로세스를 생성할까?
자식 프로세스가 exec()을 수행한다는 것은 부모 프로세스로부터 복제한 모든 state 정보를 버리고 새로운 것을 파일에서 읽어온다는 뜻이다.
굳이 비싼 복제 operation을 하고 그 결과를 버리고 오버라이드하는 것은 불합리해보이기도 한다. 그런데도 UNIX보다 최신 운영체제인 리눅스 운영체제는 여전히 이 fork() 메커니즘을 사용한다. 왜일까?
초창기 UNIX 시스템은 매우 간단한 형태였고 IPC (Interprocess communication mechanism)이 빠져 있었다.
IPC를 하려면 메모리를 공유해야 한다. 그런데 각각의 프로세스는 자신만의 논리 메모리 주소를 가지고 이 주소들을 서로 격리되어 있어서 공유 메모리를 통한 IPC의 구현에는 한계가 있었다. 당시에는 성능이 중요한 시기가 아니었기 때문에 파일을 통해 데이터를 공유했다. 그런데 이때의 또 원천적인 문제는 파일 이름을 공유하기 위해서 또 IPC가 필요하다는 것이다. 최소한 파일 이름은 공유할 수 있도록 하기 원했던 것이 초창기 UNIX 연구자들의 목표였을 것이다.
이렇게 하면 자식 프로세스가 부모 프로세스가 open한 file descriptor를 읽을 수가 있다. 부모 프로세스와 자식 프로세스가 기본 정보를 공유할 수 있게 되는 것이다. 지금은 memory management 기술이 많이 발전해서 공유 메모리도 가능하고 다른 기술들도 사용할 수 있다.
왜 아직도 fork ()를 사용할까?
성능상 단점도 있고 복잡한 데 왜 아직도 fork ()를 사용할까?
지금은 deep copy를 하지 않고 얕은 복사를 함으로써 오버헤드를 극복했기 때문이다.
데이터 콘텐츠의 base 주소만 복제한다. 4개의 세그먼트 시작 주소는 PCB에 저장되어 있다.
단, 이 기법의 단점이 있다. 자식 프로세스가 부모 프로세스를 복제하고 나서 write를 하게 될 수가 있다. (코드 세그먼트는 read만 일어나서 계속 공유할 수 있다.) 이런 경우를 위해 copy-on-write 기법을 이용한다. 어떤 세그먼트에 write가 일어나게 되면 그때 비로소 두 개의 분리된 데이터 세그먼트로 만들어주자는 것이다. lazy copy operation이다.
프로세스 종료
프로세스 종료는 두 군데서 trigger된다.
1. 자식 프로세스가 수행을 종료했을 때 자신의 수행을 종료시키는 것이다. exit()를 호출한다.
2. 자식 프로세스를 제3자 (부모 프로세스)가 수행을 종료시키는 것이다. abort
exit()하면 할당받은 자원들을 반환해준다. 컴파일러는 자동으로 exit()을 호출해준다.
어떤 자식 프로세스가 부모 프로세스가 없는 고아라면 문제가 없다. 그런데 부모 프로세스가 wait()를 아직 호출하지 않았는데 자식 프로세스가 죽어버리면 부모 프로세스는 영원히 깨어나지 못하고 기다리게 된다. 이러한 문제를 해결하기 위해서 자식 프로세스가 exit()을 할 때 부모가 아직 wait()을 호출하지 않았다면 parent()에게 자신이 exit()을 했다는 정보를 전달하기 위해 일부 상태 정보는 남겨놓고 나머지 자원만 반환한다. 이런 프로세스 상태를 좀비 상태라고 한다.
고아 프로세스의 경우 프로세스0이 나중에 좀비 상태의 프로세스이 차지하고 있는 자원을 모두 회수한다.
한편 흔히 부모 프로세스가 종료될 때는 자식 프로세스도 같이 종료시킨다.
'Computer System > 운영체제' 카테고리의 다른 글
[운영체제] 04-5. Multithreading (1) | 2025.01.03 |
---|---|
[운영체제] 04-3. Context Switching (0) | 2025.01.03 |
[운영체제] 04-1. Process Concepts (0) | 2024.02.15 |
[운영체제] 03. Stack and Dynamic Memory Allocation of Local Variables (0) | 2024.01.22 |
[운영체제] 02-3. Hardware Protection Mechanisms (0) | 2024.01.22 |