그림으로 배우는 Linux 구조 - 7 / 파일 시스템
By Bys on October 23, 2024
파일 시스템
6장에서 각종 장치는 디바이스 파일
로 접근 가능하다고 설명했습니다. 하지만 대부분의 저장 장치는 이 장에서 설명하는 파일 시스템
으로 접근합니다.
파일 시스템이 존재하지 않으면 데이터를 디스크 어떤 위치에 저장할지 직접 정해야 합니다. 게다가 다른 데이터를 훼손하지 않도록 비어 있는 영역도 관리가 필요합니다. 그리고 쓰기가 끝나서 나중에 다시 읽어 오려면 어느 위치에, 파일 크기가 얼마이고, 어떤 데이터를 배치했는지 기억해야 합니다.
파일 시스템은 이런 정보를 대신해서 관리해 줍니다. 파일 시스템은 사용자에게 의미 있는 데이터 뭉치를 파일 단위로 관리합니다. 각각의 데이터가 어디에 있는지 사용자가 직접 관리하지 않아도 저장 장치의 관리 영역에 기록됩니다.
위 그림에서 파일 형식으로 데이터를 관리하는 저장 장치의 영역(관리 영역 포함)과 해당 저장 영역을 다루는 처리(피일 시스템 코드) 양쪽을 모두 합쳐서 파일 시스템
이라고 부릅니다.
리눅스 파일 시스템은 각 파일을 디렉토리라고 하는 특수한 파일을 사용해서 분류할 수 있습니다. 디렉토리가 다르면 동일한 파일명을 사용할 수 있습니다. 또한 디렉토리 안에 또 다시 디렉토리를 만들어서 트리 구조를 만들 수 있습니다.
파일 시스템에는 데이터와 메타 데이터 두 종류의 데이터가 있습니다. 데이터는 사용자가 작성한 문서나 영상, 동영상, 프로그램 등에 해당합니다. 이에 반해 메타 데이터는 파일을 관리할 목적으로 파일 시스템에 존재하는 부가적인 정보입니다.
파일접근 방법
파일 시스템에는 POSIX(유닉스 계통 OS의 C 언어 인터페이스 규격) 에서 정한 함수로 접근할 수 있습니다.
- 파일 조작
- 작성, 삭제: create(), unlink() 등
- 열고 닫기: open(), close() 등
- 읽고 쓰기: read(), write(), mmap() 등
- 디렉토리 조작
- 작성, 삭제: mkdir(), rmdir()
- 현재 디렉토리 변경: chdir()
- 열고 닫기: opendir(), closedir()
- 읽기: readdir() 등
이런 함수 덕분에 사용자는 파일 시스템에 접근할 때 파일 시스템 종류의 차이를 의식할 필요없이 파일 시스템이 ext4 혹은 XFS 이라도 관계없이 파일을 만들고 싶다면 create() 함수를 사용합니다.
bash 같은 shell로 다양한 프로그램에서 파일 시스템에 접근할 때 내부적으로는 이러한 함수를 호출합니다.
파일 시스템 조작용 함수를 호출하면 다음과 같은 순서로 처리가 진행됩니다.
- 파일 시스템 조작용 함수가 내부적으로 파일 시스템을 조작하는 시스템 콜을 호출합니다.
- 커널 내부 가상 파일 시스템(Virtual File System, VFS) 처리가 동작하고 각각의 파일 시스템 처리를 호출합니다.
- 파일 시스템 처리가 디바이스 드라이버를 호출 합니다.
- 디바이스 드라이버가 장치를 조작합니다.
메모리 맵 파일
리눅스에는 파일 영역을 가상 주소 공간에 매핑하는 메모리 맵 파일 기능이 있습니다. mmap() 함수를 특정한 방법으로 호출하면 파일 내용을 메모리로 읽어서 그 영역을 가상 주소 공간에 매핑할 수 있습니다.
메모리 맵에 저장된 파일은 메모리와 같은 방법으로 접근할 수 있습니다. 데이터를 변경하면 나중에 저장 장치에 있는 파일에도 정해진 타이밍에 반영합니다. 이런 타이밍은 8장에서 설명합니다.
- hello 문자열이 들어 있는 testfile 작성.
- filemap 작성 ```golang package main
import ( “fmt” “log” “os” “os/exec” “strconv” “syscall” )
func main() { pid := os.Getpid() fmt.Println(“** testfile 메모리 맵 이전의 프로세스 가상 주소 공간 **”) command := exec.Command(“cat”, “/proc/”+strconv.Itoa(pid)+”/maps”) command.Stdout = os.Stdout err := command.Run() if err != nil { log.Fatal(“cat 실행에 실패했습니다”) }
file, err := os.OpenFile("testfile", os.O_RDWR, 0)
if err != nil {
log.Fatal("testfile을 열지 못했습니다")
}
defer file.Close()
// mmap() 시스템 콜을 호출해서 5바이트 메모리 영역을 확보
data, err := syscall.Mmap(int(file.Fd()), 0, 5, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
if err != nil {
log.Fatal("mmap() 실행에 실패했습니다")
}
fmt.Println("")
fmt.Printf("testfile을 매핑한 주소: %p\n", &data[0])
fmt.Println("")
fmt.Println("*** testfile 메모리 맵 이후의 프로세스 가상 주소 공간 ***")
command = exec.Command("cat", "/proc/"+strconv.Itoa(pid)+"/maps")
command.Stdout = os.Stdout
err = command.Run()
if err != nil {
log.Fatal("cat 실행에 실패했습니다")
}
// 매핑한 파일 내용을 변경
replaceBytes := []byte("HELLO")
for i, _ := range data {
data[i] = replaceBytes[i]
} } ```
AdminDevAccountRole:~/environment/temp $ ./filemap
*** testfile 메모리 맵 이전의 프로세스 가상 주소 공간 ***
00400000-004b6000 r-xp 00000000 103:02 1549630 /home/ubuntu/environment/temp/filemap
004b6000-00591000 r--p 000b6000 103:02 1549630 /home/ubuntu/environment/temp/filemap
00591000-0059e000 rw-p 00191000 103:02 1549630 /home/ubuntu/environment/temp/filemap
0059e000-005c2000 rw-p 00000000 00:00 0
c000000000-c000400000 rw-p 00000000 00:00 0
c000400000-c004000000 ---p 00000000 00:00 0
7cdb55280000-7cdb55300000 rw-p 00000000 00:00 0
7cdb55300000-7cdb57400000 rw-p 00000000 00:00 0
7cdb57400000-7cdb67580000 ---p 00000000 00:00 0
7cdb67580000-7cdb67581000 rw-p 00000000 00:00 0
7cdb67581000-7cdb87580000 ---p 00000000 00:00 0
7cdb87580000-7cdb87581000 rw-p 00000000 00:00 0
7cdb87581000-7cdb99430000 ---p 00000000 00:00 0
7cdb99430000-7cdb99431000 rw-p 00000000 00:00 0
7cdb99431000-7cdb9b806000 ---p 00000000 00:00 0
7cdb9b806000-7cdb9b807000 rw-p 00000000 00:00 0
7cdb9b807000-7cdb9bc00000 ---p 00000000 00:00 0
7cdb9bc31000-7cdb9bca2000 rw-p 00000000 00:00 0
7cdb9bca2000-7cdb9bd22000 ---p 00000000 00:00 0
7cdb9bd22000-7cdb9bd23000 rw-p 00000000 00:00 0
7cdb9bd23000-7cdb9bda2000 ---p 00000000 00:00 0
7cdb9bda2000-7cdb9be02000 rw-p 00000000 00:00 0
7ffdf186f000-7ffdf1890000 rw-p 00000000 00:00 0 [stack]
7ffdf18bf000-7ffdf18c3000 r--p 00000000 00:00 0 [vvar]
7ffdf18c3000-7ffdf18c5000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
testfile을 매핑한 주소: 0x7cdb9bc30000
*** testfile 메모리 맵 이후의 프로세스 가상 주소 공간 ***
00400000-004b6000 r-xp 00000000 103:02 1549630 /home/ubuntu/environment/temp/filemap
004b6000-00591000 r--p 000b6000 103:02 1549630 /home/ubuntu/environment/temp/filemap
00591000-0059e000 rw-p 00191000 103:02 1549630 /home/ubuntu/environment/temp/filemap
0059e000-005c2000 rw-p 00000000 00:00 0
c000000000-c000400000 rw-p 00000000 00:00 0
c000400000-c004000000 ---p 00000000 00:00 0
7cdb55280000-7cdb55300000 rw-p 00000000 00:00 0
7cdb55300000-7cdb57400000 rw-p 00000000 00:00 0
7cdb57400000-7cdb67580000 ---p 00000000 00:00 0
7cdb67580000-7cdb67581000 rw-p 00000000 00:00 0
7cdb67581000-7cdb87580000 ---p 00000000 00:00 0
7cdb87580000-7cdb87581000 rw-p 00000000 00:00 0
7cdb87581000-7cdb99430000 ---p 00000000 00:00 0
7cdb99430000-7cdb99431000 rw-p 00000000 00:00 0
7cdb99431000-7cdb9b806000 ---p 00000000 00:00 0
7cdb9b806000-7cdb9b807000 rw-p 00000000 00:00 0
7cdb9b807000-7cdb9bc00000 ---p 00000000 00:00 0
7cdb9bc30000-7cdb9bc31000 rw-s 00000000 103:02 258904 /home/ubuntu/environment/temp/testfile
7cdb9bc31000-7cdb9bca2000 rw-p 00000000 00:00 0
7cdb9bca2000-7cdb9bd22000 ---p 00000000 00:00 0
7cdb9bd22000-7cdb9bd23000 rw-p 00000000 00:00 0
7cdb9bd23000-7cdb9bda2000 ---p 00000000 00:00 0
7cdb9bda2000-7cdb9be02000 rw-p 00000000 00:00 0
7ffdf186f000-7ffdf1890000 rw-p 00000000 00:00 0 [stack]
7ffdf18bf000-7ffdf18c3000 r--p 00000000 00:00 0 [vvar]
7ffdf18c3000-7ffdf18c5000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
$ AdminDevAccountRole:~/environment/temp $ cat testfile
HELLO
- mmap() 함수 실행에 성공해서 testfile 파일 데이터 시작 주소가 0x7cdb9bc30000 가 되었습니다.
- 이 주소로 시작하는 영역이 실제로 메모리 매핑된걸 알 수 있습니다.
일반적인 파일 시스템
리눅스는 ext4, XFS, Btrfs 같은 파일 시스템을 주로 사용합니다. 파일 시스템은 아래와 같은 특징이 있습니다.
파일 시스템 | 특징 |
---|---|
ext4 | 예전부터 리눅스에서 사용하던 ext2, ext3 에서 전환하기 편함 |
XFS | 뛰어난 확장성 |
Btrfs | 풍푸한 기능 |
각 파일 시스템은 저장 장치에서 만드는 데이터 구조와 관리하는 처리 방식이 다릅니다. 각 파일 시스템은 다음과 같은 차이점이 존재합니다.
- 파일 시스템 최대 크기
- 파일 최대 크기
- 최대 파일 개수
- 파일명 최대 길이
- 동작별 처리 속도
- 표준 기능 이외의 추가 기능 유무
쿼터(용량 제한)
다양한 용도로 시스템을 사용하다 보면 파일 시스템 용량을 무제한으로 사용하다가, 다른 기능실행에 필요한 용량이 부족해지는 경우가 있습니다. 특히나 시스템 관리용 처리를 하는 데 필요한 용량이 부족하면 시스템 전체가 불안정해집니다. 이런 문제를 방지하려면 용도별로 사용 가능한 파일 시스템 용량을 제한하는 기능이 필요합니다. 이런 기능을 쿼터라고 부릅니다.
용도 A에 쿼터 제한을 두는 예시.
쿼터에는 다음과 같은 종류가 있습니다.
- 사용자 쿼터: 파일 소유자인 사용자마다 용량을 제한합니다. 예를 들어 알반 사용자 때문에 /home/ 디렉토리가 가득 차는 사태를 방지합니다.
- 디렉토리 쿼터: 특정 디렉토리 마다 용량을 제한합니다. 예를 들어 어떤 프로젝트 멤버가 공유하는 디렉토리 용량 제한을 둡니다.
- 서브 볼륨 쿼터: 파일 시스템 내부의 서브 볼륨 단위마다 용량을 제한합니다.
파일 시스템 정합성 유지
시스템을 운영하다 보면, 파일 시스템 내용에 오류가 생기기도 합니다. 전형적인 예를 들면 파일 시스템 데이터를 저장 장치에서 읽거나 쓰는 도중에 시스템 전원이 강제적으로 끊기는 경우 입니다.
root 아래에 foo, bar 라는 2개의 디렉토리가 있고, 이 상태에서 bar를 foo 아래로 이동시키면 파일 시스템은 위와 같이 처리합니다.
이런 처리 흐름은 프로세스에서 보자면 중간에 끼어들 수 없는 아토믹 처리입니다. 첫 번째 쓰기(foo 파일 데이터 갱신)가 끝나고 두 번째 쓰기(root 데이터 갱신)가 끝나기 전에 전원이 꺼졌다면 파일 시스템이 어중간한 오류 상태가 될지 모릅니다.
이후에 파일 시스템이 오류를 감지하는데 마운트 작업 중이라면 파일 시스템이 마운트 불가능하거나, 읽기 전용 모드로 다시 마운트 합니다. 또는 시스템 패닉이 일어납니다. 파일 시스템의 오류 방지 기술에는 여러 종류가 있지만 널리 사용되는 건 저널링과 카피 온 라이트 두 가지 방식입니다. ext4와 XFS는 저널링, Btrfs는 카피 온 라이트로 각각 파일 시스템 오류를 방지합니다.
저널링을 사용한 오류 방지
저널링은 파일 시스템 내부에 저널 영역이라고 하는 특수한 메타 데이터 영역을 준비합니다. 이 때 파일 시스템 갱신 방법은 다음과 같습니다.
- 갱신에 필요한 아토믹한 처리 목록을 일단 저널 영역에 기록합니다. 이 목록을 저널 로그라고 부릅니다.
- 저널 영역에 기록된 내용에 따라 실제로 파일 시스템 내용을 갱신합니다.
저널 로그를 갱신하던 도중에 강제로 전원이 끊겼다면 단순히 저널 로그를 버리기만 하면 실제 데이터는 처리 전 상태와 변함이 없습니다.
실제 데이터를 갱신하던 도중에 강제로 전원이 끊기면 저널 로그를 다시 시작해서 처리를 완료 상태로 만듭니다.
카피 온 라이트로 오류 방지
ext4나 XFS 등은 일단 저장 장치에 파일 데이터를 썼다면 이후 파일을 갱신할 때는 저장 장치의 동일한 위치에 데이터를 써넣습니다.
(파일 시스템에서 저널(journal)은 데이터 무결성을 보장하기 위해 사용하는 기술입니다. 저널링 파일 시스템은 데이터가 실제로 저장되기 전에 변경 사항을 저널에 기록합니다. 이렇게 함으로써 시스템 충돌이나 전원 장애와 같은 예기치 않은 상황에서도 데이터의 일관성을 유지할 수 있습니다.)
Reference
- 그림으로 배우는 리눅스 구조 (다케우치 사토루)
- E-Book
book
linux
filesystem
]