Bullshitting Blog

개소리하는 블로그

[ os ]

OS - 2) Process API

이 단원은 실용적인 부분들, 즉 실제로 손맛 느껴보는 단원이다. 그래서 이론 위주의 공부를 하고싶은 사람은 넘겨도 된다고 써있었다. 하지만, 그거 아는가? 답정너식 화법 말이다. “난 이거 너희들이 읽었으면 좋겠지만, 이론 위주의 공부만 할거라면 넘겨도 돼. 근데 난 너가 이걸 읽었으면 좋겠어. 여튼 그렇다고. 아 네버 마인드.”

아아아아 알았다고! 그래서 여튼 읽었다. 이 공부, 급하지 않으니까. 손맛 한 번 느껴보자(하면서 실상 코드 손으로 안침 ㅋ).

이 단원은 fork, wait, exec 함수를 실제로 사용해보면서 무슨 일이 일어나는지를 이해하는 것을 목적으로 하고 있었다. 그래서 사용해보면서 또 GPT 선생 붙잡고 내가 이해한 내용이 맞는지 체크를 받으면서 추가적인 이해를 더했다.

fork()

프로세스를 복제떠서 자식 프로세스로 실행하는 함수이다. 처음엔 현재 상태의 메모리 자원을 공유한다. 그러다 수정이 일어나는 시점에 해당 자원을 카피해서 독립성을 부여한다.

Copy-on-Write(COW)라 칭한다.

그리고 좀 더 파봤는데, 커널 객체는 공유되고, 스택/힙/전역변수와 같은 유저공간 데이터는 복사가 일어난다. 커널 객체는 기본적으로 파일 디스크립터(fd)가 가리키는 객체인데, 파일, 소켓 버퍼, 파이프 버퍼 등이 있다. 사실 그냥 입출력 객체는 다 fd로 접근한다.

pipe는 ls | grep hello 할 때 그 pipe 맞다.

wait()

기다리는거 맞다. 누가 누굴 기다릴까? 부모가 자식을 기다린다.

exec()

다른 프로그램을 실행시켜서 프로세스를 전환하는 함수이다. 이 시점에서 프로세스는 전혀 다른 프로세스가 되어 있다. pid는 그대로다. 말 그대로 바꿔치기다.

왜 이걸 알아야 하는가

이게 미묘하다. 일반적인 케이스에서 이걸 다룰 일은 흔치 않다. 아니, 거의 없다고 봐도 무방하다. 하지만, OS관점에서 이 함수들은 아주 중요하다. OS가 하는 일 중 가장 중요한 일은 동시에 여러 프로세스를 돌리는 일이다. 그리고 OS도 하나의 프로그램이다. 즉, OS가 프로세스로서 다른 프로세스를 생성하고 관리하기 위해 fork, exec, wait은 필수불가결한 존재이다.

Process Control And Users

방금 본 저 3개의 함수 외에도, 프로세스를 컨트롤하는 함수들이 더 있다. 대표적으로 kill()이 있다. 말 그대로 kill이다. 근데, 죽인다고 생각하는게 아니라, 슬슬 꺼져랏! 하고 저주를 보내는 함수라고 보면 좋다. 프로세스로 시그널을 보내는 것이다. 익히 사용하는 Ctrl+C는 SIGINT, Ctrl+Z는 SIGSTOP이다. 둘 사이의 차이는, 쉽게 설명하면 “꺼져랏!”과 “비켜봐!”의 차이다. SIGINT는 정말 프로세스를 중단할 때 이용한다. 그에 반해, SIGSTOP은 잠시 멈춰주는 신호다. 실제로 프로세스의 state가 stop으로 변경된다. 그리고 그걸 다시 실행할 때는 fg 명령을 이용한다. 리눅스 사용하다가 간혹 Ctrl+Z를 잘못 누르면, 이렇게 살려내면 된다. 몰랐다면 참고 바람.

백그라운드 프로세스
SIGSTOP 후에 멈춘 프로세스는 백그라운드 프로세스가 아니다. 멈춰 있는 프로세스일 뿐이다. 이와 다르게, 백그라운드 프로세스는 돌고 있는 프로세스다. 단지 포그라운드가 아닐 뿐이다. 끝에 & 을 붙여서 실행한다던가 했을 때 생기는 프로세스가 백그라운드 프로세스다. SIGSTOP 후에는 bg 명령을 통해 백그라운드로 재개해야 백그라운드 프로세스로서 돌릴 수 있다.

프로그램에서 시그널을 받았을 때, 원하는 동작을 더 끼워넣으려면 signal() 함수를 이용해서 작정하면 된다.

그러면 한 가지 궁금증이 더해진다. 시그널은 어디로 들어올까? 부모? 자식? 알아보니, 해당 프로세스 그룹 전체에 도달한다. 부모와 자식 모두다.

다음은 유저로서 시그널을 어디까지 보낼 수 있는가에 대한 문제가 아직 남아있다. 기본적으로 시그널을 보낼 수 있는 대상은 같은 유저 소유의 프로세스에 한한다. 정리하면, 시그널은 같은 유저 소유의 프로세스 그룹에 보내진다.

그래서 이런 것도 가능하다. tty1에서 로그인한 후에 GUI를 올렸는데 그게 먹통이 되었다. 그럼 Ctrl+Alt+F#키를 이용해서 다른 tty로 진입하면 해결할 수 있다. 같은 유저로 로그인하거나, root로 로그인해서 그 GUI을 죽일 수 있다. 본체 버튼을 억지로 눌러서 비상종료할 필요가 없다.

RTFM: Read The Man Pages

동료에게 설명하기 귀찮을 때 RTFM을 시전하라고 한다. 이거 무슨 나같은 감성이다. 실제로 RTFM의 의미는 “Read The Fucking Manual”이라고 한다. 내 생각에 이건 그냥 “Read The Man Pages”로 풀어쓰는게 나을 듯 하다. 논쟁할 동료 학생이 없다. 회사에서 RTFM 그대로 쓰는 순간, 그냥 “책임지고 사퇴하겠습니다” 해야 한다. 만약 내가 RTFM을 외치면 퇴사 신호로 봐야 한다.

이후에 Useful Tools 챕터가 나왔는데, 리눅스 커맨드에서 자주 이용하는 기본 명령어들(ex. ps , top , kill)을 RTFM하라는 내용이라 굳이 남겨둘 만한 메모는 없다.

OS - 1) 프로세스

진지하게? 는 역시 어렵다. 사실 시도도 안했다. 회고록같은건 진지하게 써놓고 스터디는 안 진지하게 간다고? 그래야 스터디에 흥미를 안 잃기 때문이다. 회고같은 과거로 돌아가는 짓거리는 한 번이면 족하다. 그래서 진지하고 재미없게 써야 한다.

첫 단원은 프로세스다. OS에게 그 짓거리(가상화)를 시키게 한 주범이다. 프로그램을 실행하면 그게 프로세스다. 프로그램은 Disk에 잠들어 있는 프로세스의 전신이다. 대충 관짝에 잠들어 있는 미라를 생각하면 된다. 그걸 실행하면 프로세스로서 도는 것이다. 미라가 든 관짝을 툭툭 쳐서 주문을 읊는 것이다. 약속된 방법으로 읽어다가 프로그램을 메모리에 로드하고, CPU로 연산을 진행하면 프로세스다. 프로세스를 동시에 여러개 돌리고 싶으니, CPU를 시간분할하고, 메모리를 공간분할한다. 마찬가지 이유로, 컴퓨터가 프로그램을 하나만 가지고 있으면 나머지 공간이 아까우니, 공간분할을 통해 복수의 파일을 보관할 수 있게 한다.

분할

분할의 종류를 아주 간단하게 설명하면 아래와 같다.

  • 시간 분할(Time Sharing): 이거 하다가, 저거 하다가, 그거 하다가, …
  • 공간 분할(Space Sharing): 여기부터 여기까진 이거, 저기부터 저거까진 저거, …

프로세스는 시간분할의 예로 나와 있긴 한데, 그냥 뭐든 분할의 개념이 뒤섞여 있다. 프로세스들이 올라간 메모리의 각 위치는 공간 분할의 결과다. 프로세스들의 실제 연산은 시간분할을 통해 병렬인 것처럼 돌아간다.

디스크도 마찬가지다. 공간분할의 예로 나오지만, 디스크의 읽기/쓰기 동작은 시간분할이다. 얘도 어쨌든 하드웨어가 뭔 짓을 해야 일을 할 것 아닌가.

그래서 뭐는 뭐! 하는 사고방식은 오개념을 낳기 아주 쉽다고 본다.

시간 분할의 레벨

  • High-level(Policy): Scheduling - 이럴땐 이렇게… 지금이닷! Context Switching!
  • Low-level(Mechanism); Context Switching - 눼, 실행! @_@

그래서 프로세스가 뭔데?

책에서는 실행중인 프로그램에 대해 제공하는 추상화를 프로세스라 했다. 사실 프로세스가 뭐냐 물으면 실행중인 프로그램이라고 할텐데, 다음 설명을 위한 밑밥으로 저렇게 어려운 표현을 쓴 듯 하다. 근데, 이렇게 생각하면 재미가 없다. 추상화는 주판과 계산기를 생각하면 직관적으로 이해하기 쉽다. 사람이 주판 사용법을 익혀서 손으로 하나하나 튕구는건 불편하다. 그 과정은 따로 배워야 할 정도로 복잡하다. 그런데 그 과정을 묶어다가 버튼 기반 인터페이스를 올린다면? 물론 계산기 내부가 주판으로 되어 있는건 아니겠지만 말이다. 여튼 계산기는 a + b를 구하기 위해 손가락을 이래저래 튕길 필요 없이, a + b를 버튼으로 입력하기만 하면 된다. 계산하는 과정을 모아다가 사람이 쓰기 좋게 추상화한 결과인 것이다.

프로세스의 구성 요소

프로세스에서 접근할 수 있는 Machine State의 범위라고 보면 된다. Machine State는 CPU + 메모리 + I/O 장치 전체의 현재 상태를 뜻한다. 컴퓨터 켜는데 최소로 필요한게 CPU, 메모리, 디스크다. 그것들의 현재 상태라는 이야기다. 그냥 컴퓨터의 지금 상태다. 그리고 거기서 프로세스가 접근할 수 있는 범위는 아래와 같다.

  • 프로세스가 접근 가능한 메모리 영역(address space)

    • 프로그램을 메모리에 로드했을 때 그 구역이다.
    • 익히 들은 스택, 힙, 코드 영역 등이 그거다.
  • 레지스터(register)

    • 대충 CPU가 연산 중에 값을 잠시 보관해두는 곳이다. CPU에선 아래와 비슷하게 사용한다.
    MOV AX, 10 ; 10을 AX로 보내라
    MOV BX, 20 ; 20을 BX로 보내라
    ADD AX, BX ; BX를 AX에 더해라
    MOV BX, AX ; AX를 BX로 보내라
    ...
    
  • 영구저장장치(persistent storage device)

    • 디스크와 같이 영구저장을 위한 장치들이다. SSD, HDD, Flash 메모리가 대표적.
    • 근데, NVRAM은? → GPT선생이 가라사대, 그렇게 분류할 수는 있는데, OS 교재에서는 그거 취급 안한다 하였다.

Process API

아래는 OS에서 프로세스 관련 API로 제공해야 하는 것들이다.

  • Create
  • Destroy
  • Wait
  • Miscellaneous Control
  • Status

켜고(create) 끄는건(destroy) 당연히 있어야 하고, 부모가 자식 프로세스가 끝날 때까지 기다릴 수 있어야 하고(wait), 근데 이런저런 기능도 했으면 좋겠고(miscellaneous control), 프로세스의 현재 상태도 볼 수 있어야(status) 한다는 것이다.

Process Creation

잠든 미라를 깨워보자. 이 미라는 광부였다. 일단 디스크에서 파일이라는 관짝을 열어다가 미라를 갱도의 담당 구역으로 옮기는데(물론 실제로는 이동이 아니라 복사다), code와 static data을 메모리의 적절한 address space에 로드하는 것이다. 다음엔 담당 구역에 표시를 해준다. 스택과 힙 영역을 할당하는 것이다. 이후 미라를 깨워서 곡괭이를 손에 쥐어주고, “일해라” 하면, 그 미라는 일을 하게 될 것이다. CPU가 메인 함수를 시작으로 로직을 실제로 실행하는 것이다. 이 때, 메인함수여야 하는 이유는 간단하다. “일해라” 해야 일에 필요한 모든 것을 한다. “내려쳐라” 하면 그냥 그 자리에서 곡괭이를 아래로 내려치고 멈출 것이다. 다음은, 채광한 광물을 어딘가 저장하고 옮겨야 한다. 그래서 수레를 사용하면 이게 I/O 작업이다. 그 수레를 옮겨 광물을 창고로 넣으면 이건 영구저장장치에 저장한 무언가가 된다.

Process States

프로세스의 상태는 세 가지가 있다. 이거 state다. Status 아니다.

  • Running: 내 일 지금 진행되고 있음. 땡큐
  • Ready: 나 이제 준비됨. 시간 되면 내 일도 해줘.
  • Blocked: 아 뭐 기다리는 중임. 딴거 먼저 하세요. 아 건들지 말라고!

Running과 Ready 상태 사이의 전환은 스케줄러가 해준다. “피카츄, 너로 정했다! 가랏 백만볼트!” 해주는 것이다.

여기서, Blocked가 뭔지 궁금할 수 있다. 간단히 설명하면 정말 지금 다음껄 실행하면 안되는 상태다. 예를 들어, 파일의 내용을 읽는 작업이 있다고 치자. 그 작업을 디스크에 요청하면, 그 디스크가 그 작업을 끝내거나 중단할 때까지 프로세스의 다음 절차를 실행할 수 없다. 읽으라 해놓고 읽는걸 기다리지도 않고 “빨리 설명해봐!“ 하면 미친놈이다. 그래서 그걸 기다려달라고 표시하는 상태가 blocked다. Ready는 그냥 뒷전으로 밀려나 있는 일거리일 뿐, 언제든 실행될 수 있다.

하지만 실제로 예시로 제공된 xv6의 enum을 살펴보면, 다른 상태들도 몇 개 있다. 미사용(UNUSED), init 과정(EMBRYO), task가 다 돌고 난 이후(ZOMBIE)가 또 있다. 그리고 리눅스껀 또 다르다. 결국 이것도 구현하기 나름이라는 것이다.

UNUSED는 PCB(process control block)의 프로세스를 올릴 수 있는 슬롯이 미사용중이니 여기다 넣어도 된다는걸 나타내는 상태이다. 그래서 더 파보니, xv6같은 단순 OS는 미리 이 슬롯들을 정해진 크기로 할당해 둔 상태이기 때문에 있는 상태였다. 실제로 리눅스와 같이 실제로 이용하는 OS들은 동적으로 할당하는 방식이라 UNUSED 상태가 없다.

Data Structures

OS도 결국 다른 프로그램처럼 하나의 프로그램이기 때문에, 내부에 각종 데이터구조가 있다. 프로세스 구조체는 직접 그 소스를 보면 이해할 수 있다. 그중에서 가장 중요한 것은 context라 생각한다. 그리고 그 context 구조체를 보면, 그냥 레지스터의 값을 담는 구조체다. context switching이 발생하면, 직전에 running 상태였던 프로세스는 자신의 context에 레지스터를 저장하고, deregistered될 것이고, 이 프로세스는 context에 있던 레지스터의 값들을 레지스터로 다시 배치해서 EIP에 있는 코드를 실행할 듯 하다. 물론 context에 매 절차 마다 레지스터를 저장하는지, 컨택스트 스위칭이 일어날 때 저장하는지는 아직 모른다. 그건 OS를 개발하는 사람이 선택하면 될 일이기 때문에, 뚜렷한 답은 없을 것이고, 구현의 차이가 있을 듯 하다. 매번 저장하고 있으면 성능에 손해를 보기 때문에 아마 대부분 context switching이 일어날 때 저장하지 않을까 싶다. 물론 또다른 이유로 저장 주기를 타이트하게 가져갈 수도 있다. 장애를 대비한다던가? 우주방사선(????)을 대비한다던가?

다음으로, 프로세스의 실행 공간에 대한 정보가 있다. address space에 대한 것이다. 그 외에, 프로세스의 현재 상태에 관한 정보들이 포함되어 있다. process state 뿐만 아니라, 다른 더 필요한 것들도 있다. 쥐고 있는 파일들이라던가, 현재 디렉터리라던지 말이다.

OS - 0) Introduction

결국 여기까지 왔다. 네, Rust 커널 책 1-2주요? 개뿔! 초반에 Rust 언어의 사용법이 책 두께의 반 이상을 차지해서 얕봤다. 리눅스 커널 버전부터 다르고, Rust의 커널 관련한 부분의 개발이 활발한 바람에 이미 책은 EUC-KR 수준의 레거시가 되어 있었음. 실습 코드를 그냥 이해한 다음 그 목적을 하는 모듈을 짜보려다보니, 왜? 왜? 왜에에에에??? 가 연속되어 그냥 OS까지 들어가서 커널로 돌아오기로 했다.

정신 차리니 OS에 도착하는 과정
/dev/random 을 Rust로 구현하기 → 왜 딴 애들은 module! 매크로 이용해서 모듈 선언하는데, 얘는 module_misc_device! 지? → 그건 그렇고… 없네? → 대체제 찾았다! → file operations에서 read, write 정의 못하네? → c shim 가자 → 이 삽질 할 시간에 본진을 때리는게 낫지 않음?

최고의 방어는 공격이다. 그래서 ChatGPT에게 커리큘럼 내놓으라 함. 그래서 접한 책은 아래와 같다.

https://pages.cs.wisc.edu/~remzi/OSTEP/

pdf로 그냥 공개되어 있다.

여튼 그래서 슥 보고 있는데, 일단 개론 부분을 요약하면, OS가 존재하는 이유는 하드웨어 위에서 동시에 여러 일을 시키기 위해 가상화를 지원하는 것이다. cpu 1개짜리 머신이라고 프로그램 하나만 돌리면 아까우니까, 젖먹던 힘까지 다 끌어다 쓰기 위해 생긴거다. 힘내라 컴퓨터야. 인간들이 그렇지 뭐.