system calls (implemented using the exception mechanism)
프로세스가 커널에게 정식으로 일을 부탁하는 경우
exception mechanism 위에 구현
즉 하드웨어 입장에서는 시스템 콜도 일종의 trap/exception처럼 처리되지만, 운영체제 설계 관점에서는 제한된 수의 carefully coded entry points, 즉 아주 엄격하게 준비된 공식 입구를 통해 커널 서비스를 요청하는 방식이라고 본다.
Mode switch: Kernel → User
새로운 process나 thread가 시작될 때
이때 커널은 해당 프로그램의 실행 환경을 다 준비한 뒤, 프로그램의 첫 번째 instruction 부터 실행되도록 넘겨준다.
interrupt, exception, system call 처리 후 원래 실행 흐름으로 돌아갈 때
사용자 프로그램이 실행 중이었는데 interrupt가 들어오면 잠깐 커널이 처리하고, 끝나면 다시 그 프로그램의 중단된 지점부터 실행을 이어간다.
context switch
커널이 꼭 원래 실행 중이던 프로세스로 되돌아가야 하는 것은 아니다.
인터럽트를 처리하고 나서 스케줄러가 판단했을 때, 지금은 다른 프로세스를 돌리는 것이 더 적절하다면 커널은 다른 process/thread의 문맥을 복원해서 user mode로 내려보낼 수 있다.
user-level upcall (UNIX signal)
커널이 사용자 프로그램에게 비동기적으로 사건을 알려 주는 방식이다.
어떤 signal이 도착하면, 커널은 원래 user code로 그냥 돌아가는 대신 사용자 공간 안에 준비된 signal handler 쪽으로 제어를 넘길 수 있다.
즉 kernel → user 이동 중에서도 “원래 코드로 복귀"와 “사용자 handler로 진입"이 구분된다고 보면 된다.
Handling interrupts (or exceptions)
현재 실행 중이던 사용자 프로그램의 상태((IP, SP)를 interrupt stack에 저장한다.
상태 저장이 끝나면 CPU의 제어는 interrupt handler, ISR로 넘어간다.
Interrupt stack
CPU 코어마다 하나씩(per-core) 존재하고, kernel memory 안에 위치한다. 인터럽트나 예외가 발생했을 때 커널은 사용자 stack을 그대로 쓰지 않기 때문이다.
보통의 프로세스/스레드는 user stack과 kernel stack을 둘 다 가지며, interrupt stack은 kernel stack의 일부이다.
사용자 stack은 user space에 있으므로 신뢰할 수 없고, 커널은 자기 보호된 메모리 안에서 안전하게 상태를 저장하고 handler를 실행해야 한다. 그래서 평소에는 user stack 위에서 실행되던 프로그램이, 사건이 발생하는 순간 kernel stack 쪽으로 내려와서 그 위에 사용자 CPU 상태를 저장하고, 그다음 커널 함수 호출 프레임들을 쌓으며 처리를 진행하게 된다.
Interrupt Masking
interrupt handler는 interrupts off 상태에서 실행됨 / 그리고 처리가 끝나면 다시 enable된다.
왜 이럴까? 이유는 간단하다. 커널이 어떤 민감한 상태를 업데이트하는 중에 또 다른 인터럽트가 들어오면 자료구조가 꼬이거나 race condition이 생길 수 있기 때문이다. 그래서 보통 인터럽트 핸들러가 실행되는 동안은 새로운 인터럽트를 잠시 막아 두고, 처리가 끝난 뒤 다시 허용한다.
OS kernel도 필요에 따라 직접 interrupts를 껐다 켤 수 있다.
CLI 는 disable interrupts, STI 는 enable interrupts다.
다만 이 효과는 현재 CPU에만 적용된다는 점도 같이 강조한다. 멀티코어 환경에서 한 CPU에서 인터럽트를 끈다고 해서 시스템 전체 인터럽트가 모두 꺼지는 것은 아니다.