파일을 읽는 코드는 한 줄이다.
ssize_t bytes = read(fd, buffer, size);
fd는 읽을 대상을 가리키는 번호, buffer는 읽은 데이터를 담을 메모리 주소, size는 이번 호출에서 최대 몇 바이트까지 읽을지 정하는 값이다. 반환값 bytes에는 실제로 읽은 바이트 수가 담긴다.
이 호출은 사용자 공간에서 커널 공간으로 넘어간다. 커널은 요청한 범위 안에서 데이터를 읽어 buffer에 채운다.
짧은 왕복 안에서 CPU, 메모리와 장치가 만난다.
CPU 스케줄링
요청한 데이터가 메모리에 없다면 저장 장치의 응답을 기다려야 한다. 이때 read()를 호출한 작업은 실행을 멈춘다.
CPU까지 함께 멈추지는 않는다. 스케줄러는 실행 가능한 다른 작업을 고른다. 장치가 읽기를 마치면 대기하던 작업이 다시 실행 후보가 된다.
멀티태스킹은 여러 작업을 동시에 보여주는 기술보다, 기다리는 작업을 비켜 세우고 실행 가능한 작업에 CPU를 배분하는 기술에 가깝다.
가상 메모리
buffer는 프로세스가 보는 가상 주소다. 커널은 이 주소가 현재 프로세스에 유효한지 확인하고 실제 메모리와 연결한다.
프로세스마다 가상 주소 공간이 분리되어 있기 때문에 같은 숫자의 주소도 서로 다른 메모리를 가리킬 수 있다. 한 프로세스가 다른 프로세스의 메모리를 마음대로 읽지 못하는 이유도 이 경계에 있다.
가상 메모리는 부족한 메모리를 보충하는 기능만이 아니다. 프로세스가 자기 공간만 보고 실행되게 만드는 구조다.
파일 디스크립터
fd에는 파일의 경로나 데이터가 들어 있지 않다. 현재 프로세스가 열어 둔 대상을 가리키는 작은 정수다.
커널은 이 값을 통해 일반 파일, 파이프나 소켓 같은 대상을 찾는다. 애플리케이션은 대상마다 다른 장치 명령을 알 필요 없이 읽기와 쓰기라는 공통 인터페이스를 사용한다.
파일 디스크립터는 서로 다른 입출력 대상을 같은 방식으로 붙잡게 해주는 손잡이다.
시스템 호출과 커널
커널은 CPU 시간을 배분하고, 주소 공간을 보호하며, 파일 디스크립터를 실제 입출력 대상으로 연결한다. 시스템 호출은 애플리케이션이 이 기능을 요청하는 경계다.
시스템 호출은 사용자 공간과 커널 공간 사이에 커널이 정해 둔 출입구다. 애플리케이션은 하드웨어에 직접 손을 뻗지 않고 이 경계를 통해 요청한다.
운영체제는 커널보다 넓은 말로 쓰일 수 있다. Linux는 커널이고, Linux 배포판은 여기에 시스템 라이브러리와 기본 도구, 사용자 환경을 더한 실행 체계다.
다시 보면 이 코드는 단순히 파일에서 데이터를 꺼내는 함수 호출이 아니다.
ssize_t bytes = read(fd, buffer, size);
fd로 커널이 관리하는 입출력 대상을 고르고, buffer로 데이터를 받을 메모리 위치를 지정하며, size로 그 범위를 제한한다. 데이터가 준비되지 않았다면 운영체제는 이 작업을 재우고 다른 작업에 CPU를 넘긴다.
운영체제는 CPU, 메모리와 입출력 장치를 여러 프로그램이 안전하게 나눠 쓰도록 대상과 경계, 실행 순서를 관리하는 체계다.
read() 한 줄은 그 체계에 “이 대상에서, 이 메모리로, 이만큼 읽어 달라”고 요청하는 인터페이스다.
