[OS] File System

Operating Systems 2013. 5. 17. 03:13

※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)


OS 폴더에 포스팅하는게 얼마만인지.. 사실 할 필요는 있었다. OS 블로그가 파일 시스템에 대한 이야기가 없다는 것이 말이 되는가? 그래서 포스팅하려고 한다. 요즘 포스팅에 재미가 들려서 계속 이것 저것하게 된다.


우선 파일에 대해서 이야기하기 전에 파일이 저장되는 물리적인 장치에 대해서 먼저 이야기해보자. 그렇다. 디스크가 바로 파일이 저장되는 물리적인 장소이다. 그 디스크는 어떻게 이루어져있을까? Sector, Track, Cylinder, Platter, Surface 이 다섯개가 디스크를 이루고 있고 그 이외에 Head, Arm, Positioner, Spindle 등이 디스크의 작동을 돕는다.

아래의 그림과 같이 되어있다.



각각을 설명하자면 이렇다.

우선 하나의 디스크는 회전축을 갖는다. 이 회전축을 Spindle이라고 한다. 이 Spindle에 이상한 원판이 몇 개 끼어져서 돌아가는데 그 각각을 Platter이라고 한다. 그 트랙 위에 데이터가 저장이 되는 것인데, 앞 뒤면(Surface) 모두 따로 따로 저장이 가능하다. 따라서 하나의 Platter는 두 개의 Surface를 갖는다는 당연한 말을 하고 있네;; 위의 그림에서 오른쪽에 Platter 0, Platter 1, Platter 2가 있다. 각각이 왼쪽의 그림처럼 여러 개의 Track으로 이루어져있다. 나무를 보면 나이태가 있는데, 그 나이태가 바로 Track이라고 보면 된다. 이 나이태를 세로로 나누어 보자. 아래와 같이 말이다.


Track을 세로로 나누어서 나온 여러 개의 작은 조각들을 Sector라고 한다. 파일은 섹터 단위로 저장이 된다. 여기서 잠깐. 트랙 안쪽의 섹터와 바깥쪽의 섹터 크기가 다른데, 어느 쪽에 더 많은 데이터를 저장할 수 있을까? 옛날의 디스크에서는 넓이가 더 넓은 바깥쪽의 섹터와 더 좁은 안쪽의 섹터에 저장할 수 있는 양이 같았다고 한다. 하지만 지금은 바깥쪽에 더 많은 데이터를 저장할 수 있다고 한다. (저장 용량 효율을 높이기 위함) 하나의 디스크에 여러 개의 Platter가 있다. 그 각각의 Platter가 같은 개수의 Track을 가지고 있을 것인데, (가령 Track 0 ~ Track k-1)

각각의 Platter의 같은 Track 번호를 갖는 Track들을 모아둔 것이 바로 Cylinder이다. 즉, Track의 집합체인 샘이다.


이와 같은 요소를 사용하기 위해서 우리는 Head, Arm, Positioner, Spindle 등이 필요하다. 


보면 대충 어떤 역할을 하는지 짐작이 갈 것이라고 믿는다. 간단하게 말하면 같은 속도로 돌아가는 디스크 위에서 Head가 위/아래로 움직이면서 데이터를 읽어 들이는 것이다. 


자, 이제는 물리적인 디스크에서의 주소를 이야기해보자. 한 때 가상 메모리에 대해서 엄청나게 고민했을 무렵, 이런 저런 가설?들을 머리 속에서 마구 세우고 있었을 때, 이런 생각도 해봤었다. 혹시 Logical Address라는 것이 하드디스크 내에 있는 주소를 의미하는 것이 아닐까? 맞는 말이기도 하고 틀린 말이기도 하다는 것을 지금은 알지만 Virtual Address의 개념을 모르고 있었을 때의 내 개념을 가정하고 말하면 완전 틀린 말이었겠지. 아무튼 실제 물리적인 하드디스크에서의 주소는 어떤 식으로 되어있는지 공부해보자.


실제 물리적인 주소는 어떻게 되어있을까? 앞서 말했듯이, 데이터는 섹터 단위로 디스크에 저장된다. 그렇다면 그 섹터를 알 수 있는 주소 형태면 되겠다. 하나의 섹터를 구별하기 위해서 필요한 요소는 실린더 번호, Surface 번호, 섹터 번호 세 가지가 되겠다. 어떤 실린더의 몇 번째 면? 그 중에서도 어떤 섹터? 이렇게만 구별하면 정확히 하나의 섹터가 구분될 수 있겠지.


그래서 실제로 컴퓨터에서는 다음과 같이 두 가지 방법의 형태 중 하나를 선택한다고 한다.



그러나 이것은 실제 디스크 주소일 뿐, 실제 OS는 디스크 주소를 위와 같이 인식하지 않는다. 가상 메모리 개념처럼 어떤 logical disk address를 어떤 방법을 써서 실제 disk 주소로 변형을 시킨다. 어떻게 변형하는지는 나도 모른다. 우선 여기에서는 OS가 가상 디스크 주소를 어떤 방법으로 변형시켜서 접근한다는 것만 알자.


이제 조금 더 파일 시스템에 가깝게 가서 이야기를 해보자. 우선 파일은 무엇인가? 

위키피디아를 참고하면 다음과 같다.(http://en.wikipedia.org/wiki/Computer_file)


computer file is a resource for storing information, which is available to a computer program and is usually based on some kind of durable storage. A file is durable in the sense that it remains available for programs to use after the current program has finished. Computer files can be considered as the modern counterpart of paper documents which traditionally are kept in offices' and libraries' files, and this is the source of the term. A group of files used by the same program can be packed into one archive file


즉, 파일이라는 것은 정보를 저장하기 위한 컴퓨터가 사용할 수 있는 자원같은 것이다. 이러한 파일들은 여러 가지 형태의 타입으로 존재한다. 우리가 흔히 말하는 그림 파일, 실행 파일, 텍스트 파일들이 그것이다. OS에서 파일을 논할 때 중요한 것은 해당 파일의 내용이 아니다. 바로 파일에 대한 정보들인 metadata들이다. 파일에 대한 정보라.. 무엇인지 아래 예시를 통해 보자.


Metadata of a file

1. Name

2. Identifier

3. Type

4. Location

5. Size

6. Protection

7. User identification

8. Time, date

...


이와 같은 정보들이 바로 metadata이다. 이제 metadata가 어떤 정보를 의미하는지는 모두가 이해했으리라고 생각한다. 다 중요하지만 여기서 특히나 중요한 정보가 location과 protection이다. 


Protection의 경우 multi user 환경의 OS에서 특히 중요하다. multi user 환경의 OS에서는 파일마다 권한을 가지고 있다. 나만 볼 파일을 누군가가 봐서 수정하거나 하면 좋겠나? 아니지. 이 protection에 대한 이야기는 뒤에서 조금 더 자세하게 다룰 예정이다. 


Location의 경우 파일을 Access하기 위해서 쓰인다. OS는 파일을 어떻게 접근할까? 기본적으로 OS는 하나의 파일을 읽을 때 우선 그 파일의 metadata를 메모리로 가져와서 location정보를 읽어서 파일의 위치를 찾아온다. 음... 무슨 말인지 모르겠으니 하나 하나 차근차근 보자. 


예를 들어 우리가 하나의 C프로그램에서 파일을 하나 오픈하려고 하면 보통 open()함수를 사용한다. 가령, test.txt라는 파일을 읽기 위해 C코드를 작성한다고 하자. 그러면 실제로 read명령어를 사용하기 전에 아래와 같이 open() 함수를 사용해야 한다.


f1 = open("/anything/abc/test.txt", O_RDONLY);


test.txt파일에 대해서 읽기만 수행하겠다는 뜻이다. 이 open()함수의 실제 역할이 사실은 하나의 파일의 metadata를 메모리로 로딩하는 역할이다. 왜 metadata를 메모리로 로딩해둘까? 여러가지 이유가 있겠지만 효율을 높이기 위한 이유가 있다. 만일 파일을 open() 함수를 사용하지 않고 그냥 read 혹은 write 한다면 매번 read할 때마다 directory search를 해야한다. 매번 read할 때마다 root 디렉토리에서 anything을 찾아서 그 밑에 있는 abc를 찾아서 그 밑에 있는 test.txt에 접근해야 한다. 얼마나 비효율적인가. 따라서 read 혹은 write하기 전에 먼저 open()함수를 통해서 한번만 디렉토리 탐색을 한다. 그리고 그 파일의 metadata를 메모리에 로딩해두면 나중에 read 혹은 write할 때 메모리에 로딩되어있는 metadata의 loacation 정보만 보면 그 파일의 위치를 찾을 수 있다는 뜻이 된다. 기본적으로 CPU가 하드디스크를 접근하는 속도와 메모리를 접근하는 속도는 엄청난 차이가 있다는 것은 모두가 알고 있을 것이다. 대략 10배 정도 차이나는 것으로 알고 있다. 


open()함수 본래의 역할을 알았으니 조금 더 자세하게 open()함수의 수행과정과 그 결과를 알아보자. f1 = open("/anything/abc/test.txt", O_RDONLY); 함수의 수행과정은 아래와 같다.


1. 파일 이름을 얻는다. (/anything/abc/test.txt)

2. 디렉토리 탐색 (해당 파일이 실제 존재하는 것인지 검사한다.)

3. 실제로 존재할 경우, 해당 파일의 metadata를 메모리에 로딩


지금까지는 위에서 다 설명했던 내용이다. 그 다음 과정에는 open-file table이라는 새로운 개념이 들어간다. open-file table은 open()함수에 의해 open된 파일의 list 형태로 나타낸 테이블이다. 즉 이 테이블에는 open된 파일이 모두 들어와 있다고 보면 된다.


4. metadata가 있는 주소의 위치를 가리키는 포인터를 open-file table에 저장한다.

5. 해당 open-file table 내의 index의 주소를 return 한다. (f1이 그 주소값을 갖게 될 것이다.)


그림으로 설명해보자. (역시 어려운 것은 그림으로 설명하면 쉬워진다.)



open-file table은 결국 파일의 metadata가 저장되어 있는 주소값을 가지고 있는 것이고 그 각각의 index에 대한 주소가 바로 open()함수의 return 값이 되는 것이다.


이제 파일을 어떻게 access하는지 대략적으로 감을 잡았으리라고 본다. 그러면 이제는 조금 더 구체적으로 과정을 이야기 해보자. 방금전에 open-file table에 대해서 이야기를 했다. 그런데 이 open-file table은 사실 두 가지 형태로 존재한다. per-process open-file table 그리고 system-wide open-file table이다.


per-process open-file table은 프로세스마다 가지고 있는 open-file table이다. 

system-wide open-file table은 시스템의 모든 open된 파일들에 대한 open-file table이다. 


Unix의 예를 가지고 이야기 해보자. 가령 unix에서 C 코드를 다음과 같이 작성했다고 하자.


fd1 = open("/etc/passwd", "O_RDONLY");

fd2 = open("/anything/abc/test.txt", "O_RDWR");

fd3 = open("/etc/passwd", "O_WRONLY");



위의 그림에서와 같이 UNIX 시스템에서는 per-process open-file table과 system-wide open-file table이 모두 존재한다. per-process open-file table이 system-wide open-file table를 가리키고 system-wide open-file table이 inode table을 가리키는데, inode table은 각 파일의 metadata를 가지고 있다. 이렇게 해서 아무튼 하나의 프로세스에서 사용될 fd1, fd2, fd3 값은 per-process open-file table index의 주소값이 된다.


※ Unix system에서 per-process open-file table은 file descriptor table이라고 하고 system-wide table은 file table이라고 한다. inode는 index node의 약자이다.


이번 포스팅에서는 파일 시스템에 대한 이야기보다는 OS가 파일을 접근하는 방법에 대해서 주로 알아보았다. 다음 포스팅에서는 파일 시스템에 대해서 이야기해볼 것이고 마운팅에 대한 개념을 설명해보려한다.

'Operating Systems' 카테고리의 다른 글

[OS] File System (Protection)  (1) 2013.05.18
[OS] File System  (0) 2013.05.18
[OS] File System  (4) 2013.05.17
[OS] Deadlock Detection  (2) 2013.02.03
[OS] Deadlock Avoidance (Banker's Algorithm)  (5) 2013.02.03
[OS] Deadlock Prevention and Deadlock Avoidance  (0) 2013.02.03
Posted by 빛나유

댓글을 달아 주세요

  1. 2014.02.26 22:53  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

  2. AA 2014.06.08 20:45  댓글주소  수정/삭제  댓글쓰기

    정말 잘 읽었습니다 교수님 설명보다 낫네요

  3. 임재훈 2017.06.13 15:57  댓글주소  수정/삭제  댓글쓰기

    교수님 설명보다 났네요 정말 ㅋㅋㅋㅋㄱㅋ

  4. 날진 2019.09.10 17:56  댓글주소  수정/삭제  댓글쓰기

    잘읽었습니다!

※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)


신나게 다시 공부를 해보자. 실제 C 코드를 가지고 설명을 해볼 생각이다.



위의 C 코드를 보자. 아주 간단한 코드이다. 단순히 X와 Y의 크기를 비교한 후 X가 크면 X를 return 그렇지 않으면 y를 return 하는 것이다.

이 코드에 대한 assembly code는 다음과 같이 작성될 수 있다.



젤 위의 두 줄은 이전의 Procedure call 포스팅에서 굉장히 자세히, 내가 할 수 있는한 최대한 자세히 설명했으므로 지금은 과감하게 생략한다.


그 다음 명령어인

movl        8(%ebp), %eax

부터 시작해보자.


8(%ebp)에는 x값이 있는데, 그 값을 %eax에 저장한다는 것이다. 왜 x값이 거기에 있냐고?? 이전의 포스팅에 전부 다 설명되어있다.


cmpl        12(%ebp), %eax

%eax - 12(%ebp)하여 그 값을 고려하여 CC(Condition Code)를 수정하겠다는 뜻이다. 현재 %eax는 3이고, 12(%ebp)는 5이다. 즉, 3 - 5을 해서 CC를 계산해보자.


1. CF = 0

2. ZF = 0

3. SF = 1

4. OF = 0


그 다음 명령어를 보자.

jle            .L2


jle는 jump if less or equal이다. 즉, 작거나 같으면 점프라하는 뜻이다. 그런데 뭐보다? 비교해야할 대상이 있을 것 아니냐? 쉽게 말하면 두번째 파라미터(%eax)가 첫번째 파라미터12(%ebp)보다 작거나 같냐? 하고 물어보는 것이다. 3이 5보다 작으면 .L2 코드로 점프하라는 것인데, True이니까 L2로 점프하여 다음 명령어들를 실행하는 것이다.


movl        12(%ebp), %eax


위의 명령어를 실행하게 되겠다. :-)


만일 jle 조건을 만족하지 않았다면 아래의 코드를 수행했을 것이다.


mov1        8(%ebp), %eax

jmp           .L3                    # 무조건 L3로 점프하게 된다.


.L3

popl         %ebp

ret


결론적으로 다음의 두 가지 경우의 수가 나오게 된다.


for 문이든 while 문이든 C 코드의 모든 조건절은 test 또는 cmpl 명령어와 jle와 같은 조건절 점프 문과의 조합으로 수행된다. 조건절 점프문은 jle 명령어 말고도 여러가지가 있다. 아래의 표를 보자.



어라? 위에서 보니까 조금 생소한 것이 있다. 바로 Condition 열인데, CC 값을 가지고 XOR, &, ~등의 연산을 수행하고 있다. 내가 일일이 검사해보지는 않았지만 아마도 이럴 것이다.

jle와 같은 경우 (SF ^ OF) | ZF 연산을 수행해서 그 값을 레지스터에 넣는데.

아마도 저 연산이 %edx - %eax 한 값이 0보다 작거나 같은지를 검사하는 조건일 것이다.


Posted by 빛나유

댓글을 달아 주세요

  1. . 2018.12.25 12:48  댓글주소  수정/삭제  댓글쓰기

    대부분이 jle가 작거나 같다라고만 설명하고, 뭐가 어떤것보다 작은지를 설명하지도 않은 쓰레기 글만 쓰는데, 여기서 사이다 마시고 갑니다.

※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)


오랜 만에 포스팅을 하는 것 같다. 요즘 포스팅에 재미가 들리다 보니까 애초의 나의 의도와는 다른 사심이 생겼다. 방문자 수;;; 이게 은근히 재미가 쏠쏠하다. 비록 맛집이나 제품 사용 후기 또는 프로그래밍과 같은 주류의 블로그는 아니지만 나름 하루에 대략 25~30명은 온다. 나름 쏠쏠한 재미. 내 본래의 의도를 되세기며 다시 한번 포스팅을 해보자.


오늘은 Conditional Jump에 대해서 이야기를 해볼 생각이다. 조건적으로 점프하는 것이다. 물론 여기에서 말하는 점프란, 코드 상에서의 점프를 말하는 것이다. 이 정도는 다들 아실 것이라 믿는다.


우선, 어셈블리 언어에서 점프를 어떤 원리를 통해서 수행하는지를 설명해보도록 하자. 이번 포스팅에서는 실제로 jmp명령어에 대한 이야기는 나오지 않을 예정이다. jmp 명령어를 수행하기 전에 jmp할 조건을 어떻게 세팅하는지에 대한 설명이 주를 이룰 것이다.


어떻게 점프를 하는가? 한 마디로 말하면 레지스터 값을 변경하여 그 값에 따라 점프를 수행한다. 지금까지 언급되어왔던 %ebp, %esp, %eax와 같은 값들과는 다른 레지스터 값이다. 바로 CF, ZF, SF, OF값들이다. 이 네가지의 값들을 적당히 바꿔서 그 값으로 연산해서 점프한다. 무슨 말인가 싶을 것이다. 차근 차근 알아보자.


CF, ZF, SF, OF는 각각 1바이트로 전부 4바이트의 크기를 차지하는 레지스터이다. 각각의 의미를 알아보자.

CF : Carry Flag : 연산시 Carry가 발생하면 1로 세팅된다. 오버플로우를 탐지하는데 쓰이기도 한다.

ZF : 가장 최근에 계산된 명령어의 결과값이 0이면 1로 세팅된다.

SF : 가장 최근에 계산된 명령어의 결과값이 음수면 1로 세팅된다.

OF : 가장 최근에 계산된 명령어가 2's complement 오버플로우를 일으키면 1로 세팅된다.


아직까지도 의문인 점이 있을 것이다. 저 값들을 도대체 어떻게 세팅을 하곘다는 것인가? 답은 '저절로 세팅된다' 이다. 어셈브릴 명령어는 두 가지로 구분될 수 있다. CF ZF 등등과 같은 Condition Code(CC)에 영향을 주는 명령어와 그렇지 않은 명령어.


대부분의 산술/논리 연산 명령어는 CC에 영향을 준다. 가령,

%ebx = -5, %ecx = 5일 때


add        %ebx, %ecx

계산할 때 %ecx값이 0으로 세팅되었다고 하자. 그러면 저절로 ZF는 1로 세팅된다. 또한 SF는 0으로 세팅되겠지? 0이 음수는 아니니까. 그리고 OF는 0으로 세팅된다. 오버플로우는 없었으니까. CF도 0이다. 딱히 Carry가 발생하지 않았으니까.


leal명령어는 CC값을 전혀 바꾸지 않는다. 아무리 leal명령어를 수행해도 CC값은 전혀 변하지 않는다는 뜻이다. leal 명령어 같은 경우 다른 레지스터 값은 수정할 수 있지만 CC는 수정할 수 없는 명령어이다. 반대로 CC만 수정할 수 있고 다른 레지스터 값은 바꿀 수 없는 명령어들도 있다. 대표적으로 test명령어와 cmpl명령어가 있다.


cmpl 명령어부터 설명해볼까? cmpl 명령어는 두 개의 파라미터를 갖는다. 다음과 같은 명령어를 수행했다고 가정하자.


cmpl        %ebx, %eax


이럴 경우, 다음과 같은 계산들이 수행된다.

우선 두 개의 파라미터를 뺀다. (%eax - %ebx) 여기서 중요한 점이 있다. 뺄셈을 진행했을 때 그 값은 어디에도 저장되지 않는다는 것이다. 뺄셈한 값이 0이든 100이든 -100이든 신경쓰지 않는다. 신경쓰는 것은 오로지 다음의 네 가지 사항들이다.


1. Carry가 있는가? 있으면 CF를 1로 세팅

2. 뺄셈한 값이 0인가? 0이면 ZF를 1로 세팅

3. 뺄셈한 값이 음수인가? 음수면 SF를 1로 세팅

4. 오버플로우가 발생헀는가? 발생했으면 OF를 1로 세팅


test 명령어를 설명하면 다음과 같다. cmpl이 CC의 모든 값에 영향을 준 반면, test 명령어는 CC 중에서 ZF와 SF만 영향을 주고 CF와 OF는 무조건 0으로 세팅한다. 가령 다음의 test 명령어가 있다고 하자.


test        %ebx, %eax


이럴 경우, 다음과 같은 계산들이 수행된다.

이전에 cmpl에서는 두 개의 파라미터를 뺐던 반면, test 명령어의 경우 두 개의 파라미터 값에 대해 &연산을 한다. %ebx & %eax를 계산한 후, 다음의 두 가지를 신경쓴다.


1. 연산한 결과가 0인가? 0이면 ZF를 1로 세팅

2. 연산한 결과가 음수인가? 음수면 ZF를 1로 세팅


왜 test 명령어에서 CF와 OF는 무조건 0으로 세팅될까? CF와 OF가 1이 될 수가 없기 때문이다. 생각해봐라. 어떤 숫자를 &연산을 하는데 어떻게 Carry가 발생하며 오버플로우가 발생할 수 있남?


도대체 이러한 결과들을 가지고 어떻게 점프를 한다는 것인지 아직은 감이 안올 것이다. 다음 포스팅에서는 cmpl과 test 명령어를 통해 어떻게 점프를 하고 더욱 나아가 어떻게 C코드의 if문 혹은 for문을 구현할 수 있는지를 설명하겠다.

'System Programming' 카테고리의 다른 글

[SP] Optimizing a program with a hardware independent perspective  (1) 2013.05.21
[SP] Conditional Jump  (1) 2013.05.14
[SP] Conditional Jump (Basic)  (0) 2013.05.14
[SP] Procedure Call  (0) 2013.05.04
[SP] Assembly Language Example  (0) 2013.04.30
[SP] Assembly Language  (1) 2013.04.30
Posted by 빛나유

댓글을 달아 주세요