본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

운영체제 기본/Linux/파일 시스템 콜/read와 write: 두 판 사이의 차이

~ldmsys
역링크 갱신 완료 후 불필요한 호환 span id 제거
새 문서 제목을 위키 링크에 반영
 
(같은 사용자의 중간 판 8개는 보이지 않습니다)
1번째 줄: 1번째 줄:
[[운영체제 기본|목차]] · 관련 총론: [[운영체제 기본/추상화|추상화]], [[운영체제 기본/파일과 입출력|파일과 입출력]]
{{이전 다음 문서
|이전=← 이전 문서<br />[[운영체제 기본/Linux/파일 시스템 콜/openat과 경로 탐색|openat과 경로 탐색]]
|목차=[[운영체제 기본/Linux/파일 시스템 콜|파일 시스템 콜 목차]]
|다음=다음 문서 →<br />[[운영체제 기본/Linux/파일 시스템 콜/파일 오프셋과 열린 파일의 수명|파일 오프셋과 열린 파일의 수명]]
}}


응용 프로그램이 <code>read(fd, buf, count)</code>를 호출하면 커널은 fd로 열린 파일 객체를 찾고, 그 객체에 연결된 읽기 구현을 호출한다. 기준은 '''Linux v6.12'''다.
Linux v6.12의 <code>read()</code>와 <code>write()</code>는 fd에서 같은 열린 파일 객체를 찾고, VFS 검사를 거쳐 대상별 연산을 호출한다. 읽기는 데이터를 사용자 버퍼로 가져오고, 쓰기는 사용자 버퍼의 데이터를 대상에 전달한다. 먼저 일반 파일의 동기식 버퍼드 입출력을 기준으로 살펴본다.


일반적인 <code>read()</code> 경로를 대상으로 한다. <code>io_uring</code>은 별도 경로이며, [[운영체제 기본/Linux의 파일 메모리 매핑|mmap을 통한 접근]]은 주소 공간 설정과 페이지 폴트로 이어진다.
== 공통 파일 조회와 위치 처리 ==


== 파일 디스크립터와 파일 객체 ==
<pre>
<pre>
fd = 3
read(fd, buf, count)                write(fd, buf, count)
   |
   -> ksys_read()                      -> ksys_write()
  +--> current->files    (files_struct)
      fdget_pos(fd)                      fdget_pos(fd)
            |
      현재 f_pos 확보                    현재 f_pos 확보
            +--> fdt    (fdtable)
      vfs_read()                          vfs_write()
                    |
      성공한 위치를 f_pos에 반영          성공한 위치를 f_pos에 반영
                    +--> fd[3] --> struct file
      fdput_pos()                        fdput_pos()
</pre>
</pre>


<code>current</code>는 현재 실행 중인 태스크를 나타낸다. <code>current->files</code>로 접근하는 <code>files_struct</code>에 파일 디스크립터 테이블이 연결되어 있고, <code>fdtable.fd</code>는 <code>struct file</code> 포인터의 배열이다. 같은 fd 값도 어느 테이블에서 찾는지에 따라 다른 열린 파일을 가리킬 수 있다.
두 함수는 일반 파일의 <code>f_pos</code>를 지역 변수로 복사해 VFS 연산에 넘기고, 반환값이 음수가 아니면 갱신된 위치를 파일 객체에 기록한다. 위치가 없는 스트림은 <code>file_ppos()</code>가 <code>NULL</code>을 반환하므로 같은 위치 갱신 모델을 그대로 적용하지 않는다.<ref name="rw">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/read_write.c?h=v6.12 Linux v6.12 fs/read_write.c], file_ppos, ksys_read, ksys_write, vfs_read, vfs_write, new_sync_read, new_sync_write.</ref>


파일 디스크립터 테이블의 참조 관계:
<code>vfs_read()</code>와 <code>vfs_write()</code>는 열린 모드, 연산 지원 여부, 사용자 주소 범위와 요청 범위 등을 검사한다. 사용자 주소가 범위 검사에 통과해도 모든 페이지가 이미 준비되어 있다는 뜻은 아니다. 실제 복사 중 페이지 폴트를 처리하거나 오류를 만날 수 있다. 쓰기 쪽은 파일시스템의 쓰기 동결과 조정하기 위한 <code>file_start_write()</code>, <code>file_end_write()</code> 처리도 한다.<ref name="rw"/>


<pre>
== 읽기와 쓰기 연산의 선택 ==
files = current->files;
 
table = files->fdt;
{| class="wikitable"
opened_file = table->fd[fd];
! 요청 !! 직접 콜백 !! iterator 콜백을 사용하는 경로
</pre>
|-
| 읽기 || <code>f_op->read</code> || <code>new_sync_read()</code> → <code>f_op->read_iter</code>
|-
| 쓰기 || <code>f_op->write</code> || <code>new_sync_write()</code> → <code>f_op->write_iter</code>
|}


실제 <code>ksys_read()</code>는 <code>fdget_pos()</code>를 통해 객체를 얻는다. 그 아래의 <code>fdget()</code>, <code>__fget_light()</code>와 조회 함수들은 참조와 동시 접근을 처리한다. 위 그림에서는 이 부분을 생략했다. 직접 배열을 조회하는 코드에는 이러한 보호가 없으므로 실제 조회 함수를 대신할 수 없다.
VFS는 직접 콜백이 있으면 그것을 사용하고, 없으면 iterator 콜백을 선택한다. <code>new_sync_read()</code>와 <code>new_sync_write()</code>는 요청 상태인 <code>kiocb</code>와 버퍼 범위를 나타내는 <code>iov_iter</code>를 준비한다. 읽기의 버퍼는 데이터를 받을 목적지(<code>ITER_DEST</code>), 쓰기의 버퍼는 데이터를 제공할 원본(<code>ITER_SOURCE</code>)이다. 동기식 <code>read()</code>도 내부에서는 <code>read_iter</code>를 사용할 수 있다. 함수 이름에 iter가 있다고 해서 사용자 요청이 비동기 입출력인 것은 아니다.<ref name="rw"/>


소스:
== 일반 파일의 버퍼드 입출력 ==
* [https://elixir.bootlin.com/linux/v6.12/source/include/linux/fdtable.h include/linux/fdtable.h]의 <code>fdtable</code>, <code>files_struct</code>, <code>files_lookup_fd_raw()</code><ref name="kernel-1">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/fdtable.h?h=v6.12 Linux v6.12, include/linux/fdtable.h].</ref>.
* [https://elixir.bootlin.com/linux/v6.12/source/fs/file.c fs/file.c]의 <code>__fget_light()</code>, <code>fdget_pos()</code><ref name="kernel-2">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/file.c?h=v6.12 Linux v6.12, fs/file.c].</ref>.


== 파일 객체와 읽기 연산 ==
ext4의 일반 파일에는 <code>.read_iter = ext4_file_read_iter</code>, <code>.write_iter = ext4_file_write_iter</code>가 연결된다. DAX와 직접 입출력이 아닌 기본 경로는 다음과 같다.<ref name="ext4">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/ext4/file.c?h=v6.12 Linux v6.12 fs/ext4/file.c], ext4_file_operations, ext4_file_read_iter, ext4_file_write_iter, ext4_buffered_write_iter.</ref>
<code>struct file</code>의 <code>f_op</code>는 연산 함수들을 모은 <code>file_operations</code>를 가리킨다. 파일마다 응용 프로그램의 호출 형태를 바꾸는 대신, 커널 안에서 해당 대상에 맞는 연산을 선택하는 구조다.


<pre>
<pre>
struct file
읽기: ext4_file_read_iter()
    |
        -> generic_file_read_iter() -> filemap_read()
    +--> f_op (file_operations)
        -> 페이지 캐시의 데이터를 사용자 버퍼로 복사
              |
 
              +--> read
쓰기: ext4_file_write_iter()
              |
        -> ext4_buffered_write_iter() -> generic_perform_write()
              +--> read_iter
        -> 사용자 버퍼에서 캐시로 복사하고 수정 상태 반영
        -> 필요한 동기화 또는 이후 라이트백(Writeback)
</pre>
</pre>


<code>ksys_read()</code>는 얻은 객체를 <code>vfs_read()</code>에 넘긴다. <code>vfs_read()</code>는 접근 모드, 사용자 버퍼 등에 관한 검사를 거쳐 읽기 구현을 선택한다.
읽을 데이터가 캐시에 준비되어 있으면 장치 읽기 없이 복사할 수 있다. 준비되지 않았다면 파일시스템을 통해 데이터를 가져오며, 그동안 요청한 스레드가 기다릴 수 있다.<ref name="filemap">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/filemap.c?h=v6.12 Linux v6.12 mm/filemap.c], generic_file_read_iter, filemap_read, generic_perform_write.</ref> 일반적인 버퍼드 쓰기는 메모리의 파일 데이터를 수정하고 라이트백으로 나중에 장치에 반영할 수 있으므로, 성공한 <code>write()</code>가 곧 전원 손실에도 보존되는 저장 완료를 의미하지는 않는다. 동기식 쓰기 플래그와 <code>fsync()</code>는 이 완료 조건에 관여한다. 자세한 캐시 흐름은 [[운영체제 기본/Linux/파일시스템/페이지 캐시|페이지 캐시]], ext4의 블록 읽기는 [[운영체제 기본/Linux/파일시스템/ext4의 파일 읽기|ext4의 파일 읽기]]에서 이어진다.<ref name="ext4"/>
 
procfs의 <code>/proc/uptime</code>은 같은 VFS 읽기 인터페이스에서 커널 상태로 문자열을 만든다. 키보드 이벤트를 읽는 장치 파일은 이벤트가 올 때까지 기다릴 수 있다. 따라서 fd와 반환 형식은 공통이어도, 데이터의 출처와 대기 조건은 대상의 구현에 달려 있다. 각각 [[운영체제 기본/Linux/파일시스템/procfs의 파일 읽기|procfs의 파일 읽기]], [[운영체제 기본/Linux/x86-64/i8042 키보드 입력/evdev의 이벤트 전달과 대기 해제|evdev의 이벤트 전달과 대기 해제]]에서 비교할 수 있다.
 
== 짧은 읽기와 짧은 쓰기 ==
 
<code>count</code>는 요청량이고 반환값은 실제 처리량이다. 파일 내용이 <code>ABCDE</code>이고 위치가 0일 때 다음처럼 진행된다.


<pre>
<pre>
if (opened_file->f_op->read)
read(fd, buf, 3) -> 3, "ABC", f_pos = 3
    opened_file->f_op->read(...);
read(fd, buf, 3) -> 2, "DE",  f_pos = 5
else if (opened_file->f_op->read_iter)
read(fd, buf, 3) -> 0, EOF,  f_pos = 5
    new_sync_read(...);
</pre>
</pre>


<code>new_sync_read()</code>는 <code>read_iter</code> 호출에 필요한 요청과 버퍼 정보를 구성한다. 사용 가능한 함수 포인터에 따라 한 경로를 선택한다.
0보다 큰 짧은 반환은 오류가 아니라 그만큼 처리했다는 뜻이다. 읽기는 파일 끝, 장치의 현재 데이터량 등으로 짧아질 수 있고, 쓰기도 자원 부족이나 신호 등의 조건에서 일부만 처리할 수 있다. 전체 버퍼를 보내려면 실제 반환량만큼 버퍼 포인터와 남은 길이를 진행시켜야 한다. 이미 처리된 바이트가 있는 상태에서는 그 처리량이 먼저 반환되고 오류가 다음 호출에서 드러날 수도 있다.


소스:
반환값 <code>-1</code>과 <code>errno</code>는 라이브러리 수준의 실패 표시다. <code>EINTR</code>을 재시도하는 경우에도 양수로 반환된 부분 처리량을 버려서는 안 된다. 이 반환 규약은 단순히 함수를 한 번 호출하는 것과 요청한 전체 데이터를 처리하는 것이 다른 이유다.<ref name="man-read">[https://man7.org/linux/man-pages/man2/read.2.html Linux man-pages, read(2)].</ref><ref name="man-write">[https://man7.org/linux/man-pages/man2/write.2.html Linux man-pages, write(2)].</ref><ref name="rw"/>
* [https://elixir.bootlin.com/linux/v6.12/source/include/linux/fs.h include/linux/fs.h]의 <code>struct file</code>, <code>struct file_operations</code><ref name="kernel-3">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/fs.h?h=v6.12 Linux v6.12, include/linux/fs.h].</ref>.
* [https://elixir.bootlin.com/linux/v6.12/source/fs/read_write.c fs/read_write.c]의 <code>ksys_read()</code>, <code>vfs_read()</code>, <code>new_sync_read()</code><ref name="kernel-4">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/read_write.c?h=v6.12 Linux v6.12, fs/read_write.c].</ref>.


== 파일시스템별 읽기 연산 ==
== 파일 위치와 추가 쓰기 ==
<code>vfs_read()</code>는 [[운영체제 기본/Linux의 파일시스템/VFS와 파일 객체|VFS의 공통 파일 연산]]을 통해 실제 구현으로 연결된다. 열린 파일에 어떤 연산표가 설정되는지는 <code>open()</code>과 파일시스템의 객체 구성에서 결정된다.


응용 프로그램에는 '''fd와 읽기 인터페이스'''가 보인다. 커널 내부에서는 '''테이블 조회로 객체를 찾고, 연산 테이블로 구현을 선택'''한다.
보통의 순차 쓰기는 현재 위치에서 시작해 실제 쓴 만큼 위치를 진행시킨다. <code>O_APPEND</code>로 연 파일에서는 쓰기 위치를 파일 끝으로 정하는 처리가 쓰기 연산에 포함된다. 반면 <code>lseek(fd, 0, SEEK_END)</code>와 <code>write()</code>를 따로 호출하면 그 사이에 다른 쓰기가 끼어들 수 있다.<ref name="man-write"/> 파일 위치를 공유하는 fd, 명시적 위치의 <code>pread()</code>·<code>pwrite()</code>는 다음 [[운영체제 기본/Linux/파일 시스템 콜/파일 오프셋과 열린 파일의 수명|파일 오프셋과 열린 파일의 수명]]에서 다룬다.


다만 함수 포인터를 호출하는 것만으로 읽기가 끝나는 것은 아니다. 선택된 구현 아래에서 파일시스템, 캐시, 장치 드라이버 등의 처리가 이어질 수 있다.
[[운영체제 기본/Linux/메모리 매핑/파일 매핑과 COW|파일 메모리 매핑]]은 <code>read()</code>로 사용자 버퍼에 복사하는 대신 파일 페이지를 주소 공간에 연결한다. 매핑의 공유·사유 여부에 따라 쓰기 결과도 달라지므로, <code>write()</code>와 같은 동작으로 뭉뚱그릴 수 없다.


[[운영체제 기본/Linux의 파일시스템/ext4의 파일 읽기|ext4의 일반 파일 읽기]]는 공통 페이지 캐시로 이어진다. [[운영체제 기본/Linux의 파일시스템/procfs의 파일 읽기|/proc/uptime 읽기]]는 커널 상태로 문자열을 만든다. 같은 <code>read()</code>를 호출해도 데이터를 얻는 경로가 달라지는 지점이다.
== 참고 문헌 ==
<references />


== 관련 각론 ==
{{이전 다음 문서
* [[운영체제 기본/Linux의 파일시스템|Linux의 파일시스템]]: VFS, ext4, procfs와 공통 페이지 캐시.
|이전=← 이전 문서<br />[[운영체제 기본/Linux/파일 시스템 콜/openat과 경로 탐색|openat과 경로 탐색]]
* [[운영체제 기본/Linux의 파일 메모리 매핑|mmap을 통한 파일 읽기]]: 주소 영역 등록, 첫 메모리 접근, 파일 폴트와 PTE 설치.
|목차=[[운영체제 기본/Linux/파일 시스템 콜|파일 시스템 콜 목차]]
|다음=다음 문서 →<br />[[운영체제 기본/Linux/파일 시스템 콜/파일 오프셋과 열린 파일의 수명|파일 오프셋과 열린 파일의 수명]]
}}


== 참고 문헌 ==
{{운영체제 기본 둘러보기}}
<references />

2026년 11월 1일 (일) 03:32 기준 최신판

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. ↑ 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. ↑ 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.
  3. ↑ Linux v6.12 mm/filemap.c, generic_file_read_iter, filemap_read, generic_perform_write.
  4. ↑ Linux man-pages, read(2).
  5. ↑ 5.0 5.1 Linux man-pages, write(2).