앞서 2번글에서 OS가 프로세스를 위한 인터페이스를 제공한다고 했다.
이번 글에서는 OS가 process API를 어떻게 제공하는지 실제로 코드를 알아보자.
코딩은 UNIX가 제공하는 시스템으로 진행 할 것이다.
The fork() System Call
fork()는 새로운 process를 생성하는 시스템 콜이다.
fork() 시스템 콜을 호출하면 새로운 프로세스를 생성하고, fork()를 호출한 프로세스를 부모 프로세스,
새로 생성된 프로세스를 자식 프로세스라고 한다.
프로세스는 각 프로세스를 식별하기 위해 id를 가지고 있는데, 이를 PID라고 한다.
fork()를 통해 프로세스 생성에 성공 했을 때 새로 생성된 자식 프로세스에는 0을 반환하고, 부모 프로세스에는 자식 프로세스의 pid를 반환한다. 생성에 실패한다면 부모 프로세스에 -1을 반환한다.
그럼 새로 생성된 process는 어떤 실행 파일을 갖고 있을까?
fork()로 새로 생성된 자식 프로세스는 부모 프로세스의 실행 파일을 똑같이 복사해 온다.
따라서 아래 코드를 실행해 프로세스를 생성한다면 자식 프로세스 또한 똑같은 코드를 갖고 실행하게 된다.
단, 처음부터 실행하는 것이 아닌 fork를 통해 자식 프로세스가 생성된 시점부터 실행된다.
그럼 이제 아래 코드를 이해해보자.
부모 프로세스는 6번째 라인에서 hello world를 출력한다.
7번 라인에서, fork()가 실행되고 자식 프로세스가 생성된다.
프로세스 생성에 성공했을 때 부모 프로세스라면 rc변수에 자식 프로세스의 pid가 저장될 것이고,
자식 프로세스라면 0이 저장될 것이다.
8번 라인에서부터의 분기문에서 rc를 통해
부모 프로세스인지 자식 프로세스인지 판별하여 각자 다른 출력문을 출력한다.
프로세스의 흐름을 그려보면 위와 같다.
실제 실행해 보면 output은 다음과 같고 부모 프로세스와 자식 프로세스의 pid가 다른 것을 알 수 있다.
여기서 또 다른 중요한 점은 output이 deterministic하지 않다는 것이다!!!
fork()의 실행 후 프로세스를 생성함으로써 우리는 2개의 프로세스를 갖고 있다.
그럼 두 프로세스 중 어떤게 먼저 실행될까?
위의 output을 보면 부모 프로세스가 먼저 출력되고 자식 프로세스가 나중에 출력되었다.
하지만 이 순서는 결정된 것이 아니다. 반대 또한 가능하다!
즉 프로세스의 실행 순서는 non-determinism하다.
고정적이지 않고 CPU scheduler에 의해 결정된다.
두번째로 중요한 점은 자식 프로세스가 부모 프로세스의 프로그램을 복제해 온다 하더라도 address space가 같지 않다!
즉, 부모 프로세스와 자식 프로세스는 각각의 주소 공간을 갖는다.
같은 변수 값을 복제해 오더라도 변수 저장 주소는 다르다는 것이다.
The wait() System Call
가끔은 부모 프로세스가 자식 프로세스가 끝날 때 까지 기다려야 할 때도 있을 것이다. 이때 사용되는 시스템 콜이 wait()시스템 콜이다.
위의 코드를 실행하면 output은 다음과 같다.
p1.c와 달리 p2.c에서는 무조건 자식 프로세스가 먼저 출력된다.
부모 프로세스가 먼저 실행되더라도 wait()시스템 콜을 통해 자식 프로세스가 종료될 때까지 기다리기 때문이다.
프로세스의 흐름은 다음과 같다.
The exec() System Call
앞선 fork() 시스템 콜은 자식 프로세스가 부모 프로세스와 같은 프로그램을 복제하여 실행하였다.
exec() 시스템콜은 프로세스를 생성하여 자식 프로세스가 새로운 프로그램을 실행하고 싶을 때 유용하다.
아래 실습을 보자.
19번 라인의 execvp()는 현재 프로그램을 새로운 프로그램으로 대체한다.
따라서 자식 프로세스는 복제한 부모의 프로그램을 wc라는 프로그램으로 덮어 씌워 새 프로그램을 실행한다.
여기서 wc란 특정 파일의 바이트 수, 단어 수, 행 수를 카운트 해 출력해 주는, 리눅스에서 제공하는 명령어다.
따라서 19번 라인의 코드는 p3.c파일의 행, 단어, 바이트 수를 출력해주는 wc 명령어를 실행하라는 뜻이다.
p3.c의 output은 다음과 같다.
프로세스 흐름은 다음과 같다.
자식 프로세스는 부모 프로세스의 프로그램과 같은 14~18번 라인을 수행하고 execvp() 시스템 콜로 인해 wc 명령의 프로그램으로 덮어쓴다. 이때 프로세스의 힙, 스택 등의 메모리는 다시 초기화된다!
(새로운 프로세스를 생성해서 wc를 수행하는 것이 아니다! 덮어쓰는 것이다!)
가장 중요한 3가지 시스템 콜에 대해 알아봤다. 하지만 누군가는 이런 의문이 있을 것이다.
+ 왜 굳이 fork()와 exec()을 구분해놨을까?
→ 둘을 구분함으로써 shell이 fork()가 실행된 후 exec()가 실행 되기 전까지 코드를 실행할 수 있게 하기 때문이다.
이게 무슨 뜻인지는 다음의 예로 설명할 수 있다.
위 명령어는 p3.c파일로 wc 명령어를 수행하되 출력값을 newfile.txt에 출력하라는 뜻이다.
p3.c에서는 wc의 출력값이 standard output으로 출력되었지만, 이 예에선 shell이 standard output을 닫고 newfile.txt파일을 open해 redirect할 필요가 있는 것이다.
즉, fork()를 통해 자식 프로세스를 생성하고 standard output을 닫고 newfile.txt를 연다. 그 후 exec()을 통해 wc명령어를 실행한다.
위의 프롬트 명령어를 c코드로 짜면 다음과 같다.
위 프롬프트 명령과 같게 하기 위해 17번 라인에서 "./p4.output"은 "newfile.txt"로 변경하여 실습하였다.
fork와 exec의 분리를 통해 fork()이후 부터 exec()호출 전까지 표준 output에서 새로운 파일로 redirect를 할 수 있다.
실행 결과는 다음과 같다.
(실행 전)
(실행 후)
newfile.txt가 생성되고 newfile.txt로 출력이 된 것을 확인할 수 있다.
이 외에도 process를 컨트롤 할 수 있는 시스템 콜들은 더 많지만 다음 기회에 다뤄보도록 하겠다.
'운영체제 > Operating Systems in Three Easy Pieces' 카테고리의 다른 글
5. Virtualization) CPU Scheduling (0) | 2023.07.05 |
---|---|
4. Virtualization) Direct Execution (0) | 2023.06.29 |
2. Virtualization) Processes (0) | 2023.06.21 |
1. Virtualization) dialogue (0) | 2023.06.19 |
Operating Systems in Three EasyPieces 책을 통해 운영체제를 공부하며 (0) | 2023.06.19 |