https://www.yes24.com/Product/Goods/122109062
그림으로 배우는 리눅스 구조 - 예스24
선배가 옆에서 하나하나 알려주듯 친절히 설명해주는실습과 그림으로 배우는 리눅스 지식의 모든 것 * Go 언어와 Python, Bash 스크립트 실습 코드 제공* 이 도서는 『실습과 그림으로 배우는 리눅
www.yes24.com
1. 개요
6장에서 디바이스 파일로 저장 장치에 접근하는 방법을 배웠지만, 대부분의 저장 장치는 파일 시스템(file system)으로 접근한다. 파일 시스템을 통해 사용자는 디스크의 어느 위치에 어느 크기만큼 파일을 저장했는지 알 필요 없이 간단히 파일 시스템에 시스템콜을 보내기만 하면 파일을 쓰거나 읽을 수 있다.
리눅스 파일 시스템은 각 파일을 디렉터리(directory)라고 하는 특수한 파일을 사용하여 분류하는데, 디렉터리가 다르면 동일한 파일명을 사용할 수 있으며, 디렉터리는 트리 구조를 사용한다.
또한, 파일 시스템에는 데이터와 함께 메타데이터(meta date)가 있는데, 메타 데이터는 파일을 관리할 목적으로 파일 시스템에 존재하는 부가적인 정보이다. 프로그램의 헤더(header)와 비슷한 역할을 한다고 보면 된다.
2. 파일 접근 방법
파일 시스템에는 POSIX에서 정한 함수로 접근할 수 있다.
- 파일 조작
- 작성, 삭제: creat(), unlink()
- 열고 닫기: open(), close()
- 읽고 쓰기: read(), write(), mmap()
- 디렉터리 조작
- 작성, 삭제: mkdir(), rmdir()
- 현재 디렉터리 변경: chdir()
- 열고 닫기: opendir(), closedir()
- 읽기: readdir()
하지만 이런 시스템콜을 user는 거의 사용할 일이 없이, 그냥 bash 같은 셸로 명령어(touch, mkdir, cd, rm, ..) 등을 입력하면 이러한 함수들을 호출해준다. 이렇게 파일 조작용 함수를 호출하면 다음과 같은 상황이 벌어진다.
- 파일 시스템 조작용 함수가 내부적으로 파일 시스템을 조작하는 시스템 콜을 호출한다.
- 커널 내부 가상 파일 시스템(Virtual File System, VFS) 처리가 동작하고 각각의 파일 시스템 처리를 호출한다.
- 파일 시스템 처리가 디바이스 드라이버를 호출한다.
- 디바이스 드라이버가 장치를 조작한다.
즉 프로세스 -> VFS -> 로컬 파일 시스템 -> 디바이스 드라이버 -> 장치 순이다.
3. 메모리 맵 파일
리눅스에는 파일 영역을 가상 주소 공간에 매핑하는 메모리 맵 파일(memory-mapped file) 기능이 있는데, mmap() 함수를 특정한 방법으로 호출하면, 파일 내용을 메모리로 읽어서 그 영역을 가상 주소 공간에 매핑할 수 있다. mmap() 함수를 특정한 방법으로 호출하면 파일 내용을 메모리로 읽어서 그 영역을 가상 주소 공간에 매핑할 수 있다. 그리고 mmap()으로 쓰기를 하면 메모리에 변경 내용을 저장했다가 나중에 디스크에도 저장한다.
그러면 echo hello > testfile로 testfile 파일을 만들어두고 다음 filemap.go 프로그램으로 파일 데이터를 변경해보자.
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]
}
}
실행해보면 testfile 파일 데이터 시작 주소가 0x7f0a0f22a000 -> 0x7f0a0f229000가 되면서 여기에 있던 HELLO로 메모리가 매핑됨을 알 수 있으며, 결국 최종적으로 디스크에도 이 내용이 저장됨을 확인했다.
4. 일반적인 파일 시스템
리눅스는 다양한 파일 시스템을 사용하고, 그 종류는 다음과 같다.
파일 시스템 | 특징 |
ext4 | 예전부터 리눅스에서 사용하던 ext2, ext3에서 전환하기 편함 |
XFS | 뛰어난 확장성 |
Btrfs | 풍부한 기능 |
각각의 차이는 파일 시스템의 특징을 설명하면서 비교하겠다.
(1) 쿼터
특정 기능의 용량을 과도하게 늘리다보면 시스템 전체가 불안정해진다. 이를 방지하기 위해 용도별로 사용 가능한 파일 시스템 용량을 제한하는 기능이 필요하고, 이것이 쿼터(quota, 용량 제한)이다. 쿼터의 종류는 다음과 같다.
- 사용자 쿼터: 파일 소유자인 사용자마다 용량을 제한한다. 예를 들어 /home/ 디렉터리의 크기에는 제한이 있다. ext4와 XFS에서 사용 가능하다.
- 디렉터리 쿼터: 특정 디렉터리마다 제한이 있다. 예를 들어 어떤 프로젝트가 공유하는 디렉터리마다 용량 제한을 둔다. ext4와 XFS 파일 시스템에서 사용 가능하다.
- 서브 볼륨 쿼터: 파일 시스템 내부의 서브 볼륨 단위마다 용량을 제한한다. Btrfs가 사용한다.
(2) 파일 시스템 정합성 유지
예를 들어 다음과 같은 파일 시스템 트리가 있다고 하자.
- root
- foo
- hoge
- huga
- bar
- foo
이 때 root의 하위 폴더 bar를 foo 아래로 이동시킨다고 하자. 이동시키려면, foo의 하위 폴더로 bar를 연결(링크 연결)하고, root와 bar 사이의 연결을 끊어주면(링크 삭제) 된다.(이 과정은 아토믹이라서 원래대로면 다른 프로세스가 끼어들 순 없다) 그런데 정확히 그 사이에 시스템이 강제 종료되어서 다음과 같은 상태가 되었다고 치자.
- root
- foo
- hoge
- huga
- bar
- bar
- foo
이러면 파일 시스템이 오류에 빠진다. 심각한 경우에는 시스템 패닉(블루 스크린)이 발생한다. 이러한 오류를 방지하는 기술로는 저널링과 카피 온 라이트가 있다.
(2)-1 저널링
저널링이란 파일 시스템 내부에 저널 영역이라는 특수한 메타 데이터 영역을 이용한다. 이때 파일 시스템 갱신 방법은 다음과 같다.
- 갱신에 필요한 아토믹한 부분을 먼저 저널 영역에 기록한다. 이를 저널 로그(Journal log) 라고 한다.
- 저널 영역에 기록된 내용에 따라 실제로 파일 시스템 내용을 갱신한다.
- 갱신이 끝나면 저널 로그를 파기한다.
그러면 시스템 종료 시점에 따라 대처 방법을 알아보면 다음과 같다.
- 저널 로그 작성 "도중" 전원이 꺼졌을 경우 : 저널 로그를 버리면 된다. 파일 시스템을 만지지 않았으므로 오류 없다. 이러면 유저가 다시 파일 시스템 변경을 명령하면 된다.
- 실제로 데이터를 갱신하던 "도중" 전원이 꺼졌을 경우 : 파일 시스템이 오류 상태가 되고, 이 경우에는 저널 로그를 다시 시작해서 데이터를 처리하면 된다.
(2)-2 카피 온 라이트
카피 온 라이트로부터의 방지 방법을 설명하기 이전, 파일 시스템 각각의 데이터 저장 방법에 차이가 있다.
- ext, XFS: 저장 장치에 파일 데이터를 썼다면, 이후 갱신할 때는 동일한 위치에 데이터를 써넣는다. -> 카피 온 라이트가 아님!!
- Btrfs : 일단 파일에 데이터를 쓴 이후로는, 갱신할 때마다 다른 장소에 데이터를 작성하고, 링크를 옮긴다. -> 카피 온 라이트!
따라서 Btfs에서 디렉토리 move를 어떻게 처리하는지를 살펴보면,
- 1. 새로운 foo를 만들고, 여기에 hoge, huga, bar를 링크한다.
- 2. root는 foo에서 새로운 foo로 링크를 옮긴다.
- 3. 기존 foo를 버린다.
이러한 과정에서 작업 도중에 전원이 종료되면 어떻게 될까? 결국 새로운 foo에서 조작을 하는 것이기 때문에, atomic이 깨지는 현상이 발생한다면, 모두 그냥 새로운 foo를 없애버리면 된다. 그러면 정합성이 유지된다.
5. 백업
하지만 저널링과 COW를 다 쓴다해서 모든 하드웨어 장애를 막아낼 수 있는 것은 아니다. 그렇기 때문에 정기적인 파일 백업을 하는 것이 일반적이다. 하지만 백업이 어렵다면 정합성을 회복하는 복구용 명령어를 사용할 수 있다.
파일 시스템에 따라 다르지만, 일반적으로 fcsk 명령어(ext4: fsck.ext4, XFS: xfs_repair, Btrfs: btrfs check)가 있다. 하지만 fcsk 사용은 그다지 추천하지 않는데,
- 파일 시스템 전체를 검색하므로 검사 시간이 기하급수적으로 늘어남.
- 복구 검사에 오랜 시간을 들여도 실패하는 경우가 많다.
- 사용자가 원하는 대로 복구된다는 보장이 없다. 억지로 마운트하고, 데이터와 메타데이터를 가차없이 삭제하기 때문.
6. Btrfs의 고급 기능
앞서 보았듯, ext4와 XFS는 비슷한 면이 있지만 Btrfs는 조금 특별한 기능이 많다. 그들에 대해 알아보자.
(1) 스냅샷
스냅샷(Snapshot)은 '순간적인 장면을 촬영'하는 기술이다. 용어 때문에 전체 파일 시스템을 임시로 저장한다는 오해가 있을 수도 있지만, 그러면 정~말 비효율적일 것이다. 실제로 복사하는건 전체 데이터가 아니라 메타 데이터이므로, 일반 복사 작업보다 훨씬 빠르다. 그러면 구체적으로 어떻게 메타 데이터를 복사한다는 것일까? 바로 "링크" 만 복사하는 것이다.
예를 들어 root -> {foo, bar} 라는 파일 시스템을 생각해보자. 여기에서 스냅샷을 찍으면 root에서 foo, bar로 향하는 링크만 복사된다. 그러면 이후 foo 데이터를 변경한다고 가정하면 다음과 같은 일이 일어난다.
- foo 데이터를 다른 새로운 영역에 복사한다.
- 새로운 영역 데이터를 갱신한다.
- root -> foo 링크를 root -> new foo로 교체한다.
이렇게 스냅샷을 "이용"해서 백업을 하면 어떤 장점이 있을까? 일반적으로 백업을 하려면 시스템 전체를 복사해야 하고, 이 복사 과정에서 당연하게도 "입출력 작업"이 있으면 안된다. 하지만 스냅샷을 활용하면, 링크를 작성하는 스냅샷 시간만 입출력이 금지되고, 그 이후부터는 백업 과정에서도 입출력이 있어도 상관없다.(이미 링크 관계를 알고있으므로)
(2) 멀티 볼륨
ext4나 XFS는 1개의 파티션에 1개의 파일 시스템을 작성한다. 예를 들면 /dev/sda, /dev/sdb, ... 같이 하나의 파티션에 하나의 시스템이 존재한다. 당연한 일이라고 생각되지만, Btrfs에서는 1개 또는 여러 개의 저장 장치와 파티션으로 커다란 저장소 풀(storage pool)을 만들어서 거기에 마운트 가능한 서브 볼륨(subvolume) 영역을 작성한다. 따라서 저장소 풀은 Logical Volume Manager(LVM)에서 다루는 볼륨 그룹과 비슷하고, 서브 볼륨은 LVM의 논리 볼륨과 파일 시스템을 합친 것에 가깝다. 그림으로 표현하면 다음과 같다.
따라서 예를 들어 Partition A(sda)가 망가져도 나머지 볼륨은 안전하다. 이러한 멀티 볼륨은 RAID와 비슷한 점이 많다.
7. 데이터 손상 감지와 복구
하드웨어 비트 오류 등의 이유로 파일 시스템이 오류가 있을 수 있다. Btrfs는 모든 데이터에 checksum이 있어서 데이터 손상을 감지할 수 있고, 이러한 체크섬 에러를 감지하면 해당 데이터를 버리고, 요청한 프로그램에 에러 메시지를 보낸다. 만약 레이드 구성을 해두었으면 올바른 데이터를 보내서 손상된 데이터를 복구할 수 있다.
8. 기타 파일 시스템
앞서 소개한 ext4, XFS, Btrfs 외에 리눅스가 채택하는 파일 시스템은 다음과 같다.
(1) tmpfs - 메모리 기반 파일 시스템
tmpfs는 메모리를 사용해 저장하는 시스템으로, 컴퓨터 전원이 꺼지면 날라가긴 하지만, 메모리에 저장하므로 다른 저장 장치에 비해 매우 빠르다. 이러한 특징 때문에 tmpfs는 파일을 저장할 필요가 없는 /tmp나 /var/run에서 주로 사용한다. 다음 명령어로 tmpfs의 위치를 알 수 있다.
free 출력 결과에서 shared가 tmpfs의 메모리 용량을 의미한다.
(2) 네트워크 파일 시스템
네트워크로 연결된 원격 호스트의 데이터에 파일 시스템 인터페이스를 사용해서 접근하는 네트워크 파일 시스템(Network File System, NFS)가 존재한다. 또는 Common Internet File System(CIFS)라고 하고, 이는 원격 호스트에 있는 파일 시스템을 로컬에 있는 파일 시스템처럼 조작할 수 있다. NFS는 리눅스를 포함한 유닉스 계통 OS 원격 파일 시스템에 접근할 때 주로 사용하고, CIFS는 윈도 기기의 파일 시스템 접근에 이용한다. 이 외에도 여러 기기의 저장 장치를 하나로 묶어서 네트워크 통신하는 파일 시스템인 CephFS 같은 것도 있다.
(3) procfs - 프로세스 정보 파일 시스템
시스템에 있는 프로세스 정보를 얻기 위한 파일 시스템으로 /proc 아래에 마운트 되며, /proc/pid/아래의 파일에서 프로세스 정보를 얻을 수 있다. 그 구체적인 예시는 다음과 같다.
- /proc/<pid>/maps : 프로세스 메모리 맵
- /proc/<pid>/cmdlind : 프로세스 명령줄 인수
- /proc/<pid>/stat : 프로세스 상태. CPU 시간, 우선도, 사용 메모리 용량
이 외에도 proc을 통해 cpuinfo, diskstat, memoinfo 등 프로세스 이외의 정보를 얻는 명령을 할 수 있다.
(4) sysfs
앞서 보았듯, 프로세스 이외의 정보를 다 procfs에 넣는 것은 바람직하지 못하므로, 이들을 관리하기 위해 sysfs가 생겨났고, /sys/에 마운트된다. 앞서 블록 계층을 공부했는데, /sys/block에는 sda, sdb, loop, ram 등 저장 장치에 대한 정보도 다루고 있다.
'Linux > 그림으로 배우는 리눅스 구조' 카테고리의 다른 글
[그림으로 배우는 리눅스 구조] 9. 블록 계층 (0) | 2024.08.31 |
---|---|
[그림으로 배우는 리눅스 구조] 8. 메모리 계층 (0) | 2024.08.31 |
[그림으로 배우는 리눅스 구조] 6. 장치 접근(2) (0) | 2024.08.30 |
[그림으로 배우는 리눅스 구조] 6. 장치 접근(1) (0) | 2024.08.30 |
[그림으로 배우는 리눅스 구조] 5. 프로세스 관리(응용편) (0) | 2024.08.29 |