최근 혼자 공부하는 운영체제 + 컴퓨터 구조라는 책을 읽었다. 책 내용은 OS 전반적인 내용을 쉽게 잘 설명해 주었지만 추상적이라 그걸 알아서 어디에 써먹을 수 있을까? 하는 의문이 들었다. 또, 백엔드 개발자에게 OS가 중요하다는 말이 자주 나와서 왜 알아야 할까 궁금하여 읽었다.
들어가기 전에
이 책에서는 백엔드 개발자가 OS를 공부해야 하는 이유에 대해서 우선 말한다.
우리가 생각했을 때 OS는 계층화가 잘 이루어져 있어서 좌측과 같이 예쁜 모델로 생각하기 쉬워 애플리케이션 레벨만 혹은 바로 그 아래 레벨만 잘 알면 되겠지 생각하기 쉽지만, 사실은 계층들 끼리 복잡하게 섞여 있어 저 밑바닥까지 이해하지 못하면 해결할 수 있는 문제를 만날 수 있다.
그 문제를 만났을 때 대처할 수 있도록 OS나 HW에 대한 공부를 해둬야 하는 것이라고 한다. 그렇게 됐을 때
HW 특성을 고려한 SW 개발이 가능해진다.
시스템 설계 시 기준으로 삼을 지표가 무엇인지 알 수 있다.
OS나 HW 관련 오류를 대처할 수 있다.
라는 장점이 있다.
내 생각에는 물론 요즘 클라우드가 너무 잘 되어 있어 점점 추상화가 잘 되어간다고 생각하긴 하는데, Docker를 다룰 때라던지 모니터링 지표를 세운다던지, DB 혹은 네트워크 공부 할때 등 아래 부분에 대한 영역에 대한 지식이 필요할 때가 종종 필요한 것 같긴 하다.
여기에 더해 저자는 네트워크 관련 지식도 쌓으면 좋겠다고 말하는데, 이것만 해도 따로 공부할 영역이 엄청나게 많아 다음에 한 번 각잡고 공부해야겠다는 생각이 들었다.
몰랐는데 알게 된 것들 1. 시스템콜과 프로세스
글로는 알고 있었는데 정확히 어떤 경우에, 왜 필요한지 몰라 정확히 알고 싶어 이번 기회에 다시 정리해 보았다.
시스템 콜이라 함은 커널 자원을 사용하겠다는 이야기고, 인터럽트는 CPU에게 시스템 콜이 들어왔다고 알리는 과정이다.
시스템 콜이 하는 일에는 다음과 같은 것들이 있는데 다른 말로 하면 애플리케이션 단에서 커널 단에 요청할 수 있는 일들 목록이라고 생각할 수 있다.
시스템 콜 종류
프로세스 생성
메모리 확보, 해제
프로세스 간 통신(IPC)
네트워크
파일시스템 다루기
파일 다루기(디바이스 접근)
앞에서 시스템 콜 이야기를 한 이유는 대부분의 프로세스의 동작에는 입/출력이 필요로 한다. 이때 커널에게 요청해야 하기 때문이다. 그 이유는 입/출력하는 통로인 소켓, 파이프 등도 결국 하나의 파일인데, 파일을 관리하는 주체는 파일 시스템이라는 커널의 일부이기 때문이다.
프로세스에서 system call을 날리면 커널 모드로 전환하고, 디바이스 드라이버를 통해 IO 요청하는 식으로 동작한다. C라는 언어도 결국 system call을 위한 언어고, 시스템 프로그래밍을 한다는 것 또한 결국 C를 다룬다는 이야기라고 이해했다.
추가로 이번에 알게 된 것인데 리눅스 명령어들도 모두 C로 작성된 프로그램들이고 명령어를 실행할 때마다 프로세스를 실행해서 system call로 IO를 받아오고 동작을 수행하고 다시 IO를 수행하는 식으로 동작하는 것이었다.
sar 명령어는 시스템의 CPU, 메모리, 입출력 사용량 정보를 수집 및 리포트해주는 명령어다. 주로 시스템 자원을 모니터링할 때 많이 사용하고, System Activity Report의 약자이다.
여기서 %user는 사용자 모드에서 CPU가 사용된 시간 비율, %system은 시스템 모드, 그러니까 커널 모드에서 CPU가 사용된 시간의 비율, $idle은 CPU가 사용되지 않고 유휴상태로 소비한 시간 비율을 의미한다.
여기서 세 가지 조건에 따라 sar를 실행해 보았다.
첫 번째 그림에선 현재 켜놓은 작업들이 없으니까 idle 비율이 높은 걸 볼 수 있다.
두 번째 상황은 무한루프를 도는 프로그램을 짜서 실행시켰다. 그림을 보면 11번 CPU의 idle은 0, user는 100을 치고 있다. 이건 저 코어에서 한 번도 안 쉬고 CPU를 사용했다는 이야긴데 전부 유저 모드에서 돌아갔다는 걸 의미한다.
세 번째 상황은 앞의 무한 루프 프로그램에 부모 프로세스의 pid를 얻는 ppid() 시스템 콜을 추가했다. 유저 모드의 시간은 줄고 시스템 비율이 높은 걸 볼 수 있다.
한 편으로는 user 비율보다 system 비율이 훨씬 높은데 이게 다 시스템콜로 인한 대기 시간을 포함하는 작업이 그만큼 비싸다는 걸 의미하지 않나 하는 생각도 들었다.
몰랐는데 알게 된 것들 - 2. 가상 메모리와 공유 페이지, 요구 페이징
가상 메모리 개념은 대충 알고 있었는데, 이 책을 읽으면서 공유 페이지와 요구 페이징 기법에 대해서는 처음 알게 되었다.
페이징 과정에 대해서 간단하게 정리해 보자면 페이징은 결국 외부 단편화를 해결하기 위해 나온 해결책이이다. 메모리를 페이지라는 단위로 잘게 쪼개놓고, 프로세스를 통째로 저장할게 아니라 페이지로 나누어 메모리에 적재하자는 전략이다.
원래 이전에는 CPU에서 메모리를 읽는 MMU라는 장치에서 프로세스가 할당된 메모리 전후만 알아도 되었지만, 이제는 흩어진 페이지들의 위치 전부를 알아야만 한다. 그래서 도입된 게 페이지 테이블과 페이지 테이블 엔트리다.
페이지 테이블은 쉽게 말해 흩어진 이 프로세스의 페이지 위치들을 모아놓은 자료구조다. 그런데 이제는 MMU에서 메모리에 접근하려면 페이지 테이블에 한 번, 테이블에서 메모리에 한 번 이렇게 2번 접근해야 하니까 더 느려진다. 메모리 접근 시간은 CPU 접근 시간보다 최소 5배 ~ 400배 정도 까지도 차이나는데 이런 작업을 2번 하니까 느려지는 것이다.
그래서 이 문제를 해결하기 위해 TLB라는 캐시를 둔다. 그럼 이제 1번씩만 접근할 수 있다.
이제는 메모리 크기를 더 활용적으로 쓰고 싶어 이 문제를 해결하기 위한 기법이 2개 등장한다. 공유 페이지와 요구 페이징이라는 기법들이다.
요구 페이징 기법은 디스크에서 메모리에 바로 올리는 것이 아니라, swap out 해뒀다가 필요할 때 swap in 하는 걸 말한다.
만약 페이지 테이블에 해당 페이지가 없어서 swap in 해야 한다면 페이지 폴트를 발생시킨다.
페이지 폴트는 swap in 할거라는 인터럽트다. swap in은 디스크에서 메모리로 데이터를 퍼오는 작업인데, 디스크 읽기는 메모리 읽기에 비해 10^5배 이상 느린 작업이다. 이 작업 하는동안 CPU는 놀 수 없기 때문에 context switching해서 다른 작업 하라는 이야기다.
공유 페이지는 여러 프로세스에서 접근하는 자원을 같이 사용할 수 있도록 하는 걸 말한다. 그러니까 페이지 테이블에서 가리키는 주소가 같다는 말이다.
이때 그 자원은 read-only여야 하고, 만약 쓰기 작업이 들어가면 Copy On Write라고 하는 해당 부분을 고대로 복사하고 할당해서, 기존에 가리키던 걸 교체하는 작업을 수행한다.
이 부분을 보면서 궁금증이 하나 들었다. 그럼 TLB와 페이지 테이블 간 정합성은 어떻게 지키지? 왜냐하면 페이지 테이블은 메모리 상에 존재하고, TLB는 작은 부품일텐데 페이지 테이블들의 크기가 꽤 커서 TLB에서 모든 페이지 테이블의 영역을 커버하진 못할 것 같았다. 그런데 페이지 폴트는 TLB에서 일으키니까 문제가 될 수 있지 않나? 하는 의문이 들었다.
그래서 TLB concurrency 이런 키워드들로 찾아보니 앞에서 말한 copy on write하고 나서 다른 프로세서들에게 이 페이지 변경됐다고 알리는 TLB shoot down이라는 현상에 대해서만 다루고 있었다.
그리고 또, TLB도 마찬가지로 OS에서 systemcall 날려서 관리하는 것 같았다. 찾아보니까 flush_tlb_page라는 시스템콜이 있던데 이걸로 "어떤 페이지 swap out 했다." 이런 식으로 명령을 날려서 관리하는 걸로 이해했다.
마치면서
이 책의 저자는 OS를 공부하면 HW를 고려한 설계 / 개발 / 오류 대처가 가능해진다고 했는데, 그것 외에도 여러 기법들을 많이 알게 돼서 좋다는 생각이 들었다.
기법들을 많이 알 여러 프로그램 구현체들을 볼때 여기서 배운 기법이나 용어들이 적용되어 있는 경우 더 잘 이해할 수 있고, 그 기법들이 적용될만한 상황이 닥치면 비슷하게 해결할 수 있어, 전부 기술적 자산이 된다는 생각이 들었다.
내가 이 책을 읽고 수월하게 이해한 기법들은 다음과 같다.
1.이 책에서 파일 시스템의 저널링이라는 언급이 나오는데, 이게 MySQL redo log / 레디스의 AOF와 비슷한 느낌이라는 생각이 들었다. 그래서 내용을 세부적으로 보지 않아도 아 이거 이렇게 동작하겠구나 싶었는데 실제로도 비슷하게 동작했다.
2.Copy On Write를 읽었을 때도, MySQL에서 ALTER 쿼리 날릴 때 동작이랑 비슷한데? 싶었기도 했고, Java에서 CopyOnWriteArrayList 같은 것들이 있던데 그냥 이 개념을 알고 보니까 직접 들어가서 자바독을 읽어보지 않아도 됐다.
3.최근에 HikariCP 소스를 좀 볼 일이 있었는데, idle 이라는 개념이 나왔었는데 어 이거 OS 책 볼때 나온 개념인데 하면서 뭘 찾아보지 않아도 돼서 집중력이나 시간을 아낄 수 있었다.
4.미리 읽기 같은 것도 어 이거 MySQL에서 풀스캔할 때 스레드 추가 할당해서 하는 거랑 같은 개념이잖아. 하는 생각이 들기도 했다.
이런 느낌으로 OS를 공부한다는 게 꼭 시스템 상황을 알게 되는 것을 넘어, 문제 해결 전략의 총집합을 보고 각종 상황에 OS에서 해결한 사례들을 배울 수 있다는 걸 느꼈다.