운영체제 기본/파일과 입출력: 두 판 사이의 차이
출처·문체 정리: 커널 원문은 git.kernel.org, em dash는 콜론으로 변경 |
총론 재작성: 이름·객체·열린 상태 분리와 버퍼링·비동기·영속성의 trade-off |
||
| 1번째 줄: | 1번째 줄: | ||
[[운영체제 기본|목차]] · 이전: [[운영체제 기본/가상 메모리|가상 메모리]] | [[운영체제 기본|목차]] · 이전: [[운영체제 기본/가상 메모리|가상 메모리]] | ||
메모리의 데이터는 프로세스가 종료되거나 전원이 꺼진 뒤에도 필요한 경우가 많다. 파일 시스템은 데이터를 지속적으로 보관하고 다시 찾을 수 있게 하며, 입출력 계층은 속도와 동작 방식이 다른 장치 사이에서 데이터를 전달한다. 이때 프로그램이 사용하는 이름, 현재 접근 중인 대상, 실제 저장장치의 상태를 하나로 취급하면 동시 접근과 장애를 다루기 어렵다. | |||
== | == 저장 위치 대신 이름과 바이트를 제공하기 == | ||
프로그램이 저장장치의 어느 구역에 데이터가 있는지 직접 관리한다면, 파일이 커지거나 다른 프로그램이 공간을 사용했을 때 배치를 함께 조정해야 한다. 파일 시스템은 저장 공간의 할당과 회수를 맡고, 프로그램에는 이름으로 찾을 수 있는 파일을 제공한다. 디렉터리는 이름을 파일 객체에 연결하며, 파일 객체는 내용과 크기, 접근 권한 같은 정보를 가진다. 이름을 찾는 문제와 내용을 어디에 놓을지는 분리된다. | |||
일반 파일을 바이트의 나열로 제공하면 프로그램은 저장장치의 세부 구조에 맞춰 매번 코드를 바꾸지 않아도 된다. Unix는 파일뿐 아니라 파이프와 터미널에도 읽기와 쓰기라는 공통 인터페이스를 제공한다. 덕분에 출력 대상을 바꾸어 프로그램을 조합하기 쉽다. 다만 인터페이스가 비슷해도 모든 대상의 성질이 같지는 않다. 일반 파일은 위치를 바꾸어 읽을 수 있지만 파이프는 그렇게 할 수 없으며, 바이트 스트림의 데이터에 붙일 레코드 구조는 프로그램이 정해야 한다. <ref name="source-1">[https://man7.org/linux/man-pages/man2/read.2.html Linux man-pages: read(2)].</ref> | |||
== 이름, 객체, 열린 상태의 수명을 나누기 == | |||
파일을 열 때마다 경로를 다시 검색하는 대신, 운영체제는 찾은 객체를 계속 참조할 수 있는 핸들을 돌려준다. Unix의 파일 디스크립터가 그 예다. 핸들이 가리키는 열린 상태에는 읽고 쓰는 현재 위치 등이 들어 있다. 같은 파일을 따로 열면 위치를 독립적으로 유지할 수 있고, 열린 상태를 공유하는 핸들은 위치도 함께 사용한다. 파일 내용의 공유와 읽기 위치의 공유는 다른 선택이다. <ref name="source-2">[https://man7.org/linux/man-pages/man2/open.2.html Linux man-pages: open(2)].</ref> | |||
이 분리는 이름이 바뀌어도 진행 중인 작업을 유지하게 한다. Unix에서 로그 파일의 마지막 이름을 삭제해도 프로세스가 파일을 열어 두었다면 계속 쓸 수 있다. 같은 이름으로 새 파일을 만들어도 기존 핸들이 새 파일로 옮겨 가지는 않는다. 이름이 사라진 시점과 저장 공간을 회수할 수 있는 시점이 달라지는 대신, 경로의 변경 때문에 이미 연 대상이 갑자기 바뀌지 않는다. <ref name="source-3">[https://man7.org/linux/man-pages/man2/unlink.2.html Linux man-pages: unlink(2)].</ref> | |||
== | == 버퍼링과 비동기로 기다리는 비용 줄이기 == | ||
CPU가 작은 입출력 요청마다 느린 장치의 완료를 기다리면 계산할 시간이 낭비된다. 버퍼는 속도가 다른 두 쪽 사이에서 데이터를 잠시 보관한다. 작은 쓰기를 모아 보내면 요청 횟수를 줄이고, 읽은 내용을 캐시에 남기면 같은 데이터를 다시 가져오는 비용을 줄인다. 대신 버퍼가 RAM을 차지하며, 모아서 보내기를 기다리는 동안 개별 데이터의 전달은 늦어질 수 있다. 처리량의 개선과 한 요청의 지연 시간 단축은 항상 같은 방향이 아니다. | |||
비동기 입출력은 요청과 완료 확인을 분리해, 장치가 일하는 동안 프로그램이 다른 일을 하게 한다. 버퍼링과 함께 사용할 수 있지만 서로 같은 개념은 아니다. 비동기로 실행한 작업도 완료 결과를 확인해야 하며, 완료 전까지 사용 중인 버퍼의 수명을 보장해야 한다. 동시에 너무 많은 요청을 쌓으면 대기열과 메모리 사용량이 늘어난다. 읽기 캐시 역시 원본이 다른 경로로 바뀔 수 있다면 갱신을 어떻게 알아낼지 정해야 한다. 재사용의 이득에는 최신성 관리 비용이 따른다. <ref name="source-4">[https://docs.kernel.org/admin-guide/mm/concepts.html 커널 문서: 페이지 캐시].</ref><ref name="source-5">[https://man7.org/linux/man-pages/man7/aio.7.html Linux man-pages: 비동기 입출력].</ref> | |||
== 요청한 양과 완료된 양을 구분하기 == | |||
입출력은 요청 전체의 성공과 실패 두 상태만으로 표현하기 어렵다. 읽기는 파일 끝에 도달하거나 현재 도착한 데이터가 적으면 요청보다 적은 바이트를 돌려줄 수 있다. 쓰기도 공간 부족이나 실행 중단 등의 이유로 일부만 끝날 수 있다. 따라서 반환된 양은 실제로 어디까지 진행했는지를 나타내는 정보다. 남은 부분을 재시도하려면 이미 처리한 위치를 보존해야 한다. 처음부터 다시 보내면 중복된 데이터가 생길 수 있다. | |||
양수를 반환한 부분 완료, 입력의 끝, 오류는 각각 다르게 처리해야 한다. 특히 바이트 스트림에서는 송신 측이 한 번에 쓴 범위와 수신 측이 한 번에 읽는 범위가 일치하지 않는다. 메시지의 길이나 구분자는 상위 프로토콜이 정해야 한다. 또한 버퍼에 쓰기를 받아들인 뒤 실제 저장 과정에서 오류가 드러날 수 있으므로, 최초 쓰기의 성공만으로 이후 실패 가능성까지 사라지지는 않는다. <ref name="source-6">[https://man7.org/linux/man-pages/man2/write.2.html Linux man-pages: write(2)].</ref> | |||
== 보이는 변경과 남는 변경을 구분하기 == | |||
버퍼드 쓰기의 성공은 데이터가 운영체제의 캐시에 전달되었다는 뜻일 수 있다. 다른 읽기에서 변경을 관찰하는 것과 전원 장애 후에도 변경이 남는 것은 별개다. 내구성이 필요하면 데이터를 저장장치까지 반영하는 동기화 절차와 그 실패를 처리해야 한다. 매번 동기화하면 대기 비용이 커지고, 여러 변경을 묶으면 비용을 나누는 대신 마지막 묶음이 보장되기까지의 시간이 길어진다. | |||
이름 변경의 원자성도 내구성과 구별한다. 같은 파일 시스템에서 완성한 새 파일로 기존 이름을 원자적으로 교체하면, 새로 여는 쪽이 교체 도중의 빈 이름을 보는 문제를 피할 수 있다. 이미 열린 참조는 이전 객체를 유지한다. 그러나 이것만으로 여러 파일의 변경이 하나의 트랜잭션이 되거나 장애 후 복구 결과가 보장되지는 않는다. 파일 내용과 그 이름을 담은 디렉터리는 각각 영속화가 필요한 상태다. Linux에서 파일에 대한 동기화만으로 디렉터리 항목까지 저장되었다고 볼 수 없는 이유도 여기에 있다. <ref name="source-7">[https://man7.org/linux/man-pages/man2/rename.2.html Linux man-pages: rename(2)].</ref><ref name="source-8">[https://man7.org/linux/man-pages/man2/fsync.2.html fsync(2)].</ref> | |||
< | |||
= | |||
== 관련 각론 == | == 관련 각론 == | ||
* [[운영체제 기본/Linux의 파일 읽기]]: | * [[운영체제 기본/Linux의 파일 읽기]]: 파일 읽기 인터페이스가 Linux 내부 구현으로 이어지는 과정. | ||
== 참고 문헌 == | == 참고 문헌 == | ||
<references /> | |||
2026년 10월 31일 (토) 07:02 판
메모리의 데이터는 프로세스가 종료되거나 전원이 꺼진 뒤에도 필요한 경우가 많다. 파일 시스템은 데이터를 지속적으로 보관하고 다시 찾을 수 있게 하며, 입출력 계층은 속도와 동작 방식이 다른 장치 사이에서 데이터를 전달한다. 이때 프로그램이 사용하는 이름, 현재 접근 중인 대상, 실제 저장장치의 상태를 하나로 취급하면 동시 접근과 장애를 다루기 어렵다.
저장 위치 대신 이름과 바이트를 제공하기
프로그램이 저장장치의 어느 구역에 데이터가 있는지 직접 관리한다면, 파일이 커지거나 다른 프로그램이 공간을 사용했을 때 배치를 함께 조정해야 한다. 파일 시스템은 저장 공간의 할당과 회수를 맡고, 프로그램에는 이름으로 찾을 수 있는 파일을 제공한다. 디렉터리는 이름을 파일 객체에 연결하며, 파일 객체는 내용과 크기, 접근 권한 같은 정보를 가진다. 이름을 찾는 문제와 내용을 어디에 놓을지는 분리된다.
일반 파일을 바이트의 나열로 제공하면 프로그램은 저장장치의 세부 구조에 맞춰 매번 코드를 바꾸지 않아도 된다. Unix는 파일뿐 아니라 파이프와 터미널에도 읽기와 쓰기라는 공통 인터페이스를 제공한다. 덕분에 출력 대상을 바꾸어 프로그램을 조합하기 쉽다. 다만 인터페이스가 비슷해도 모든 대상의 성질이 같지는 않다. 일반 파일은 위치를 바꾸어 읽을 수 있지만 파이프는 그렇게 할 수 없으며, 바이트 스트림의 데이터에 붙일 레코드 구조는 프로그램이 정해야 한다. [1]
이름, 객체, 열린 상태의 수명을 나누기
파일을 열 때마다 경로를 다시 검색하는 대신, 운영체제는 찾은 객체를 계속 참조할 수 있는 핸들을 돌려준다. Unix의 파일 디스크립터가 그 예다. 핸들이 가리키는 열린 상태에는 읽고 쓰는 현재 위치 등이 들어 있다. 같은 파일을 따로 열면 위치를 독립적으로 유지할 수 있고, 열린 상태를 공유하는 핸들은 위치도 함께 사용한다. 파일 내용의 공유와 읽기 위치의 공유는 다른 선택이다. [2]
이 분리는 이름이 바뀌어도 진행 중인 작업을 유지하게 한다. Unix에서 로그 파일의 마지막 이름을 삭제해도 프로세스가 파일을 열어 두었다면 계속 쓸 수 있다. 같은 이름으로 새 파일을 만들어도 기존 핸들이 새 파일로 옮겨 가지는 않는다. 이름이 사라진 시점과 저장 공간을 회수할 수 있는 시점이 달라지는 대신, 경로의 변경 때문에 이미 연 대상이 갑자기 바뀌지 않는다. [3]
버퍼링과 비동기로 기다리는 비용 줄이기
CPU가 작은 입출력 요청마다 느린 장치의 완료를 기다리면 계산할 시간이 낭비된다. 버퍼는 속도가 다른 두 쪽 사이에서 데이터를 잠시 보관한다. 작은 쓰기를 모아 보내면 요청 횟수를 줄이고, 읽은 내용을 캐시에 남기면 같은 데이터를 다시 가져오는 비용을 줄인다. 대신 버퍼가 RAM을 차지하며, 모아서 보내기를 기다리는 동안 개별 데이터의 전달은 늦어질 수 있다. 처리량의 개선과 한 요청의 지연 시간 단축은 항상 같은 방향이 아니다.
비동기 입출력은 요청과 완료 확인을 분리해, 장치가 일하는 동안 프로그램이 다른 일을 하게 한다. 버퍼링과 함께 사용할 수 있지만 서로 같은 개념은 아니다. 비동기로 실행한 작업도 완료 결과를 확인해야 하며, 완료 전까지 사용 중인 버퍼의 수명을 보장해야 한다. 동시에 너무 많은 요청을 쌓으면 대기열과 메모리 사용량이 늘어난다. 읽기 캐시 역시 원본이 다른 경로로 바뀔 수 있다면 갱신을 어떻게 알아낼지 정해야 한다. 재사용의 이득에는 최신성 관리 비용이 따른다. [4][5]
요청한 양과 완료된 양을 구분하기
입출력은 요청 전체의 성공과 실패 두 상태만으로 표현하기 어렵다. 읽기는 파일 끝에 도달하거나 현재 도착한 데이터가 적으면 요청보다 적은 바이트를 돌려줄 수 있다. 쓰기도 공간 부족이나 실행 중단 등의 이유로 일부만 끝날 수 있다. 따라서 반환된 양은 실제로 어디까지 진행했는지를 나타내는 정보다. 남은 부분을 재시도하려면 이미 처리한 위치를 보존해야 한다. 처음부터 다시 보내면 중복된 데이터가 생길 수 있다.
양수를 반환한 부분 완료, 입력의 끝, 오류는 각각 다르게 처리해야 한다. 특히 바이트 스트림에서는 송신 측이 한 번에 쓴 범위와 수신 측이 한 번에 읽는 범위가 일치하지 않는다. 메시지의 길이나 구분자는 상위 프로토콜이 정해야 한다. 또한 버퍼에 쓰기를 받아들인 뒤 실제 저장 과정에서 오류가 드러날 수 있으므로, 최초 쓰기의 성공만으로 이후 실패 가능성까지 사라지지는 않는다. [6]
보이는 변경과 남는 변경을 구분하기
버퍼드 쓰기의 성공은 데이터가 운영체제의 캐시에 전달되었다는 뜻일 수 있다. 다른 읽기에서 변경을 관찰하는 것과 전원 장애 후에도 변경이 남는 것은 별개다. 내구성이 필요하면 데이터를 저장장치까지 반영하는 동기화 절차와 그 실패를 처리해야 한다. 매번 동기화하면 대기 비용이 커지고, 여러 변경을 묶으면 비용을 나누는 대신 마지막 묶음이 보장되기까지의 시간이 길어진다.
이름 변경의 원자성도 내구성과 구별한다. 같은 파일 시스템에서 완성한 새 파일로 기존 이름을 원자적으로 교체하면, 새로 여는 쪽이 교체 도중의 빈 이름을 보는 문제를 피할 수 있다. 이미 열린 참조는 이전 객체를 유지한다. 그러나 이것만으로 여러 파일의 변경이 하나의 트랜잭션이 되거나 장애 후 복구 결과가 보장되지는 않는다. 파일 내용과 그 이름을 담은 디렉터리는 각각 영속화가 필요한 상태다. Linux에서 파일에 대한 동기화만으로 디렉터리 항목까지 저장되었다고 볼 수 없는 이유도 여기에 있다. [7][8]
관련 각론
- 운영체제 기본/Linux의 파일 읽기: 파일 읽기 인터페이스가 Linux 내부 구현으로 이어지는 과정.