이번에는 이전 글(https://shyeon.tistory.com/13)에서 만든 레코드파일을 관리하는 simple index 파일을 생성하고 생성한 simple index파일을 활용하여 이진탐색 방식을 이용해 원하는 레코드의 정보를 불러오는 기능을 구현해 보았다.

simple index파일에서의 key 값으로는 14바이트짜리 id가 사용된다 그리고 원하는 주소에 접근하기 위해서 페이지번호와 페이지에서의 레코드 번호가 주소값으로 사용된다. 그렇기 때문에 뒤에 널문자를 제외한 13바이트의 id, 페이지 번호로 4바이트, 레코드 번호로 4바이트 총 21 바이트가 simple index 파일의 하나의 레코드를 구성한다. 레코드 파일을 하나 넘겨줄 경우 record.idx라는 이름의 simple index 파일이 생성된다. 이때 생성된 레코드는 id에 대해 오름차순으로 정렬된다. 이 id는 정수형으로 정렬되기 때문에 자리수가 클 수록 큰 수로 판단된다. 레코드파일은 다음과 같은 명령어로 실행된다.

$ a.out i records.dat records.idx

이렇게 두 번째 인자로 i를 줄 경우 records.dat의 레코드파일로부터 records.idx라는 이름의 index 파일을 생성한다.

    만약 두 번째 인자로 b 옵션을 줄 경우 정렬된 index 파일로부터 원하는 키 값을 주었을 때 원하는 레코드의 정보를 출력한다. 실행할 경우 다음과 같이 나타나게 된다.

$ a.out b records.dat records.idx "9800001111111"

#reads:3

id=9800001111111

name=shyeon

age=24

addr=Seoul

phone=010-0000-0000

email=shyeon@shyeon.com

이렇게 records.idx에서 9800001111111이라는 키를 이진 탐색으로 찾는데 3번의 읽기를 했다는 의미로 #reads:3을 출력하고 records.idx에서 찾은 주소값으로 records.dat이라는 레코드파일에서 해당 레코드를 읽은 결과를 밑에 출력하게 된다. 만약 레코드가 존재하지 않는 경우 "no persons"를 출력한다.

https://github.com/Seo-sang/simple_index_record_file

이전 글에서 설명한 가변길이 레코드 방식을 실제로 구현해보았다. 레코드파일 person.c의 구조는 다음과 같다.

 - 레코드 파일

    1. 레코드 파일은 헤더 영역과 데이터 영역으로 이루어져있다.

    2. 헤더 영역에는 헤더 레코드가 존재하고 데이터 영역에는 여러 개의 데이터 페이지가 존재한다.

 - 헤더 레코드

    1. 헤더 레코드는 전체 데이터 페이지 수 4바이트, 레코드 파일에 존재하는 모든 레코드 수 4바이트, 마지막 8바이트에는 4바이트씩 삭제 레코드를 관리하기 위해 가장 최근에 삭제된 레코드의 페이지 번호와 레코드 번호를 가지고 있다.      2. 만약 삭제 레코드가 존재하지 않는다면 헤더 레코드의 마지막 8바이트의 값은 모두 -1이다.

    3. 레코드가 삭제되었을 때 헤더 레코드의 페이지 수나 모든 레코드의 수는 수정하지 않는다.

    4. 삭제 레코드는 연결리스트 방식으로 관리된다.

 - 데이터 페이지

    1. 데이터 페이지는 헤더 영역과 데이터 영역으로 나누어져있다.

    2. 헤더 영역에는 페이지에 저장된 레코드의 수, 각 레코드의 주소, 길이(offset, length)가 있으며 데이터 영역에는 실제 저장하려는 레코드가 저장된다.

    3. 레코드의 수, 주소, 길이는 각각 4바이트이며 첫 번째 레코드의 주소는 0이다.

    4. 데이터 페이지의 번호는 0부터 부여한다.

 - 레코드

    1. 레코드는 가변길이 방식으로 저장한다.

    2. 레코드의 필드는 구분자(delimiter)를 이용한다. 이때 구분자로 '#'을 사용한다. 

    3. 새로운 레코드는 파일의 마지막 페이지의 마지막 레코드 다음 위치에 저장되고 공간이 부족할 경우 데이터 페이지를 새로 할당하여 저장한다.

    4. 만약 삭제레코드에 새로운 레코드를 저장할 수 있는 경우 삭제 레코드에 저장한다. 이때 first-fit 방식을 이용한다.

    5. 삭제 레코드에 추가할 경우 연결리스트의 앞과 뒤를 연결해주어야한다.

    6. 삭제 레코드 중에 추가할 수 없는 경우 3번과 같이 추가한다.

    7. 레코드를 삭제할 경우 delete mark로 '*'를 사용하며 delete mark 뒤에 연결리스트에 사용될 다음 삭제 레코드의 페이지 번호와 레코드 번호가 4바이트씩 binary integer 형식으로 저장된다.

    8. internal fragmentation은 고려하지 않는다.

 

레코드는 다음과 같이 이루어져있다.

#define MAX_RECORD_SIZE	100 // including 6 delimeters
#define PAGE_SIZE		256 // 임의로 지정
#define HEADER_AREA_SIZE	(PAGE_SIZE/10)
#define DATA_AREA_SIZE	(PAGE_SIZE - HEADER_AREA_SIZE)

typedef struct _Person
{
	char id[14];		
	char name[18];		
	char age[4];		
	char addr[22];	   
	char phone[16];		
	char email[26];		
} Person;

#endif

위의 6개의 정보로 이루어진 구조체가 하나의 레코드로 저장된다.

완성된 코드는 아래 링크에서 확인할 수 있다.

https://github.com/Seo-sang/variable_length_record

 

Seo-sang/variable_length_record

Contribute to Seo-sang/variable_length_record development by creating an account on GitHub.

github.com

개발 환경은

OS Linux Ubuntu 20.04

gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0

이다.

    이전 글에서 파일에서 레코드 저장하는 고정길이와 가변길이 방식을 설명하였다. 이번 글에서는 저장된 레코드를 삭제했을 때의 관리 방법에 대해 알아보겠다.

    레코드를 삭제한 경우 가장 단순한 방법은 삭제된 레코드가 차지한 공간을 없애는 방법이다. 즉 삭제된 레코드 오른쪽에 있는 모든 데이터들을 이동(shift)시키는 방법이다. 이 방식을 Storage Compaction이라고 한다. 레코드가 삭제되었을 때 표시를 해놓고 그 레코드를 제외하고 다른 파일에 그대로 옮겨서 저장하는 방식이다. 이 방법은 파일 크기를 줄이는 장점이 있지만 파일의 크기가 클 경우 굉장히 많은 비용이 들기 때문에 삭제가 될 때마다 할 수 없다. 그렇기 때문에 얼마나 자주 compaction을 할지는 삭제 레코드의 수나 시간 등을 정해놓고 수행하는 것이 좋다. 다른 파일에 쓰는 경우 out-of-place 방식이지만 시간이 더 걸리고 복잡한 방식으로 in-place 방식으로 같은 파일에 쓸 수 있는 방법도 있다.

    Storage Compaction방식은 현실적으로 어려우니 삭제된 공간을 재사용하는 방법이 이상적이다. 이 경우 고정길이 방식과 가변길이 방식에 큰 차이점이 있다.

    먼저 고정길이 레코드 방식의 경우 레코드가 삭제되었다고 생각해보자. 이 경우 삭제된 레코드에 특수문자인 delete mark를 맨 앞에 써놓아 이 레코드는 삭제되었다는 것을 표시한다. 고정길이 레코드는 모든 레코드의 길이가 동일하기 때문에 어떤 레코드든 삭제된 레코드에 쓸 수 있다. 이때 레코드를 새로 추가할 때 삭제된 레코드의 위치를 빠르게 찾는 것이 중요하다. 가장 좋은 방법으로 링크드리스트 방법이 있다. 삭제된 레코드의 위치를 링크드리스트 방법으로 관리하는 방법이다. 아래의 그림과 같이 표현할 수 있다. 

헤더영역에 삭제된 레코드의 첫번째 주소(RRN : Relative Record Number)를 쓴다. 그 다음 4번레코드에는 2가 있고 2번에는 5번, 5번에는 -1이 들어있다. 링크드리스트의 마지막에는 -1을 넣어 끝임을 알린다. 이렇게 삭제된 레코드의 번호를 링크드리스트 방식으로 다룰 수 있다. 만약 새로 레코드를 추가할 겨우 4번 레코드에 쓰게 되며 헤더의 레코드번호는 그 다음 주소인 2번이 존재하게 된다. 이때 추가로 삭제되는 레코드를 가장 연결리스트의 첫번째 노드에 오게 할 지 맨 마지막에 오게 할 지는 스스로 결졍하면 된다. 이 글에서는 가장 최근에 삭제된 레코드를 헤더에 쓰겠다. 이렇게 고정길이 방식에서의 삭제 레코드 관리를 알아보았다. 그 다음 가변길이 레코드 방식에서의 삭제 레코드 관리방법에 대해 알아보겠다.

    가변길이 레코드 방식은 고정길이보다 더 복잡하다. 고정길이 방식은 레코드의 길이가 고정되어있으므로 어떤 삭제레코드든 삽입할 수 있었다. 하지만 가변길이는 모든 레코드의 길이가 다르므로 레코드의 길이를 판별하는 과정이 추가된다. 또한 레코드의 길이가 다르기 때문에 RRN이 아닌 레코드의 주소인 offset을 사용한다. 따라서 delete mark를 쓰고 레코드의 RRN이 아닌 레코드의 길이와 다음 연결리스트 노드의 주소를 쓴다. 따라서 레코드를 삽입하고자 할 때 처음 연결리스트의 노드부터 길이를 비교하여 추가하는 레코드의 길이가 삭제된 레코드의 길이보다 작거나 같은지 확인하고 맞지 않을 경우 다음 노드의 offset으로 이동하여 비교하는 과정을 거친다. 모든 연결리스트의 레코드 길이가 맞지 않은 경우 맨 마지막에 추가해야한다.

    만약 추가하려는 레코드의 길이가 60이고 삭제된 레코드의 길이가 100이라고 가정하자. 추가 레코드의 길이가 더 작으므로 삭제 레코드에 쓸 수 있다. 쓴 경우 40의 길이가 남게 된다. 이렇게 낭비되는 공간을 internal fragmentation이라고 한다. 이 공간은 다시 재사용할 수 있게된다. 그 이후 길이 35의 레코드를 추가한다고해보자 이 경우 40의 공간을 또 재사용하여 35길이의 레코드를 추가했다. 남은 공간은 5가 된다. 크기가 5인 공간은 또 다른 레코드를 할당하기에 너무 작아 재사용되지 못하고 낭비하게 된다. 이 공간을 external fragmentation이라고 한다. 그렇다면 internal fragmentation과 external fragmentation을 관리하려면 어떻게 해야할까? 삭제레코드를 관리하는 연결리스트에서 레코드를 추가할 때의 순서에 영향을 받는다. 이 방법에 세 가지가 존재한다.

    첫 번째로 first-fit이다. 말 그대로 첫 번째로 발견하는 삭제 레코드를 할당하겠다는 말이다. 만약 40 -> 72 -> 68 -> 57 크기의 연결리스트로 연결된 삭제 레코드가 있다고 하자 이때 추가하고자 하는 레코드의 길이가 55라면 55보다 크거나 같은 가장 첫 번째인 72에 할당한다는 말이다.

    두 번째로 best-fit이 있다. 이 방식은 크기가 가장 가까운 공간에 할당하는 방법이다. 위의 예시에서 55의 길이와 가장 가까운 57에 할당하게 된다. 이 방법은 external fragmentation이 발생할 확률이 늘어나게 된다.

    마지막으로 worst-fit이 있다. 이 방식은 크기의 차이가 가장 많이나는 공간에 할당하는 방법이다. 위의 예시에서 55와 가장 차이가 많이나는 72에 할당한다. 이 방식은 internal fragmentation을 발생시켜 재사용할 수 있는 가능성을 높이는 방법이다.

    지금까지 파일에서 레코드를 삭제할 경우 삭제된 레코드를 관리하는 방법에 대해 알아보았다. 실제에서 적용할 경우 레코드의 특징과 길이, 파일의 크기 등을 고려하여 가장 적절한 방법을 찾아 적용해야할 것이다.

같은 형식을 가진 데이터들을 파일에 저장하고자할 때 어떤 방식을 이용할 수 있을까?

예를들어 사람들의 이름, 나이, 거주지, 직업의 필드를 가진 레코드들을 파일에 저장한다고 해보자.

그렇다면 이 네 가지의 정보가 들어있는 구조체를 선언하여 한 사람마다 저장한 다음 구조체단위로 파일에 써야할 것이다. 그렇다면 아래와 같은 구조체를 만들 수 있을 것이다.

struct Person {
    char name[10];
    char age[4];
    char address[10];
    char job[10];
};

이렇게 만든 구조체에 한 사람의 정보를 담고 파일에 쓴다고 생각해보자. 그렇다면 각 필드의 크기를 모두 합친 10+4+10+10 = 34byte를 하나의 레코드가 차지하는 고정길이필드 방식을 사용할 수 있다. 각 필드를 일렬로 줄세워 파일에 쓰는 것이다. 이러한 방식은 굉장히 쉽고 단순하다는 장점이 있지만 만약 이름이 4byte, 나이가 1byte, 주소가 5byte, 직업이 5byte라고 한다면 낭비되는 공간이 훨씬 많아진다는 치명적인 단점이 존재한다. 그렇기 때문에 우리는 가변길이 필드 방식을 사용할 수 있다. 가변길이 방식에는 3가지 방식이 있다.

    첫 번째로 길이 지시자(length indicator)가 있다. 이 방식은 각 필드 앞에 필드의 길이를 쓰는 것이다. 예를들어 다음과 같이 쓸 수 있다.

5James2255Seoul10Programmer

이렇게 James는 5바이트이므로 앞에 5를, 25살을 뜻하는 25는 2바이트이므로 앞에 2를 쓰는 방법을 이용한다. 하지만 실제 구현하였을 때 앞의 길이지시자를 읽고 그 다음 해당 길이만큼 각 필드를 읽어야하기 때문에 번거롭다는 단점이 있다.

    두 번째로 구분자(Delimiter)가 있다. 이 방식은 필드와 필드 사이에 구분을 지을 수 있는 문자를 넣는 방법이다. 다음과 같이 사용할 수 있다.

James|25|Seoul|Programmer|

이렇게 필드와 필드 사이에 '|' 문자를 넣어 필드를 구분할 수 있게 하는 방법이다. 이 방법은 가변길이기 때문에 그 delimiter를 만나기 전까지 읽어야하므로 길이를 알 수 없다는 단점이 있다.

    세 번째로 Keyword = Value 방식이 있다. 이 방식은 어떤 필드인지를 앞에 써주는 방법이다.

NAME=James|AGE=25|ADDRESS=Seoul|JOB=Programmer|

이 방식을 사용하였을 때 사라진 필드가 어떤 필드인지를 알 수 있는 장점이 있다. 예를 들어 ADDRESS필드가 사라졌다고 생각해보자. 그렇다면 Keyword를 보고 빠진 필드가 무엇인지 알 수 있고 Seoul이 없을 대 직업인 Programmer가 거주지라고 착각할 일이 생기지 않는다. 하지만 이 방법은 Keyword가 너무 많은 공간을 차지하기 때문에 실질적으로 사용이 어렵다.

    지금까지 필드를 구분하는 방법을 알아보았으니 필드로 이루어진 레코드를 구분하는 방법을 알아보자. 레코드 구분 방식에도 고정길이와 가변길이 방식이 있다. 고정길이의 경우 레코드의 길이를 일정하게 지정해놓는 방법이다. 다음은 고정길이 레코드 방식에 고정길이방식 필드와 가변길이방식 필드를 나타낸 것이다. 남은 공간은 '_' 로 나타내었다.

고정길이필드 : James_____25__Seoul_____Programmer

가변길이필드 : James|25|Seoul|Programmer|__________

고정길이 레코드 방식일 경우 위와 같이 나타날 수 있다. 두 방법 모두 낭비되는 공간이 생기게 된다. 이 남은 공간이 생기지 않기 위해서는 가변길이 레코드 방식을 사용하여야한다. 가변길이 레코드 방식은 4가지가 있다.

    첫 번째로 헤더 영역에 메타데이터를 저장하는 것이다. 파일의 맨 첫 부분에 레코드를 구성하는 필드의 수, 수정 날짜, 삭제된 레코드 정보 등을 저장해놓아 레코드를 구분할 수 있는 방식이다. 예를 들어 우리의 구조체는 4개의 필드를 가지고 있으므로 헤더영역에 4를 저장해놓아 하나의 레코드는 4개의 필드로 이루어져 있음을 인지하고 레코드를 읽는 방식이다.

    두 번째로 길이 지시자(length indicator)가 있다. 가변길이 필드 방식에서와 마찬가지로 각 레코드의 맨 앞에 레코드의 길이를 쓰는 방식이다. 아래와 같이 표현할 수 있다.

26James|25|Seoul|Programmer|

다음 예시는 필드 구분을 구분자를 사용하였다. 이 경우 구분자의 byte 수까지 포함하여 앞에 써준다. 이 방식에서는 앞에 26의 숫자를 ASCII 문자인 '2'와'6'으로 쓸지 26을 binary integer방식으로 십진수 26의 ASCII문자를 저장할 지 결정해야한다. 이때 길이지시자로 2바이트를 사용한다고 할때 ASCII문자 방식은 0~99까지의 100개의 숫자만 나타낼 수 있지만 binary integer방식은 2^16의 굉장히 많은 길이를 표현할 수있는 차이점이 있다.

    세 번째로 Index file을 사용하는 방식이 있다. 레코드들이 저장된 데이터파일에 저장하는 것이 아닌 각 레코드의 시작 주소(offset)을 저장하는 index file을 따로 만드는 방식을 사용할 수 있다. 만약 하나의 레코드당 길이를 2바이트를 사용하였다면 1000번째 레코드를 읽고자할 때 offset만 2*1000으로 이동하여 2바이트를 읽으면 되는 방식이다.

    마지막으로 구분자(Delimiter)방식이 있다. 필드 구분 방식과 같이 레코드를 구분할 수 있는 문자를 레코드 사이에 넣어 구분할 수 있는 방식이다. 이 경우 가변길이 필드 방식의 구분자와 같아서는 안되며 1000번째 레코드를 가져오고자 할 때 굉장히 많은 시간이 걸리고 레코드를 읽기에 힘들다는 단점이 있다.

 

    지금까지 고정길이와 가변길이 필드, 레코드 방식에 대하여 알아보았다. 실제 파일을 처리할 때 이전 글에서 말했듯이 디스크 접근 비용을 줄이는 것이 가장 중요하므로 위 방식을 적절히 변형하거나 조합하여 사용하는 것이 중요하다.

    이번에는 저번 파일처리와 파일구조에 대하여 설명한 글에서 설명한 sequential access, simple index 방법에 이어서 BST와 B-Tree(B+Tree)에 대하여 알아보자.

    우선 잘 알려진 BST인 이진탐색 트리에 대하여 설명하자면 한 노드로부터 왼쪽에는 그 노드보다 작은 값, 오른쪽에는 큰 값이 있는 트리를 말한다. 아래의 그림과 같이 삽입 순서대로 노드를 탐색하며 해당 리프노드까지 비교한 다음 추가된다. BST는 평균적으로 O(logN)의 시간 복잡도를 가지며 레코드를 삽입, 삭제가 쉽다는 장점이 있다. 하지만 아래의 오른쪽 그림과 같이 한 쪽 방향으로 치우치며 트리가 형성되는 skew현상이 발생할 수 있다. 이 경우에는 시간 복잡도가 O(N)으로 순차처리와 같게 된다. 

이러한 문제점이 발생하기 때문에 이를 보완한 방법으로 AVL 트리가 나타났다. AVL 트리는 일정한 k값을 정하여 임의의 한 노드로부터 왼쪽 SubTree와 오른쪽 SubTree의 높이가 같게 유지하는 방법이다. 하지만 이 방법을 사용하더라도 파일처리에서 BST를 사용한다는 점은 이전 글에서 말했듯이 Secondary Storage로부터 적은 비용에 많은 정보를 얻어오기에 한계가 있다. 예를 들어 1000번째 있는 레코드를 탐색한다고 생각해보자 이 경우 BST를 사용했을 때 9번~10번의 디스크 접근이 필요하다(2^10이 1024이므로). 약 10번의 디스크 접근도 상당히 부담스러울 수 있다. 이를 보완한 방법으로 B-Tree가 나왔다.

    B-Tree 전에 B+Tree를 설명하겠다. B+Tree는 BST와 달리 한 노드에 하나의 키값이 아니라 여러개의 키값을 가진다. 또한 트리를 balance한 상태로 유지할 수 있기 때문에 skew가 발생하지 않는다. 따라서 모든 리프노드들이 같은 레벨을 가지고 있다. BST와의 또다른 차이점은 트리의 모양이다. BST의 경우 아래 그림의 왼쪽과 같이 좁은 너비에 큰 높이를 가지고 있지만 B+Tree의 경우 오른쪽 그림과 같이 형성되기 때문에 넓은 너비에 낮은 높이를 가지고 있다. 이러한 특징은 파일처리에서 상당히 중요하다. 그만큼 적은 디스크 접근이 발생하기 때문이다. 

아래 그림은 B+Tree 예시이다.

    이 B+Tree에서는 한 노드에 최대로 들어갈 수 있는 key의 개수가 3인 B+Tree이다. 여기서 리프노드가 아닌 non-leaf 노드의 키 왼쪽, 오른쪽에는 다음 노드를 가리키는 포인터가 존재한다. 실제 구현에서의 포인터는 주소이다. 그리고 leaf노드에서는 simple index 방식에서 주소를 가지는 것과 같이 각 key마다 실제 레코드파일의 포인터가 존재한다 위 그림에서는 빨간색 화살표 표시로 표현했다. 그리고 leaf노드는 다음 노드에 대한 포인터를 가진다(파란색 화살표). 이 포인터를 이용해 B+Tree에서는 순차탐색을 할 수 있는 장점이 있다. 또 하나의 특징은 임의의 노드에서 하나의 키값은 그 키 값의 오른쪽 SubTree의 가장 왼쪽 leaf node와 같다. 즉 루트노드인 60의 오른쪽 서브트리에서 가장 왼쪽에 있는 값이 60이라는 말이다. 다음 B-Tree에 대하여 설명하겠다. 아래 그림은 위 그림을 B-Tree로 나타낸 것이다.

    전체적인 구조는 B+Tree와 동일하다. 하지만 큰 차이점들이 있다. 우선 리프노드들이 서로 링크드리스트로 이어져있지 않다. 즉 순차탐색이 불가능하다는 단점이 있다. 하지만 B-Tree의 장점도 있다. 각 노드에서 레코드 파일에 대한 포인터가 존재한다(빨간 화살표). 즉 B+Tree와 달리 B-Tree는 리프노드까지 탐색하지 않고도 레코드파일의 주소를 알 수 있다. 또 하나의 차이점은 B+Tree와 달리 트리에 중복된 키가 존재하지 않는다. B+Tree에서는 임의의 노드에서 키값과 그 키값의 오른쪽 서브트리의 가장 왼쪽 키값이 같다는 특징이 있었다. 즉 중복된 키가 트리에 존재한다는 것이다. 하지만 B-Tree에서는 각 노드에서 곧바로 포인터를 가지기 때문에 중복된 키를 가지지 않게 된다.

    우선 File에 대하여 설명하자면 파일이란 secondary memory(HDD, SSD, CD, tape 등)에 저장된 같은 형식을 가진 record들의 집합이다. 그렇다면 File Structure는 무엇인가? 파일 구조란 File Structure는 데이터를 표현하는 representation과 그 데이터를 접근하는 operation의 조합이다. 여기서 representation의 예시로는 C언어의 구조체가 있고 operation은 삽입(insert), 삭제(delete), 검색(search) 등이 있다. 우리가 좋은 파일 구조를 만들기 위해서는 데이터로부터 정보를 얻을 때 가장 적은 비용이 들어야한다. 파일처리에서 가장 많이 드는 비용은 디스크 접근이다. 그렇기 때문에 좋은 File Structure를 만들기 위해서는 Disk I/O를 줄여야한다. 앞의 내용을 요약해보면 따라서 파일처리란 파일에 접근하여 직접 구현한 연산들로 데이터를 다루는 것이라고 할 수 있다.

    우리가 앞서 말한 Disk I/O를 줄이기 위해서는 어떻게 해야할까? 얘를 들어 하드디스크(HDD)에 저장된 100명의 사람들 중에 홍길동씨의 정보를 찾는다고 생각해보자. 인간이 아니라 컴퓨터가 홍길동씨를 찾기 위해서는 가장 쉬운 방법으로 첫 번째 사람부터 차례대로 비교해보는 것이다. 이런 방식을 썼을 때 만약 홍길동씨가 77번째 record에 존재한다면 컴퓨터는 77번의 비교를 해야 찾을 수 있을 것이다. 이러한 방식을 Sequential access 순차접근이라고 한다. 그렇다면 빠른 방법은 어떤 것이 있을까? 우선 Simple index 방식을 알아보자 아래 그림과 같이 HDD에 저장된 모든 record파일을 읽어오기엔 파일이 너무 클 수가 있다. 따라서 index file을 따로 만들어 그림 왼쪽과 같이 이름과 record의 주소만 정렬하여 저장한다. 이 index file은 크기가 작기 때문에 main memory인 RAM에 모두 올릴 수 있다. 그 다음 RAM에서 이진탐색과 같은 방식으로 홍길동을 찾아 HDD의 해당 위치부터 record를 읽어오면된다.

따라서 순차처리의 경우 record파일을 한 줄씩 읽어온다면 총 3번의 Disk I/O가 발생하지만 Simple index 방식의 경우 index file을 읽어오는 비용 1번과 알아낸 주소로 접근하여 홍길동의 record를 읽어오는 1번으로 총 2번의 Disk I/O가 발생한다. 위 그림의 경우는 작은 크기이기 때문에 큰 차이가 있지 않지만 만약 record의 수가 굉장히 많은 경우 그 비용의 차이가 굉장히 커질 것이다.

   이렇게 simple index 방식은 Random access가 가능하기 때문에 시간적인 비용이 줄어든다. 하지만 record가 추가될 경우 index file도 같이 수정해야하는 번거로움이 존재한다. 이 외에도 Binary Search Tree, B-Tree, B+Tree 등 많은 방식이 존재하지만 다른 글에서 설명하도록 하겠다.

 

Flash memory의 Software의 구성 방식은 다음과 같이 계층적 구조로 이루어져 있다.

 

우리가 사용하는 Application에서 발생하는 read, write와 같은 연산들은 우리에게는 보이지 않지만 아래의 layer에 의해 투명하게 처리된다.

    우선 Flash File System은 대표적으로 FAT(file allocation table) file system이 있다. 이 계층에서는 application layer에서 호출한 read, write 연산을 수행하는 일반적인 파일 시스템이다.

    FTL(Flash Translation layer)는 우리가 사용하는 NAND 플래시 메모리를 일반 하드디스크와 같은 블록장치와 같이 행동하기 위해 논리주소를 물리주소로 mapping하는 기능을 수행한다. 이때 논리주소와 물리주소를 mapping되어 있는mapping table을 사용한다. Flash memory는 덮어쓰는 overwrite 기능을 제공하지 않기 때문에 out-of-place update를 사용한다. 따라서 block mapping table에는 항상 overwrite를 할 때 사용하는 free block을 두어 out-of-place update이 이루어질 때 사용한다. 또 하나의 block에 많은 erase, write가 이루어지는 bad block이 발생하지 않도록 하는 wear-leveling 기능도 제공한다.

    Flash Device Driver는 직접 NAND 플래시 메모리에 물리적으로 접근하여 데이터를 읽고 쓰는 역할을 수행하는 계층이다. 또 bad block와 ECC(Error Correction Code)를 처리한다.

    위와 달리 Flash File system 계층과 FTL 계층을 하나의 계층인 Dedicated Flash File System도 존재한다. 그러한 구조는 플래시 메모리 전용으로 설계된 파일 시스템에서 사용된다.

    FTL에서 논리주소와 물리주소를 mapping하는 방법에는 여러가지가 있다. 대표적으로 sector mapping, block mapping, hybrid mapping이 존재한다. 글쓴이가 구현한 방식은 block mapping 기법을 사용하였다. 이 세가지 기법을 간단히 설명하자면 sector mapping은 lsn(logical sector number)와 psn(physical sector number)가 1대1로 mapping되어 있는 방법이다. 이 경우 mapping table이 굉장히 커질 수 있는 단점이 존재한다. block mapping table 기법은 lsn을 블록당 페이지의 수로 나눈 몫을 block번호로 사용하고 나머지를 page번호로 사용하는 방법이다. 예를 들어 lsn이 9이고 한 블록당 page의 수가 4개인 경우 9/4 = 2가 블록 번호(pbn), 9%4 = 1이 블록의 page번호(ppn)인 것이다. 이 경우에 한 블록에 4개의 page가 존재하므로 sector mapping 방식에 비해 mapping table이 1/4로 줄일 수 있는 이점이 있다. 마지막으로 hybrid mapping 방식은 block mapping과 굉장히 유사하지만 나머지인 offset을 ppn으로 사용하는 것이 아닌 pbn의 비어있는 곳에 순서대로 할당한 후 page의 spare영역에 메타데이터인 lsn을 저장하여 어디서 저장된 데이터인지 알 수 있게한다.

    아래 링크는 C언어를 활용해 구현한 FTL의 block mapping table이다.

https://github.com/Seo-sang/file_processing_FTL

+ Recent posts