운영체제는 여러 작업들이 동시에 실행되는 것처럼 보이도록 물리적인 CPU를 공유한다. 이를 CPU 시간을 나누어 돌아가면서 잠깐 잠깐 실행하는 방식을 통해 CPU 가상화를 구현한다.
근데 이러한 가상화 기법을 위해서는 몇가지 문제를 해결해야 한다.
“직접 실행”이라는 말은 CPU를 진짜로 그냥 실행시키는거다.
위 방식으로 가상화를 하게 되면 몇가지 문제가 있다.
이를 위해 “제한적”으로 프로그램을 실행해야 한다. 그렇지 않다면 운영체제는 어떠한 것도 제한할 수 없으며 단순한 라이브러리일 뿐이다.
직접 실행의 장점은 빠르다는 것이다. 그러나 디스크 IO요청이나 메모리같은 시스템 자원 추가 할당 요청과 같은 특수한 종류의 연산을 수행한다면 어떤 일이 발생할까?
파일에 대한 접근을 허용하기 전에 접근 권한을 검사하는 파일 시스템 구현을 예시로 들어 보자. 프로세스에 대해 디스크 IO가 제한되지 않는다면 프로세스가 전체 디스크를 읽고 쓸수 있어 접근 권한을 검사하는 기능은 아무런 의미가 없다.
이 때문에 사용자 모드(user mode)와 커널 모드(kernel mode)가 도입되었다. 사용자 모드에서는 할 수 있는 작업은 제한되어 있고, 그 작업은 커널 모드에서 수행할 수 있다. 입출력 요청의 경우 커널 모드에서만 수행 가능하다.
그렇다면 사용자가 디스크 읽기 같은 특권 명령어를 실행해야할 때는 어떻게 해야하는가? 이런 제한 작업의 실행을 위해 시스템 콜(System Call)을 제공한다. 파일 시스템 접근, 프로세스 생성 및 제거, 다른 프로세스와 통신, 메모리 할당 등의 기능이 있다.
시스템 콜의 실행을 위해 trap 명령어를 통해 커널 안으로 분기 후 특권 수준을 커널 모드로 상향 조절한다. 커널 모드에서 요청한 작업을 처리한 뒤 return-from-trap 특수 명령어를 통해 특권 수준을 유저 모드로 하향 조정 후 사용자 프로그램으로 돌아간다.
하드웨어는 trap 명령어 실행 시 주의가 필요한데, return-from-trap 명령어 실행 시 정상적인 리턴을 위해서 호출한 프로세스의 필요한 레지스터를 저장해야 한다. x86의 경우 PC, 플래그 등 몇가지를 프로세스의 커널 스택에 저장한다.
커널은 부팅 시에 트랩 테이블(trap table)을 만들어 시스템을 통제한다. 운영체제가 하는 초기 작업 중 하나는 하드웨어에게 예외 사건이 발생했을 때 어떤 코드를 실행할지 알려주는 일이다. 예를 들어, 하드 디스크 인터럽트, 키보드 인터럽트, 시스템 콜이 발생했을 때, 운영체제는 특정 명령어를 통해 하드웨어에게 트랩 핸들러(trap handler)의 위치를 알려준다. 하드웨어는 이 정보를 받으면 해당 위치를 기억하고 있다. 따라서 예외적인 사건에 대해 하드웨어는 무엇을 해야할지(어느 코드로 분기할 지) 알 수 있다.

직접 실행의 두 번째 문제점은 프로세스 간 전환을 할 수 있어야 한다는 것이다.
프로세스 간 전환은 간단해야 한다. 운영체제는 실행 중인 프로세스를 계속 실행할 것인지, 멈추고 다른 프로세스를 실행할 것인지를 결정해야한다. 간단해 보이지만 실제로는 까다로운 문제인데, CPU에서 프로세스가 실행 중이라는 것은 운영체제는 실행하고 있지 않다는 것이다. 운영체제가 CPU에서 실행되지 않는다면 아무런 조치를 할 수 없다.
운영체제는 어떻게 CPU를 다시 획득하여 프로세스를 전환할 수 있는가?
운영체제는 프로세스들이 합리적으로 행동할 것이라고 신뢰한다. 오래 실행할 가능성이 있는 프로세스가 주기적으로 CPU를 포기할 것이라고 가정한다. 이러한 방식의 운영체제에서는 시스템 콜을 통해 yield 같은 시스템 콜을 통해 CPU의 제어권을 운영체제제에게 넘겨준다.
프로세스가 협조하지 않거나 시스템 콜을 호출하지 않아 운영체제에게 제어를 넘기지 않을 경우, 운영체제는 추가적인 도움 없이 제어를 획득하기 어렵다. 이를 해결하기 위해 타이머 인터럽트 기능을 사용한다. 타이머 인터럽트는 정기적으로 발생하여 운영체제에게 CPU 제어를 양도한다.
운영체제가 제어권을 다시 획득 했을 때, 현재 프로세스를 계속 실행할지 또는 다른 프로세스로 전환할 것인지를 결정해야 한다. 이는 운영체제의 스케줄러를 통해 이루어진다.
다른 프로세스로의 전환이 결정되면 운영체제는 문맥 교환(Context Switch)를 수행한다. 현재 실행 중인 프로세스의 레지스터 값을 커널 스택 같은 곳에 저장하고, 곧 실행될 프로세스의 커널 스택으로부터 복원한다. 전환을 위해 프로세스의 범용 레지스터 값과 PC, 현재 커널 스택 포인트를 저장한다.
타이머 인터럽트가 발생하면, 다음으로 실행될 프로세스가 결정된다. 그 이후 현재 실행중인 프로세스의 레지스터 값을 저장하고, 다음에 실행될 프로세스의 레지스터가 복원한다. 이런 문맥 교환 과정을 거쳐 다음 프로세스가 실행될 수 있다.
이 내용이 병행성과 관련된 내용이며 나중에 다룰 예정이다.
가장 간단한 해법은 인터럽트를 비활성화하는 것이다. 그러나 인터럽트를 오랫동안 비활성화시키면 성능적으로 좋지 않다.
운영체제는 또한 내부 자료 구조에 동시 접근을 방지하기 위해 락(lock) 기법을 사용한다.