관리 메뉴

Linux Programmer

파일(file)에 대한 토막글 본문

컴퓨터 관련/리눅스, 유닉스

파일(file)에 대한 토막글

sunyzero 2019.03.08 13:13

파일(file)에 대한 토막글


토막글인데 쓰다보니 길어졌다. 보기 귀찮은 분들은 TL;DR을 참고. (페북에 썼다가 짬이 생긴 시간에 정리해서 블로그에 올려둡니다.


기업체 특강을 가보면 의외로 file의 개념을 절반만 알고 있는 programmer나 system engineer들이 꽤 있었다.
대부분 파일이라고 하면 그냥 TXT나 PDF 같은 데이터가 저장된 파일만 생각하는 경우가 많은데, 그런 경우는 추상적인 개념을 눈에 보이는 부분만 이해한 경우다. 그래서 file에 대해 간단한 상식 글을 가볍게 써볼까 했는데..... 1시간 가까이 쓰게 됐다. 이런...


여기서 다루는 용어들은 대부분 한글과 영어를 병기한다. 그리고 대부분은 영어로 쓴다. 그 이유는 나중에 검색을 할 때도 영어 단어를 실제로 쓰는 경우가 많기 때문에 영단어를 주로 표기하도록 한다.


1. 이 글을 쓰게된 이유

기업체 특강에서는 다수를 상대로 강의하다보니 모르는 용어나 개념이 나와도 모두들 질문하지 않는 암묵적인 불문율이 있다.(shy programmer) 그래서 강의 도중에 아는지 모르는지 아리송할 때가 많다.

원래는 다들 어느 정도 알고 있을거라고 판단해서 file과 memory 개념을 말할 때 용어나 개념은 생략하고 강의를 진행 했다. 그러다가 몇 년부터 출강하러가면 강의 전후로 시험문제를 출제해달라는 요청이 있어서, 시험 문제에 file과 memory에 대한 용어와 개념에 대한 문제를 출제 했었다. 그런데 인사팀의 피드백에 의하면 file과 memory의 관계에 대해 틀리는 분들이 의외로 많았다는 것을 알게되었다. 원래 점수를 주려고 학부 교과서에 나오는 개념을 물어본 것인데, 오히려 대부분 틀리는 경우가 많았다는 아이러니가... (오픈북, 인터넷 사용가능인데 의외로 많이 틀린다)


그래서 한 번은 파일에 대해 제대로 된 글을 써둘까 했었다. 그리고 글을 쓰겠다고 마음 먹은지 한 3년 만에 쓰게 되었다. 절대 게을러서 그런게 아니라 그냥 바빠서다. -_-; (페북에다가 대충 쓰고나서 나중에 블로그에 정리해서 올려놓고, 강의할 때 읽어보라고 하면 편할 것 같다.)


1.1. POSIX 

먼저 여기서 자주 나오는 용어인 POSIX를 이야기 하고 넘어가겠다. POSIX[각주:1]파직스(pahz-icks)라고 읽는다. 절대로 포식스라고 읽는거 아니다. 번역서에 죄다 포식스라고 적는데 틀린 발음이다. POSIX 발음의 기원에 대한 것은 각주에 적어두었다. (포식스라고 하면 positive 46으로 읽히기 때문에 조심해야 한다.)


POSIX이야기를 하는 것은 여기서 다루는 개념들은 POSIX 시스템을 기반으로 하는 것들이기 때문이다. 물론 특수한 목적의 OS(Operating system)를 빼고, 범용 OS 대부분이 POSIX 기반 운영체제이므로 대학의 운영체제론에서 배우는 개념의 구현(implementation)이 바로 POSIX 운영체제라고 간주해도 된다. 그래서 OS를 제대로 이해하려면 modern OS의 근간이 되었던 POSIX system, 그 중에서도 UNIX와 Linux에 대한 이해가 필수다.

그리고 특수한 운영체제라고 하더라고 POSIX의 기본 개념을 차용하고 슬림화시켜서 만드는 경우가 많으므로 POSIX 개념을 알아두는 것은 운영체제 전반을 이해하는데 있어서 굉장히 중요하다.


2. 파일(file)이란?

우리가 탐색기나 파일관리자에서 볼 수 있는 oooo.txt, oooo.pdf 같은 이름을 가진 파일, 즉 데이터를 저장하는 파일들은 정식 명칭으로는 regular file이라고 부른다. 그러면 regular file이 아닌 것도 있다고 생각할 수 있을 것이다.

가장 쉽게 볼 수 있는 non-regular file로는 디렉터리(directory)가 있다. 디렉터리는 파일 목록을 담고있는 파일이다. 더 쉽게 추상적으로 설명하면 파일목록을 담고 있는 HTML 파일이라고 간주하면 이해가 쉽다.(당연히 디렉터리는 HTML 파일로 구현된 것은 아니다. 하지만 개념을 이해할 때는 그냥 파일 목록을 담고 있는 hyper text라서 클릭하면 다른 파일로 점프 시켜주는 거라고 생각하면 이해가 빠르다.)


이 외에 특수하게 pipe라든지 socket, 혹은 device들도 전부 file의 종류다. 그러면 왜 file이라는 형태로 만들었을까? 그건 interface의 통일성 때문이다. OS론과 그 외 프로그래밍언어를 공부해보면 interface와 abstraction에 대한 직관을 얻을 수 있을 것이다.


3. 경로(path)

위에서 파일의 종류 중에 디렉터리를 이야기 했는데, 이 파일들이 유저의 눈에 보일려면 탐색기나 파일관리자에서 파일이 들어있는 디렉터리로 이동해야 한다. 즉 파일의 정확한 위치 정보는 directory + file name이 되겠다. 이것을 경로(path)라고 부른다.

path를 좀더 고상하게 표현하면 file을 외부에서 액세스할 수 있는 접점(interface)이라고 할 수 있다. 이렇게 path가 존재하는 경우를 named file, path가 존재하지 않는 경우를 anonymous file이라고 부른다.

그럼 path가 존재하지 않는 anonymous file이 있을까? 외부에서 보이지 않는다면 anonymous file로 생각하면 된다. 가장 쉽게 찾으려면 last | sort 같은 명령을 치면 중간에 보이는 "|" 문자의 구현체가 바로 익명 파이프(anonymous pipe)라고 불리는 파일이다. last 명령의 실행 결과의 표준출력(stdout)이 sort의 표준입력(stdin)으로 전달될 때 anonymous pipe 파일을 통해 전달되는데 그 과정에서 anonymous pipe의 path는 존재하지 않는다.


익명 파이프



그럼 anonymous pipe말고 path가 존재하는 named pipe[각주:2]도 존재할까? 당연히 있다. named pipe로 last | sort와 같은 결과를 만들려면 mkfifo /tmp/myfifo; last > /tmp/myfifo &; sort /tmp/myfifo 으로 명령하면 된다. 이렇게하면 named pipe로 /tmp/myfifo를 만들어서 통신할 수 있다. 명령어 실행 후 ls -l /tmp/myfifo 해보면 file attribute에 pipe를 뜻하는 'p'가 표시되는 것을 볼 수 있다. 

이것보다 더 직관적으로 last > last.txt; sort last.txt 로 실행해서 last.txt라는 regular file을 생성해서 넘겨줘도 된다. 하지만 뭔가 수준 낮아보이기 때문에 이렇게 하는 사람은 거의 없다.


여기서 직관이 있는 사람들은 anonymous 접두어가 붙으면 임시적인 lifetime을 가지는 객체구나~하면서 무릎을 칠 수 있을 것이다. 반대로 named 접두어가 붙으면 persistency를 가지겠구나~하면서 무릎을 연타할 수 있을 것이다.


3.1. Anonymous vs Named

그리고 직관이 뛰어난 사람은 anonymous와 named의 접두어를 memory의 개념까지 확장할 수 있게 된다.

원래 학교에서 배운 주기억 장치(main memory)는 RAM을 의미한다. 그리고 disk에 존재하는 file을 읽거나 쓰려면 memory에 로딩되어야 한다는 것을 배웠을 것이다. (관련되는 용어중에 locality, memory hierarchy, memory mapped I/O 등등 배울 때 다뤄졌을 것이다.)


그러면 process가 실행되어 디스크에 path가 존재하는 file을 읽어 들이면 memory에 로딩되는데 이것을 named memory라고 부르게 된다. 반대로 C언어의 malloc같은 기능, 혹은 C++의 new로 메모리를 할당하면 path가 존재하지 않는 heap memory에 할당되는데, 이것은 anonymous memory가 된다. 왜 이렇게 되는지는 이젠 굳이 설명하지 않아도 왜 그런지 알 것이다.


실제로 Linux의 /proc/meminfo를 anonymous 영역이 있는데, 아래의 예시에서는 10348760 kB로 표시되고 있다.

 $ cat /proc/meminfo
MemTotal:       65881540 kB
MemFree:        23760816 kB
MemAvailable:   51603980 kB
Buffers:         1946552 kB
Cached:         25071184 kB
SwapCached:            0 kB
Active:         23043564 kB
Inactive:       14178812 kB
Active(anon):   10226884 kB
Inactive(anon):   121876 kB
Active(file):   12816680 kB
Inactive(file): 14056936 kB
Unevictable:          96 kB
Mlocked:              96 kB
SwapTotal:      33554428 kB
SwapFree:       33554428 kB
Dirty:               736 kB
Writeback:             0 kB
AnonPages:      10204852 kB
Mapped:          3262644 kB
Shmem:            144132 kB
Slab:            2089756 kB
SReclaimable:    1685968 kB
SUnreclaim:       403788 kB
KernelStack:       26448 kB
PageTables:       178036 kB
NFS_Unstable:          0 kB
Bounce:                0 kB

즉 시스템에서 사용하는 모든 데이터를 외부에서 액세스가 가능한지, 즉 경로의 유무로 구분지으면 anonymous, named로 분류될 수 있는 것이다. 그래서 /proc/meminfo를 봤을 때 anonymous 영역이 늘어나는 것은 heap memory나 stack같은 것들이 증가했다는 것을 알 수 있게 해주는 것이다.


한 번 실행했던 프로그램이나 한 번 읽어들인 파일을 다시 읽을 때는 속도가 빨라지는 이유가 바로 file을 읽는 대신에 named memory에서 가져오기 때문이다. 가끔 이걸 cache effect라고 부르기도 하는데, 사실상 cache effect는 여기서만 사용되는 기능은 아니므로 정확한 표현은 아니다.


그러면 간단 퀴즈를 풀어보자. shared memory는 anonymous일까? 아니면 named일까? (>찍어도 확률은 1/2이다)


심심풀이 땅콩으로 문제를 하나 더 풀어보자. 이거 은근히 틀리는 사람들이 많았던 문제다. 디스크의 파일을 읽기(reading) 작업과 쓰기(writing) 작업 중에 어떤 작업이 더 느린가? (다른 말로 어떤 것이 latency를 유발하는 작업인가?) 이거 대부분 writing이라고 하는 분들이 많은데, 일반적으로 reading쪽이다. 물론 OS가 embedded 환경이라든지, 반복 작업, 혹은 작업의 개수, 용량이 얼마나 큰지에 따라 조금씩 답이 달라질 수 있지만 일반적으로는 reading이 더 느리다. 왜 그런지는 anonymous, named를 제대로 이해했다면 알 수 있을 것이다.


또한 가상 메모리(virtual memory)로 개념을 확장하면 page in/page out, swap in/swap out까지도 영향을 받는다. 예를 들어 읽기 전용으로 사용되는 named memory는 page out은 되지만 swap out은 안되는 것이 바로 이런 개념 때문이다.


덤으로 dirty page의 개념도 named와 연관시켜서 알아두면 큰 그림을 보는데 도움이 될 수 있다. DB로 따지면 update SQL구문이나 random walking이 왜 더 큰 비용을 발생시키는지도 이해할 수 있게 된다.


이 개념을 network으로 확장하면 소켓(socket) 중에 named socket은 뭐가 있을지도 생각해보면 이해가 빨라진다. 그리고 소켓에서 bind 한다는 개념이 바로 이 이름을 붙여주는 행위라는 것을 이해한다면 소켓 프로그래밍도 파일의 연장선이라는 것을 이해할 수 있게 된다.


4. Data, Meta data

앞에서는 path의 유무(有無)에 따른 분류인 anonymous, named 개념에 대해 이야기 했다. 그 결과 anoymous file, named file 및 memory와의 관계에 대해서 설명했는데, 이번에는 조금 다른 기준에 대해서 이야기 해보고자 한다.


예를 들어 디지털 카메라를 이용해서 "우주소녀"가 노래부르는 것을 녹화했다고 가정하자. (우주소녀 팬이라서 예시를 그렇게 들었다고 생각한다면... 맞았다. 팬이다.) 녹화된 영상의 파일명을 "우주소녀.mp4"로 저장했는데, 이 파일의 이름을 "BTS.mp4"로 변경했다면 영상의 내용이 방탄소년단(BTS)의 영상으로 바뀔까? 만일 바뀐다고 대답했다면 심각한 인지오류가 존재하는 것이므로 가까운 병원부터 가봐야 한다.


파일명을 바꾼다고 파일의 내용은 바뀌지 않는다는 것은 알지만 그러면 파일명(file name)은 어디에 저장되는 것일까라는 의문이 생길 것이다. 안생긴다면 유투브에 가서 BTS영상이나 보자.


여기서 직관이 있는 사람들은 이마를 탁 치면서 파일의 내용(data)외에 파일명(file name)을 저장하는 공간이 어딘가 따로 존재하겠구나~라고 생각할 것이다. 


실험을 위해 "touch 우주소녀.txt" 라고 명령을 내려 empty file을 생성해보자. "stat 우주소녀.txt"로 확인 해보면 access time (atime), modify file(mtime), change time(ctime)이 보이는데 처음에는 모두 동일한 시간으로 되어있다. 그러나 우주소녀.txt를 방탄소년단.txt로 이름을 변경하면 ctime만 바뀐다. 

만일 echo "I love BTS" > 방탄소년단.txt 명령을 실행하여 파일에 I love BTS란 내용을 추가한 뒤에 보면 mtime, ctime이 모두 변경된다. 즉 mtime은 data의 변경 시간을 보여주는 것이고, ctime은 meta data가 변경되었다는 것을 보여주는 것이다. 그런데 내용을 변경하면 왜 mtime, ctime이 다 변경될까? 제대로 이해를 했다면 즉시 답이 나올 것이다.

$ touch 우주소녀.txt
$ stat 우주소녀.txt
  File: 우주소녀.txt
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: fd02h/64770d    Inode: 25956399    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/sunyzero)   Gid: ( 1001/sunyzero)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2019-03-08 12:50:48.986567472 +0900
Modify: 2019-03-08 12:50:48.986567472 +0900
Change: 2019-03-08 12:50:48.986567472 +0900
 Birth: -

$ mv 우주소녀.txt 방탄소년단.txt
$ stat 방탄소년단.txt
  File: 방탄소년단.txt
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: fd02h/64770d    Inode: 25956399    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/sunyzero)   Gid: ( 1001/sunyzero)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2019-03-08 12:50:48.986567472 +0900
Modify: 2019-03-08 12:50:48.986567472 +0900
Change: 2019-03-08 12:51:19.762699257 +0900

$ echo "I love BTS" > 방탄소년단.txt
$ stat 방탄소년단.txt
  File: 방탄소년단.txt
  Size: 11              Blocks: 8          IO Block: 4096   regular file
Device: fd02h/64770d    Inode: 25956399    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/sunyzero)   Gid: ( 1001/sunyzero)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2019-03-08 12:50:48.986567472 +0900
Modify: 2019-03-08 12:54:48.641593693 +0900
Change: 2019-03-08 12:54:48.641593693 +0900

참고로 위 부분이 좀 더 궁금해지면 i-node와 hard link, file hole등에 대해서 공부해보면 POSIX 파일 시스템의 구조와 컨셉에 대해 좀 더 깊이 알 수 있다.


4.1. Process에서 data, meta data

그럼 이번에는 프로세스로 무대를 넓혀보자. 예를 들어 chrome web browser 아이콘을 클릭하면 매번 크롬 브라우저가 실행될 것이다. chrome 실행했는데 firefox가 실행되지는 않을 것이다.


즉 chrome 실행 파일을 가리키는 path의 파일을 실행시키면 항상 동일한 binary file과 부가적인 파일들을 로딩한다는 것이다. 물론 재실행일 때는 disk로부터 읽기는 생략되고 사본인 named memory로부터 가져온다. 따라서 재실행일 때는 좀 더 빠르게 실행될 수 있는 것이다.


하지만 PID(Process ID)라든지 User ID, 실행 권한 같은 정보들은 실행할 때마다 매번 변경된다. 즉 프로세스의 data 부분은 동일하지만 meta data는 매번 변경될 수 있다는 것이다. 즉 PID같은 정보가 저장되는 프로세스의 메타 정보가 어딘가에 존재한다는 뜻이다. 이 부분이 PCB(Process Control Block)에 존재한다. 참고로 여기서 말하는 PCB는 녹색 기판의 PCB(Printed Circuit Board)를 말하는 것이 아니다.



결론적으로 모든 data는 그 데이터를 관리하기 위한 meta data가 어딘가에는 존재해야 한다. 그리고 이 둘은 별개로 취급된다. 이 관계를 잘 이해하면 zombie process가 왜 defunct process(현존하지 않는 프로세스)라고 불리는지도 이해할 수 있다. == 즉 zombie process란 meta 부분만 살아있는 상태인 것이다.


덧붙여서 깨진 파일을 복구하면 왜 파일명이 제대로 안나오고 무슨 일련번호 붙여서 나오는지도 이해할 수 있을 것이다. 그러면 돌발 퀴즈를 하나 해보자. 10테라 바이트의 정보를 저장할 때 파일 1개로 저장하는 것이 효율적일까? 아니면 1TB짜리 10개로 저장하는게 효율적일까? 그것도 아니면 10MB짜리 1백만개로 저장하는 것이 효율적일까?


5. 잡담

POSIX 시스템을 제대로 이해하려면 file이라는 추상적 개념을 이해하는게 중요한데, file에 대해 이해가 깊지 못하면 짠밥에서 우러나오는 통밥스킬(i.e. perception)이 생기지 않는다. 반대로 추상적 개념을 제대로 이해하면 하나를 보면 최소한 둘 이상을 직관으로 깨닫게 되므로 새로운 인터페이스나 기술이 나온다고 하더라도 통밥스킬에 의해 자연스럽게 연관된 개념을 이해하거나 빠르게 습득 할 수 있게 된다. 이는 마치 고전을 많이 읽어서 직관이 생긴 사람들은 다른 인문 서적이나 과학 서적을 볼 때 책이 담고 있는 메시지를 빠르게 간파할 수 있는 것과 비슷하다. (반대로 직관이 너~무 없으면 같은 책을 읽어도 잘못 이해해서 주화입마에 빠지곤 한다.)


뛰어난 프로그래머나 엔지니어가 되려면 직관은 필수다. 그리고 직관을 키우는데는 알고리즘으로 논리력을 키우는 것도 중요하지만, 그 알고리즘과 데이터구조가 왜 나왔는지 인과관계를 이해해야 한다. 그래서 역사가 중요한 법이다.


수십년동안 대가들은 컴퓨터가 좀 더 빠르게 작동하도록 노력해왔는데, 그 노력의 정점에는 운영체제가 있다. 따라서 운영체제를 어떻게 구현했는지 이해하는 것은 매우 중요하다. 그래서 운영체제 친화적인 프로그래밍 기술은 아무나 할 수 있는게 아니다. (웃픈 건 인터넷에 나와있는 잘못된 기법을 사용해서 순정 상태보다 더 느리게 만드는 경우도 있는데, 이게 다 운영체제의 이해부족이다.)


만일 1천대의 서버를 운영하는 회사에서 운영체제, 네트워크를 제대로 이해해서 5%만이라도 최적화된 서비스를 만들면 회사는 대략 50대를 감축할 수 있다. 반대로 논리력은 있지만 직관이 없어서 겨우 20% 더 느린 시스템을 만들면 회사는 수백대의 서버를 더 확보해야 한다. 이것은 cost 상승을 가져오고, 경쟁사에 의해 망할 수도 있는 결과를 가져온다.


* TL;DR

File은 추상적인 interface이다. path의 유무에 따라 anonymous, named로 나뉜다. memory도 path를 가진 backed file의 유무에 따라 anonymous memory, named(file-backed) memory로 나뉠 수 있다.
File은 data, meta data가 따로 관리된다. 프로세스들도 data, meta data가 따로 있다. 프로세스의 meta data 부분은 가지고 있는 부분은 PCB이다.

이걸 잘 이해하면 character 능력치 중에 perception skill이 +1 된다. 고로 이 문서는 구양진경처럼 베이스 내공을 1칸 정도는 올려줄 것으로 예상된다.


2019-03-18 문장 오타 수정 및 보완

2019-03-08 블로그로 옮겨옴 (문장을 다듬고 예제를 넣음)

2019-01-03 페북에 대충 씀

  1. https://www.opengroup.org/austin/papers/posix_faq.html [본문으로]
  2. UNIX 계열 시스템에서는 named pipe를 FIFO라고 부른다. 이는 선입선출의 방식으로 작동하는 것에 기인한다. 따라서 선입선출 작동방식인 FIFO와 스펠링은 같지만 다른 의미로 쓰인다. [본문으로]
2 Comments
댓글쓰기 폼