시스템 호출: 응용 프로그램이 커널에 일을 맡기는 문
프로그램이 파일을 읽거나 새 프로세스를 만들려면 다른 프로그램의 자원까지 관리하는 운영체제의 도움이 필요하다. 시스템 호출은 응용 프로그램이 커널 서비스를 요청하는 통제된 인터페이스다. 프로그램이 임의의 장치나 다른 프로세스 메모리를 직접 바꾸는 대신, 정해진 요청과 인수를 전달하고 커널이 권한과 상태를 확인하도록 만든다.
시스템 호출은 커널 서비스를 요청하고 검증된 결과를 받는 경계다.
함수 호출처럼 보여도 경계를 넘는다
코드에서 read나 open을 호출하면 일반 함수 호출처럼 보일 수 있다. 실제로는 라이브러리의 래퍼가 인수를 준비하고 아키텍처에 맞는 방식으로 커널에 진입하는 경우가 흔하다. 커널은 요청 번호와 인수를 받아 해당 서비스를 수행하고 결과를 돌려준다. Linux 시스템 호출 문서는 시스템 호출과 라이브러리 래퍼가 항상 일대일로 대응하는 것은 아니라고 설명한다.
사용자 모드에서는 할 수 없던 일을 커널 모드에서 수행할 수 있지만, 요청자의 권한까지 자동으로 높아지는 것은 아니다. 예를 들어 읽기 권한이 없는 파일을 요청하면 커널은 접근을 거부한다. 사용자 메모리에서 전달된 포인터도 검증하고 적절히 복사해야 한다. 경계가 존재하는 이유는 자원 공유의 편의와 시스템 보호를 함께 제공하기 위해서다.
파일 읽기를 따라가 보면
프로그램이 파일 기술자, 버퍼 주소, 최대 읽기 길이를 전달했다고 하자. 커널은 그 기술자가 유효하고 읽을 수 있는지 확인한 뒤 필요한 데이터를 준비한다. 데이터가 이미 캐시에 있다면 장치를 기다리지 않을 수 있다. 없으면 입출력을 요청하고 작업이 대기할 수 있다. 완료 후 실제 읽은 바이트 수를 반환하며, 요청한 길이보다 적게 읽히는 상황도 정상적으로 가능하다.
반환값을 확인하지 않고 버퍼 전체에 새 데이터가 있다고 가정하면 오류가 된다. 파일 끝에서는 더 읽을 내용이 없고, 일부 읽기나 인터럽트 때문에 재시도가 필요할 수도 있다. 시스템 호출을 이해하는 것은 호출 이름을 외우는 일보다 성공·부분 성공·실패가 프로그램 상태에 어떤 의미인지 읽는 일에 가깝다.
모든 표준 함수가 시스템 호출은 아니다
문자열 길이를 계산하는 함수는 사용자 공간에서 끝날 수 있다. 메모리 할당 함수도 이미 확보한 영역을 나눠 주기만 할 때는 매번 커널에 들어가지 않는다. 반대로 출력 함수는 사용자 공간 버퍼에 모아 두었다가 여러 번의 출력을 한 번의 쓰기 요청으로 묶을 수 있다. 함수 호출 횟수와 시스템 호출 횟수가 서로 다른 이유다.
시스템 호출에는 경계 진입·검증·복귀 비용이 있다. 아주 작은 입출력을 반복하면 일을 묶는 편이 유리할 수 있다. 하지만 비용은 요청 종류와 데이터 상태에 따라 달라지므로, 시스템 호출 하나의 시간을 모든 작업에 적용하면 안 된다. 커널에 들어갔다가 같은 스레드로 바로 돌아올 수도 있어 모든 시스템 호출이 문맥 교환을 일으키는 것도 아니다.
운영체제마다 호출 집합과 인수 규약이 달라 응용 프로그램은 보통 표준 라이브러리나 이식 가능한 API를 사용한다. 시스템 호출을 학습하면 권한 오류, 입출력 지연, 버퍼링, 자원 누수의 위치를 구체적으로 찾을 수 있다. 응용 코드와 커널의 책임이 어디서 나뉘는지 이해하는 것이 디버깅의 출발점이다.