드디어 대망의 프로세스 관련 시스템 콜이다..! 앞에서 소개했던 파일 입출력 관련 시스템 콜은 거의 래핑 함수 위주여서 구현이 어렵지는 않았던 반면, 프로세스 관련 시스템 콜들은 구현할 양도 앞의 포스팅에 비해 많은 편이고 무엇보다도 개념 자체가 쉽게 와닿지 않아 많이 어려웠다고 느꼈다.
프로세스와 관련된 시스템 콜은 fork, exit, exec, wait 으로 총 네 가지가 있다. 이 파트가 특히 어려웠던 점은, 네 개의 시스템 콜 중 하나라도 제대로 구현이 되어있지 않으면 거의 모든 프로세스 시스템 콜 테스트가 통과되지 않아 네 개를 모두 구현하고 나서야 결과를 확인할 수 있었다는 점이다. 구현할 양도 꽤나 많은 편이라 네 개를 모두 구현하고 나서 문제가 생기면 그 문제를 찾기가 꽤나 힘들어진다... 특히 fork가 구현되고 나서부터는 동기화 문제도 발생하기 때문에 디버깅이 아주 힘들어진다.
Fork
fork 시스템 콜은 현재 프로세스와 완전히 동일한 프로세스를 하나 더 만드는 시스템 콜이다. fork 함수의 리턴 값은 크게 세 가지 종류로 나눌 수 있는데, 정리해보면 아래와 같다.
fork()의 return value
1. Negative Value : 자식 프로세스가 정상적으로 만들어지지 않았을 때를 의미한다.
2. Zero : 새로 만들어진 자식 프로세스가 받는 값이다. fork를 호출한 부모 프로세스와 새로 만들어진 자식 프로세스를 구분하기 위해 자식 프로세스에게는 0을 리턴한다.
3. Positive Value : 자식 프로세스가 정상적으로 만들어졌을 때, 부모 프로세스가 받는 리턴 값이다. 자식 프로세스의 pid 값이 리턴된다.
부모와 자식의 리턴 값이 다르다는 것이 무슨 의미일까? 아래의 예시를 보면서 이해해보자!
위와 같은 코드를 실행한다고 해보자. forkexample() 함수가 실행되면 함수 안에서 fork()를 호출하는 것을 볼 수 있다. 조건문을 보면 fork()의 리턴 값을 기준으로 분기되는 것을 볼 수 있는데, 분기 조건이 fork()의 리턴 값이 0인지를 확인하는 것이다. 그렇다면 부모 프로세스가 fork를 통해 자식 프로세스를 만들면 어떻게 될까?
fork 시스템 콜을 호출하면 자식 프로세스는 부모 프로세스의 context 까지 동일하게 복사를 하기 때문에, 자식 프로세스는 부모 프로세스가 마지막으로 실행하던 코드부터 실행하게 된다. 이때 부모의 마지막 context가 fork() 함수를 호출했을 때 이므로, 자식 또한 fork() 함수가 종료된 시점부터 코드를 실행한다. 이때 fork 함수가 끝난 뒤에 코드를 실행하는 프로세스가 fork 함수를 원래 호출했던 부모 프로세스인지, 아니면 fork 함수를 통해 새로 만들어진 자식 프로세스인지를 구분하기 위해 fork()의 리턴 값을 자식에게는 0으로 리턴한다.
따라서 부모와 자식이 각각 코드를 더 실행하게 되면, if문에서 분기처리되어 위의 그림과 같이 실행된다.
핀토스에서 fork를 구현할 때 주의해야 할 점이 있다. 위의 그림과 같이 fork에서는 자식 프로세스가 부모 프로세스의 context를 복사해 오는 과정이 필요하다. fork는 시스템 콜이므로 유저 프로그램에서 fork를 호출하면 먼저 syscall_handler 함수로 진입하게 된다.
핀토스에서 thread 구조체에는 context switching을 위해 인터럽트 프레임을 따로 저장해둔다. 이 인터럽트 프레임은 context switching이 발생할 때마다 값이 바뀐다. 이제 fork의 진행 과정을 살펴보자. 먼저 유저 프로그램에서 fork 시스템 콜을 호출하고, syscall_handler로 진입하게 된다. 이때 syscall_handler의 인자로 user context의 interrupt frame이 전달된다. 이후 fork 과정에서 부모 프로세스는 자식 프로세스를 thread_create() 함수를 통해 생성하고, 자식 프로세스가 부모 프로세스의 리소스를 모두 load 할 때까지 sema_down을 통해 block 상태로 대기하게 된다. 여기에서 자식 프로세스 또는 또 다른 프로세스로 context switching이 발생하게 되는데, 위에서 언급했다시피 context switching에 의해 부모 프로세스의 interrupt frame이 바뀌게 된다. 부모 프로세스가 커널의 코드를 실행하는 도중에 context switching이 발생하여 interrupt frame이 바뀐 것이기 때문에, 부모의 interrupt frame에는 kernel의 context가 담겨있다. 따라서 자식 프로세스가 부모 프로세스의 리소스를 복사하는 과정이 담겨있는 __do_fork 함수에서 부모의 user context interrupt frame을 가져오기 위해서는 다른 방법이 필요하다..! 단순히 부모 프로세스에 접근해 parent->tf와 같은 방식으로 부모의 interrupt frame을 가져오면 kernel context의 interrupt frame을 가져오게 된다. 나는 thread 구조체에 interrupt frame을 저장하는 변수를 하나 더 만들어 syscall_handler에 들어올 때마다 user context의 interrupt frame을 해당 변수(parent_if)에 저장하고, __do_fork 함수에서는 부모 스레드의 parent_if를 가져와 값을 복사해 주는 방식으로 구현했다.
Wait
다음으로 살펴볼 시스템 콜은 wait이다. 파라미터로 기다릴 프로세스의 pid 값이 들어가며, 해당 프로세스가 종료될때까지 다른 일을 하지 않고 멈춰있게 하는 시스템 콜이다. 핀토스가 처음에 실행되고 initial thread에서 idle thread를 생성하는 경우에도 wait 시스템 콜과 비슷하게 동작한다. 이때 또한 세마포어를 활용하여 idle thread가 다 만들어지고 스스로 block 될 때까지 initial thread는 기다리게 된다.
wait을 호출한 부모 프로세스는 wait 함수의 리턴 값으로 자식 프로세스의 exit status를 받게 된다. 만약 자식이 정상적으로 종료되었다면exit status는 -1이 아닌 다른 값이 나올 것이고, 비정상적으로 종료되었다면 wait의 리턴 값으로 -1을 받을 것이다. 이를 통해 부모 프로세스는 자식 프로세스가 정상적으로 종료되었는지 확인할 수 있다. 또한 wait 시스템 콜을 통하여 자식이 자신의 종료를 부모에게 알리고, 자식이 할당받은 자원들을 모두 반환한 뒤 종료하는 작업을 추가해주어야 한다! 예를 들면 현재 실행중인 파일을 닫거나, 실행 과정 중에 열었던 파일들을 닫거나 하는 방식이다.
Exec
fork와 거의 짝꿍처럼 쓰이는 시스템 콜이다. fork가 부모의 리소스를 모두 복사해 자식 프로세스를 만드는 시스템 콜이었다면, exec은 현재 실행 중인 context를 버리고 새로운 파일을 실행하는 시스템 콜이다. 프로세스가 옷을 갈아입는 느낌으로 생각하면 이해하기 편하다! exec은 fork와는 달리 새로운 프로세스가 생기는 것이 아니다. exec 시스템 콜은 파라미터로 실행할 파일의 이름을 넘겨준다. 현재 context를 아예 전환하는 함수이므로 가지고 있던 리소스를 지워줘야한다. 이 작업은 process_cleanup 함수에서 해주니까 지금은 넘어가자..! 하지만 열어두었던 파일 디스크립터들은 exec이 호출되어도 계속 가지고 간다.
Exit
마지막으로 exit 시스템 콜이다. fork와 exec이 자주 같이 쓰이는 것처럼 exit은 wait과 관련이 깊다. exit 시스템 콜은 현재 프로세스의 exit status를 출력하고 프로세스가 가지고 있던 리소스를 모두 지워주는 시스템 콜이다. 또한 프로세스가 종료할 때 부모 프로세스에게 자신의 종료를 sema_up을 통해 알린다.
wait과 exit의 큰 흐름을 보면 위의 그림과 같다. 위의 그림처럼 구현하면서 의문점이 하나 들 수 있다. 바로 굳이 왜 child process를 exit_sema에서 다시 block 상태로 만드는걸까? 이러한 이유는 부모 프로세스가 child list에서 해당 프로세스를 지워야하는데, 만약 child가 sema_down을 통해 block 되지 않는다면 먼저 모든 리소스를 free 해주었을 수도 있기 때문이다. 따라서 child process를 진짜 destroy 하기 전에 먼저 sema_down을 통해 block 상태로 만든 후, parent process에서 child process에 관한 모든 정보를 지운 뒤 child process가 destroy list에 들어가야 한다.
'Operating System' 카테고리의 다른 글
[Pintos] Project 4 - File System(FAT) (0) | 2023.05.30 |
---|---|
[Pintos] User VA, Kernel VA (0) | 2023.05.23 |
[Pintos] Project 3 - Memory Management (0) | 2023.05.16 |
[Pintos] System Call - File I/O (1) | 2023.05.09 |
[Pintos] Multi-Level Feedback Queue Scheduler(mlfqs) (1) | 2023.05.08 |