운영체제 기본/Linux의 파일 시스템 콜/read와 write
Linux v6.12의 read()와 write()는 fd에서 같은 열린 파일 객체를 찾고, VFS 검사를 거쳐 대상별 연산을 호출한다. 읽기는 데이터를 사용자 버퍼로 가져오고, 쓰기는 사용자 버퍼의 데이터를 대상에 전달한다. 먼저 일반 파일의 동기식 버퍼드 입출력을 기준으로 살펴본다.
공통 파일 조회와 위치 처리
read(fd, buf, count) write(fd, buf, count)
-> ksys_read() -> ksys_write()
fdget_pos(fd) fdget_pos(fd)
현재 f_pos 확보 현재 f_pos 확보
vfs_read() vfs_write()
성공한 위치를 f_pos에 반영 성공한 위치를 f_pos에 반영
fdput_pos() fdput_pos()
두 함수는 일반 파일의 f_pos를 지역 변수로 복사해 VFS 연산에 넘기고, 반환값이 음수가 아니면 갱신된 위치를 파일 객체에 기록한다. 위치가 없는 스트림은 file_ppos()가 NULL을 반환하므로 같은 위치 갱신 모델을 그대로 적용하지 않는다.[1]
vfs_read()와 vfs_write()는 열린 모드, 연산 지원 여부, 사용자 주소 범위와 요청 범위 등을 검사한다. 사용자 주소가 범위 검사에 통과해도 모든 페이지가 이미 준비되어 있다는 뜻은 아니다. 실제 복사 중 페이지 폴트를 처리하거나 오류를 만날 수 있다. 쓰기 쪽은 파일시스템의 쓰기 동결과 조정하기 위한 file_start_write(), file_end_write() 처리도 한다.[1]
읽기와 쓰기 연산의 선택
| 요청 | 직접 콜백 | iterator 콜백을 사용하는 경로 |
|---|---|---|
| 읽기 | f_op->read |
new_sync_read() → f_op->read_iter
|
| 쓰기 | f_op->write |
new_sync_write() → f_op->write_iter
|
VFS는 직접 콜백이 있으면 그것을 사용하고, 없으면 iterator 콜백을 선택한다. new_sync_read()와 new_sync_write()는 요청 상태인 kiocb와 버퍼 범위를 나타내는 iov_iter를 준비한다. 읽기의 버퍼는 데이터를 받을 목적지(ITER_DEST), 쓰기의 버퍼는 데이터를 제공할 원본(ITER_SOURCE)이다. 동기식 read()도 내부에서는 read_iter를 사용할 수 있다. 함수 이름에 iter가 있다고 해서 사용자 요청이 비동기 입출력인 것은 아니다.[1]
일반 파일의 버퍼드 입출력
ext4의 일반 파일에는 .read_iter = ext4_file_read_iter, .write_iter = ext4_file_write_iter가 연결된다. DAX와 직접 입출력이 아닌 기본 경로는 다음과 같다.[2]
읽기: ext4_file_read_iter()
-> generic_file_read_iter() -> filemap_read()
-> 페이지 캐시의 데이터를 사용자 버퍼로 복사
쓰기: ext4_file_write_iter()
-> ext4_buffered_write_iter() -> generic_perform_write()
-> 사용자 버퍼에서 캐시로 복사하고 수정 상태 반영
-> 필요한 동기화 또는 이후 라이트백(Writeback)
읽을 데이터가 캐시에 준비되어 있으면 장치 읽기 없이 복사할 수 있다. 준비되지 않았다면 파일시스템을 통해 데이터를 가져오며, 그동안 요청한 스레드가 기다릴 수 있다.[3] 일반적인 버퍼드 쓰기는 메모리의 파일 데이터를 수정하고 라이트백으로 나중에 장치에 반영할 수 있으므로, 성공한 write()가 곧 전원 손실에도 보존되는 저장 완료를 의미하지는 않는다. 동기식 쓰기 플래그와 fsync()는 이 완료 조건에 관여한다. 자세한 캐시 흐름은 페이지 캐시, ext4의 블록 읽기는 ext4의 파일 읽기에서 이어진다.[2]
procfs의 /proc/uptime은 같은 VFS 읽기 인터페이스에서 커널 상태로 문자열을 만든다. 키보드 이벤트를 읽는 장치 파일은 이벤트가 올 때까지 기다릴 수 있다. 따라서 fd와 반환 형식은 공통이어도, 데이터의 출처와 대기 조건은 대상의 구현에 달려 있다. 각각 procfs의 파일 읽기, evdev와 프로세스 깨우기에서 비교할 수 있다.
짧은 읽기와 짧은 쓰기
count는 요청량이고 반환값은 실제 처리량이다. 파일 내용이 ABCDE이고 위치가 0일 때 다음처럼 진행된다.
read(fd, buf, 3) -> 3, "ABC", f_pos = 3 read(fd, buf, 3) -> 2, "DE", f_pos = 5 read(fd, buf, 3) -> 0, EOF, f_pos = 5
0보다 큰 짧은 반환은 오류가 아니라 그만큼 처리했다는 뜻이다. 읽기는 파일 끝, 장치의 현재 데이터량 등으로 짧아질 수 있고, 쓰기도 자원 부족이나 신호 등의 조건에서 일부만 처리할 수 있다. 전체 버퍼를 보내려면 실제 반환량만큼 버퍼 포인터와 남은 길이를 진행시켜야 한다. 이미 처리된 바이트가 있는 상태에서는 그 처리량이 먼저 반환되고 오류가 다음 호출에서 드러날 수도 있다.
반환값 -1과 errno는 라이브러리 수준의 실패 표시다. EINTR을 재시도하는 경우에도 양수로 반환된 부분 처리량을 버려서는 안 된다. 이 반환 규약은 단순히 함수를 한 번 호출하는 것과 요청한 전체 데이터를 처리하는 것이 다른 이유다.[4][5][1]
파일 위치와 추가 쓰기
보통의 순차 쓰기는 현재 위치에서 시작해 실제 쓴 만큼 위치를 진행시킨다. O_APPEND로 연 파일에서는 쓰기 위치를 파일 끝으로 정하는 처리가 쓰기 연산에 포함된다. 반면 lseek(fd, 0, SEEK_END)와 write()를 따로 호출하면 그 사이에 다른 쓰기가 끼어들 수 있다.[5] 파일 위치를 공유하는 fd, 명시적 위치의 pread()·pwrite()는 다음 파일 위치와 수명에서 다룬다.
파일 메모리 매핑은 read()로 사용자 버퍼에 복사하는 대신 파일 페이지를 주소 공간에 연결한다. 매핑의 공유·사유 여부에 따라 쓰기 결과도 달라지므로, write()와 같은 동작으로 뭉뚱그릴 수 없다.
참고 문헌
- ↑ 1.0 1.1 1.2 1.3 Linux v6.12 fs/read_write.c, file_ppos, ksys_read, ksys_write, vfs_read, vfs_write, new_sync_read, new_sync_write.
- ↑ 2.0 2.1 Linux v6.12 fs/ext4/file.c, ext4_file_operations, ext4_file_read_iter, ext4_file_write_iter, ext4_buffered_write_iter.
- ↑ Linux v6.12 mm/filemap.c, generic_file_read_iter, filemap_read, generic_perform_write.
- ↑ Linux man-pages, read(2).
- ↑ 5.0 5.1 Linux man-pages, write(2).