Chapter 7. 파일시스템
리눅스의 파일시스템
리눅스의 파일시스템에서는 데이터를 저장하는 일반 파일이 있고, 디렉터리directory는 일반 파일이나 다른 디렉터리를 보관할 수 있다. 이는 트리 구조로 되어있다.
리눅스 파일시스템은 ext4, XFS, Btrfs 등 여러가지가 존재하고, 각 파일시스템은 다룰 수 있는 파일의 사이즈, 파일시스템 사이즈, 파일 작성이나 삭제 및 읽기 쓰기의 처리 속도 등이 모두 다르다. 하지만 다음 시스템 콜을 통해 통일된 인터페이스로 접근할 수 있다.
시스템 콜 |
동작 |
creat(), unlink() |
파일 생성, 삭제 |
open(), close() |
파일 열기, 닫기 |
read() |
파일로부터 데이터 읽기 |
write() |
파일에 데이터 쓰기 |
lseek() |
파일의 특정 위치로 이동 |
ioctl() |
파일시스템에 의존적인 특수한 처리 |
위의 시스템 콜이 호출되면 다음 순서로 파일의 데이터를 읽는다.
커널 내의 파일시스템 공통 처리가 동작하고, 대상 파일의 파일시스템을 판별한다.
판별한 파일시스템을 처리하는 프로세스를 호출하여 시스템 콜에 대응되는 처리를 한다.
데이터 읽기의 경우 디바이스 드라이버를 통해 데이터를 읽는다.
데이터data와 메타데이터metadata
메타데이터는 다음과 같은 요소도 포함한다.
종류 : 데이터를 보관하는 일반 파일이나 디렉토리인지, 아니면 다른 종류인지에 대한 정보
시간 정보 : 작성한 시간, 최종 수정 시간, 최종 접근 시간
권한 정보 : 파일 접근 권한 관련 정보
df 명령어로 얻은 파일시스템의 스토리지 사용량은 메타데이터도 포함한다.
$ script/df_test.sh
Filesystem 1K-blocks Used Available Use% Mounted on
4,5c4,5
< /dev/nvme0n1p5 51343840 13559544 35146472 28% /
< tmpfs 3864512 77616 3786896 3% /dev/shm
---
> /dev/nvme0n1p5 51343840 13586916 35119100 28% /
> tmpfs 3864512 82436 3782076 3% /dev/shm
용량 제한 - 쿼터quota
시스템의 원활한 동작을 위해 파일시스템의 용량을 용도별로 제한하는 기능이 쿼터이다.
사용자 쿼터 : 사용자별로 용량을 제한하여 /home이 가득 차는 것을 방지할 수 있다. ext4와 XFS에서 사용할 수 있다.
디렉터리 쿼터(프로젝트 쿼터) : 특정 디렉터리별로 용량을 제한할 수 있다. ext4와 XFS에서 사용할 수 있다.
서브 볼륨 쿼터 : 디렉터리 쿼터와 유사한 사용법으로, 파일시스템 내의 서브 볼륨이라는 단위별 용량을 제한할 수 있다. Btrfs에서 사용할 수 있다.
파일시스템 깨짐 방지
파일시스템에서 어떤 디렉터리 foo를 다른 디렉터리 bar 안으로 이동하는 등의 동작은 다음과 같다.
foo에서 bar로 링크를 연결한다.
bar의 부모 노드에서 bar로의 링크를 삭제한다.
이 처리는 중간에 누락되면 안되므로 아토믹atomic 처리라고 부른다.
만약 1을 완료한 상태에서 전원이 차단되는 등 처리가 중단되면, bar로의 링크가 2개이므로 파일시스템이 깨진 상태라고 할 수 있다. 이렇게 되면 파일시스템이 읽기 전용 모드로 다시 마운트remount하거나, 시스템 패닉이 발생할 수 있다.
이렇게 파일시스템이 깨지는 경우를 방지하기 위해 일반적으로 저널링(ext4, XFS)이나 Copy on Write(Btrfs) 방식을 사용한다.
저널링
저널링은 파일시스템 내부에 저널 영역이라는 메타데이터를 보관한다. 저널링 방식에서 업데이트 처리 순서는 다음과 같다.
저널로그 작성 : 아토믹한 처리의 목록을 저널 영역에 작성한다.
ex. foo -> bar 링크 연결
저널로그를 바탕으로 실제 파일시스템의 데이터 변경
저널 영역 삭제
전원 차단 시나리오
1을 완료한 뒤 전원이 차단될 경우 : 단순히 저널 영역의 데이터만 사라지고, 실제 데이터는 처리 전과 같은 상태이다.
2 중간에 전원이 차단될 경우(앞에서처럼 bar 디렉터리에 2번 링크가 되는 경우 등) : 저널로그를 처음부터 다시 수행하여 처리를 완료한다.
파일시스템의 Copy On Write
일단 메모리에서의 COW와는 살짝 다른 개념인 듯 하다.
고전적인 파일시스템(ext4, XFS 등)은 한번 파일을 작성하면 그 파일의 배치 장소는 원칙적으로 변하지 않고, 업데이트할 때마다 같은 장소에 새로운 데이터를 쓴다. 반면에 Btrfs 등의 Copy On Write형의 파일시스템은 업데이트할 때 업데이트된 부분만 다른 장소에 데이터를 쓴다.
만약 아토믹으로 여러 처리를 실행할 경우, 업데이트되는 데이터들을 다른 장소에 전부 쓴 뒤 링크를 고쳐쓰는 방식으로 동작한다. 중간에 전원이 차단될 경우, 새로 작성한 데이터를 삭제하면 문제가 발생하지 않는다. 파일의 변경 기록은 사라지지만 파일시스템이 깨지는 것은 방지할 수 있다.
대책
만약 파일시스템이 깨졌다면, 일단 가장 좋은 방법은 정기적으로 백업하여 마지막 백업 상태로 복원하는 것이다. 이게 불가능하다면 각 파일시스템에 준비된 복구용 명령어를 사용할 수 있다.
ext4 : fsck.ext4
XFS : xfs_repair
Btrfs : btrfs check
하지만 위의 명령어는 소요 시간이 길고(파일시스템 전체를 조사하기 때문), 실패율이 높고, 처리하면서 깨진 데이터는 삭제하기 때문에 별로 추천하지 않는다고 한다.
결국 대책은 정기적인 백업이다..
파일의 종류
리눅스의 파일에는 일반 파일과 디렉터리, 그리고 디바이스 파일이 존재한다.
리눅스는 하드웨어상의 장치를 거의 파일로 표현한다(네트워크 어댑터는 예외). 즉, 장치를 파일과 똑같이 open(), read(), write() 등의 시스템 콜로 사용하며 장치 고유의 복잡한 조작을 위해서는 ioctl() 시스템 콜을 호출한다. 일반적으로 root만 디바이스 파일에 접근할 수 있다.
디바이스 파일은 캐릭터 장치와 블록 장치로 구분된다.
$ ls -l /dev
...
brw-rw---- 1 root disk 259, 5 9월 25 2021 nvme0n1p5 # 블록 장치
crw------- 1 root root 10, 144 9월 25 2021 nvram # 캐릭터 장치
...
c로 시작하면 캐릭터 장치, b는 블록 장치이다.
5번째 필드가 Major number, 6번째 필드가 Minor number이다.
캐릭터 장치
캐릭터 장치는 읽기와 쓰기가 가능하지만, 탐색seek이 되지 않는다.
- ex. 터미널(bash 등의 셸), 키보드, 마우스
디바이스 파일로 터미널 조작
터미널의 디바이스 파일은 다음 시스템 콜로 다룰 수 있다.
write() : 터미널에 데이터 출력
read() : 터미널에 데이터 입력
현재 터미널의 디바이스 파일 찾기
$ ps ax | grep zsh | grep -v grep
55850 pts/0 Ss 0:00 zsh
- 두 번째 필드를 통해 /dev/pts/0 이 디바이스 파일인 것을 알 수 있다.
디바이스 파일에 문자열을 써서(내부적으로 write() 시스템 콜) 현재 터미널에 hello 출력해보기
$ sudo su
# echo hello > /dev/pts/0
hello
다른 터미널 조작해보기 : 터미널 2개 열고 다음을 실행
기존 터미널
$ ps ax | grep zsh | grep -v grep
55850 pts/0 Ss+ 0:00 zsh
56385 pts/1 Ss 0:00 zsh
$ sudo su
# echo hello > /dev/pts/1
새로 연 터미널
$ hello
실제로 애플리케이션이 터미널의 디바이스 파일을 직접 조작하기 보다는, 리눅스가 제공하는 셸이나 라이브러리가 직접 디바이스 파일을 다루는 편이다. 애플리케이션은 셸이나 라이브러리가 제공하는 인터페이스를 쉽게 사용하는 편이다.
블록 장치
블록 장치는 파일의 읽기, 쓰기 및 랜덤 접근이 가능하다. 대표적으로 HDD나 SSD 등의 저장 장치가 있다.
일반적으로는 블록 장치에 직접 접근하지 않고, 파일시스템을 작성하고 마운트함으로써 파일시스템을 통해 접근한다. 하지만 직접 블록 장치를 다뤄야 하는, 다음과 같은 경우들도 존재한다.
블록 장치를 직접 다루는 방법은 일반 파일과 똑같이 dd 명령어로 처리하면 된다. 우분투에서 파티션 크기를 축소하려면 usb 부팅으로 live mode에 들어가서 처리하면 되는데, 귀찮아서 실습은 생략...
책에서 실습한 방법은 다음과 같다.
mkfs.ext4
명령어로 파티션에 ext4 파일시스템을 생성한다.
mount
명령어로 생성한 파일시스템을 마운트하고, testfile을 만든 뒤 내용에 "hello world" 같은 문자열을 작성하고 unmount
명령어로 마운트를 해제한다.
string -t x
명령어를 사용하여 해당 파일시스템의 데이터가 들어있는 블록 장치에서 문자열 정보만을 추출하여 주소를 확인한다. hello world의 경우 0x803d000에 있었다.
다음과 같이 dd
명령어로 블록 장치에 직접 접근하여 파일 내용을 변경할 수 있다.
$ dd if=testfile-overwrite of=/dev/sdc7 seek=$((0x803d000)) bs=1
파일시스템의 내용을 블록 장치로부터 직접 변경할 때의 주의사항
ext4 외의 파일시스템이나 나중에 버전업된 ext4에서 똑같이 동작한다고 보장할 수 없다.
파일시스템의 내용물을 직접 바꾸는 것은 굉장히 위험하므로 반드시 테스트용 파일시스템에서 실습한다.
파일시스템을 마운트한 상태에서 해당 블록 장치에 동시에 접근하는 것은 불가능하다. (파일시스템이 깨질 위험 존재)
여러 가지 파일 시스템
리눅스에는 아직까지 언급한 ext4, XFS, Btrfs 외에도 여러 가지의 파일시스템이 있다.
메모리 기반 파일시스템 - tmpfs
tmpfs는 저장 장치 대신 메모리에 존재하여 고속으로 사용할 수 있다. 메모리에 존재하므로 전원이 차단되면 데이터가 사라지므로, 재부팅 후 남아있을 필요가 없는 /tmp나 /var/run에 사용하는 경우가 많다고 한다.
내가 사용하는 환경에서도 tmpfs를 사용하고 있었다.
$ mount | grep ^tmpfs
tmpfs on /run type tmpfs (rw,nosuid,nodev,noexec,relatime,size=772904k,mode=755,inode64)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,inode64)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k,inode64)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755,inode64)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=772900k,mode=700,uid=1000,gid=1000,inode64)
tmpfs on /run/snapd/ns type tmpfs (rw,nosuid,nodev,noexec,relatime,size=772904k,mode=755,inode64)
파일시스템 tmpfs는 마운트 할 때 작성하며, size 마운트 옵션으로 최대 크기를 지정한다. 이 사이즈를 바로 확보하는 건 아니고, 디맨드페이징처럼 파일시스템 내의 각 영역에 처음 접근할 때 페이지 단위로 메모리를 확보한다.
free의 출력 결과에서 shared 필드가 tmpfs에 의해 실제로 사용된 메모리의 양이다.
$ free
total used free shared buff/cache available
Mem: 7729028 2284288 2212032 936804 3232708 4219772
스왑: 2097148 0 2097148
네트워크 파일시스템
네트워크 파일시스템은 네트워크를 통해 연결된 원격 호스트에 있는 파일에 접근하는 파일시스템을 의미한다.
- ex. 윈도우 기반 호스트에는 cifs, 리눅스 등 UNIX 계열의 호스트에는 nfs 파일시스템을 사용
가상 파일시스템
커널 안의 정보를 얻거나 커널의 동작을 변경하기 위해서 다양한 파일시스템이 존재한다.
procfs
시스템에 존재하는 프로세스에 대한 정보를 얻을 수 있다.
procfs는 일반적으로 /proc 이하에 마운트되고, /proc/PID/ 이하의 파일에 접근함으로써 각 프로세스의 정보를 얻을 수 있다.
현재 셸에 관한 정보를 얻는 과정은 다음과 같다.
$ ps | grep zsh
58714 pts/0 00:00:00 zsh
$ echo $$ # $$는 현재 프로세스의 PID이다.
58714
$ ls /proc/$$
arch_status cwd mem patch_state stat
attr environ mountinfo personality statm
autogroup exe mounts projid_map status
auxv fd mountstats root syscall
cgroup fdinfo net sched task
clear_refs gid_map ns schedstat timens_offsets
cmdline io numa_maps sessionid timers
comm limits oom_adj setgroups timerslack_ns
coredump_filter loginuid oom_score smaps uid_map
cpu_resctrl_groups map_files oom_score_adj smaps_rollup wchan
cpuset maps pagemap stack
procfs의 파일은 크게 다음과 같다. 자세한 사항은 man proc
으로 알 수 있다.
파일 |
설명 |
/proc/PID/maps |
프로세스의 메모리 맵 |
/proc/PID/cmdline |
프로세스의 명령어 라인 파라미터 |
/proc/PID/stat |
프로세스의 상태, 사용한 CPU 시간, 우선도, 사용 메모리의 양 |
/proc/cpuinfo |
시스템에 탑재된 CPU에 대한 정보 |
/proc/diskstat |
시스템에 탑재된 저장 장치에 대한 정보 |
/proc/meminfo |
시스템에 탑재된 메모리에 대한 정보 |
/proc/sys 이하의 파일 |
커널의 각종 튜닝 파라미터. sysctl과 /etc/sysctl.conf로 변경하는 파라미터와 1:1로 대응된다. |
sysfs
procfs에 잡다한 정보가 많아져서, 이러한 정보들을 배치하는 장소를 어느정도 정하기 위해 만들어졌다. sysfs는 보통 /sys 이하에 마운트된다.
파일 |
설명 |
/sys/devices 이하의 파일 |
시스템에 탑재된 디바이스에 대한 정보 |
/sys/fs 이하의 파일 |
시스템에 존재하는 각종 파일시스템에 대한 정보 |
cgroupfs
일반적으로 cgroupfs는 /sys/fs/cgroup 이하에 마운트된다.
cgroup
Btrfs
ext4나 XFS는 UNIX가 만들어졌을 때부터 필요했던 파일의 작성, 삭제, 읽고 쓰기 등의 단순한 기능만을 제공한다. 최근에 이 이상으로 풍부한 기능을 제공하는 파일시스템들이 나왔는데, 대표적으로 Btrfs가 있다.
멀티 볼륨
ext4나 XFS가 하나의 파티션에 대응하여 하나의 파일시스템을 만드는 것에 비해, Btrfs는 여러 저장 장치나 파티션으로부터 거대한 스토리지 풀을 만들고, 거기에 마운트가 가능한 서브 볼륨 영역을 작성한다. LVMLogical Volume Manager에 비교하면, 스토리지 풀은 LVM으로 구현된 볼륨 그룹이고 서브 볼륨은 LVM으로 구현된 논리 볼륨과 파일시스템을 더한 것과 비슷하다. 즉 Btrfs는 파일시스템 + LVM과 같은 볼륨 매니저라고 생각하면 편하다.
생성된 Btrfs은 마운트 중에도 운영을 멈추지 않고 저장 장치의 추가, 삭제, 교환 등의 작업을 할 수 있다.
스냅샷
스냅샷이란 시스템을 특정 상태로 되돌리기 위해, 해당 지점에서 마치 사진을 찍듯 파일들에 대한 정보를 저장하는 것을 말한다. Btrfs는 서브 볼륨 단위로 스냅샷을 만들 수 있다. 스냅샷 생성 시 데이터를 전부 복사하는게 아니라, 메타데이터를 작성하거나 스냅샷 내의 더티 페이지를 라이트 백write back(git처럼 변경 이력을 관리하여 돌아갈 수 있는 것 같다.) 하는 것만으로 처리할 수 있으므로 일반적인 복사보다 빠르다고 한다. 또한 원본 서브 볼륨과 데이터를 공유하므로 공간적인 낭비도 적다.
RAID
Btrfs는 파일시스템 레벨에 RAIDRedundant Array of Inexpensive Disks, 레이드 작성을 포함한다.
지원되는 RAID
RAID 0 : 패리티가 없는 data striping 구조
RAID 1 : 패리티가 없는 데이터 미러링 구조
RAID 10(RAID 1+0) : 먼저 디스크를 미러링(RAID 1)하고, 그 이후 스트리핑(RAID 0)한 구조 (최소 디스크 4개)
RAID 5 : 패리티가 배분되는(distributed) 스트리핑된 세트 (최소 디스크 3개)
RAID 6 : 패리티가 배분되는(distributed) 스트리핑된 세트 (최소 디스크 4개)
dup : 같은 데이터를 같은 저장 장치에 이중화..라는데 찾아봐도 잘 안나옴
데이터 파손 검출, 복구
Btrfs는 데이터와 메타데이터 모두 일정 크기마다 체크섬checksum을 가지고 있어서 데이터 파손을 검출할 수 있고, 따라서 데이터 파손을 모르는 채로 계속 운영하는 경우가 발생하는 경우가 적다.
데이터나 메타데이터를 읽는 도중에 체크섬 에러를 검출하면, 그 데이터를 버리고 읽기를 요청한 프로그램에 I/O 에러를 알린다.
이 때 RAID 1, 10, 5, 6, dup 구성일 경우 데이터 파손을 복구할 수 있다. 이 때 읽기를 요청한 프로그램에서는 데이터 파손을 알지 못하고(성능 저하 등으로는 알 수 있겠지만) 지나간다.
참고
소스 코드