정의
임계 구역(critical section)은 공유 자원에 접근하는 코드 구간으로, 둘 이상의 실행 흐름이 동시에 실행해서는 안 되는 부분이다.[1]
임계 구역은 자원이 아니라 코드에 속한다. 같은 세마포어가 지키는 데이터를 세 태스크가 쓴다면 임계 구역도 셋이고, 한 태스크가 서로 다른 세마포어로 보호되는 데이터를 차례로 만지면 그 태스크 안에 임계 구역이 여럿 생긴다. 실시간 문헌이 임계 구역을 “job 의 번째 임계 구역”처럼 job 단위로 번호 붙여 다루는 것도 그래서다.[2]
구간을 식별하는 일과 구간을 지키는 일은 다른 작업이다. 이 페이지는 지켜야 할 성질과 지키는 수단의 대비까지 다루고, 지켜야 하는 이유는 경쟁 상태에, 락의 내부 구현과 종류는 락에 있다.
상호 배제와 원자성
상호 배제(mutual exclusion)는 한 실행 흐름이 임계 구역 안에서 실행 중이면 다른 흐름은 그 구역에 들어가지 못한다는 성질이다. 원자성(atomicity)은 묶인 동작들이 전부 일어나거나 전혀 일어나지 않은 것처럼 보이고 중간 상태가 관측되지 않는다는 성질이다.[1]
둘은 층위가 다르다. 원자성은 우리가 원하는 결과이고, 상호 배제는 그 결과를 얻는 수단이다. 하드웨어가 임의의 명령열을 원자적으로 실행해 주지는 않으므로, 짧은 명령열을 원자적 블록처럼 보이게 만들려고 동기화 원시로 상호 배제를 건다.
스케줄링 금지
가장 간단한 보호는 임계 구역이 끝날 때까지 재스케줄링을 막는 것이다. OSEK OS는 이 목적을 위해 스케줄러 자체를 자원으로 다룬다. 모든 태스크가 접근할 수 있는 RES_SCHEDULER라는 자원이 자동 생성되고, 태스크는 이 자원을 점유해 다른 태스크의 선점을 막는다. 다만 인터럽트는 이 자원의 상태와 무관하게 수신되고 처리되며, 막히는 것은 태스크의 재스케줄링뿐이다.[3]
따라서 같은 데이터를 인터럽트 서비스 루틴도 만진다면 이 수단만으로는 부족하다. OSEK OS가 자원 관리를 태스크와 인터럽트 서비스 루틴 사이의 조정까지 확장할 수 있게 열어 둔 것도, 그리고 재스케줄링을 유발하지 않는 인터럽트 금지 서비스를 따로 둔 것도 이 때문이다. 인터럽트 쪽 규정은 OSEK 인터럽트 처리에서 다룬다.
인터럽트 금지
임계 구역 앞에서 인터럽트를 끄고 뒤에서 다시 켜면 구간이 중단 없이 실행되므로 원자적으로 실행되는 것과 같아진다. 스케줄링도 인터럽트에 실려 오므로 함께 막힌다. 단순하다는 것이 이 방법의 유일한 장점이고 단점은 여럿이다.[4]
첫째, 인터럽트를 켜고 끄는 것은 특권 연산이라 호출하는 코드를 신뢰해야 한다. 욕심 많은 코드가 인터럽트를 끈 채 프로세서를 독점하거나 무한 루프에 빠지면 운영체제가 제어를 되찾을 방법이 없다. 둘째, 여러 프로세서에서는 통하지 않는다 — 다른 코어의 흐름은 인터럽트와 무관하게 계속 돌면서 같은 임계 구역에 들어간다. 셋째, 오래 끄고 있으면 인터럽트를 놓치게 되고, 외부 이벤트가 도착했다는 사실 자체가 사라진다.
응답 시간과의 맞바꿈
인터럽트를 끈 구간은 그대로 인터럽트 응답이 지연되는 구간이다. 오래 끌수록 인터럽트를 놓칠 위험이 커지므로[4], 실시간 시스템에서 이 수단은 짧고 경계가 분명한 구간에만 쓴다.
락을 통한 합의
셋째 수단은 실행 흐름들이 공유 변수 하나를 두고 서로 합의하는 것이다. 코드 구간을 락으로 감싸면 프로그래머는 그 코드 안에서 동시에 활성인 흐름이 하나를 넘지 않는다고 보장할 수 있고, 그만큼 스케줄링에 대한 통제권을 되돌려 받는다.[4] 특권 연산에 기대지 않고 여러 프로세서에서도 성립한다는 점이 인터럽트 금지와 다르다.
대신 새로운 문제가 따라온다. 여러 락을 겹쳐 잡는 순서가 어긋나면 교착이 생기고, 우선순위가 다른 태스크들이 같은 락을 쓰면 우선순위 역전이 생긴다. 어느 쪽도 임계 구역을 어디서 어디까지로 잡았는지와 무관하지 않아서, 구간을 식별하는 일은 결국 락 설계로 이어진다.