운영체제에게 서비스 요구하기 (Command Interpreter, API)
참고 서적 - 운영체제 공룡 책
작성할 때 노동요로 틀었던 노래.. ^.^ - https://youtu.be/YrlnU1ogdEU?si=sqY-8iNOv2m6z_BB
0. 개요
우리는 컴퓨터, 휴대폰 등 많은 전자기기를 사용한다. 이 기기로 간단한 응용프로그램을 연다고 가정해보자. 우리가 보는 것은 특정 정보를 나타내는 UI일 것이다. 하지만 이 UI를 띄우기 위해 운영체제는 수많은 서비스를 제공하고 있다. 이번 글에서는 운영체제가 제공한다는 서비스가 그래서 뭐고, 서비스를 요청하기 위해서는 어떻게 해야되는지 알아볼 것이다.
운영체제의 대부분 서비스들은 커널에서 이뤄진다. 따라서 운영체제에게 서비스 요구 == 커널에게 서비스 요구와 유사하다. 하지만 이 커널은 잘못 건드리면 컴퓨터 시스템이 망가질 수 있고, 사용자 보안이 위험할 수 있다. 따라서 커널에게 서비스를 요구하려면 엄격한 플로우를 거쳐야 한다.
먼저, 부팅이 완료되어 프로그램을 시작할 즈음에는 운영체제의 모드가 유저 모드일 것이다. 여기서는 커널에게 직접 작업을 요청할 수 없다. 먼저 시스템 콜을 호출해 커널 모드로 변경한 후, 시스템 콜 작업을 수행한다. 이때 서비스가 제공될 것이다. 그리고 작업이 끝나면 다시 유저 모드로 복귀한다.
여기서 알 수 있는건, 우리가 운영체제에게 서비스를 요구하기 위해서는 System Call을 사용해야 된다는 것이다. 이 시스템 콜을 요청하는 주체가 누구냐에 따라 크게 두 가지로 나뉜다.
- command interpreter로 System call 요청하기.
- API로 System call 요청하기.
이 두 가지를 하나씩 살펴볼 것이며, 전체 목차는 다음과 같다.
1. 운영체제의 서비스
앱 하나 열 때도 운영체제의 수많은 서비스를 사용한다고 했다. 이 "서비스"가 대체 뭔지 이번 단락에서 소개하고자 한다. 운영체제가 하는 일, 즉 운영체제가 제공하는 서비스는 아래 사진에 나와있는 것 외에도 많다. 하지만 중심이 되는 것 위주로 소개하고자 한다.
이 네 가지 서비스가 서로 연결되어 있다.
1 - a) 운영체제의 서비스 1 - 프로그램 수행
운영체제가 제공하는 가장 주된 서비스, 바로 프로그램을 수행할 수 있는 능력이다. 나는 이것이 운영체제가 있는 주 목적이라고 생각한다. 왜냐하면, 다른 서비스들의 목적이 결국 프로그램 수행이기 때문이다. << 이렇게 생각한 이유는 프로그램 실행 과정에 있다.
우리가 GUI에서 아이콘 딸깍해서 여는 프로그램은, 사실 마법같은 과정을 거쳐 나온다. 왜냐하면, 처음 시작 단계는 그냥 코드 + 데이터 덩어리이기 때문이다. 그러나 이 덩어리를 실행하기 위해서 일단 메모리에 올린다. 이걸로 되는게 아니고, 실행하기 위한 자원을 할당 받아야 한다. CPU한테 자원을 할당 받았다면, Program Counter가 실행해야 되는 위치를 하나씩 가리키면서 ...중략... 프로그램을 결론적으로 실행할 수 있는 것이다. (중략이라고 써둔 것처럼 굉장히 많은 과정이 추가적으로 들어간다.)
어쨌든 프로그램 실행에 필수적인 이 작업들을 운영체제가 해준다. 따라서, 운영체제의 메인 서비스가 "프로그램 수행"이라고 생각한다. 😎 그리고 앞으로 소개할 3가지의 서비스도 프로그램 수행을 하기 위해 제공해야 하는 서비스이다.
1 - b) 운영체제의 서비스 2 - 자원 할당
프로그램을 실행하기 위해서는 반드시 리소스를 할당받아야 한다. 따라서 운영체제는 프로그램 수행을 담당하기 때문에, 자원 할당도 관리해준다. multi-tasking을 지원하는 운영체제라면 요 자원 할당이 굉장히 중요하다. 왜냐하면, 한정된 자원으로 여러 프로그램을 돌려야 하기 때문이다. 어떻게 공평하고 효율적이게 자원을 할당할지에 관련된 내용은, CPU 스케줄링 개념을 찾아보면 된다.
어쨌든 중요한 점은 프로그램을 실행하기 위해서는 자원이 필요한데, 이 자원을 할당해주는 친구가 운영체제라는 점이다.
1 - c) 운영체제의 서비스 3 - 파일 시스템 조작
세 번째로 소개할 운영체제의 주요 서비스는 파일 시스템 조작이다. CS적으로 파일 시스템 조작은 파일의 소유권에 따라 CRUD가 가능한가? 라는 말이다. 그리고 이 파일 시스템 조작은 프로그램 실행에 있어서 필수적인 요소이다. 왜냐하면, 프로그램의 초기 상태는 코드+데이터 덩어리이기 때문이다. 이 코드+데이터 덩어리는 주로 파일 시스템에 있다. 따라서 프로그램을 실행하기 위해서는 이 코드+데이터 파일에 접근부터 가능해야 한다.
그리고 프로세스가 실행되는 동안 우리는 파일에 CRUD할 일이 왕왕 있다. 로깅 파일을 남긴다든지, 워드 파일을 생성해서 저장한다든지.. 등등 생각해보면 많을 것이다. 안드로이드에서는 뭐.. 앱 루트 디렉터리 하위에 다운로드한 파일을 저장하는 일 등이 있을 것이다.
1 - d) 운영체제의 서비스 4 - 입출력 연산
이건 일반적으로 우리가 사용하는 프로그램을 생각해보면 된다. 우리는 프로그램을 실행할 때 마우스 or 키보드 or 터치 스크린으로 수많은 입력을 한다. 그리고 화면은 우리에게 어떤 정보들을 출력해준다. ➡️ 정리해보자면, 프로세스는 런타임 중 수많은 입출력을 요구한다. 그러나 프로세스는 입출력을 처음으로 처리하는 단계는 아니다. 운영체제가 가장 먼저 입출력을 받고, 프로세스에게 "이런 입출력이 일어났는데 너 필요해?"라고 전달해주는 형태이다.
유저
⬇️
키보드 (HW)
⬇️
운영체제
⬇️
APP
왜 운영체제가 대신 해줄까?
하드웨어에 직접 접근하는걸 막기 위함이다.
입출력 이벤트를 받기 위해서는 하드웨어에 접근해야 된다. 근데 이 하드웨어 아무 프로그램이나 접근 권한을 주면 어떻게 될까? 은행 프로그램인척 가장해서 당신 비밀번호를 털 수도 있을 것이다 . . . . . 따라서 보안상의 목적으로, 응용 프로그램이 직접 HW에 접근하여 이벤트를 받지는 않는다.
하드웨어 너무 다양한데, 이거 하나하나 프로그램에서 대응하는게 불가능하기 때문.
프로그램에서 하드웨어에 직접 접근한다는 말은, 프로그램이 하드웨어 사용법을 알고 있다는 뜻이다. 프로그램이 대부분의 입출력 하드웨어 스펙을 알고, 처리 방법도 알아야 우리가 지금처럼 편하게 쓸 수 있을 것이다. 근데 이건 하드웨어가 너무 많아서 불가능하다.🫨 그래서 이 입출력 이벤트를 추상화해서 알려주는 친구가 바로 os이다. os는 입출력 이벤트를 직접 받고, 프로그램에게 추상화된 형태로 전달한다.
(그러면 운영체제는 각 입출력 하드웨어 장치 대응을 어떻게 하냐..? << 요거에 대한 건 OS의 입출력 드라이버 쪽을 살펴보면 된다.)

2. 그래서 운영체제에게 일을 시키는 방법?
지금까지는 운영체제가 제공해주는 서비스에 대해 알아보았다. 서비스의 메인 목적은 프로그램 실행이다. 그리고 이 프로그램 실행을 위해 자원 할당, 파일 시스템 조작, 입출력 연산 등의 서비스를 제공한다고 설명했다. 지금부터는, 운영체제에게 이 서비스들을 어떻게 요구할 수 있는지에 대해 알아보려고 한다. 여기서는 OS GUI를 사용해 프로그램을 실행하는 경우는 제외한다. 이건 너무 당연하잖아~!
개요에서 언급했듯이 운영체제에게 서비스를 요구하려면, System Call을 사용해야 한다. 우리는 주로 크게 두 가지 방법으로 이 System call을 호출한다. 첫 번째는 주로 User가 터미널을 통해 시스템 콜을 호출하는, Command Interpreter를 활용한 방식이다. 두 번째는 개발자가 주로 사용하는인 API를 활용한 방식이다.
2 - a) 방법 1 - Command Interpreter
Command Interpreter를 활용해서 OS에게 서비스를 요청하는 그 흐름은 다음과 같다.
- 유저가 명령어를 Terminal에 입력한다.
- Terminal은 입력받은 명령어를 실행 주체인 Command Interpreter, Shell에게 전달한다.
- Shell은 이걸 운영체제가 알아들을 수 있는 명령어로 Interpreting 작업을 한다.
- 주로 적절한 System call이 될 것이다.
- 운영체제에게 요청을 한다.
왜 커맨드 인터프리터, 즉 쉘이 있는걸까? 가장 큰 목적은 운영체제의 서비스를 유저도 쉽게 사용하게 하기 위함이다.
(조금 더 내 생각을 덧붙이면 이렇다. 시스템 콜은 어쨌든 함수의 형태다. 그러다 보니 문법이 복잡한데, 이걸 유저가 사용하게 하기엔 좀 난감한거다. 그래서 자연어 친화적인 걸 만들었다. 이게 바로 우리가 터미널에서 사용하는 명령어다. 근데 운영체제는 명령어는 모르니까, 중간에 통역가(쉘)이 필요해진거다. 따라서 쉘은 일종의 번역기이다.)
그리고 명령어로 반복 작업할 일 있을때, 쉘이 유용하다. 왜냐하면 여러 쉘 명령어를 한 번에 모아서 실행할 수 있는 방법이 있기 때문이다. 그게 바로 쉘 스크립트(.sh), 배치 파일(.bat)이다.
추가로, 터미널과 쉘은 다른 것이다. 터미널은 영단어에서 알 수 있듯 창구를 의미한다. 따라서 유저가 요청을 하거나, 응답을 받을 때 사용하는 end point의 단말이다. 유저가 입력한 명령어를 터미널이 받으면, 터미널은 이것을 쉘에게 전달한다. 터미널은 껍데기일 뿐이기에, 터미널 혼자서는 명령어를 실행할 능력이 없다. (쉘은 터미널 없이도 백그라운드에서 실행될 수 있다.)
유저의 명령어를 운영체제가 알아들을 수 있는 시스템 콜로 번역해주는 친구가 쉘이라는 것은 알았다. 근데 이 명령어는 두 가지로 카테고라이즈가 가능하다. 외부 프로그램의 도움이 필요한가로 내부 명령어, 외부 명령어로 나뉜다. 내부 명령어는 command interpreter가 명령을 실행하기 위한 코드를 자체적으로 가지고 있다. 따라서 그 코드에 필요한 매개변수를 적절하게 설정하고 시스템 콜을 한다. 대표적인 내부명령어로는 cd가 있다. 외부 명령어는 command interpreter가 명령을 실행하기 위해 시스템 프로그램에게 요청을 해야 되는 경우다. 왜냐하면 자기 코드에는 이 명령어를 실행할 로직이 없기 때문이다. 쉘은 어떤 시스템 프로그램을 사용해서 명령을 실행할 수 있는지만 안다. 익숙한 command 중 외부명령어는 ls가 있다.
- 외부명령어 관련해서 조금 더 깊게 들어가고 싶으면 환경변수(PATH)와 외부명령어의 관계를 보면 좋을 것 같다. (관련 링크: 리눅스 쉘내부명령어와 외부명령어를 구분하는 방법)
2 - b) 방법 2 - API(Application Programming Interface)
우리는 고오급 언어를 활용해서 안드로이드 어플리케이션을 개발한다. 고오급 언어로 API들을 활용해서 앱을 만들어낸다. 하지만 low level에서는 운영체제가 열일을 하고 있다. 다시 말하면, API가 호출한 수많은 시스템 콜의 연속으로 어플리케이션이 실행된다는 것이다. 그러나 대부분의 개발자들은 이 로우 레벨까지는 신경쓰지도 않고, 몇몇은 이런 일들이 일어나고 있는지도 모르고 있다. 일단 나는 이 사실을 몰랐었다 ^^...
왜 몰랐을까? 그게 바로 API의 설계 의도이기 때문이다. 우리는 주로 API만 사용해서 개발을 한다. API doc을 보고, 이런 일을 하겠구나라고 이해하고 사용한다. 실제로 로우 레벨에서 뭔 일이 일어나고 있는지는 우리의 관심사가 아니다. 즉, API가 실제로 뭘 하는지는 신경쓰지 않아도 되는 슈퍼 장점이 생긴 것이다.
이게 무슨 말이냐면, 안드로이드 앱에서 특정 파일을 읽고 싶어서 Java File IO의 read 메소드를 사용했다고 생각해보자. 그러면 내부적으로는 아래와 같은 시스템 콜들이 호출될 것이다. (사실 더 많이 호출됨.)
- 입력 파일 이름 획득
- 입력 파일 열기
- 입력 파일로부터 읽어 들인다. (n회 til EOF)
- 입력 파일 닫기
그러나 우리는 read 메소드를 사용할 때 이런 시스템 콜들을 하나하나 따지지 않는다. 우리가 read api로 기대하는 건, 특정 파일을 읽어줄 것이라는 것 뿐이다. 로우 레벨에서 뭘 하는지는 전혀 고려하지 않는다.
그러니까 여기서 Application Programming Interface의 개념을 잡을 수 있다. API는 프로그래머가 기대하는 시스템 콜들을 추상화한 형태이다. 위키백과의 API 소개는 다음과 같다.
API의 한 가지 목적은 시스템이 동작하는 방식에 관한 내부의 세세한 부분을 숨기는 것으로, 내부의 세세한 부분이 나중에 변경되더라도 프로그래머가 유용하게 사용할 수 있고 일정하게 관리할 수 있는 부분들만 노출시킨다.
여튼 API의 개념까지 소개하기는 했다. 근데 정확히 왜 추상화한게 슈퍼 장점일까?
- 개발자는 API를 호출하면 작업이 일어날 것임을 기대하는 걸로 끝이다. 내부 사정은 몰라도 된다.
- 프로그램이 실행되는 시스템 종류 하나하나 대응해주는 건 개발자의 고려사항이 아니게 된다.
- 개발자가 내부 사정을 알고 하는 것보다 버그 발생 가능성이 줄어든다.
- 파일 읽는데만 해도 수많은 시스템 콜이 발생하는데, 이 순서를 개발자가 확실하게 지켜 호출할 것이라고 보장할 수 있을까?
- API 내부에서 알아서 처리해주는게 1단 개발자 맘이 편하다. 🫠
RTE(Runtime Environment)
윗 단락에서는 API가 직접 시스템 콜을 요청한다는 것처럼 말했는데, 완전히 틀린 말은 아니긴 한데 ... 사실 한 단계가 더 숨어져 있다. 어플리케이션이 일단 API 호출을 하면 최종적으로는 System Call이 호출 되는 것은 맞다. 하지만 어떤 System Call을 호출해주면 좋을지 판단을 해주는 친구가 필요하다.
그게 바로 RTE의 System Call Interface다. 이 System Call Interface가 필요한 이유는, 운영체제의 다양성과 관련해서 생각해보면 된다. 운영체제는 대부분 비슷한 System Call 형식을 가지고 있지만, 조금씩 다를 수 있다. 즉 이 말은, 운영체제의 환경에 맞게 적절한 시스템 콜이 불려야 한다는 말이다. 그래서 이 역할을 해주는 것이 바로 System Call Interface이다. 그래서 API 요청이 들어오면, System Call Interface가 요청을 먼저 가로챈다. 그리고 어떤 시스템 콜이 필요할지 판단하고 호출해준다. 그리고 그 return 값을 반환한다.
이렇게만 작성하면 RTE가 시스템 콜 매퍼처럼 느껴지는데, 그건 아니다. 이외에도 운영체제 환경을 알아야만 수행 가능한 일들을 해준다. 그래서 책에서는 RTE를, Applcation을 실행하기 위해 필요한 전체 소프트웨어라고 소개한다.
안드로이드의 RTE
Android의 어플리케이션은 코틀린, 자바로 작성된다. 이 어플리케이션을 실행하기 위해서는 RTE가 필요한데, 안드로이드에선 ART이다. 🥸 (물론 코드 자체는 JVM을 사용해서 .class 파일로 컴파일 되기는 한다)
Android 런타임(ART)
AOSP에서 제공하는 자바 런타임 환경입니다. ART는 앱의 바이트 코드를 기기의 런타임 환경에서 실행되는 프로세서별 명령으로 변환합니다.

JAVA의 RTE
JAVA의 런타임 환경은 JRE이다.
AWS 문서에서는 다음과 같이 소개하고 있다. 설명은 Java의 특수성인, JRE만 깔려 있다면 자바 코드는 어디서든지 실행 가능해요 ~ 와 연관이 있다. 그러나 어쨌든 요지는, 자바 프로그램을 돌리는데 필요한 운영체제 리소스를 연결해주는 친구가 JRE라는 점이다.

3. 정리하자면..
아주 간단하게 정리해보면 다음과 같다. 운영체제는 여러 서비스를 제공한다. 이중 가장 메인이 되는 것은 프로그램 수행이다. 그리고 자원할당, 파일 시스템 조작, 입출력 연산 등과 같은 서비스들도 제공한다. 이 서비스를 요청하기 위해서는 크게 두 가지 방식이 있다. 첫 번째가 사용자들이 주로 쓰는 Command Interpreter를 활용한 방식이다. Command Interpreter를 통해 명령어를 시스템 콜로 번역해서 요청한다. 그리고 두 번째는 개발자들이 주로 사용하는 API를 활용하는 방식이다. API를 호출하면 RTE's System Call Interface가 적절한 시스템 콜을 호출한다.
여기까지가 운영체제의 서비스를 사용하는 방법이자 운영체제에게 일하게 만들기에 관련된 내용이다.
# 대문용 이미지