새해 첫 주 아침이 밝았다! 기분좋게 지난 주 강의 들었던 권영진 교수님 운영체제 강의를 정리하면서 하루를 시작해보자.
왜 OS를 배울까?
왜 요즘 프로그래머가 OS 공부를 해야 할까? 사실 살면서 커널 볼 일은 거의 없다. 디바이스 드라이버 만질 일도 크게 필요하지 않다. 하지만 모든 CS 학과에서 전필로 다룬다. 교수님들은 바보가 아니다.
OS 배우는 이유 1: 기본적인 퍼포먼스에 대한 지식을 이해
위와 같이 코드를 돌렸다고 해보자. mmap()으로 메모리를 1메가바이트 할당받고 addr로 해당 메모리 주소를 받아온다. 이후 memset 함수로 해당 addr에 넣을 값을 모두 1로 세팅하는 작업을 거친다. 이후 memset 함수를 돌리는데 걸린 시간을 출력하는 printf가 나온다. 이 동일한 작업(두 번째에는 메모리에 값을 2로 초기화)을 정확히 두 번 반복한다. 함수 내용이 똑같으니 memset하는데 걸린 시간 역시 똑같을 것 같다. 그럼 출력 결과를 보자.
엥? 10배나 차이난다. 대체 왜...? 바로 캐시를 사용했는가의 차이다. 처음에는 DRAM 자체에서 memset 작업을 수행한다. 두번째는 캐시 메모리에서 memset을 수행한다. 왜 성능이 다르냐, 바로 캐시 메모리 때문이다. 바로 이전에 작업한 내역이 캐시 메모리에 저장되어 있어 캐시 메모리에서 바로 가상 주소에 접근하기 때문. 첫번째는 맨 첫 작업이다보니 캐시에 저장되어있지 않아 디램에서 작업을 수행한다.(이때 우리가 작업하는 위치는 물리 주소가 아닌 가상 주소라는 점을 헷갈리지 말자! 이전에 정리해둔 자료 참조)
이번에는 1메가바이트 크기가 아닌 1기가바이트라고 해보자. 캐시 메모리에는 1기가만큼 저장해둘 공간이 없기 때문에 이번에는 캐시가 통하지 않을 것이다. 그럼 이번에는 정말로 속도가 같을 것 같다는 게 우리 생각인데,, 결과를 보자.
엥..여전히 2배 차이가 난다. 여기는 페이지 콜 메커니즘 때문이다. 이 역시 이전에 가상 메모리에 대해 정리해둔 자료를 보면 도움이 될텐데, 가상 메모리에서 mmap을 시행하면 실제 1기가에 해당하는 물리 메모리를 디램으로부터 전부 제공하는 게 아니라 가상 메모리를 준다. 이때 page fault가 발생한다. 왜냐면 첫 작업이다보니 아직 물리 메모리에는 매핑되지 않았기 때문에 exception이 떠서 그렇다. 가상 메모리에는 분명히 해당 공간이 있다고 세팅해서 물리 메모리 가보니까 없다고 뜨면? exception을 띄우고 이때 memory allocation을 해준다. 이 작업이 첫 단계 내에서 메모리를 매번 할당할 때마다 뜬다.
반면, 두번째 작업에서는 이미 물리 메모리에 가상 메모리 주소가 전부 매핑되어 있으니 exception 없이 바로 디램에 가서 값을 바꿔주면 된다.
이런 개념을 운영체제를 공부했다면 쉽게 이해할 수 있다. 성능 차이가 얼마나 나는지를 보고서 어디에서 문제가 날지 대략적으로 파악할 줄 아는 감이 있어야 한다. N사 백엔드 엔지니어 정도만 가더라도 이정도 지식이 필요하다. 파일 시스템 만들다가 전원이 나갔다, 그럼 정보가 어디까지 저장됐을까? 이런 개념도 실무에서 중요한데 이 역시도 운영체제 개념을 알아야 한다. 즉, 프로그래머로서 기본적인 퍼포먼스에 대한 지식을 이해하는데 필요한 게 운영체제이다.
OS 배우는 이유 2: 컴퓨터 시스템 디자인에 대한 이해
요즘에 운영체제를 새로 만드는 회사는 거의 없다. 기술이 거의 안정화됐기 때문이다. 그럼 운영체제를 왜 배울까? 단순 백그라운드를 넘어서더라도 이를 배울 필요가 있다.
운영체제 코드 길이가 어느 정도일까? 대략 4~50만에서 100만줄 넘기도 하다고 한다. 매우 길다. 그러면 크롬 웹 브라우저 엔진 코드 길이는 어떨까? 이보다 훨씬 더 길다고 한다. 하지만 우리는 크롬 웹 브라우저 엔진을 전필 과목으로 배우지 않는다. 그럼 왜 배울까?
바로, 컴퓨터 디자인 원리(Design Principle)에 대한 이해를 키우기 위해서이다. 운영체제 디자인에는 옛날부터 운영체제를 만들기 위해 수많은 천재 엔지니어들이 고심해서 쌓아올린 지식이 고스란히 녹아 있다. 단순히 위에서처럼 page fault에 대한 지식을 외우는 게 아니라, 컴퓨터 시스템 디자인에 대한 이해를 돕는 게 중요하다. 하지만 이 design은 매우 추상적이기 때문에 이를 실제 구현 단계에서는 어떻게 하는지 함께 이해를 돕고자 실습 과목이 함께 있는 것이다.
즉, 우리가 컵을 만들겠다고 하는 엔지니어라면 컵을 바로 뚝딱 만들기 전에 컵의 본질에 대해 고민하는 것이 필요하다. 어떤 컵이 목적에 충실한 디자인일까? 이 본질이 담겨있는 게 디자인이고 그 중에서도 운영체제가 컴퓨터 시스템 디자인의 정수라고 할 수 있다.
OS 디자인
OS 디자인 원칙: 추상화 - Hide details
이번에는 프로그램 입장에서 운영체제는 왜 필요할지 생각해보자. 운영체제는 응용 프로그램에게 하드웨어를 쓸 수 있게 API를 제공한다. 이게 무슨 말이냐, 옛날 컴퓨터를 생각해보자. 예전에는 이메일 프로그램, 계산기 프로그램 등 모든 응용 프로그램이 하드웨어까지 통제했어야 했다. 이메일 프로그램에서 CPU도 만져야 하고, 메모리도 만져야 하고, 키보드 등 입출력 장치까지 컨트롤할 수 있어야 했다. 생각만 해도 얼마나 복잡하겠나.. 각 하드웨어마다 표준도 다를텐데.
그러니 중간에 레이어를 하나 만들자는 것이 디자인 원리다. OS라는 하나의 레이어를 응용 프로그램과 하드웨어 사이에 끼워서 어떤 하드웨어를 쓰던 OS하고만 소통하게 한다.
Hide details of hardware. 복잡한 것을 숨긴다. 이것이 시스템 디자인의 첫번째 원리이다.
복습 정리하면서 조금 소름돋는게, 얼마 전에 친구(D사 ML 엔지니어)와 얘기를 하다가 이 추상화에 대한 얘기를 나눈 적이 있었다. ("이쪽 업계에서 핵심은 추상화야.") 이게 운영체제의 디자인 레벨까지 내려와서 매칭되는구나. 지금 배우는 게 전혀 쓸모 없는 게 아님을 다시 한 번 더 느낀다. 이런 말도 했던 기억이 난다. "개발자가 협업하는 비개발자에게 제대로 이해시키지 못했다는 것은 곧 추상화를 제대로 하지 못했다는 것과 같다. 이런 개발자는 좋은 개발자가 아님." (대략 이런 뉘앙스였던 것 같음)
Key roles of OS: 디자인 원칙
그러면 OS 디자인 원칙에 대해 좀 더 알아보자. 크게 세 가지가 있다.
- Design abstractions to user hardware - 탄생과 가장 가까운 디자인 원칙
- Define APIs - API 역시 뒷단 복잡한 코드 다 배제하고 API라는 애만 받으면 쉽게 사용할 수 있도록 하는 것!
- Protection & Isolation
- 응용 프로그래머가 하드웨어 코딩하다가 잘못 건드리면 큰일난다. 하드웨어와 운영체제를 보호하기 위해서도 운체가 필요하다.
- 자원 공유
What is "Abstraction"?
추상화란 무엇일까? 시스템 개발자들이 만든 것 중 가장 재밌으면서도 중요한 개념이 바로 추상화이다. 위에서도 말했듯, 이는 현업에서도 매우 중요한 개념이다.
추상화의 반대는 무엇일까? 구체화이다. 그럼 다시. 왜 추상화를 할까? 구체적인 건 다른 말로 복잡하다는 말이기도 하다. 이 모든 구체적인 것들 중 우리가 인식하고 있는 관심 분야만 뽑아낸다. 핵심 내용만을, 정수를 뽑아서 정의하는 것이 추상화이다.
Abstraction: The process or outcome of making something easier to understand by ignoring some of details that may be unimportant.
추상화: 중요하지 않은 세부사항을 배제함으로써 과정 혹은 결과물을 쉽게 이해시키기 위한 과정 혹은 결과물.
올바른 추상이란 곧 청중으로 하여금 설득할 줄 알아야 한다는 것이다. 위에서 얘기했듯, 비개발자와 개발자가 대화하는데 있어 비개발자를 이해시킬 수 있도록 추상하는 것 역시 개발자의 덕목이라는 것이다. 개발자는 소프트웨어와 비개발자 사이의 인터페이스!
How to make it easy to use hardware?
컴퓨터 시스템에서 각 요소가 어떻게 추상화되어있는지 살펴보자.
- CPU: virtualizing CPU로 추상화
- Memory: virtual address space로 추상화
- Storage: file로 추상화
위 세가지는 운영체제를 만드는 사람들이 동의하는 추상 개념이다.
Process: 프로그램의 추상화
OS 디자이너는 아까 얘기한 것처럼 OS를 세 가지 관점에서 추상화한다고 했다. 1)하드웨어를 숨기고 쉽게 프로그래밍하도록 한다, 2) 운영체제와 하드웨어를 보호한다, 3) 자원을 공유한다. 이를 프로그램 관점에서 추상화하기 위해 프로세스(process)라는 것을 만들었다. 프로세스는 프로그램의 실행단위를 추상화한 것으로 위 세 가지를 만족한다. 각 프로세스는 하나의 컴퓨터 머신을 가지는 것처럼 행동한다. 따라서 각 프로세스는 각자의 가상 메모리를 가지는데, 여기에 대해 좀 더 파보자.
How process looks like?
서로 다른 프로세스 P1, P2, P3이 있다고 하자. 얘네들은 서로 다른 주소 공간을 갖는다. 이 다른 주소 공간은 DRAM 내 서로 다른 영역에 물리 주소를 분리하고 있다. 서로 다른 프로세스에 대해서 둘다 int a;를 시전했다고 해보자. 이 int a는 스택에 저장되는데, 다른 주소 공간이다.
이때, 어떻게 가상 주소를 물리 주소로 매핑할까? 이전에 정리했던 가상 메모리에서 페이징과 segmentation으로 이뤄진다. 페이징은 MMU가 물리 주소를 논리 주소로 바꾸는 역할을 하는 유닛이며 하드웨어이다. 따라서 page fault 역시 하드웨어가 일으킨다.(디테일한 건 따로 정리하기) 운영체제는 하드웨어가 알려주면 그제서야 반응한다.
아래 이미지와 같이 TLB는 PTE를 캐싱한다. 페이지 넘버에는 logical address가 들어가고 frame number에는 physical address가 일대일 대응으로 들어간다. 이 TLB는 DRAM에 저장되어 있다.
TLB가 hit하면 그때서야 디램에서 페이지 테이블을 불러온다. 느리면? 캐싱한다.
Memory Allocation
이번에는 메모리 할당에 대해 보자. 언제 physical memory를 할당해줄까? 메모리를 요청하는 족족 다 주면 낭비가 심해진다. 꼭 필요할 때만 나중에 주는데, 이를 demand paging이라 한다.
처음에는 가상 메모리만 할당해주고 물리 메모리에는 하나도 넣어주지 않는다. (맨 처음 예제에서 mmap()을 생각해보자. memset()을 실행하기 전까지도 가상 메모리는 분명 존재했다. 하지만 물리 메모리에는 반영되지 않은 것.) 여기서 process를 진행하면?
- 커널에서 page fault exception handler가 페이지를 할당해서 커널에서 실제 물리 메모리를 할당해준다.
- 이 물리 memory manager는 free page list를 관리하고 있다. 여기서 페이지를 할당해준다.
- 다시 어플리케이션을 실행(Application resumes)한다. 그러면 이제 page hit이 이뤄진다.
이 page fault handler는 심장과도 같다. 얘가 느리면 개망하는 거다. 윈도우에서는 이 핸들러 코드가 8000라인이고 리눅스도 4000라인쯤 된다. 그만큼 어렵고 얘를 잘 짜야 한다.
*위에서 zero the pages를 하는데 (page에 있는 값을 0으로 초기화) 이걸 왜 할까? 이거 안하면 난리난다. 이게 page fault에서 가장 많은 시간을 차지하는데, 보안 문제 때문이다. malloc을 생각해보면, 메모리를 동적으로 할당해줄 때 초기화하지 않는다. 근데 여기서 페이지를 0으로 초기화해주지 않으면 기존에 있던 정보를 그대로 page table에 할당해주니 거기 있던 정보를 해커가 슉 가져갈 수 있다.
Page fault handling
너무 길어지고 있어서 일단 여기서 끊기 (file-backed memory / anonymous memory 정리로 마무리할 것인데 아래 링크 참조)