‘컴퓨터를 쓴다’는 건 무엇을 의미할까?
컴퓨터를 쓰는 목적은 다양하다. 엄청나게 다양하다.
하지만 단순화해본다면, 컴퓨터를 쓴다는 건, 곧 '프로그램을 실행한다'와 같은 말이다. 컴퓨터 사용자는 프로그램을 실행시켜서 컴퓨터(하드웨어)를 사용한다. (그리고 엄청나게 다양한 프로그램이 있겠지.)
프로그램은 커다란 명령어 덩어리다. 컴퓨터가 알아들을 수 있는 언어로 작성돼있다.
프로그램을 실행하면 프로세서가 명령어를 한 줄씩 처리한다.
1 더하기 1을 계산하거나, 더한 값을 메모리 0x495942 에 저장하거나. 뭐 이런 일이다.
연산, 저장만 있는 건 아니다. 입출력도 있다.
코드에 따라서 실행중인 프로그램은 컴퓨터에 연결된 키보드나 마우스에서 입력을 받아오기도 한다. 그렇게 받아온 입력을 디스크에 있는 파일에 쓰기도 한다. 인터넷을 사용해 디스크에 있는 파일 내용을 다른 컴퓨터로 보내기도 한다.
프로그램은 명령어를 통해서 다양한 하드웨어를 이용한다. 그런데 프로그램이 하드웨어에 직접 접근하면 문제가 생긴다.
만약 프로그램이 하나뿐이라면, 직접 하드웨어에 접근해서 사용해도 별 문제가 없을지 모른다. (엄청 초창기의 컴퓨터는 실제로 그랬다고 한다.)
하지만, 실제로는 컴퓨터에서 하나의 프로그램만 돌아가지 않는다.
컴퓨터는 여러 프로그램을 동시에 실행시킨다.
지금 여러분이 쓰고 있는 컴퓨터에서 실행 중인 프로그램 목록을 확인해보자. 100개는 가뿐히 넘는다.
즉, 이런 다양한 프로그램이 다같이 프로세서, 메모리, 디스크 같은 하드웨어를 사용해야 한다는 뜻이다.
하드웨어는 무한하지 않다. 항상 효율적으로 써야 한다. 각각 특성도 다 다르다. 다양한 프로그램들이 자기 마음대로 하드웨어에 접근해서 쓰면 많은 문제가 생길 수밖에.
그래서 하드웨어를 관리하고, 프로그램이 필요할 때 하드웨어와 관련된 서비스를 제공하는 시스템 관리자가 필요하다.
이게 바로 운영체제가 존재하는 이유다.
운영체제는 여러 프로그램이 ‘컴퓨터 시스템'을 효율적이고, 안전하고, 편리하게 사용할 수 있게 도와주는 소프트웨어다.
설명이 조금 딱딱하다. 쉽게 비유해보자.
컴퓨터 시스템은 호텔 같은 거다. 방, 침대, 화장실, 식당, 주방, 헬스장, 수영장 등 다양한 시설을 갖추고 있다.
실행 중인 프로그램은 호텔에 체크인한 고객이다. 호텔은 동시에 수백명의 고객이 사용한다.
여기에 아무런 관리 시스템이 없다고 생각해보자. 호텔에 들어갔는데 프론트에 아무도 없다?
고객이 알아서 방을 정해서 들어가고, 주방에 가서 음식을 꺼내먹고. 세탁실 가서 직접 침대 시트를 가져온다.
갑자기 다른 고객이 내 방에 들어왔다가 ‘어이쿠 사람 있었네요' 하고 나간다. 관리 시스템이 없으니 당연한 일이다.
그야말로 카오스다.
그래서 현실의 호텔에는 관리 시스템이 있다.
호텔 관리 시스템은 사용자가 체크인 가능한 사람인지 확인한다. 효율적으로 방과 침대를 배분한다. 정해진 사람만 출입할 수 있게 보호한다. 체크아웃한 방은 깔끔하게 청소되도록 한다. 제 시간에 사람들이 조식을 먹을 수 있도록 준비한다. 혹시 고객이 방에 필요한 게 있으면 룸서비스로 갖다준다.
호텔 관리 시스템은 '호텔이라는 한정된, 물리적 시설'을 '여러 명의 고객'이 불편없이 안전하고 효율적으로 이용할 수 있도록 한다.
이 ‘관리 시스템’이 바로 운영체제다. (말 그대로 관리(Operating) 시스템(System)이네.)
운영체제는 프로그램들이 문제없이 실행되도록 하기 위해 어마무시하게 많은 일을 한다.
특히 그 중에서도 가장 중요한 건 자원 배분이다. 프로세서, 메모리, 디스크 자원은 모든 프로그램들이 필요로 한다.
운영체제는 ‘가상화'를 통해 각 프로그램들이 편리하게 이 자원을 사용하도록 하고, 하드웨어를 최대한 효율적으로 사용할 수 있도록 배분한다.
갑자기 좀 어려운 단어들이 나왔지만, 걱정할 필요는 없다. 사실 저 한 줄이 앞으로 우리가 주구장창 공부할 내용이라고 봐도 되니까.
지금은 '왜 컴퓨터에 운영체제가 필요하고 어떤 일을 하는지'만 이해하면 된다.
호텔은 다소 단순화시킨 비유다. 하지만 추상적인 컴퓨터 공학을 이해하는 데 있어서 약간의 단순화와 구체화는 필수적이다.
개념이 잘 와닿지 않을 때 호텔을 하드웨어, 실행 중인 프로그램을 투숙객, 호텔 관리자를 운영체제라고 생각해보자. 개인적으로 운영체제의 개념을 이해하는 데 도움이 많이 된다고 생각한다.
💡 ‘운영체제’와 ‘커널’
프로그램에게 하드웨어 자원을 배분하고 관리하는 부분을 ‘커널'이라고 한다.
우리가 위에서 말한 ‘시스템 관리자’가 커널이다. 좁은 의미로는 커널이 운영체제 그 자체라고 봐도 문제없다.
하지만 넓은 의미의 운영체제는 커널뿐 아니라 각종 유틸리티를 포함한다. 반드시 커널의 일부일 필요는 없지만, 보통 운영체제를 깔면 같이 들어있는 프로그램까지도 포함하는 개념이다. 그래픽이나 멀티미디어를 지원한다든지, 사용자의 파일 탐색을 도와주는 프로그램 같은 것들 말이다.
우리는 운영체제의 핵심 작동 원리를 배우고 있기 때문에, 운영체제라고 하면 좁은 의미의 운영체제, ‘커널’을 뜻한다. 따라서 이 글에서 운영체제와 커널은 섞여서 나올 것이고, 같은 의미라고 보면 된다.
실행 중인 프로그램은 운영체제에게 다양한 요청을 할 수 있다.
‘디스크에 파일 데이터를 써주세요’
‘이 프로그램을 실행시켜주세요’
‘이 IP 주소로 메시지를 보내주세요'
실제로 프로그램이 어떻게 운영체제에게 서비스를 요청할까?
다시 호텔의 비유로 돌아가보자.
우리는 컴퓨터라는 호텔에 묵고 있는 고객, 즉 실행 중인 프로그램이다. 아침 식사를 방으로 배달시키고 싶다. 그렇다고 해서 직접 주방에 가서 요리사한테 ‘아침 좀 배달해주세요~’하지는 않는다.
호텔 서비스를 이용할 때는 일단 무조건 ‘프론트 데스크'에 전화를 한다.
프론트 데스크에서는 해당 전화 요청을 받고, 가능한 요청인지 확인한다. 프론트는 해당 서비스를 해줄 부서 (주방)에 요청을 전달하고, 고객에게 결과물을 갖다준다.
프론트 데스크는 고객과 호텔 서비스의 접점, 즉 ‘인터페이스'다.
(출처: 그랜드 부다페스트 호텔 스틸 컷)
마찬가지로 운영체제의 서비스를 이용할 때도, 정해진 접점이 있다.
이걸 ‘시스템 콜'이라고 한다. (방에 있는 프로그램이 시스템에 ‘전화'를 한다고 연상해보자.)
"엄밀히 말해, 시스템 콜이 곧 운영체제다. 시스템 콜이 운영체제의 서비스를 정의하기 때문이다."
<유닉스의 탄생>, 브라이언 커닝핸
운영체제의 기능은 운영체제 내부에 구현이 돼있다. 실행 중인 프로그램은 이 기능을 건드릴 수 없다. 다만 요청할 뿐이다. 운영체제에서 미리 정해놓은, ‘시스템 콜'을 통해서.
"시스템 프로그래밍은 시스템 콜에서 시작해서 시스템 콜로 끝난다."
<리눅스 시스템 프로그래밍>, 로버트 러브
시스템 콜을 배우는 건 운영체제를 사용하는 법을 배우는 것이다. 시스템 소프트웨어는 운영체제의 기능을 활용해 애플리케이션 소프트웨어를 지원해주는 소프트웨어다.
그러니 시스템 프로그래밍은 시스템 콜을 다루는 프로그래밍이라고 해도 된다.
대표적인 시스템 콜 하나를 보자.
read()
함수는 파일을 읽는 시스템 콜이다.
인자로 파일을 가리키는 파일 디스크립터와, 읽은 데이터가 저장될 버퍼, 읽어들일 바이트 수를 넘겨준다. 함수의 return 값은 읽어들인 데이터의 바이트 개수다.
#include <unistd.h>
ssize_t read (int fd, void *buf, size_t len);
프로그램에서 이 read()
함수를 사용한 순간, 커널에 시스템 호출이 전달된다.
그 순간, 실행의 제어권이 ‘사용자 영역’에서 ‘커널 영역'으로 넘어간다.
실행 중이던 프로그램의 컨텍스트는 잠시 정지된다. 커널 영역의 코드를 실행한다. read()와 매칭된 파일을 읽는 기능이 커널 내부에 구현돼있다. 커널 영역에서는 모든 하드웨어에 접근할 수 있다.
커널은 사용자 영역에서 넘겨준 데이터를 사용해 파일 데이터를 읽어온다. 그리고 읽어온 데이터를 버퍼에 저장하고, 바이트 개수를 return한다.
커널이 제어권을 가지고 있을 때는 커널 모드, 또는 커널 영역이라고 하고, 실행중인 프로그램이 제어권을 갖고 있을 때는 ‘사용자 모드' 또는 ‘사용자 영역'이라고 한다. (커널 입장에서 실행 중인 프로그램은 ‘사용자'이기 때문이다.)
이제 프로그램이 어떻게 운영체제의 서비스를 이용하는지 이해가 되었겠지?
유닉스 계열 운영체제에는 시스템 콜이 수백 개 정도 있다고 한다. 중요한 것을 몇 개 나열해보면 다음과 같다.
시스템 프로그래밍을 얘기하기 위해서, 시스템 콜 말고도 반드시 알아야할 것이 바로 C 언어다.
위에서 설명한 시스템 콜은 모두 C 언어 함수 형태였다. 시스템 콜은 반드시 C 언어로 사용할 필요는 없지만, 거의 대부분 C 언어로 사용된다.
왜? C 언어가 시스템 프로그래밍의 표준 언어이기 때문이다. 대부분 개발 문서가 국제 표준어인 영어로 되어있는 이유와 비슷하다.
왜 C 언어가 시스템 프로그래밍의 표준어가 되었을까?
C 언어는 원래 시스템 프로그래밍용으로 태어난 언어다. C 언어는 유닉스(UNIX)라는 운영체제를 프로그래밍하기 위해 만들어졌다. 이후 다시 얘기하겠지만, 유닉스는 윈도우를 제외한 거의 모든 운영체제의 조상이다.
원래 유닉스 운영체제는 저수준 언어인 어셈블리어로 작성됐다. 하지만 어셈블리어는 컴퓨터 아키텍쳐마다 달랐다. 그래서 컴퓨터 구조가 바뀌면 운영체제 코드를 새로 짜야했다.
하지만 C 언어는 (상대적인) 고수준 언어다. C 코드는 아키텍처와 상관없이 사람이 읽기 좋은 형태로 작성했고, C 컴파일러가 컴퓨터 구조에 맞게 컴퓨터가 알아들을 수 있는 바이너리로 변환해주었다.
<유닉스의 탄생> 책을 보면 C 언어로 유닉스를 코딩하자, 유닉스 개발이 탄력을 받았다고 한다.
초기 운영체제는 하나의 프로그램이 다른 컴퓨터에서 실행될 수 있는지, 이식성이 중요한 이슈였다. 그런데 C 컴파일러만 있으면 운영체제를 크게 바꾸지 않고도 여러 컴퓨터에서 사용할 수 있었기 때문이다.
C 언어를 만든 ‘데니스 리치’는 유닉스 운영체제를 만든 사람이다. 유닉스 운영체제를 만들다가 이식성을 높이기 위해 C 언어(컴파일러)를 뚝딱 만들어낸 것이다. 그래서 유닉스와 C 언어는 뗄레야 뗄 수 없는 관계다.
C 언어는 '당시 다른 저수준 언어보다' 훨씬 사용하기 편했다. 유닉스의 인기와 함께 계속 퍼져나갔다.
대부분의 웹 브라우저, 거의 모든 언어의 컴파일러, 거의 모든 데이터베이스, 대부분의 운영체제, 텍스트 에디터부터 MS 오피스까지 모두 C로 만들어졌다. 이후 나온 Java, C#, Python, PHP, Javascript 같은 고수준 언어에도 엄청난 영향을 미쳤다.
아무튼 결론은, C 언어는 절대적인 위치를 차지하는 표준어라는 것. 적어도 시스템 수준의 프로그래밍에서는 거의 '영어'급의 표준이다.
‘시스템 프로그래밍 한다’는 곧 C 언어로 프로그래밍한다라는 뜻이다.
시스템 콜 인터페이스도 대부분 C 언어로 표준화돼있다. 시스템 프로그래밍을 할 때는 C 라이브러리와 C 컴파일러를 사용해서 코딩을 하게 된다.
C 컴파일러는 C 언어 소스코드를 컴퓨터에서 실행할 수 있는 바이너리(기계어) 파일로 만드는 소프트웨어다. C 컴파일러 세상에 존재하는 거의 모든 컴퓨터에서 실행 가능하다.
C 라이브러리는 자주 사용하는 코드를 미리 컴파일해둔 것으로, 흔히 libc(립씨)라고 부른다. 데이터를 조작하고 계산하는 함수들 뿐만 아니라, 시스템 콜을 더 편하게 사용할 수 있도록 만든 함수들도 존재한다.
<리눅스 시스템 프로그래밍>에서는 시스템 프로그래밍의 3가지 주춧돌이 시스템 콜, C 라이브러리, C 컴파일러라고 한다.
나는 C 언어로 프로그래밍을 해본 적이 한번도 없었다. 시스템 프로그래밍을 배우면서 처음으로 공부하게 됐다.
확실히 어려웠다. 기존에 배웠던 Swift와 비교해봤을 때 확실히 C 언어는 운영체제와 매우 가깝고, 추상화가 덜 되어있다. 특히 직접 메모리 상태를 생각하고 관리해야하는 부분이 그렇다.
하지만 Swift도 C 언어의 손자이기 때문에 물려받은 부분도 꽤 있었다. 그 차이를 이해하는 건 재미있는 경험이었다.
사용자 영역, 즉 실행 중인 프로그램에서 커널을 사용하는 방법은 몇 가지가 더 있다.
그림을 보자.
시스템 콜은 커널을 둘러싸고 있고, 직접 애플리케이션이 시스템 콜을 사용할 수도 있다.
하지만 ‘라이브러리 함수’이나 ‘쉘’을 사용하는 방법도 있다.
먼저 라이브러리 함수. 라이브러리란 쉽게 말해 남이 쓴 코드를 내 코드에 가져다 쓰는 것이다.
그 중에서도 ‘시스템 콜’의 기능을 좀 더 편리하게 사용하기 위해서 만들어둔 코드 모음이 있다. 시스템 라이브러리다.
시스템 콜은 아무래도 운영체제에 대해 지식이 필요하거나 명세가 복잡하고, 추가적인 기능이 부족하다. 그렇기 때문에 시스템 콜을 좀 더 쉽게 쓰기 위해서 만들어둔 라이브러리 함수를 만들어둔 것이다.
이전에 나온 read()
시스템 콜을 예로 들어보자. read()
는 커널을 직접 호출하는 시스템 콜이다. 하지만 C 표준 라이브러리에서 제공하는 fread()
, fgetc()
, fgets()
라는 함수도 있다. 이 함수들은 내부적으로 read()
를 사용하지만 좀 더 편리하게 파일을 읽을 수 있도록 미리 만들어놓은 함수다.
시스템 호출: 운영체제가 제공하는 좀 더 날것 그대로의 인터페이스.
C 라이브러리 함수: 편의를 위해 추상화된 층이 씌워진 버전.
시스템 호출은 곧바로 커널을 호출하지만, 라이브러리 함수는 C 언어 라이브러리에서 구현하고 있다는 점에서 분명 다르다.
하지만 사용자 입장에서는 크게 다를 건 없다. 커널을 사용하기 위해서 함수를 호출하는 형태는 비슷하다.
사용자가 커널을 사용할 수 있는 또 다른 방법은 쉘이다.
쉘은 쉽게 말해, ‘프로그램을 실행하는 프로그램’이다.
개발자라면 터미널을 켜고 ls
, cd
, rm
같은 명령어를 입력해서 컴퓨터를 조작해본 적이 있을 것이다. 이건 커맨드라인 인터페이스라고 한다. 쉘은 이 커맨드라인 인터페이스를 가능하게 해주는 프로그램이다.
쉘은 사용자가 입력한 명령어를 한 줄씩 읽고 해석한다. 특정한 방식으로 특정한 프로그램을 실행시킨다.
예를 들어, 디렉터리 내 파일을 확인하는 ls
라는 명령어가 있다. 유닉스/리눅스에는 ls라는 시스템 소프트웨어가 이미 들어있다. ls
는 현재 디렉터리 혹은 특정 디렉터리의 파일 내용을 출력하는 간단한 프로그램이다.
쉘에 ls
를 입력하면 쉘이 ls라는 소프트웨어를 실행시킨다. ls라는 소프트웨어는 C 라이브러리 함수나 시스템 콜을 호출해서 파일 정보를 출력한다. 프로그램 실행을 마치면 다시 쉘로 돌아온다.
이런 식으로 명령어를 해석하고 프로그램을 실행해준다. 간단한 작업을 할 때 C 언어로 코딩을 하고 컴파일을 해서 실행시키지 않아도 된다. 직접 파일을 만들거나, 특정 프로그램을 실행하려면 쉘에 명령어만 입력하면 된다.
쉘은 기본으로 제공되지만, 커널에 포함되지 않는 유틸리티 프로그램이다. 얼마든지 다른 쉘을 설치해서 쓸 수 있다.
리눅스나 Mac의 표준 쉘은 bash다. 그 외에 zsh, csh 등 다양한 쉘이 있다.
복잡한 쉘 명령어가 필요하다면, 여러 명령어를 한꺼번에 모으고 조건문, 반복문 등을 사용해 쉘에 입력할 수도 있다. 이런 언어를 쉘 스크립트라고 한다.
정리하자면, 사용자가 커널을 사용할 수 있는 주요한 방법은 다음과 같다.
1) 시스템 콜을 호출하는 것
2) 라이브러리 함수를 호출하는 것
3) 쉘을 사용하는 것
물론 3가지 모두 궁극적으로는 시스템 콜을 사용한다.
시스템 콜은 운영체제마다 구현이 다 다르다. C도 사실 굉장히 많은 버전이 존재한다.
하지만 오늘날 프로그래머들은 그런 차이를 크게 신경 쓰지 않는다. 시스템 콜과 C 언어의 표준이 있기 때문이다. 운영체제와 그 구현은 엄청나게 다양하지만, 대부분이 표준을 따른다.
그래서 시스템 프로그래밍을 공부한다는 건 이 표준을 배우는 일이기도 하다. 다음 글에서는 다양한 운영체제의 종류와 표준에 대해서 알아보자.
덕분에 운영체제를 너무 재밌게 이해하고 있습니다 최고에요