시스템 콜과 이중 모드 — 여러 프로그램을 안전하게 돌리는 법
1. 남은 신호 하나
1편에서 운영체제가 신호에 반응해 일한다는 것을 봤습니다. 신호는 셋이었습니다. 바깥 장치가 보내는 하드웨어 인터럽트, 잘못된 상황에서 일어나는 예외, 그리고 프로그램이 일부러 일으키는 시스템 콜입니다. 셋 중 시스템 콜만 이름을 확인하고 지나갔습니다. 이 글은 그 안쪽으로 들어갑니다. 운영체제가 무엇을 해 주는지에서 출발해서, 여러 프로그램을 한꺼번에 돌릴 때 생기는 위험을 지나, 그 위험을 막는 장치가 시스템 콜을 어떤 모양으로 만들었는지에 닿습니다.
2. 운영체제가 내놓는 서비스
운영체제가 해 주는 일은 크게 두 갈래입니다.
- 사용자를 위한 것 — 명령을 입력받는 통로, 프로그램 실행, 입출력, 파일과 디렉터리 다루기, 프로그램끼리 주고받기, 오류 검출.
- 시스템 자신이 효율적으로 돌기 위한 것 — 자원 할당, 기록, 보호와 보안.
앞의 여섯 가지는 우리가 직접 쓰는 것이고 뒤의 세 가지는 눈에 띄지 않는 곳에서 돌아갑니다.
사용자가 운영체제에 말을 거는 통로부터 봅니다. 가장 오래된 방식이 명령줄 인터페이스입니다. 글자로 명령을 적어 넣으면 그것을 읽어 실행합니다. 이 일을 하는 프로그램을 셸이라고 합니다. 다음은 아이콘과 창과 마우스로 다루는 그래픽 인터페이스입니다. 터치스크린은 마우스가 없으니 손짓으로 고르고 화면 위 자판으로 글자를 넣습니다. 최근에는 일상적인 말로 시키면 그 뜻을 풀어 대신 명령을 실행해 주는 계층이 더 붙었습니다. 통로가 무엇이든 도착하는 곳은 운영체제입니다.
3. 부팅과 커널의 구조
전원을 켜면
전원을 켜면 부트스트랩 프로그램이 먼저 돕니다. 시스템을 초기화하고 커널을 메모리에 올리는 짧은 코드입니다. 커널이 올라오면 그다음 시스템 데몬을 띄웁니다. 데몬은 사용자 눈에 띄지 않는 곳에서 계속 돌며 요청이 오기를 기다리는 프로그램입니다. 인쇄 요청을 받아 처리하는 프로그램이 그런 예입니다. 부팅이 끝나면 커널은 가만히 있습니다. 신호가 올 때만 움직이기 때문입니다.
커널은 한 덩어리가 아니다
- 모놀리식 커널 — 모든 기능을 하나의 덩어리에 넣고 같은 메모리 영역 안에서 돌립니다. 기능끼리 곧장 연결되어 있어 빠릅니다. 대신 한 곳을 고치면 커널 전체를 다시 번역해 통째로 올려야 합니다.
- 모듈 방식 — 기능을 조각으로 나누고 필요할 때만 커널에 끼워 넣습니다.
- 마이크로커널 — 꼭 필요한 것만 커널에 남기고 파일 관리 같은 나머지는 바깥의 별도 프로그램으로 뺍니다. 한 조각이 망가져도 커널은 살아 있습니다. 대신 조각끼리 커널을 거쳐 메시지를 주고받아야 하고 그만큼 느려집니다.
실제 운영체제는 대개 이들을 섞어 씁니다. 어떤 방식이든 커널이 맡는 일은 같습니다.
4. 멀티프로그래밍과 멀티태스킹
프로세서를 놀리지 않는다
커널이 맡는 일 중 하나가 어느 프로그램에 프로세서를 줄지 정하는 것입니다. 프로그램 하나만 메모리에 올려 두면, 그 프로그램이 디스크에서 값을 읽어 오기를 기다리는 동안 프로세서는 아무 일도 하지 않습니다. 전체 시간 중 프로세서가 실제로 일한 비율을 이용률이라고 하는데, 멈춰 있는 구간이 길면 이용률이 떨어집니다. 그래서 여러 프로그램을 한꺼번에 메모리에 올려 두고, 하나가 디스크를 기다리느라 멈추면 다른 프로그램에 프로세서를 넘깁니다. 이것이 멀티프로그래밍입니다. 목적은 이용률을 끌어올리는 것입니다.
넘기는 간격을 아주 짧게
멀티프로그래밍은 지금 돌던 프로그램이 스스로 멈출 때만 프로세서를 넘깁니다. 한 프로그램이 계산만 오래 하면 그동안 다른 프로그램은 아무 반응이 없습니다. 그래서 스스로 멈추기를 기다리지 않고 아주 짧은 간격으로 계속 넘깁니다. 이것이 멀티태스킹이고 시분할이라고도 부릅니다. 간격이 충분히 짧으면 사람은 여러 프로그램이 동시에 도는 것처럼 느낍니다. 사용자가 무언가를 입력하고 첫 반응이 나올 때까지 걸리는 시간을 반응 시간이라고 하는데, 여기서 목표로 삼는 값은 일 초 미만입니다.
문제 1
프로그램 두 개가 메모리에 올라가 있습니다. 프로그램 가는 계산만 계속해서 20초 동안 한 번도 멈추지 않습니다. 프로그램 나는 사용자가 글자를 칠 때마다 화면에 바로 보여 줘야 합니다. 지금 프로그램 가가 프로세서를 잡고 있습니다. 첫째, 멀티프로그래밍만 하는 시스템에서 사용자가 글자를 치면 언제 화면에 나타납니까. 둘째, 멀티태스킹을 하는 시스템에서는 어떻게 달라집니까.
풀이
첫째, 멀티프로그래밍은 돌던 프로그램이 스스로 멈출 때만 프로세서를 넘기는데 프로그램 가는 20초 동안 멈추지 않습니다. 그래서 글자는 20초가 지난 뒤에야 화면에 나타납니다. 쓸 수 없는 상태입니다. 둘째, 멀티태스킹은 정해진 짧은 시간이 지나면 강제로 넘깁니다. 간격이 충분히 짧으면 프로그램 나는 거의 바로 차례를 받아 글자를 띄웁니다.
멀티프로그래밍이 노린 것은 이용률이고, 멀티태스킹이 노린 것은 반응 시간입니다. 프로그램 가만 놓고 보면 두 방식의 이용률은 같습니다. 달라진 것은 사용자가 겪는 시간입니다.
5. 프로그램 하나가 전체를 망칠 수 있다
여러 프로그램을 같이 돌리면 메모리와 장치를 함께 쓰게 됩니다. 한 프로그램이 잘못된 명령을 실행해 다른 프로그램의 메모리를 건드리면 그 프로그램까지 망가집니다. 운영체제가 쓰는 영역을 건드리면 시스템 전체가 멈춥니다. 빠져나오지 못하는 반복, 흔히 무한 루프라고 부르는 상태에 걸려 프로세서를 영영 놓지 않으면 나머지 프로그램은 차례가 오지 않습니다. 그래서 여럿이 함께 쓰는 운영체제에는 한 프로그램의 잘못이 다른 프로그램에 번지지 않아야 한다는 조건이 반드시 필요합니다.
6. 특권 명령과 모드 비트
막는 방법은 명령을 두 등급으로 나누는 것입니다. 입출력 장치를 직접 건드리거나 메모리 보호 설정을 바꾸는 것처럼, 잘못 쓰면 시스템 전체에 영향을 주는 명령을 특권 명령이라고 합니다. 이 등급은 커널만 쓸 수 있고 응용 프로그램에는 금지됩니다.
이를 위해 프로세서가 사용자 모드와 커널 모드 두 가지 모드를 갖습니다. 응용 프로그램은 사용자 모드에서 돌고 여기서는 위험한 등급이 막혀 있습니다. 운영체제 코드는 커널 모드에서 돌고 전부 허용됩니다. 지금 어느 모드인지는 프로세서 안의 비트 하나, 모드 비트에 적혀 있습니다. 커널 모드일 때 0이고 사용자 모드일 때 1입니다. 응용 프로그램이 금지된 명령을 시도하면 이 비트를 본 하드웨어가 그 자리에서 막아 냅니다. 소프트웨어끼리의 약속이 아니라는 점이 중요합니다.
7. 시스템 콜의 동작
응용 프로그램은 사용자 모드에 있어서 디스크를 직접 건드릴 수 없습니다. 그런데 파일은 읽어야 합니다. 그래서 커널에 대신 해 달라고 맡기는 통로가 필요하고, 그 통로가 시스템 콜입니다.
시스템 콜마다 번호가 붙어 있고, 커널 안에는 번호별로 어떤 코드를 실행할지 적어 둔 표인 시스템 콜 테이블이 있습니다. 프로그램이 번호를 넘기며 신호를 일으키면 하드웨어가 모드 비트를 0으로 바꿉니다. 커널 모드가 된 것입니다. 커널이 표에서 그 번호의 코드를 찾아 실행하고, 끝나면 결과를 돌려주고 모드 비트를 1로 되돌립니다. 1편의 용어로 말하면 이 신호가 트랩입니다. 프로그램이 그 자리에서 일부러 일으킨 것이기 때문입니다.
코드에서 다른 코드를 불러 쓰는 보통의 방식을 함수 호출이라고 하는데, 시스템 콜은 그 모양을 하고 있어도 속은 다릅니다. 모드를 바꾸는 일은 프로그램이 스스로 할 수 없고 하드웨어만 할 수 있기 때문입니다.
8. 시스템 콜의 여섯 갈래
- 프로세스 제어 — 실행 중인 프로그램 하나를 프로세스라고 하는데, 그것을 새로 만들고 강제로 끝내고 끝나기를 기다리는 일입니다. 리눅스에서는 이 셋을 각각
fork,kill,wait이라고 부릅니다. - 파일 관리 — 파일을 열고 읽고 쓰는 일입니다.
open,read,write가 여기 해당합니다. - 장치 관리 — 장치를 요청하고 놓아주는 일입니다.
- 정보 관리 — 시각이나 시스템 설정을 읽고 바꾸는 일입니다.
- 통신 — 프로그램끼리 메시지를 보내거나 메모리를 함께 쓰는 일입니다.
- 보호 — 접근 권한을 정하는 일입니다.
파일 하나를 읽는 과정
먼저 open입니다. 경로를 주면 그 파일을 열고 번호를 하나 돌려줍니다. 이 번호를 파일 서술자라고 합니다. 지금 열어 둔 파일에 붙은 이름표라고 보면 됩니다. 이후의 요청은 긴 경로 대신 이 번호로 합니다. 다음은 read입니다. 파일 서술자와 버퍼와 바이트 수를 넘깁니다. 버퍼는 읽어 온 내용을 담아 둘 메모리 공간입니다. 커널은 그만큼 읽어 버퍼에 담고, 실제로 읽어 낸 바이트 수를 알려 줍니다. 쓸 때는 write를 씁니다. 열린 파일마다 지금 몇 번째 바이트를 읽고 쓸 차례인지 표시가 따라다니는데, 그 표시를 다른 곳으로 옮기고 싶으면 lseek을 씁니다. 요청이 실패하면 네 가지 모두 -1을 돌려줍니다.
9. 같은 프로그램이 다른 운영체제에서 안 도는 이유
시스템 콜의 이름과 번호는 운영체제마다 다릅니다. 같은 번호가 다른 일을 가리킵니다. 프로세서가 바로 실행할 수 있는 형태로 번역해 둔 실행 파일의 형식도 운영체제마다 다릅니다. 그래서 같은 기계어 명령을 담고 있어도 통하지 않습니다. 여러 운영체제에서 도는 프로그램을 만드는 방법은 세 가지입니다.
- 운영체제마다 준비된 해석기가 원본 코드를 그때그때 읽어 대신 실행해 줍니다. 파이썬이 이 방식입니다.
- 가상 기계 위에서 돌립니다. 가상 기계는 운영체제 위에 올라가 진짜 컴퓨터인 척해 주는 프로그램이고, 자바가 이 방식입니다.
- 원본을 하나만 쓰고 운영체제마다 따로 번역해 둡니다. C로 쓴 프로그램이 이 방식입니다.
10. 우리가 부르는 것은 표준 라이브러리다
셋째 방식이 가능하려면 원본 코드에 운영체제마다 다른 이름이 적혀 있으면 안 됩니다. 그래서 언어마다 미리 만들어 둔 함수 묶음, 표준 라이브러리가 따라옵니다. 파일을 읽는 fread가 그런 예입니다. fread는 C 언어의 표준 라이브러리에 들어 있고 운영체제가 만든 것이 아닙니다. 그런데 디스크를 실제로 읽는 일은 커널 모드에서만 됩니다. 그래서 fread는 안에서 read를 부릅니다. 우리는 어느 운영체제에서나 같은 이름을 쓰고, 그 아래에서 각 운영체제의 시스템 콜이 불립니다.
문제 2
화면에 글자를 찍는 printf는 C 언어의 표준 라이브러리에 들어 있습니다. 어떤 프로그램이 printf를 한 번 불렀습니다. 이때 운영체제 쪽에서 실제로 무슨 일이 벌어집니까. 모드 비트와 시스템 콜 테이블이라는 말을 써서 차례대로 답해 보십시오.
풀이
printf가 하려는 것은 화면에 글자를 내보내는 것입니다. 화면은 장치이고 장치를 건드리는 명령은 커널 모드에서만 허용됩니다. 그래서 printf는 안에서 write라는 시스템 콜을 부릅니다. write의 번호를 들고 신호가 들어오면 하드웨어가 모드 비트를 0으로 바꿔 커널 모드로 들어갑니다. 커널이 시스템 콜 테이블에서 그 번호를 찾아 화면에 글자를 내보냅니다. 끝나면 모드 비트가 1로 돌아오고 프로그램이 이어서 실행됩니다. 트랩이 한 번 일어난 것입니다. 운영체제가 제공하는 쪽은 write이고 printf는 그 위에 덮인 껍데기입니다.
정리
전원을 켜면 부트스트랩이 커널을 메모리에 올립니다. 그 커널은 한 덩어리일 수도 있고 조각으로 나뉘어 있을 수도 있습니다. 프로세서를 놀리지 않으려고 여러 프로그램을 함께 돌리기 시작했고, 사용자를 기다리게 하지 않으려고 넘기는 간격을 짧게 줄였습니다. 함께 돌리니 서로를 망칠 위험이 생겼고, 그래서 명령을 두 등급으로 나눠 위험한 쪽은 커널 모드에만 허락했습니다. 사용자 모드에 갇힌 프로그램이 그 경계를 넘어 일을 맡기는 정식 통로가 시스템 콜이고, 우리가 코드에 적는 printf 같은 이름은 그 위에 덮인 껍데기입니다.
프로세스는 아직 실행 중인 프로그램이라고만 하고 넘어갔습니다. 그것이 메모리에서 어떤 모습으로 있는지, 운영체제가 무엇을 적어 두고 관리하는지는 다음 글에서 봅니다.