시스템 콜, 특히 FORK
운영체제는 사용자가 요청한 프로그램을 실행하기 위해 필요한 일련의 작업을 스레드라는 작업단위로 만들어서 처리한다. 스레드는 따라 우선순위, 걸리는 시간 등을 고려하여 스케줄링 되고 차례대로 실행되면서 유저가 원하는 동작들을 수행한다. 유저가 읽고, 쓰고, 삭제하는 데이터들은 하드웨어에 저장되어 있는데 유저데이터가 하드웨어에 마음대로 접근하게 하는 것은 위험하기 때문에 운영체제는 커널모드와 유저모드를 나눠서 리소스를 보호하도록 설계되어있다. 그리고 유저가 데이터를 원하는 경우 커널을 통해서 요청하도록 되어있는데 이럴 때 호출하는 것이 바로 시스템 콜이다. 시스템 콜은 운영체제의 아키텍처마다 규약이 정해져있다.
시스템 콜은 파일을 읽고 열고, 읽고, 쓰고, 닫는 기능을 위해서 호출되기도 하지만, 새로운 스레드를 생성하기 위해 사용되기도 한다. 만약 서로 독립된 일을 하는 스레드가 아니라 하나의 스레드에서 하는 일을 두 스레드가 나누어서 하도록 처리하고 싶다면, 자식스레드라는 새로운 스레드를 만들 수 있다. 이때 사용하는 시스템콜로는 FORK, EXEC이 있다.
FORK 가 호출되면 자식 스레드가 부모 스레드가 작업하던 환경정보(인터럽트 프레임)를 그대로 카피해서 부모가 실행했던 코드라인부터 이어서 실행한다. 부모는 자식스레드가 수행을 완료할 때까지 기다려주고, 자식이 수행을 완료했다는 신호를 주면 이어서 작업을 하거나 스레드를 종료한다. 반면에 EXEC은 자식 스레드가 부모가 하던 작업환경을 덮어쓰고 부모 스레드를 대신해서 작업을 수행한다.
이 두 시스템 콜을 구현하는 것이 나는 무척 어려워서 4일이 걸렸다. 이게 어렵게 느껴진 이유는 FORK 함수로 만들어진 자식 스레드가 부모의 인터럽트 프레임을 복사한 후 자신의 실행컨텍스트를 만들어서 코드를 실행하는 과정이 내부 어셈블리어를 통해 이루어지기 때문이었다. 내가 C언어로 구현해준 부분은 자식 스레드에게 부모 스레드의 인터럽트 프레임을 넘겨주는 부분까지였다. 부모 스레드가 WAIT이라는 시스템 콜을 호출하면 자식 스레드가 수행을 완료할 때까지 기다리도록 해주어야 하는데, 어셈블리어를 통해 이루어지는 자식 스레드의 동작을 명확하게 C언어 코드로 확인하지 못하는 상태에서 리턴시점을 예상하려고 하니까 너무 막연하게 느껴졌다. 마침내 이해한 바를 그림으로 표현해보니 다음과 같았다.
중간에 'cur_thread 없는데 어떻게 스케줄링?' 이라는 메모가 내가 지속적으로 가졌던 의문이었다. 결론적으로, 자식 스레드는 생성될 때 이미 스케줄링되고, 부모 스레드가 WAIT하는 동안에는 자식 스레드가 흐름을 넘겨받아서 cur_thread(실행스레드)로서 자신의 할일을 수행하게 된다.
FORK 구현의 또다른 장애물은 이전에 짠 코드의 방해였다. 나는 FORK에 앞서 READ, WRITE, OPEN, CLOSE 등 파일시스템 처리용 시스템 콜을 구현하면서 파일 디스크립터 테이블이라는 것을 만들었다. 파일 디스크립터 테이블은 파일이 새로 열릴 때마다 파일에게 개별 번호를 할당해주고, 닫힐 때 할당을 해제해주는 매핑 테이블이다. 이 파일 디스크립터 테이블은 FORK 시스템 콜 호출시 부모가 가진 파일 정보를 자식에게 넘겨주기 위해 다른 레지스터에 적힌 값들과 함께 복사된다. 그런데 나는 솔직히 C언어에 익숙하지 않아서 동적할당과 정적할당 시에 각각 다른 메모리 공간에 할당된다는 정도만 알고 있었고, 큰 차이가 없다고 느껴지는 경우 더 간단한 정적할당을 선호하는 나쁜 습관이 있었다. 그래서 이번에도 파일디스크립터 테이블을 정적으로 할당했다.
https://gist.github.com/yeonwooz/d74149c24e59bf543416175becdf8efe
그리고 FORK를 했더니 벌어진 일은 다음과 같았다.
사실 아직도 정확하게 이해하지 못했지만, GP protection exception 이라는 키워드로 열심히 찾아본 결과 유효하지 않은 데이터 세그먼트에 접근한 경우 발생하는 익셉션이라고 한다. 익셉션이란 컴파일중이 아닌 실행중에 발생하는 오류를 말하는 것이다. 추측하건대 자식 스레드가 부모 스레드의 유저스택에 할당된 데이터에 접근하게 되었기 때문인 것 같다. 스택은 스레드 별로 분리되므로, 아마 동적할당을 통해 HEAP 영역이나 아니면 palloc을 위해 마련된 공간에 저장했어야 스레드 간에 데이터를 공유할 수 있는 것 같다.
그렇게 고친 코드는 다음과 같다.
https://gist.github.com/yeonwooz/50074c155798a925370e62ed08306c02
그렇게 긴 여정을 마치고, 시스템 콜 구현에 성공했다. 파일시스템 동기화까지는 시간이 없어서 구현을 미처 못했지만, FORK를 비롯한 시스템 콜과 커널영역과 유저영역 사이의 커뮤니케이션에 대해 이해한 것으로 만족해야 할 것 같다.
TIL
이번 작업에서 기억해야 하는 정보가 많아서 화이트보드가 큰 도움이 되었다.