Process

실행 중인 프로그램.

Stak, heap, data, code를 포함

프로그램 != 프로세스 (실행 파일이 메모리에 적재될 때 프로그램은 프로세스가 됨)

Process state

New : 새로운 프로세스 생성 (생성 된 것이 아님)

Ready : 프로세서에 할당되기를 기다리는 중

Running : 명령이 실행되고 있는 중

Waiting : event 발생 대기 중

Terminated : 실행 종료

PCB (Process Control Block)

Process는 OS에 의해 PCB로 관리됨

 

Process state

Program counter : next instruction의 메모리 주소 위치

CPU registers : 아키텍처에 따라 저장되어야 할 register 상이

CPU-scheduling information 

Memory-management information  : limit register, pagle table, segment table...

Accounting information : real time used, time limits, account numbers, job or process numbers...

I/O status information : list of I/O devices

Process scheduling queues

1. 프로세스가 실행되면 ready queue에 삽입됨

2. CPU를 할당 받은 후 job 수행

Process scheduler

Long-term scheduler

Short-term scheduler

mid-term scheduler

 

I/O-bound process vs. CPU-bound process

Context swtich

PCB 형태로 관리되며, interrupt 처리 후 context 복구가 필요함

Context switch 동안 다른 처리를 할 수 없기 때문에 빈번하게 발생할 경우 overhead 증가

Process creation

parent/child process - tree 구조로 유지

PID (Process ID)를 통해 구분

Create: 시스템 콜의 fork(). parent process는 자신과 똑같은 자식 프로세스를 생성

child process는 exec()를 통해 내용을 모두 바꿀 수 있음.

fork() 함수는 parent process에겐 child process pid, child process에겐 0을 반환

Linux 기준 pid 0 는 ide process라고 함 (참고: m.blog.naver.com/cksdud1113/222076376311)

root@khy-H81M-DS2V:~# ps -aef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  2 03:08 ?        00:00:00 /sbin/init splash
root         2     0  0 03:08 ?        00:00:00 [kthreadd]
root         3     2  0 03:08 ?        00:00:00 [kworker/0:0]
root         4     2  0 03:08 ?        00:00:00 [kworker/0:0H]
root         5     2  0 03:08 ?        00:00:00 [kworker/u8:0]
root         6     2  0 03:08 ?        00:00:00 [mm_percpu_wq]
root         7     2  0 03:08 ?        00:00:00 [ksoftirqd/0]
root         8     2  0 03:08 ?        00:00:00 [rcu_sched]
root         9     2  0 03:08 ?        00:00:00 [rcu_bh]

Process termination

exit()를 통해 child process 종료

Interprocess communication

process가 cooperation을 하는 이유

1. information sharing

2. computation speedup

3. modularity

Cooperation을 하기 위해 IPC (Inter-Process Commuication)이 필요함

1. Shared memory

2. Message passing

Shared memory

shared memory 영역은 shared memory segment를 생성하는 process 공간에 존재

두개 이상의 프로세스가 동시에 동작할 수 있어 해당 메모리 영역의 접근 및 프로세싱에 대해 추가 처리가 필요 함

Message-passing systems

메모리 영역 공유 필요 없이 send/receive를 통해 명시적으로 통신을 수행

 

 

질문

1. I/O-bound process vs. CPU-bound process 별 차이점은 무엇이고 주로 어떤 application에 의해 발생하는지. 각각에 더 적합한 scheduling은?

2. long, short, mid - term scheduler라는 표현을 사용하는지? 무엇을 기준으로 기간을 구분하는지

3. context swtich 수행시 PCB는 어디에 저장되는 것인가 (메모리로 알고 있는데 메모리의 stack?) 안전한가 

- 유닉스 계열에서 모든 객체들(정규파일, 디렉터리, 소켓, 파이프 등등)은 모두 '파일'로써 관리가 됨.

 

- 시스템에서 이 파일들을 접근할 때, 파일 디스크립터를 이용

 

- 프로세스가 실행 중에 파일을 오픈하면 커널은 해당 프로세스의 파일 디스크립터 숫자 중에 사용하지 않는 가장 작은 값, fd값을 할당

 

이후 프로세스가 열려있는 파일에 시스템 콜을 이용해 접근할 때, fd값을 이용하여 파일을 지칭.

 

fd값에는 기본적으로 할당되는 파일 디스크립터가 존재

표준 입력 : STDIN_FILENO -> 0

표준 출력 : STDOUT_FILENO -> 1

표준 에러 : STDERR_FILENO -> 2

<unistd.h>에서 파악할 수 있음

 

- 리눅스에서는 프로세스마다 파일 디스크립터 테이블을 가지고 있음.

  파일 디스크립터 테이블은 open등에서 사용한 flags와 파일 테이블 요소의 위치정보를 포함함.

 

-> 파일 테이블 : 커널에서 모든 열려진 파일들을 관리하는 테이블.

 파일 테이블의 항목 엔트리에는 파일의 상태 flags와 현재 파일의 작업 offset과 파일의 vnode 테이블의 위치 정보등을 포함함.

-> vnode 테이블 : inode 정보와 현재 파일의 크기를 포함함.

 

 

   (셸에서 시작한 프로세스는 1번 프로세스 이자 모든 프로세스의 부모 프로세스인 "init 프로세스" 에서 fork()를 통해 파일 테이블을 복사)

 

1) 하나의 프로세스에서 같은 파일을 두 번 연 경우

서로 다른 파일 디스크립터 부여 / 서로 다른 파일 오프셋을 유지

(즉, 프로세스에서 파일 입출력은 open 함수로 연 작업을 구분. => 실제 물리적인 파일이 같은지는 구분 하지 않는다. / but, 같은 v-node를 참조)

2) 두 개의 프로세스에서 같은 파일을 연 경우

 

root@ibof-Z10PE-D16-WS:~# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 15080
max locked memory       (kbytes, -l) 16384
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 15080
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
#include <stdio.h>
#include <stdio.h>
#include <stdio.h>         // printf()
#include <string.h>        // strlen()
#include <fcntl.h>         // O_WRONLY
#include <unistd.h>        // write(), close()
#include <sys/types.h>

int main(void)
{
        for(int i=0; i< 1026; ++i)
        {
                int fd = open("./hello.txt",O_RDONLY);
                printf("%d\n", fd);

                if (fd == -1)
                {
                        printf("error\n");
                }
        }

}
sudo ls -trn /proc/[PID]/fd

http://everylwn.blogspot.kr/search?q=The+block+I/O+layer

http://ari-ava.blogspot.kr/2014/06/opw-linux-block-io-layer-part-1-base.html

 

Base concepts


지난 몇주동안 Linux kernel의 block I/O layer가 어떻게 동작하는지 명확하게 이해하기 위해 연구하고 분석했다. Modena and Regigio Emilia 대학교의 한 동료 학생이 내가 blog 게시물을 쓸것이라고 생각했다.  OPW mentor가 topic을 승인했고 block 게시물을 몇개의 part로 나누어서  지루 한번에 지루해 지지 않고 더 편안하게 읽을 수 있도록 하는것을 제안했다. Block I/O layer에 구현된  메커니즘의 기본 개념과 아이디어를 설명하는 Part one 이다 : 매우 high-level 이고 간결하다. 다음 Parts(구현 세부사항에 대해 좀더 자세하게 설명할것이다.)는 block layer가 노출하는 다양한 API들이 그러한 메커니즘을 사용하는 방법과, 잘하면 각 왜 그것들이 각 APIs들에 대해 유용한지(or 그렇지 않은지) 간략하게 설명할 것이다. 꽤 어려운 목표인것 같다(적어도 나와 같은 초급자에게). 만약 비판할게 있으면, 코멘트를 남겨라.

 

block I/O layer는 block devices에서 수행되는 input/output operations을 관리하는 kernel subsystem이다. 그러한 operations을 관리하기 위한 specific kernel component에 대한 필요성은, 예를들어, character devices에 대해 block device의 추가적인 복잡성에 의해 발생한다. 

character device(우리가 사용하고 있는 keyboard)에 대해 생각했을때,  오직 한방향만을 따라 움직이는 cursor가 있는 stream에 대해 생각한다. 다른말로, steream에 보관된 정보는 오직 하나의 순서로만 읽을수 있으며 일반적으로 특정 위치 or 특정 문자를 발견할때까지 읽는다. (EOF, the end-of-file character). block device는 다르다. 한 번 이상의 seeks후에, 어느 순서나 어느 위치에서든 data를 읽을 수 있다. I/O 자체를 처리하는것 보다 성능을 보장하기 위해서 block device에서 읽는 동안에 특별한 주의가 필요하다.

 

사실, block I/O device들은 storage device로 주로 사용되고, 가능한 그것들의 잠재력을 최대한 활용하는 것이 중요하다. Rovert Love가 그의 유명한 책에서 언급한 것처럼, block layer의 개발은 Linux kernel의 2.5 개발 cycle의 주요 목적이 된 중요한 목표였다. 

 

그것을 black box로 보았을때, block layer는 Linux kernel의 2개의 다른 관련된  subsystem과 상호 작용을 하는것을 발견했다. mapping layer and  실제 I/O가 발생하는 block device의  device driver. Mapping layer가 무엇이고 block layer아 어떻게 상호작용하는지 이해 하기위해 block I/O request가 실제로 어떻게 도달하는지 간략하게 윤곽을 설명해야 한다.

 

Figure 1:  The block I/O hierarchy of the Linux kernel

 

process는 일정 수의 bytes의 read() or write() operation을 사용하여 disk에 접근한다.  sys_read() and sys_write() system call은, library 대신 호출 되고,  먼저 Virtual File System을 활성화는, 2개의 다른 측면을 처리한다. 

파일을 opening, closing or linking할때, 상대 or 절대 경로에서 시작해서  data를 가지는 the disk and file system을 찾는다. 이미 열려진 file을 읽거나 쓸때, data가 이미 memory에 mapped되어 있는지 확인한다(즉, kernel’s page cache에 존재하는지) 만약 그렇지 않다면, 어떻게 data가 disk로 부터 읽는 방법을 결정한다. 

VFS가 data가 이미 memory에 mapped 되지 있지 않다는것을 알게 되면, physically하게 data를 찾는 kernel subsystem을 활성화 한다. mapping layer는 처음에는 data에 해당하는 file system block의 크기를 찾은 다음 전송될 disk blocks의  수로 data의 크기를 계산한다. 

이 단계에서, kernel은 또한 data를 보관하는 block numbers를 찾는다. file이 물리적으로 연속하지 않은 block에 보관 될 수 있기 때문에, file system은 logical blocks 와 physical blocks사이에서의 mapping을  추적한다. 그래서 mapping layer는 file descriptor에 접근하고 the logical-to-physical mapping을 함게 처리하는 mapping 함수를 호출해야 하므로 결국에는, file을 보관하는 실제 disk blocks의 위치를(disk의 or disk’s file system의 paritition으로 부터) 얻는다.

 

memory에 mapped된 block의 position이 이용 가능해지면, kernel은 disk device로 전달할 I/O requests를 만드는것을 시작한다.  

block I/O layer는 block I/O 구조(bio)의 리스트를 생성하는데, 각 구조는 disk에 제출되는 I/O 동작을 나타낸다. 그러한 구조는 계층의 상위레벨에게 block deivce의 특정 기능을 숨기고 장치의 추상적인 개념을 제공한다. small I/O 동작의 가장 단순한 경우, single bio가 생성되지만 보다 복잡한 경우에(larget-sized I/Os, vector I/O) block layer는 한번에 더 많은 I/O 동작을  시작할 수 있다.  결과적으로,  block layer는 I/O operation을 특정 sector에 대한 request로 변환하여 disk controller에게 전송한다.  보통 각 request는 하나 이상의 bio들을 포함한다. 이것은 per-structure processing overhead를 줄이는데 도움이 된다. 

 

이 requests들은 각각 request structure에 의해 표현된다. device driver에 대한 그들의 발송(dispatch)은 block layer의 I/O scheduling sub-component에서 구현된 service policies에 의해 제어 될 수있다. 

 

 

Figure 2: Interaction between bio and request data structures(from R. Love's "Linux Kernel Development", 3rd Edition)

마침내, block layer는 실제  data 전송을 처리하는 device driver에게 I/O request를 보낸다.

device driver는 일반적으로 사용가능한 request의 list를 제공받거나 or 일부 shared data 구조체에 request를 넣기 위한 callback을 제공한다. device driver는 interrupt가 disk에서 발생되자마자 or request service가 계층 구조의 상위레벨에서 요청되는 즉시 interface-specific command를 보낸다.

 

block layer 와 device driver사이의 상호 작용은 사용되는 block layer API에 크게 의존한다. 그리고 훨씬 많은 세부 사항을 다음 post에서 논의할것이다. 

 

I/O handling techniques

질문이 여기에서 자연스럽게 생긴다.  request가 block layer에 의해  block device driver에게 단순히 forwarded되나? 실제로 no. 만약 실제로 그렇다면 block I/O layer의 존재는 무의미하다.  대신, block layer는 I/O 개수를 제어하는 기술을 구현한다. 그것들 중 몇몇은 매우 간단하고 몇몇은 매우복잡하며 일반적인 목적은 성능 향상이다. I/O scheduling과 같은 구현된 기술중 일부는 block layer에 의해 제공되는 API들의 일부분이므로 여기서 설명하지 않는다. 그러나 일부 API는 사용된  API와 관련 없이 이익을 제공하므로 모든 사용 가능한 interface에 공통이다.

 

Merge and coalesce I/O operations

block I/O layer가  block I/O operation에서 수행하는 첫번째 동작은 이미 삽입된 구조체에 그것을 merge하는것이다. 이것은  block layer’s 내부 데이터 구조체에서 일부 근접한 requests를 찾는것으로 달성된다. 만약 적어도 하나의 인접한 request가 발견되면, 새롭게 발행된 block I/O이 기존의것에 통합된다. 기존의 operation이 새로운것에 merge될때( 새로운 것이 기존의 것 앞에 섹터에 대해 발행된것 이기 때문에);  merge는 front merge라 불린다. 그렇지 않으면, 새로운 operation이 기존것에 merge된다면(기존의 것 다음의 섹터에 대해 발생된것이기 때문이다), merge는 back merge라 불린다.

2개의 가장 흔한 경우는 후자이다. I/O 가 순차적인 방식으로 수행되는것이 가장 흔하게 발생하기 때문이다.

대신, 새로운 operation이 2개의 이미 존재하는 구조체 사이의 틈을 채울때(“fill a gap”), block layer는 3개의 동작을 병합한다(coalesce).

Figure 3:  Merge and coalesce operations


Plugging

block I/O layer는 request 가 device driver에세 전달되는 rate를 조정하는 plugging이라는 메커니즘이 포함되어 있다. 만약  request 가  block layer가 load or stress로 간주 되지 않는 (pending requests의 수가 적은) block device에 queue 되면, driver에게 newly-issued operation의 dispatch는 지연된다(devices는 plugged된다고 불린다.) 이것은 block layer가 많은 수의 merges를 수행하도록 한다, 만약 더 많은 I/O가 밀접하게(시간과 공간상) 발행되면 block layer가 더 많은 수의 merge를 수행할 수 있다.

 

일정 시간이 지나거나 또는 device에 대해 outstanding I/O의 수가 고정된 threshold를 초과하면, device는 unplugged 되고, requests는 실제로 device driver에 dispatch된다. 

 

디스크에서 I/O request를 처리하는 속도는 request의 크기에 영향을 받지만, 개수에도 영향을 받는다. 

다시 말해서, 8KB를 한 번 읽는것보다 4KB를 두 번 읽는 것이 훨씬 느리다. 실제로 디스크가 platter에서 정보를 읽어내는 시간에 비해, 그 외 준비 하는 시간이 크기 때문이다.

디스크 상에서 연속적인 request를 두 개 처리하는 것보다 하나로 merge된 request를 처리하는 것이 빠르기 때문에 

request queue를 막아서 dispatch을 멈춘 상태 (plugged)를 만들어서 최대한 request를 merge하고, dispatch를 멈추지 않는 상태 (unplugged)를 만들어서 dispatch를 하는 것이  block layer 성능 향상 기법인 'plugging' 이다.

(즉, dispatch 횟수를 줄이기 위해서 device의 request가 적은 상태일 때, 최대한 많은 수의 merge를 수행한 뒤에 dispatch 하는 것.)

 

 

처음에, plugging은 system-wide에서 수행되었다. 이 idea는 효율적이라고 증명되었지만 plugging이 per-device로 수행되면 효과적이지 않다. 이것은 점점 증가하는 SMP 머신에서 lock  경쟁을 완화하는데 도움이 될것이다.

 

2.6.39부터, plugging은 per-device로 옮겨졌다. Lock contention은 여전히 문제였다. 특히 많은 프로세스들이 동일한 block device에 많은 수의 작은 request를 발생하는 경우와 끊임없이 block I/O operation의 merge가 발생 할때마다  block layer’s per-device 자료 구조체에 접근하는 경우이다.(Hitting on) 3.0 부터 plugging은 per-process로 옮겨졌다. 이것은 block I/O operations이 많은 경우에 lockless 방식으로 merged and coalesced 되는것을 허용한다. 실제로 lock 경쟁에서 이 디자인의 선택의 영향의 범위는 사용되는 API에 의존한다. 

 

2.6.34의 plugging 구현을 종합하자면 다음과 같다.
* request queue가 비어있을 때 request가 들어오면 plug한다.
* plug하고 3ms가 지나면 자동으로 unplug한다.
* 3ms내에 request가 4개 이상 들어와도 unplug한다.

 

2.6.34의 구현에는 다소 문제가 있다. 다음 request가 3ms안에 들어오지 않을 수도 있다는 것이다. Synchronous request하나만 보내놓고 응답을 기다리는 경우, 결국 3ms timer가 expire될 것이다. 그렇다면 괜히 반응성만 3ms나빠지게 되는 것이다.
하지만 block layer에서는 다음 request가 언제 들어올지 정보가 전혀 없다. 따라서 어쩔 수 없이 기다리는 것이다.

 

앞서 Linux 2.6.34에서 plug구현의 문제점에 대해 살펴봤다. Linux 3.0에서는 plug의 구현을 수정하고, plug를 파일시스템 레벨에서 이용하게 함으로써 문제를 해결했다. 결국 request를 생성하는 것은 파일시스템인데, 파일시스템에서는 request가 산발적으로 생성되는 것이 아니라, 뭉텅이 지어서(clustered) 생성되기 때문이다.
Linux 3.0에서는 plug를 사용하는 패턴은 다음과 같이 수정됐다.

 

* plug 상태를 request queue가 아니라 task(thread)에 체크한다.

* plugged 상태가 되면, 그때부터 발생하는 request를 쌓아두었다가 unplugged 상태가 되면, 한번에 request queue에 삽입한다.

 

즉, 정리하자면 2.6에서는 'request queue'에 request가 들어왔을 때, plugging을 했다. 3.0에서는 'request queue'에 request를 삽입하기 전, 프로세스(파일시스템)에서 plugging을 한다. 

 

 

Request sorting

일부 유형의 block device는 가능한 순차적인 방식으로 request를  보낼때 큰 이익을 얻는다.  아마도 쉽게 추측할 수 있게지만, 가장 많이 이익을 얻는 경우는 data에 대한 접근 시간이 몇몇 mechanical(기계적인) 움직임에 의존하는 디바이스에서 이다. (rotational hard drivers). Sector에 접근할때 rotational disk는 sector가 head에 접근 될수 있도록 데이터가 보관된 disk plate를(rotational latency) 움직여야 하며  특정 track (disk head seek)에 head가 위치하여야 한다.



만약 requests가 랜덤하게 발행되면, sectors를 검색하기 위해 필요한 seeks의 수가 증가해서 순차적으로 발행된 동작과 관련하여  overhead가 증가한다.

 

block layer는 일반적으로 가능한 한 많은 request를 reorder해서 device driver and disk에게 순차적인 방식으로 제출한다. Plugging은 block layers는 request를 제출하기전에 내부적으로  re-order하게 해준다.

 

Figure 4:  Internals of a hard disk (from learn44.com, because I can't draw for my life)


그러나 request sorting은 data transfer time이  오직 request의 사이즈에만 의존하는 SSD와 같은 몇몇 종류의 disk에서는 유용하지 않다. 몇몇 solid state disk들은 실제로 내부적으로 병렬로 처리해서 더 좋은 성능을 얻을 수 있기 때문에 더 많은 작은 requests를 갖는것에서 이익을 얻는다. 따라이  이 테크닉은 그것이 엄격하게 필요한 경우에만 사용되도록 I/O policies에게 남겨지며  중요하지 않거나 or 성능에  불필요한 영향을 주는 overhead를 야기하는 몇몇 경우에는 직접 skipped된다.

 

The request interface


 

나의 OPW mentor가 제시한  많은 지침과 제안을 따르면서, 나는 매우 작고 간단한 block I/O driver를 구현했다. 그 driver의 multi-queue 구현은 process가 발행된 CPU core에 대해 사용 가능한 정보로 단지 I/O request 구조를 채우기만한다. 이것은 내가 개발중인 multi-queue-capable prototype의 block I/O를 테스트 하는데 매우 유용하다는것을 증명하고 있다.

이번 임무를 통해 내가 Linux kernel의 block layer가 제공하는 다양한 APIs들을 깊이 있게 더 공부할 수 있는 기회를 주었다. 그리고 그것에 대해 블로그 기사 시리즈를 계속 쓸수 있는 기회를 주었다.

 

 

 

request interface는 block layer가 device driver에게 requests를 전달할 수 있게 하는 essential data 구조를 기반으로 한다 : requet_queue 구조체;  그러한 구조체는 2개의 주요 buffering 기능을 가진다. 그것은 request가 overheads를 줄이기 위해 pre-processed되는것을 허용하고(merging and sorting) submission rate를 제한하여 hardware buffer가 over-running하는것을 방지한다. request_queue 구조체는 device별로 할당되므로 device에는 전용 request_queue가 있으며 이것은 disk에 사용중인 device-specific block I/O driver에 의해서만 접근이 가능하다. 그러한 구조는 본질적으로 block I/O request의 container이다. 실제로 더 중요한 항목은 driver가 사용할 수 있게 된 requests의  link list이다(head는 queue_head 항목에 유지된다.)

그것은 또한 제출을 위해 처리되어야 하는 bio를 제출을 위해 사용되는 function의 포인터 집합을 유지하거나  device에 request를 보내는데 사용되는 함수에 대한 포인터의 집합을 유지하거나 device에게 제출을 위해 처리 할 request를 전달한다. request mode에서 block layer는  I/O scheduling도 허용한다. 그래서 request_queue 구조체는 elevator_queue 구조체를 가리키고  elevator_queue 구조체는 elevator hooks에 대한 포인터를 가진다.

 

마지막으로  request_queue 구조는 spin_lock(queue_block)에 의해 보호된다.

Figure 1 : The block layer's layout when in  request  mode


앞으로 나아가고 각각의 black boxes안에 무엇이 있는지 알고 싶다면, 좋은 출발점은 trace 이다. 그러나
 interrupt를 실행하고 우리가 직접적으로 trace를 얻는것을 허용하지 않는 전체 driver-specific and low-level-driver-specific 함수의 방해 없이 명확하게 어떻게 request를  trace할 수 있는가? 우리는 가장 명확하게 가능한 trace를 얻기 위해 몇몇 ftrace tricks를 활용할 수 있을것이다. 나는 개인적으로 후자를 좋아한다. 그것은 Linux kernel은 module로 load할때 구성 가능한 수의 fake block devices(module의 nr_devices options을 참조)를 만들고, 선택한 block layer API(queue_mode 옵션으로 제공)을 사용하여 module을 구성한다. 






이 driver의 다른 멋진 feature는, 기본 설정일때, dispatches and completion을 포함한 모든것이 request 삽입의 context에서 발생하는 것이다. 

PID로 간단하게 trace할수 있을 만큼 완벽하다.

나는 reuquest interface를 사용하여 4KB read request 제출한 trace를 수집했으며 업로드하자 마자 이용가능하게 할것이다.

 

 

그러나 첫번째 흥미로운 사실은 device의 초기화할때 발생하기 때문에  trace에 의해 보여지지 않는다. 새로운 device를 초기화 할때, driver가 loaded되는 동안에, 특정 device에 대해 block layer에서 사용할 interface를 선택한다. 

인터페이스는 일반적으로 bulk_queue_make_request() helper 함수로 정의된 make_request_fn를 지정하는것으로 선택된다. 

block layer가 request mode일때 사용되는 기본 make_request 함수는 blk_queue_bio()이다.

block layer에게 bio를 제출하는 함수를 알게 되었고 trace에서 그것을 쉽게 발견할 수 있었으며, submit_bio()에 호출 되는 generic_make_request()에 의해 호출된다.

후자의 function은(submit_bio()) block layer에 대한 일종의 entry point이며 device에게 처리,제출되는 block I/O unit를 제출하기 위해 block I/O stack의 상위 수준에서 사용된다.

 

submit_bio() -> generic_make_request() 

 

그러나 위에 언급한 것들에서 가장 흥미로운 함수는 blk_queue_bio()이다. 그것은 Linux block layers request mode의 core routine를 구현한다. flowchart를 준비했지만 그것은 너무나 길어 질것이므로  다음의 paragraph에서 수행되는 핵심이 되는 부분만 간단히 설명한다. 여기서 일부분 생략한다. 독자들이 읽기에 지루한 것은 뺐다. 만약 전체 함수를 알고 싶다면, Linux kernel source tree의 block/blk-core.c의 1547부분에서 찾을 수 있다.


Generic block layer: the blk_queue_bio() function

어떤 lock을 획득하기 전에 수행되는 첫 번째 단계는, 새롭게 제출 된 block I/O unit를  taskss plug list에 있는 requests와 merge를 시도하는 것이다. request mode일때, plugging은 실제로 per-process로 수행된다


        if (blk_attempt_plug_merge(q, bio, &request_count))
                return;

blk_attempt_plug_merge() 함수는 bio 구조체를 현재 issuers(발행자의) plug list와 merge하려고 한다. 기본 merging paramters(blk_try_merge()의 실행으로 수집된)만을 체크하여 elevator와 어떤 상호작용을 피할수 있으므로 locking이 수행될 필요가 없다.

 

계속해서 block I/O unit은 per-device structure가 포함되므로 blk_queue_bio()함수는 결국 queue_lock을 획득해야 한다.

 

      spin_lock_irq(q->queue_lock);

       el_ret = elv_merge(q, &req, bio);
       if (el_ret == ELEVATOR_BACK_MERGE) {
               if (bio_attempt_back_merge(q, req, bio)) {
                       elv_bio_merged(q, req, bio);
                       if (!attempt_back_merge(q, req))
                               elv_merged_request(q, req, el_ret);
                       goto out_unlock;
               }
       } else if (el_ret == ELEVATOR_FRONT_MERGE) {
               if (bio_attempt_front_merge(q, req, bio)) {
                       elv_bio_merged(q, req, bio);
                       if (!attempt_front_merge(q, req))
                               elv_merged_request(q, req, el_ret);
                       goto out_unlock;
               }
       }

 

elv_merge()  함수는 이미 evelator에 queued된 request와 bio 구조체를 merge하기 위한 시도와 관련된 모든 동작들을 처리한다. 

이를 위해, block layer는 이 동작을 더 빠르게 수행할 수 있도록  몇몇 private 항목들을 유지한다.

a) 성공적으로 merge에 관여한 last request의 one-hit cache.  

b) elevator에서 현재 dispatch를 기다리고 있는 private list of requests.

 

(a)가 가장 유용하다. 만약  merge가  one-hit cache와 성공할 경우, list를 통한 동작은 필요하지 않다.

아마도 (b) elevator에서  검색을 elevator 자체에 위임 할 경우 여러 multiple search level을 암시하는 다른 service queue에 분산될수 있어서 검색 속도가 빨라 질수 있다.

 

elv_merge()함수는 bio와 request가 merge될수 있는지 확인하기 위해 elevator hooks중 하나를(elevator_allow_merge_fn) 호출하는것과 관련된다. 그것은 bio와 request가 수행할 merge의 종류를 나타내는 값을 반환한다.

 

merge가 성공한 경우, device는 즉시 unlocked되고 함수는 반환한다.

 


        req = get_request(q, rw_flags, bio, GFP_NOIO);
        if (unlikely(!req)) {
                bio_endio(bio, -ENODEV);        /* @q is dead */
                goto out_unlock;
        } 

 

 

block layer는 각 device에 대해 이미 할당된  request 구조의 pool을 유지한다. 

할당된  requests의 개수는 특별한 sysfs file인 /sys/block/<device>/queue/nr_requests를 읽어서 확인 할 수 있으며 

device에 대한 In-flight requests의 개수는 /sys/block/<device>/inflight에서 확인 할 수 있다.



모든 bio의 merge가 실패한 경우, blk_queueu_bio()는 pool에서 free request를 얻기 위해 get_request()를 실행한다. 그것이 실패한 경우, 후자에 의해 호출되는 __get_request() 함수는 request starvation logic을 활성화 하여 모든 application에 대해 모든 I/O request가 blocking되게 한다(심지어 write request조차). 대신, 만약 free request 구조체가 정확하게 검색되면, __get_request()는 elevator_set_req_fn elevator hook를 호출하여 request의 elevator-private 항목을 초기화 한다. 전자 함수가 반환될때 blk_queue_bio는 bio에 포함된 정보로 초기화 한다.


        init_request_from_bio(req, bio);

 

새로운 request가 bio에 보관된 정보에서 정확하게 검색되고 초기화 된후에, issuing tasks’s plug list에 삽입되거나 elevator에 직접 제출되어야 한다. per-process-plugging logic이 실행된다.


        plug = current->plug;
        if (plug) {
                if (request_count >= BLK_MAX_REQUEST_COUNT)
                        blk_flush_plug_list(plug, false);
                list_add_tail(&req->queuelist, &plug->list);
        } else {
                spin_lock_irq(q->queue_lock);
                __blk_run_queue(q);
                spin_unlock_irq(q->queue_lock);
        }

 

 

 

새롭게 초기화된 request는 task가 현재 plugged 되었고, 등록된 list의 길이가 BLK_MAX_REQUEST_COUNT를 초과 하지 않거나 or task가  충분한 짧은 시간동안 plugge되어 있는 경우에만 task’s plug list에 삽입된다. 그렇지 않으면, 만약 task가 plugged되었다면  unplugged되고 plug list는 blk_flush_plug_list() 함수로 flush된다. 만약 task 가 unplugged되면, driver run-of-queue가 발생하고 새로운 request가 elevator_add_req_fn hook로 추가되어 추가적인 처리를 위해 직접 elevator에 전달된다.  


The elevator and the device driver

 

Run-of-queue가 발생했을때, driver는 request queue를 살펴보고  requests가 있으면 queue에서 하나 이상의 requests를 빼낸다. Queue의 실행은 request 삽입의 결과 또는 device’s controller의 인터럽트에 따라서 발생할수 있다. Driver 초기화중에, request_fn 함수는 blk_init_queue() 함수로 설정되며 새로운 request_queue structure도 할당한다.  Device가 request_queue로 부터 새로운 requests를 얻으려는 경우 run-of-queue는 driver’s request_fn hook를 실행한다.  일반적으로 후자는 유효한 request를 반환하지 않을때 까지 block layers의 blk_fetch_request() 함수를 실행한다. Loop안에서 driver는 적절하다고 판단되는 requests를 처리한다.

 

 

 

null block driver가 request_fn  함수의 초기화를 어떻게 처리하는지 알아보자. 완전한 null_add_dev()함수의 source code는  Linux kernel source tree의  drivers/block/null_blk.c에 있다.


nullb->q = blk_init_queue_node(null_request_fn, &nullb->lock, home_node);

 

 

blk_init_queue_node() 함수는 blk_init_queue()와 유사하다. 그러나 request_queue 함수가 할당되어야 하는 memory node를 지정하는 것을 허용하다.

 

null block driver’s request_fn 함수로서, 다음 처럼 간단한 loop로 구현된다.

 

static void null_request_fn(struct request_queue *q) 
{
        struct request *rq;

        while ((rq = blk_fetch_request(q)) != NULL) {
                struct nullb_cmd *cmd = rq->special;

                spin_unlock_irq(q->queue_lock);
                null_handle_cmd(cmd);
                spin_lock_irq(q->queue_lock);
        }
}

 

blk_fetch_request() 함수는  blk_peek_request() 함수를 호출한다. request_queue에 새로운 request를 삽입하기 위해 차례로 elevator’s callbacks중 하나인 blk_peek_request() 함수를 사용한다.  각 dispatch에 삽입되는 requests의 수는 elevator에 구현된 scheduling policy의 구현에 의존한다.

 

 

 

The make request interface


지난주 동안,  bottlenecks를 찾아 lock contention issues를 해결하기 위해 작업하고 있는 driver를 검토해야 하므로 virtual machine에서 동작하는 operating system의 profiling에 관해 공부를 하게 되었다.  나의 멘토가 perf 와 lockstat와 같은 몇몇 인기있는 profiling tool을 이해할것을 제안했다. 학사 논문 중에 이미 얼마간 perf에 익숙해 지는 기회를 있었지만 Virtualized OS의 performance에 대해 정확한 data를 수집하는것에 대해 좀더 많이 배우고 있다. 

예를 들어, XEN은 performance-related data를 수집하기 위해  Intel’s Performance Monitoring Unit을 활용한다고 들었다.

profiling에 앞서 tests를 수행하는 동안에, 나는  greedy random readers and writers로 구성된 random workload로 CFQ and NOOP I/O schedulers의 성능을 비교하기 위해 null_blk block device driver를 사용할 기회가 있었다. (completion latecny가 없다.) 그러한 workload는 too-fast-to-be-real-device에서 Intel’s IOmeter를 emulate한다. CFQ I/O scheduler에 의한 처리량은  I/O를 발행하는 process의 수에 따라서  NOOP에 의해 달성된 처리량의 1/2이거나 심지어 더 낮다. 

그러나 NOOP scheduler는 여전히 requests를 merge and sort를 한다. 이것들은 I/O operations이 랜덤한 방식으로 발행되는 workload와 sort를 정당화 하는 seek penalty가 없는 작업에서는 정말로 필요하지 않아 보인다. 그래서 Linux kernel’s block layer에는 이미 무엇인가가 있는데 그것은 NOOP scheduler에 있는 request API보다 조금 더 나은 성능을 보여줄 것이 있다. 

 

 

make request interface (or bio-based interface)는 본질적으로 bio structure의 생성후에 block I/O unit의 모든 처리를 끊는 것이다. 그래서 kernel은 직접 storage device’s driver에게 bio를 직접 submit할 수 있다. 그러한 interface는 실제 underlying device에게 그것들을 제출하기전에 request의 pre-processing을 수행하는것이 필요한 모든 block device driver에게 유용한다. 심지어 그것의 목적이아니더라도, bio-based API는 block layer’s I/O request처리를 overhead로 보는 모든 device driver에게 또한 유용하다. 예를 들어, 매우 복잡한 internal request processing logic이 특징으로 하거나 or requests가 처리 될 필요가 없는 device driver or controller에게 유용하다.

그러한 interface의 drawbacks는 분명하다 : 그것을 이용하는 driver는 일반적으로 block layer의해 수행되는 모든 pre-processing을 잃을것이다.

 

Figure 1 : Block layer layout when using the  make request  interface


매우 간단하게 null_blk driver 코드에서, driver가 그러한 interface를 어떻게 사용하는지 알아보자. bio-based mode일때도, null_blk driver는 여전히 request_queue structure를 할당해야 한다. 그러나 핵심은, 기본적인 것과 관련하여 alternate make request 함수를 정의하는 것이다.



null_blk driver는 이것을 null_add_dev()함수에서 한다. 이 함수는 module를 초기화 할때 생성해야 하는 각 simulated device(가상 장치)에 대해 호출된다
.

 

 

 

 

nullb->q = blk_alloc_queue_node(GFP_KERNEL, home_node);

blk_queue_make_request(nullb->q, null_queue_bio);

 

 

 

bulk의 null_queue_bio() 함수 자체를 살펴보자. 매우 간단하고 새로운 request structure를 할당할 필요조차 없다. 그러나  이후에 completions을 다루기 위해  command structure가 필요하다. 그것은 추가적인 작업없이 단지 block operations’s command 만을 다룬다.

 

static void null_queue_bio(struct request_queue *q, struct bio *bio)

{

        struct nullb *nullb = q->queuedata;

        struct nullb_queue *nq = nullb_to_queue(nullb);

        struct nullb_cmd *cmd;

 

        cmd = alloc_cmd(nq, 1); 

        cmd->bio = bio;

 

        null_handle_cmd(cmd);

}

 

 

매우 단순한 경우, 마치 device controller에 의해 실행된것처럼 error notification없이 I/O command를 종료함으로써 completion이 처리된다. 이전에 본 null_handle_cmd() 함수의 context 내에서 직접 실행되는 end_cmd()함수에서 null_blk driver가 어떻게 수행하는지 볼 수 있다. block layer’s bio_endio()함수에 완료된  bio와 두번째 매개변수로 error code를 전달하여 함수를 호출한다.

 

 

case NULL_Q_BIO:

        bio_endio(cmd->bio, 0);

        break;

 

 

 

The multi-queue interface


몇주전에 null_blk device driver로 생성된 simulated device를 사용하여 prototype driver에 대한 몇가지 첫번째 tests를 수행했다. 그러한 테스트와 다음의 profiling은 frontend driver에 의해 유지 되는 internal lock의 경쟁에 의한 locking issues들을 강조했다. 좀더 자세하게 말하면, 그러한 lock은 block I/O driver의 fronted 와 backend parts사이에서 requests and responses를 교환하기 위해 사용되는 ring buffer를 처리하는데 도움이 된다.  lock은 새로운 request의 삽입과 response의 추출로 부터 ring을 보호하다. 그래서 나의 멘토는  ring을 2개의 부분으로 나누는것을 제안햇다. 하나는 request를 위한것, 다른 하나는 response을 위한것으로 사용 되었다.

지난 일주일 반동안, 나는 그것에 대해 연구 했다. Interface를 이전 버전의 driver의 요구와 일치하게 만드는것에 어려움을 겪는 동안 나는 series를 결론 짓는 multi-queue block layer API에 대한 마지막 blog 기사를 작성했다.

다음 주에 performance에 관련된것으로 당신을 지루하게 할것이다.

또한, 이 post는 internship의 첫주 동안에 내가 만든 일부 문서의 보상이다. 그것은 나의 멘토의  reading이 도움이 되었기 때문에 그래서 아마도 평소보다 좀더 정확할 것이다.

 

 

 

 

request interface는 초당 수백개의 I/O operation을 처리할 있는 device용으로 설계 되었다. 최근 논문에서block layer 메인테이너인 Jenx Axboe는  수천만의 IOPS를 처리할수 있는 device에서 사용될때 설계상의 문제를 겪고 있다고 언급했다.(첫 번째 블로그 게시물 or Jens Axboe 논문 참조). 중요한 issues중 하나는 lock contention은  오직 multiple cores들이 동시에 block I/O requests들을 삽입하고 계속해서 singe queue_lock에서 spinning한다면, 매우 관련된 영향 끼친다.

그러한 상황은 lock이 interrupt를 발생시키는 high-end storage device에 의해 lock을 얻으려하고 드라이버가 여전히 같은 lock에서 spinning하고 있을때 악화될것이다.

 

 

multi-queue API는 block I/O controller가 여러 requests를 병렬로 처리할 수 있는 능력을 이용하는것으로 해결하므로 lock contention이 크게 감소한다. 사실, 그것의 일반적인  구성에서, request_queue를 lock하는 필요 없이  block I/O request 삽입할 수 있다. 어떻게 하는지 살펴보자.

 

 

blk-mq API는 2개의분리된 request queues : software staging queue 그리고 block device에 의해 제공되는 실제 hardware queue의 숫자와 매칭되는 수의 hardware dispatch queue를 사용하는 two-levels block layer design을 구현했다.  software staging queues의 갯수는 hardware dispatch queues의 수보다 클수 있다. 이 경우, 2개 이상의 software의 큐가 같은  hardware context의 일부분 일것이다. 그리고 해당 hardware context에서 수행되는 dispatch는 모든 관련된 software queues에서 requests를 가져올것이다. 

대신, software staging queue의 개수는 hardware queue의 수보다 적을수 있다. 이경우 sequencial mapping이 수행된다.  3번째 그리고 가장 간단한 경우는 software queue의 갯수가 hardware queue의 개수가 같다. direct 1:1 mapping이 수행된다.

 

Figure 1: Outline of the multi-queue block layer.

Main data structures

multi-queue block layer API에 의해 사용되는 첫번째 관련 data structure는 blk_mq_req structure이며 새로운 block devicefmf block layer에  등록 하는 동안 모든 중요한 정보들 포함한다.  

이 data structure는 blk_mq_ops data 구조체애 대한 포인터를 포함하고 있으며 multi-queue block layer에서 device’s driver와 상호작용하는데 사용되느 specific routines의 track을 유지하기 위해 사용된다.

blk_mq_req 구조에는 또한 초기화될 hardware queue의 개수, queue의 dept(깊이?) 그리고 block layer에서 특정 driver와 관련된 data structures의 초기화 동안에 유용한 다른 정보들 유지한다. 

다른 중요한 data structure는 blk_mq_hw_ctx structure이다. 그것은 requesut_queue와 연관된 hardware context를 나타낸다. software staging queue에 해당하는 structures는  per-CPU별로 할당되는  blk_mq_ctx structues이다. 

이 contexts사이에서 mapping을 수행하는 함수는 drivers의  blk_mq_ops data structure의 map_queue 항목에 지정되며, 반면에 이 함수에 의해 구성된 mapping은 block device와 관련된 request_queue data structure의 mp_map으로 유지된다.

걱정하지 마시오. 그림 2를 보면 더 명확해 진다.

 

Figure 2:  Data structures used in the multi-queue block layer.


multi-queue API를 사용하는 새로운 driver가 load되면, 새로운 bulk_mq_ops 구조가 만들어지고 초기화 되고 blk_mq_req의  관련 pointer에 주소를  설정한다 . 좀더 자세하게는, 필요한 operations들은 command를 처리하는 담당하는 함수인( low-level driver에게 그것을 전달하는것으로) queue_fn 과 그리고 hardware와 software context사이에서 mapping을 수행하는 map_queue이다.
Queue initialization

 

다른 동작들은 반드시 필요하지는 않지만 I/O request의 완료나 context의 생성시에 특정한 동작을 수행하기 위해 구현될수 있다. 필요한 data에 경우, drive는 그것이 지원하는 submission queue의 갯수를  그들의 사이즈에 따라 초기화 해야만 한다. 예를 들어 driver에 의해 지원되는 command size및 block layer에 노출 되어여 하는 specific flags를 결정하기 위해 다른 data들이 필요하다. 

 

새로운 device 가 초기화 될때, driver는 device를 처리하는 device driver에 따라 type이 다양한 새로운 new data 구조체를 준비한다. - driver-specific data structure-, 그러나 device’s gendisk struct 와 device와 관련된   request_queue에 대한 포인터를 포함할 가능성이 매우 높다. driver는 이 data 구조체를 준비하자 마자, blk_mq_init_queue()함수를 실행한다 그것은 hardware and software context를 초기화 하고 그것들 사이의 mapping을 수행한다. 초기화 루틴은 또한 대체 make_request 함수를 설정한다,  전통적인 request submission path(blk_make_request()를 포함 할것이다)를 대체하는 the multi-queue submission path(대신 blk_mq_make_request()를 포함한다. )로 대체한다.  평소처럼, 대체 make_request 함수는  blk_queue_make_request() helper로 설정된다. 

 

 

Request submission

Device  초기화는 multi-queue-ready request-submission 함수인 bulk_mq_make_request()함수로 전통적인 I/O submission함수를 교체해서 multi-queue 구초제가 upper layers의 관점에서 투명하게 사용되게 한다. multi-queue block layer에 의해 사용되는 make_request 함수는 단일 hardware queue를 제원하는 driver or async request에 대해서만  per-process plugging 혜택을 볼수 있는 가능성을 포함한다. request 가 sync 이고 driver 가  적극적으로 multi-queue interface를 사용하는 경우, plugging이 수행되지 않는다.  make_request  함수는 또한 request merging을 수행하여 만약 plugging이 허영되면,  tasks’s plug list안에서 처음 후보자를 찾고 마지막으로 현재 CPU에 mapped된 software queue에서 찾는다. submission path에는 모든 I/O scheduling-related callback이 필요하지 않다. 마침내, make_request는 해당하는 hardware queue에게 즉시 모든 sync request를 보낸다. sync or flush request의 경우에 이 transition을 지연 시키므로 subsequent merging과 더 효율적인 dispatching이 가능하다.

 

Request dispatch

 

I/O request가 synchronous인 경우 (그래서 multi-queue block layer에서 plugging을 허용하지 않는다.) device driver로 dispatch는 같은 reuqest의  context에서 수행된다. 만약 request가 대신 async or flush이면, 그리고 task plugging이 존재하면, dispatch는  실행 될 수 있다.

a) 동일한 hardware queue와 연관된 software queue에 또 다른 I/O request를 제출하는 context

b) request submission / 실행 동안에 scheduled된 delayed work가  실행 되었을때

 

Multi-queue block layer의 main run-of-queue 함수는 bulk_mq_run_hw_queue()이며 기본적으로 blk_mq_ops 구조체의 queue_rq filed가 가리키는 다른 driver-specific routine에 의존한다. 이 함수는 async request에 대해 queue의 모든 실행을 지연시시키는 반면 sync request는 driver에게 즉시 전달한다. request가 sync인 경우에, blk_mq_run_queue()에 의해 호출되는 inner 함수인 blk_mq_run_hw_queue()는 먼저 현재 서비스 중인 hardware queue와 연관된 software queue을 합류한다. 모든 서비스 대상 항목들을 수집한 다음,  queue_rq 함수를 사용하여 각 request를 driver에 전달하여 처리한다. 

 

함수는 마침내, 연관된 request의 requeue or deletion로 가능한 errors들을 처리한다.

 

 

Figure 3: Functions performing request transition between different data structures.

 

내용

 

1. 하드 링크(Hard Link)

- Ext2 파일시스템에서 특정 파일이나 디렉토리를 다른 이름으로 가지고 싶거나, 다른 위치에서 접근을 쉽게 하고 싶을 경우 링크(Link) 기능을 사용할  있게 된다. 링크는 하드 링크와 소프트 링크(또는 심벌릭 링크)  종류 있다. 하드 링크에 대해 알아보자.

 

- 일반적으로 Ext2 파일시스템에서 '링크'  하드 링크를 가르킨다.

- 링크에 대한 설명을 들을  Include 추가로 만들지 않고 링크 카운트만 증가하게 된다.

- 하드 링크를 생성할  데이터 구조에 2가지 업데이트가 일어나는데, 첫째로 해당 파일의 Inode 구조체에서 링크 카운트를 나타내는 Link Count 항목의 값이 1 증가한다.

- 링크를 생성한 디렉토리 내에 새로운 디렉토리 엔트리가 추가되며,  엔트리가 가리키는 Inode 번호가 바로 원본 파일의 Inode 가리키게 된다.

 

[하드링크]

- 기존 파일에 새로운 디렉토리 엔트리가 추가되면서 링크하고자 하는 원본 파일의 Inode 가리키고, 해당 Inode 링크 카운트를 1 증가시킨다. 하드링크는 하나의 Inode 여러 개의 이름을 가진 것이라고   있다.

 

- 그러나 제약 사항이 발생하는데 하드링크가 모두 해제되지 않는  원본 파일은 삭제될  없다.  Link Count 0 되어야 파일을 삭제할  있게 되는 것이다. 또한 파일의 경로와 이름을 이용한 것이 아니라 Inode 번호를 이용하여 특정 Inode 가리키고 있는 것이므로 같은 파일시스템의 같은 파티션안에 존재하는 파일이여야 한다. 이는 Inode 번호를 이용한 것이므로 구조상 당연한 제약이지만, 리눅스와 같이 디렉토리의 성격에 따라 파티션을 세부적으로 나누는 상황에서 이런 제약은 비효율 적일 것이다.

 

- 가장 대표적인 하드 링크는 특정 디렉토리 내의 '.' '..'  같은 폴더와 상위폴더를 가리키는  엔트리들은 하드 링크로 되어 있다. 따라서 디렉토리의 경우 최소 2개의 하드 링크를 가지게 되고, 삭제  파일을 삭제하는 명령으로 디렉토리를 삭제할  없는 이유가 여기 있게 된다.

 

 

2. 소프트 링크(Soft Link)

- 소프트 링크는 심벌릭 링크(Symbolic Link)라고 하며, 하드 링크의 제약 사항을 개선하기 위해 만들어졌다.

- 파일시스템의 다른 파일이나 디렉토리를 가리키는 것은 같지만, 하드 링크와 달리 소프트 링크가 만들어지는 파티션 또는 파일시스템과  위치가 달라도 가능하다.

- 소프트 링크는 일반 파일과 같이 디렉토리 엔트리와 Inode 각기 가지고 있지만, 파일의 유형을 나타내는 File mode 항목에 소프트 링크는 0xA000 플래그가 ON되어 있다.

- 소프트 링크가 가리키는 파일명은 Inode 구조를 파악 하면   있을 것이다. 일반 파일과 마찬가지로 Inode 구조체의 i_block 항목이 가리키는 데이터 블록을 읽어 들이면 파일 경로가 나오게 된다. 그러나 파일명의 길이가 60 Byte 이하라면 i_block 배열 안에도 기록될  있기 때문에 i_block 항목을 스트링으로 읽어 들이면 된다.

 

[소프트 링크]

-  그림의 경우 소프트 링크의 2가지 방식을 표현한 것이다. 문자열의 길이 '/dir/test/txt'이기 때문에 60Byte 이하이므로 B 같은 방식으로 데이터 블록을 읽어오는 일은 발생하지 않지만 예를  것이다.

- 'A'  원본파일의 경로가 60Byte 이하인 경우 데이터 블록을 읽지 않고 원본 파일을 가리키는 것이고, 'B' 원본 파일 경로가 60Byte 넘는 경우 사용된다. 원본파일의 Inode 직접 건드리는 작업이 없기 때문에 소프트 링크는 원본파일을 지우거나 기타 변경 작업을 함에 있어서 전혀 영향이 없다.

 

 

3. Inode 생성 과정

- Ext2 파일시스템에서 파일이나 디렉토리 등을 생성하려면 비어있는 Inode 블록이 있는 블록 그룹이 있어야 한다.

- Inode 생성할 공간이 있다고 하더라도 여러 그룹들  어떤 그룹을 선택해야 할지 살펴볼 필요가 있다. 그룹의 선택에 따라 저장장치의 I/O  발생할 수도 있고, 전체적으로 단편화가  많이 일어날 수도 있기 때문에 성능면이나 사용 효율면에서 중장기 적으로  영향을 끼칠  있기 때문이다.

 

- 커널에 따라 사용되는 알고리즘은 달라질  있으나 여기서 나오는 대부분의 알고리즘의 커다란 틀은 비슷하다고   있다.

 

- 파일을 생성하는 경우 Inode 선택하는 가장  번째 조건은 상위 디렉토리의 블록 그룹과 같은 그룹이냐 하는 것이다. Ext2 파일시스템에서 블록 그룹을 나누어 놓은 이유가 바로  개념으로 관련된 디렉토리  파일들의 물리적인 위치를 최대한 가깝게 하도록 저장하여 I/O 효율을 높이고자 하는 것이다. 물리적인 공간이 부족하여 다른 그룹에 위치할 수밖에 없는 상황이면 틀리겠지만, 같은 그룹에 파일을 저장할만한 공간이 존재한다면  어떤 조건보다 높은 우선순위를 지니게 되는 것이다.

 

- 디렉토리를 생성하는 경우는 약간 다른데 디렉토리 생성시 블록 그룹 선택 우선순위는 상위 디렉토리에 크게 얽매이지 않는다. 디렉토리는 파일을 담을  있는 일종의 그릇 역할을 하는 것이므로 디렉토리 내에 파일을 담을  있는 여유 공간의 크기에 따라 우선순위를 달리하기 때문이다.

 

- 커널의 종류에 따라 알고리즘이 달라질  있으니 커널의 버전을 살펴보고 실제 소스를 참고하면 좋을 것이다. Fedora Core2 커널 파일시스템을 예로 들어 설명의 파일시스템을 예로 설명한다.

 

[Inode 생성과정]

- 커널에서 Inode 생성하고자 하면 ext2_new_inode( ) 함수(linux/fs/ext2/ialloc/c) 호출된다.  함수는 Inode 생성하고자 하는 그룹을 결정하고, 실질적인 할당  초기화를 진행한다. 이때 Inode 위치할 블록 그룹을 결정하는 과정이 생각보다 까다롭다.

- ext2_new_inode( ) 함수 내에서 Inode 생성되기 전까지의 호출 과정으로 중요한 부분은 할당된 Inode 위치할 블록 그룹을 선택하는 과정이다. 여러 가지 방법이 있는데  그림의 함수들을 통해 3가지 정도 방법을 알아보자.

 

- 블록 그룹을 선택하는 알고리즘은 디렉토리인 경우와 디렉토리가 아닌 경우에 따라 차이가 나는데 디렉토리가 정규 파일이나 소프트 링크와 달리 취급되는 이유가 상위디렉토리와 하위 디렉토리가 굳이 같은 블록 그룹에 있지 않아도 크게 영향을 미치지 않기 때문이다. 각각의 파일들은 가급적 상위 디렉토리와 같은 그룹에 있어야 저장장치 안에서 파일 데이터 블록을 읽어들이기 위한 이동 경로가 짧아지며, 단편화를 줄일  있는  이점이 있지만, 디렉토리의 경우 파일들을 묶는 상위 개념으로 생각할  있기 때문에 블록 그룹의 위치에 상대적으로  민감하다.

 

- 디렉토리의 경우 Super Block 마운트 옵션에 EXT2_MOUNT_LODALLOC(0x0002) 플래그가 ON 되어 있다면 find_group_orlov( ) 함수를 사용하고 그렇지 않으면 find_group_dir( ) 함수를 이용하여 블록 그룹을 선택한다.

 

- find_group_dir( )함수는 그룹 내에 비어 있는 Inode 개수가 평균치 이상인 것들  비어있는 블록의 개수가 가장 많은 그룹을 선택하는 간단한 알고리즘이다.

- find_group_orlov( )함수는 Orlov 라는 사람이 만든 함수로, 복잡하긴 하지만 여러가지 장점이 있다. 비어 있는 Inode 블록들이 평균을 기준으로 하여 상대적으로 수치가 높은 것들  디렉토리의 개수가 가장 적은 것을 선택한다. 이러한 조건에 부합하지 않으면 랜덤으로 블록을 선택하고, 이때 다음 조건과 부합하지 않으면  다른 방식을 사용한다.

 

- 좀더 구체적으로 알아보자

1_ 그룹 내에 비어 있는 Inode 있어야 한다.

2_ 사용되는 디렉토리의 개수 inode per group보다 적어야 한다.

3_ 비어있는 Inode 개수와 블록의 개수가 평균치 이상 이어야 한다.

 

-  조건에 맞지 않으면 조건을 낮추어 0번부터 순차적으로 검색한다.

1_ 그룹내에 비어 있는 Inode 잇어야 한다.

2_ 디렉토리의 개수가 '그룹 평균 디렉토리  + inode per group /16' 보다 작아야 한다.

3_ 비어 있는 Inode 개수와 블록의 개수가 평균치에서 'inode 또는 block per group / 4'  값보다 적어야 한다.

 

-  조건도 맞지 않으면 그룹들은 순차적으로 검색하여 Inode 개수가 평균치 이상이면 선택되어, 여기에도  미치면 하나라도 비어 있는 Inode 존재하는 그룹을 선택한다.

 

- find_group_other( )함수는 디렉토리가 아닌 일반 파일이나 소프트 링크 등의 Inode 생성할  블록 그룹을 선택하는 함수이다. 최우선적으로 파일이 속한 디렉토리와 같은 그룹에  Inode 블록이 개수에 관계없이 존재하기만 한다면 해당 그룹이 선택된다.

 

- 블록 그룹 내에 비어있는 Inode 블록이 없을 경우 다른 블록 그룹을 찾아 할당하는데 이때 커널은 이중 해시를 사용하여 할당하고자 하는 블록 그룹을 선택한다. 이중 해시는 현재의 블록 그룹 인덱스에서 2 제곱 값을 이용해서 블록 그룹을 선택하는 알고리즘을 사용한다.

- 이렇게 검색한 그룹에  Inode 블록이 있다면 이를 사용하지만, 그렇지 않다면 0 그룹부터 순차적으로 검색하여  Inode 있는 그룹을 선택한다. 이때  블록에 대해서는 조건에 넣지 않는다.

 

- 이렇게 Inode 위치할 블록 그룹을 선택한 뒤에는 Inode 생성하고 구조체 내의 모든 값들을 초기화한다. 이때 시간 정보들은 생성된 현재 시간으로 설정되고, 삭제시간은 0으로 초기화 한다.

(시간 정보 : 파일 변경시간 modification time, 접근 시간 access time, Inode 업데이트 시간 change time)

 

[ 과정을 커널 소스를 통해 참조하고자 한다면 linux/fs/ext2/ialloc.c 경로의 소스를 통해  함수들을 확인할  있다.]

 

4. Inode Time 업데이트

 

- Ext2 파일시스템에는 시간과 관련된 4가지 항목이 있다.

M-time(The last modified time)

A-time(The last access time)

C-time(The last updated time of inode)

D-time(The deleted time)

 

A-time

A-time 파일에서 Inode 읽어 들이는 시점에 업데이트 되며 시스템 내의 프로세스가 동작하면서 디렉토리나 파일에 접근해서 읽어 들일 때가 이러한 경우에 해당한다. 그러나 동일한 볼륨에 파일을 옮길 때는 A-time 업데이트 되지 않는다. 다른 볼륨에 이동 시에는 데이터 블록을 복사하는 과정에 해당하므로 A-time 업데이트 된다.

디렉토리의 경우 하위 파일이나 디렉토리들이 사용되는 시점에 업데이트되며, 단순히 디렉토리의 내용을 출력하는 명령 등에는 업데이트 되지 않는다.

소스 레벨에서 보면 'linux/fs/ext2/ialoc.c' 파일의 ext2_new_inode( )함수에서 Inode 생성되는 시점에 A-time 생성되는 시간으로 설정되며, 파일시스템 사용 시에는 'linux/fs/ext2/inode.c' 파일 내에서 Inode 읽어 들이는 함수인 ext2_read_inode( ) 파일을 쓰는 시점에 호출되는 ext2_update_inode( )함수에서 A-time 업데이트 된다.

M-time

파일이나 디렉토리의 내용이 변경되는 시점에 업데이트 된다.

파일 이동이 있는 상황에서는 파일의 내용이 변경되는 상황이 아니므로 M-time  시간이 반영되지는 않는다. 물론 복사의 경우 파일을 생성하여 내용을 쓰기 때문에 Inode 생성되면서 M-time 현재 시간으로 설정된다. 디렉토리의 경우도 파일과 그게 다르지 않으며, 파일의 데이터 블록에 해당되는 부분이 디렉토리에서는 디렉토리 엔트리의 해당하므로 엔트리가 변경되는 시점이 바로 M-time 업데이트되는 시점이다. 하위파일이나 링크 등이 생성, 삭제되었을 경우 업데이트 된다.

C-time

파일이나 디렉토리를 가리키는 Inode 정보가 변경되는 상황에 업데이트가 일어나며, 자주는 아니지만 Inode 정보를 변경할 일이 있기 마련이다. 파일 권한의 설정으로 Inode File mode 항목이 변경되거나 소유자나 그룹 설정을  수도 있는데 이때 Inode 변경되어 M-time 업데이트 되는 것이다.

D-time

파일이 생성되는 시점과 삭제되는 시점에 업데이트 되며 생성될 때는 0으로 초기화되고, 삭제될 때는 현재 시간으로 설정된다.

 

[Inode 시간 정보 업데이트 타이밍]

업데이트 시점

해당 항목

파일이 생성될 

M, A, C, D

파일 또는 디렉토리의 내용이 변경될 

M, C

파일 복사 

원본 파일  상위 디렉토리 : A

복사 파일 : M, A, C

복사 파일의 상위 디렉토리 : M, C

파일 이동 

이동 전의 상위 디렉토리 : M, A, C

이동 후의 상위 디렉토리 : M, C

이동 파일 : C

 

 

5. 파일 삭제

- Ext2 파일시스템에서 파일을 삭제하는 과정을 파악 해보면 삭제 과정은 크게 어렵지 않지만 커널에서 처리하는 파일시스템의 동기화 부분 처리가 손이 많이 가는 작업이다. 그러나 여기서는 파일시스템에서 삭제 과정을 살펴 보자.

- 삭제 과정을 단순히 생각하면 삭제 하고자 하는 파일이나 디렉토리에 할당된 Inode Block 다시 사용 가능하도록 하면  것이다.

- 우선 사용자 프로세스 쪽에서 파일을 삭제하는 명령이 오게 되면 unlink 시스템 콜을 통해 VFS(가상 파일시스템) generic_delete_inode( ) 함수를 거쳐 Ext2 파일시스템의 ext2_delete_inode( )함수가 호출된다.  과정에서 하드 링크의 수를 파악하여 하드 링크 수가 0  가능할 것이다.

- ext2_delete_inode( )함수에서는 Inode 사용 가능한지(EXT2_BAD_INO 플래그가 ON되면 RETURN)확인하고 i_dtime 항목을 업데이트 한다. 정상적인 Inode라면 본격적으로 파일에 할당된 Inode 블록을 삭제하게 된다.

 

[디렉토리 엔트리 삭제 과정]

 

- 삭제 과정을 살펴보면 삭제하고자 하는 파일을 디렉토리 엔트리에서 찾아내 삭제 작업을 진행한다.  과정에서 디렉토리 엔트리의 inode 항목(Inode 번호) 0으로 초기화하여 엔트리와 Inode 사이의 연결고리를 끊는다. 그리고 디렉토리 내에서도 엔트리가 검색되지 않도록 바로 앞의 엔트리의 크기 값을 현재의 엔트리만큼 키워서 삭제하고자 하는 파일의  엔트리가 삭제 파일의  엔트리를 가리키도록 한다.

 

-  그림처럼 삭제되는 엔트리의 Inode 번호와  엔트리의 Record Length 항목의 값이 달라지게 되며 할당되어 있던 데이터 블록과 Inode 해제시켜 다시 사용가능 하도록 만든다.

 

[파일 삭제 명령 프로세스]

-  그림은 실제 커널에서 사용되는 함수를 예로 들은 삭제 프로세스를 나타낸 것인데 리눅스 쉘에서는 파일 삭제 명령이 곧바로 ext2_delete_inode( ) 함수를 호출하는 것이 아니라 간단한 절차를 거치게 되지만, 실제 삭제 과정이 수행되는 시점은 ext2_delete_inode( )함수부터 이다.

 

- Inode 블록을 사용 가능하도록 해제한다는 것은 해당 영역을 초기화 하는 것이 아니라 Block Bitmap Inode Bitmap에서 해당 Bit OFF 시킨다는 의미이다. 이점을 염두하여 커널 소스를 참고하도록 하면 도움이  것이다.

 

6. 파일 복구

- Ext2 파일시스템에서 파일 복구가 가능한 이유는 파일을 지울  Inode 삭제하는 과정에서 실제 Inode 구조체 데이터는 삭제하지 않기 때문이다. 따라서 데이터 블록을 가리키는 직간접 포인터들이 남아있다. Inode에는 삭제시간(i_dtime)항목도 존재 하기 때문에 언제 삭제된 데이터인지도   있어서 시간 조건적으로 복구를 시도하는 것도 가능하다. 또한 용량 정보도 남아있기 때문에 복구하고자 하는 파일이 어떤 Inode인지 구분하는 것이 생각보다 어렵지 않게 된다. 물론 다른 Inode에서 할당하여 사용한다면 복구가 어렵겠지만 정상적으로 파일 데이터를 복구했다 하더라도 Inode 이용하여 디렉토리 엔트리를 찾는 것은 사실상 어려워 파일명까지 복구하는 것은 힘들다.

- 만약 데이터 복구 시도를  경우 가급적 다른 파티션에 파일을 쓰는 것이 좋은데 이는 복구하는 도중에 원래 복구하고자 하던 파일에 덮어 써버릴  있으니 주의 하도록 하자.

 

- Ext3 경우 저널링 기능이 추가되어 파일의 복구가 Ext2처럼 간단하진 않다. Ext3 파일 삭제  Inode 데이터 블록 포인터 배열(i_block 항목) 삭제해 버리기 때문이다. 만약 데이터 복구를 해야 한다면 파일을   상위 디렉토리와 같은 그룹을 사용하고자 하는 Ext2 파일시스템의 특성을 이용하여 기존의 파일이 위치했던 디렉토리의 블록 그룹번호를 알아낸다.   해당 그룹 내에서 블록 그룹을 참고하여 비어있는 블록을 읽어 들여 원하던 파일 내용인지를 확인해 보는 방법을    있다.

 

 

 

6. 실습

 

아래 소스는 최대한 간단한 방법으로 구조를 이해하는데 초점이 맞춰  것이라 실제 사용하는데 문제가 많다.

 

시작섹터와, 디바이스 번호를 정확히 지정하기 바란다. 

HDD_read, HDD_write, GetLocalPartition 함수에서 자신의 PC의 연결된 디바이스 번호를 정확히 지정해야한다.

이전 소스들 참고​하길 바란다.

[Partition.h]

#include <Windows.h>

#include <stdio.h>

#include <string.h>

#include <io.h>

 

typedef unsigned char U8;

typedef unsigned short U16;

typedef unsigned int U32;

 

//섹터 내 파티션 테이블 번지

#define PARTITION_TBL_POS 0x1BE

//확장 파티션 타입

#define PARTITION_TYPE_EXT 0x0F

 

typedef struct _PARTITION

{

    U8 active;

    U8 begin[3];

    U8 type;

    U8 end[3];

    U32 start;

    U32 length;

}PARTITION, *PPARTITION;

 

U32 HDD_read (U8 drv, U32 SecAddr, U32 blocks, U8* buf);

 

//디스크 내의 모든 파티션 검색

U32 GetLocalPartition(U8 Drv, PPARTITION pPartition);

//확장 파티션 검색(재귀 호출)

void SearchExtPartition(U8 Drv, PPARTITION pPartition, U32 *pnPartitionCnt);

//디스크 내의 부트 레코드에서 파티션 테이블 저장

void GetPartitionTbl(U8 Drv, U32 nSecPos, PPARTITION pPartition, int nReadCnt);

//파티션 테이블의 속성 출력

void PrintPartitionInfo(PPARTITION pPartition, U32 nIdx);

 

void HexDump (U8 *addr, U32 len) ; //헥사 덤프

 

void GetPartitionTbl(U8 Drv,U32 nSecPos, PPARTITION pPartition, int nReadCnt)

{

    U8 pSecBuf[512];

    

    //물리적 디스크 Drv의 nSecPos번 섹터에서 1개의 블록을 읽어온다.

    if(HDD_read(Drv, nSecPos, 1, pSecBuf) ==0) {

        printf("Boot sector read failed \n");

        return ;

    }

    memcpy(pPartition, (pSecBuf + PARTITION_TBL_POS), sizeof(PARTITION) * nReadCnt);

}

 

U32 GetLocalPartition(U8 Drv, PPARTITION pPartition)

{

    U32 i;

    U32 pPartitionCnt=0;

    PPARTITION pPriExtPartition = NULL;

 

    //주 파티션이므로 4개의 파티션 읽어옴

    GetPartitionTbl(Drv, 0, pPartition, 4);

    for(i=0;i<4 && pPartition->length !=0; i++){

        if(pPartition->type ==PARTITION_TYPE_EXT) {

            pPriExtPartition = pPartition;

        }

        pPartition++;        //다음 파티션으로 이동

        pPartitionCnt++;    //파티션 카운트 UP

    }

 

    if(!pPriExtPartition)

        return pPartitionCnt;

 

    SearchExtPartition(Drv, pPriExtPartition, &pPartitionCnt);

    return pPartitionCnt;

}

 

void SearchExtPartition(U8 Drv, PPARTITION pPartition, U32 *pnPartitionCnt)

{

    int nExtStart = pPartition->start;

    //데이터를 읽어오기 위해 포인터를 다음 파티션 번지로 이동

    pPartition++;

    //부 파티션과 확장 파티션이 있을 수 있으므로 2개의 파티션을 읽어옴

    GetPartitionTbl(Drv, nExtStart, pPartition, 2);

    while(pPartition->length !=0) {

        (*pnPartitionCnt)++;

        if(pPartition->type == PARTITION_TYPE_EXT) {

            SearchExtPartition(Drv, pPartition, pnPartitionCnt);

        }

        else

            pPartition++;

    }

}

 

U32 HDD_read (U8 drv, U32 SecAddr, U32 blocks, U8* buf){

    U32 ret;

    U32 ldistanceLow, ldistanceHigh, dwpointer, bytestoread, numread;

 

    char cur_drv[100];

    HANDLE g_hDevice;

 

    sprintf_s(cur_drv,sizeof(cur_drv),"\\\\.\\PhysicalDrive%d",(U32)drv);

    g_hDevice=CreateFile(cur_drv, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

      

    if(g_hDevice==INVALID_HANDLE_VALUE)    return 0;

 

    ldistanceLow = SecAddr << 9;

    ldistanceHigh = SecAddr >> (32 - 9);

    dwpointer = SetFilePointer(g_hDevice, ldistanceLow, (long *)&ldistanceHigh, FILE_BEGIN);

      

    if(dwpointer != 0xFFFFFFFF)    {

        bytestoread = blocks * 512;

        ret = ReadFile(g_hDevice, buf, bytestoread, (unsigned long*)&numread, NULL);

        if(ret)    ret = 1;        

        else        ret = 0;        

    }

      

    CloseHandle(g_hDevice);

    return ret;

}

 

U32 HDD_write(U8 drv, U32 SecAddr, U32 blocks, U8*buf)

{

    U32 ret = 0;

    U32 ldistanceLow, ldistanceHigh, dwpointer, bytestoread, numread;

    char cur_drv[100];

    HANDLE g_hDevice;

 

    sprintf_s(cur_drv,sizeof(cur_drv),"\\\\.\\PhysicalDrive%d",(U32)drv);

    g_hDevice = CreateFile(cur_drv, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

 

    if(g_hDevice == INVALID_HANDLE_VALUE) return 0;

 

    ldistanceLow = SecAddr << 9;

    ldistanceHigh = SecAddr >> (32-9);

    dwpointer = SetFilePointer(g_hDevice , ldistanceLow, (long*)&ldistanceHigh, FILE_BEGIN);

 

    if(dwpointer != 0xFFFFFFFF){

        bytestoread = blocks * 512;

        ret = WriteFile(g_hDevice , buf, bytestoread, (unsigned long*)&numread, NULL);

 

        if(ret)        ret = 1;

        else        ret = 0;

    }

 

    CloseHandle(g_hDevice);

    return ret;

}

 

void HexDump (U8 *addr, U32 len){

    U8        *s=addr, *endPtr=(U8*)((U32)addr+len);

    U32        i, remainder=len%16;

        

    printf("\n Offset Hex Value /t/t/t/t/t Ascii value\n");

    while (s+16<=endPtr){ // print out 16 byte blocks.

        printf("0x%08lx ", (long)(s-addr));// offset 출력.

        

        for (i=0; i<16; i++){ // 16 bytes 단위로 내용 출력.

            printf("%02x ", s[i]);

        }

        printf(" ");

        

        for (i=0; i<16; i++){

            if (s[i]>=32 && s[i]<=125)printf("%c", s[i]);

            else printf(".");

        }

        s += 16;

        printf("\n");

    }

 

    if (remainder){// Print out remainder.    

        printf("0x%08lx ", (long)(s-addr)); // offset 출력.

        for (i=0; i<remainder; i++){ // 16 bytes 단위로 출력하고 남은 것 출력.

            printf("%02x ", s[i]);

        }

        for (i=0; i<(16-remainder); i++){

            printf(" ");

        }

 

        printf(" ");

        for (i=0; i<remainder; i++){

            if (s[i]>=32 && s[i]<=125) printf("%c", s[i]);

            else    printf(".");

        }

        for (i=0; i<(16-remainder); i++){

            printf(" ");

        }

        printf("\n");

    }

    return;

}    // HexDump.

[ext2_fs.h]

#ifndef _LINUX_EXT2_FS_H

#define _LINUX_EXT2_FS_h

 

#include "Partition.h"

 

//#define min(a,b)        ( ( (a)<(b) ) ? (a) : (b) )

 

#define EXT2_BAD_INO        1

#define EXT2_ROOT_INO    2

#define EXT2_GOOD_OLD_FIRST_INO 11

 

typedef struct ext2_group_desc{

    U32 block_bitmap; //blocks bitmap block

    U32 inode_bitmap;

    U32 inode_table;

    U16 free_blocks_count;

    U16 free_inode_count;

    U16 used_dirs_count;

    U16 pad;

    U32 reserved[3];

}GROUP_DESC;

 

#define EXT2_NDIR_BLOCKS    12

#define EXT2_IND_BLOCK        EXT2_NDIR_BLOCKS

#define EXT2_DIND_BLOCK        (EXT2_IND_BLOCK +1)

#define EXT2_TIND_BLOCK        (EXT2_DIND_BLOCK +1)

#define EXT2_N_BLOCKS        (EXT2_TIND_BLOCK +1)

 

//Revision levels

#define EXT2_GOOD_OLD_REV 0

#define EXT2_GOOD_OLD_INODE_SIZE 128

 

//Inode 구조체

typedef struct ext2_inode{

    U16    i_mode;

    U16    i_uid;

    U32    i_size;

    U32    i_atime;

    U32    i_ctime;

    U32 i_mtime;

    U32    i_dtime;

    U16    i_gid;

    U16    i_links_count;

    U32    i_blocks;

    U32 i_flags;

    U32 l_i_reserved1;

    U32    i_block[EXT2_N_BLOCKS];

    U32    i_generation;

    U32    i_file_acl;

    U32    i_dir_acl;

    U32    i_faddr;

    U32    l_i_reserved2[3];

}INODE;

 

//Super Blocks 구조체

typedef struct ext2_super_block{

    U32 inodes_count;

    U32 blocks_count;

    U32 r_blocks_count;

    U32 free_blocks_count;

    U32 free_inodes_count;

    U32 first_data_block;

    U32 log_block_size;

    U32 log_frag_size;

    U32 blocks_per_group;

    U32 frags_per_group;

    U32 inodes_per_group;

    U32 mtime;

    U32 wtime;

    U16 mnt_count;

    U16 max_mnt_count;

    U16 magic;

    U16 state;

    U16 errors;

    U16 minor_rev_level;

    U32 lastcheck;

    U32 checkinterval;

    U32 creator_os;

    U32 rev_level;

    U16 def_resuid;

    U16 def_resgid;

 

    U32 first_ino;

    U16 inode_size;

    U16 block_group_nr;

    U32 feature_compat;

    U32 feature_incompat;

    U32 feature_ro_compat;

    U8 uuid[16];

    char volume_name[16];

    char last_mounted[64];

    U32 algorithm_usage_bitmap;

 

    U8 prealloc_blocks;

    U8 prealloc_dir_blocks;

    U16 padding1;

 

    //저널링 항목

    U8 journal_uuid[16];

    U32 journal_inum;

    U32 journal_dev;

    U32 last_orphan;

    U32 hash_seed[4];

    U8 def_hash_version;

    U8 reserved_char_pad;

    U16 reserved_word_pad;

    U32 default_mount_opts;

    U32 first_meta_bg;

    U32 reserved[190];

}SUPERBLOCK;

 

#define EXT2_NAME_LEN 255

//디렉토리 엔트리 1 구조체

typedef struct ext2_dir_entry{

    U32 inode;

    U16 rec_len;

    U16 name_len;

    char name[EXT2_NAME_LEN];

} DIRENTRY_1;

 

//디렉토리 엔트리 2 구조체

typedef struct ext2_dir_entry_2{

    U32 inode;

    U16 rec_len;

    U8 name_len;

    U8 file_type;

    char name[EXT2_NAME_LEN];

} DIRENTRY;

 

// 연산 결과를 미리 메모리에 저장하기 위한 구조체

typedef struct ext2_sb_info{

    U32 inodes_per_block; //블록당 inode 수

    U32 blocks_per_group; //Group당 block 수

    U32 inodes_per_group; //Group당 inode 수

    U32 itb_per_group; // group 당 inode table block 수

    U32 gdb_count; // group당 inode table block 수

    U32 desc_per_block; //block 당 group descriptor 수

    U32 group_count; //총 group 수

    SUPERBLOCK *sb; //super block 포인터

    GROUP_DESC * group_desc; //group descriptor table 포인터

    int addr_per_block_bits;

    int desc_per_block_bits;

    int inode_size; //Inode 크기(128 또는 sizeof(ext2_inode)

    int first_ino; //예약되지 않은 최초 Inode 번호

    int blocksize; //block 크기(1024, 2048, 4096)

    int lba_per_block; //1Block당 lba 개수

    int ext2_block_size_bits;

}SBINFO;

 

#endif

[ReadExt2.cpp]

#include <time.h>

#include "Ext2_fs.h"

 

int nPstart = 0;

 

SUPERBLOCK* ReadSuperBlock();

void InodeIO(SBINFO * pSbi, int nIno, INODE *pInode, BOOL bWrite);

BOOL BlockIO(SBINFO * pSbi, U32 block, U8 *pBlkBuf, BOOL bWrite);

char * ReadExtFile(SBINFO *pSbi, INODE *pInode);

int ReadBlocks(SBINFO * pSbi, char *pData, int * pnPtr, int nLimitBlock, int nLimitSize);

 

void WriteLocalFile(char *pszFile, char *pBuf, U32 nSize){

    FILE *pFile ;

    fopen_s(&pFile,pszFile, "wb+");

    fwrite(pBuf, 1, nSize, pFile);

    fclose(pFile);

}

 

GROUP_DESC* ReadGroupDesc(SBINFO* pSbi){ //Group Descriptor Table 을 읽어들임

    GROUP_DESC *pBlkGrps;

    int nBlkGrpNum = pSbi->group_count;

 

    int nBlks=0;

    while(1){

        nBlks++;

        nBlkGrpNum -= pSbi->desc_per_block_bits;

        if(nBlkGrpNum <=0)

            break;

    }

    pBlkGrps = (GROUP_DESC *)malloc(pSbi->blocksize * nBlks);

    if(HDD_read(1, nPstart+pSbi->lba_per_block, nBlks * pSbi->lba_per_block, (U8 *)pBlkGrps) == 0){

        return NULL;

    }

    pSbi->gdb_count = nBlks;

    return pBlkGrps;

}

//수정한 Super Blck 파일시스템에 업데이트

BOOL SyncSuperBlock(SBINFO *pSbi){

    int offset = 2;

    if(HDD_write(1, nPstart + offset, 2, (U8 *)pSbi->sb) == 0) {

        return FALSE;

    }

    return TRUE;

}

 

//수정한 Group Descriptor Table 파일시스템에 업데이트

BOOL SyncGroupDesc(SBINFO *pSbi){

    if(HDD_read(1,nPstart + pSbi->lba_per_block, pSbi->gdb_count * pSbi->lba_per_block, (U8 *)pSbi->group_desc) ==0){

        return FALSE;

    }

    return TRUE;

}

 

int SearchFile(SBINFO * pSbi, char *pszPath, int *pidir){ //주어진 경로의 파일을 Ext2(또는 Ext3) 파일시스템에서 찾는다.

    char aszTmp[256];

    char *pszStr = NULL;

    U8 *pBuf = NULL;

    INODE TmpInode;

    DIRENTRY *pTmpDir = NULL;

    int i, inum;

    char* type;

 

    strcpy_s(aszTmp,sizeof(aszTmp), pszPath);

    pszStr = strtok_s(aszTmp, "/",&type);

        

    //ROOT DIR

    if(pszStr == NULL)

        return EXT2_ROOT_INO;

    inum = EXT2_ROOT_INO;

    

    pBuf = (U8 *)malloc(pSbi->blocksize);

 

    while(pszStr)

    {

        InodeIO(pSbi, inum, &TmpInode, FALSE);

        if(TmpInode.i_mode & 0x4000 == 0){ //디렉토리인지 검사

            free(pBuf);

            return 0;

        }

 

        *pidir = inum;

        BlockIO(pSbi, TmpInode.i_block[0], pBuf, FALSE);

        pTmpDir = (DIRENTRY *)pBuf;

        i = inum = 0;

 

        while( i < pSbi->blocksize){ //블록내를 돌며 같은 디렉토리 찾기

            if(strncmp(pszStr, pTmpDir->name, pTmpDir->name_len) == 0){

                inum = pTmpDir->inode;

                break;

            }

 

            i += pTmpDir->rec_len;

            pTmpDir = (DIRENTRY *)((char*) pTmpDir + pTmpDir->rec_len);

        }

        

        if(!inum){ //해당 디렉토리를 찾지 못했다면 리턴

            free(pBuf);

            return 0;

        }

        pszStr = strtok_s(NULL, "/",&type);

    }

    free(pBuf);

    return inum;

}

 

//파일 데이터를 읽어들이기 위해 pnPtr i_block 배열(pnPtr)의 Block 데이터를 읽어 드림

int ReadBlocks(SBINFO * pSbi, char *pData, int * pnPtr, int nLimitBlock, int nLimitSize){

    int nReadSize = 0, nToRead, i;

    U8 *pBuf = (U8*) malloc(pSbi->blocksize);

    for(i = 0 ; i<nLimitBlock && nReadSize < nLimitSize ; i++)

    {

        BlockIO(pSbi, pnPtr[i], pBuf, FALSE);

        nToRead = min(nLimitSize - nReadSize, pSbi->blocksize);

        memcpy(pData+nReadSize, pBuf, nToRead);

        nReadSize += nToRead;

    }

    free(pBuf);

    return nReadSize;

}

 

//Ext2 파일시스템의 파일을 읽어들임

char * ReadExtFile(SBINFO *pSbi, INODE *pInode){

    char *pData = (char *)malloc(pInode->i_size);

    U8 *pBuf, *pBuf2;

    U32 nReadSize = 0, int_per_block, i;

 

    int_per_block = pSbi->blocksize/4;

    nReadSize = ReadBlocks(pSbi, pData, (int*)(pInode->i_block), EXT2_NDIR_BLOCKS,pInode->i_size );

    if(nReadSize == pInode->i_size)

        return pData;

 

    //간접 블록

    pBuf = (U8*) malloc(pSbi->blocksize);

    BlockIO(pSbi, pInode->i_block[EXT2_IND_BLOCK], pBuf, FALSE);

    nReadSize += ReadBlocks(pSbi, pData+nReadSize, (int*)pBuf, int_per_block, pInode->i_size-nReadSize);

    if(nReadSize == pInode->i_size){

        free(pBuf);

        return pData;

    }

 

    //이중 간접 블록

    BlockIO(pSbi, pInode->i_block[EXT2_DIND_BLOCK], pBuf, FALSE);

    pBuf2 = (U8*)malloc(pSbi->blocksize);

    for(i=0; i<int_per_block ; i++){

        BlockIO(pSbi, pBuf[i], pBuf2, FALSE);

        nReadSize += ReadBlocks(pSbi, pData+nReadSize, (int*)pBuf, int_per_block, pInode->i_size - nReadSize);

 

        if(nReadSize == pInode->i_size){

            free(pBuf);

            free(pBuf2);

            return pData;

        }

    }

    free(pBuf);

    free(pBuf2);

    return NULL;

}

 

SUPERBLOCK* ReadSuperBlock(){

    int offset =2;

    SUPERBLOCK *sb = (SUPERBLOCK *) malloc(sizeof(*sb));

    //물리적 디스크 drv의 nSecPos번 섹터에서 1개 블록을 읽어옴

    //sector * 2 = 1024(ext default block size)

 

    if(HDD_read(1, nPstart + offset, 2,(U8 *)sb) == 0) {

        printf("Boot sector read failed\n");

        free(sb);

        return NULL;

    }

    return sb;

}

 

//Block Input/Output

BOOL BlockIO(SBINFO * pSbi, U32 block, U8 *pBlkBuf, BOOL bWrite){

    if(bWrite){

        if(HDD_write(1, nPstart+ pSbi->lba_per_block * block, pSbi->lba_per_block, pBlkBuf) ==0){

            printf("Block Wirte Failed \n");

            return FALSE;

        }

    }else{

        if(HDD_read(1,nPstart + pSbi->lba_per_block * block, pSbi->lba_per_block, pBlkBuf) == 0){

            printf("Block Read Failed \n");

            return FALSE;

        }

 

    }

    return TRUE;

}

 

//Inode Input/Output(Read or Wirte)

void InodeIO(SBINFO * pSbi, int nIno, INODE *pInode, BOOL bWrite){

    SUPERBLOCK *sb=pSbi->sb;

    GROUP_DESC * gdp;

    U32 nBlockGroup, nBlock, nOffset;

    U8 *pBlkBuf;

 

    //Inode가 속한 Block Group을 알아냄

    nBlockGroup = (nIno - 1) / sb->inodes_per_group;

    gdp = pSbi->group_desc+nBlockGroup;

      

 

    //Inode가 저장된 Block과 offset을 구함

    nOffset = ((nIno - 1) % sb->inodes_per_group) * pSbi->inode_size;

    nBlock = gdp->inode_table + (nOffset >> pSbi->ext2_block_size_bits);

    nOffset &= (pSbi->blocksize - 1);

 

    pBlkBuf=(U8*) malloc(pSbi->blocksize);

 

    BlockIO(pSbi, nBlock, pBlkBuf, FALSE);

    

    if(bWrite){ //inode 쓰기

        memcpy(pBlkBuf + nOffset, pInode, sizeof(INODE));

        BlockIO(pSbi, nBlock, pBlkBuf, TRUE); //읽거나 쓰기

    }else { //inode 읽기

        memcpy(pInode, pBlkBuf + nOffset, sizeof(INODE));

    }

    free(pBlkBuf);

}

 

void InitSbInfo(SUPERBLOCK * sb, SBINFO * pSbi){

        memset(pSbi, 0, sizeof(SBINFO));

        pSbi->sb = sb;

        pSbi->inode_size = sb->rev_level == EXT2_GOOD_OLD_REV ? EXT2_GOOD_OLD_INODE_SIZE : sb->inode_size;

        pSbi->first_ino = EXT2_GOOD_OLD_FIRST_INO;

        pSbi->blocksize = 1024 << sb->log_block_size;

        pSbi->blocks_per_group = sb->blocks_per_group;

        pSbi->inodes_per_group = sb->inodes_per_group;

        pSbi->inodes_per_block = pSbi->blocksize / pSbi->inode_size;

        pSbi->itb_per_group = pSbi->inodes_per_group/pSbi->inodes_per_block;

        pSbi->desc_per_block = pSbi->blocksize/sizeof(GROUP_DESC);

        pSbi->addr_per_block_bits = pSbi->blocksize / sizeof(U32);

        pSbi->desc_per_block_bits = pSbi->blocksize/ sizeof(GROUP_DESC);

        pSbi->group_count= (sb->blocks_count - sb->first_data_block + sb->blocks_per_group -1) /sb->blocks_per_group;

        pSbi->lba_per_block = pSbi->blocksize/512; //블록 사이즈를 섹터 단위 (4K/512 = 8) 8개섹터가 1블럭

        pSbi->ext2_block_size_bits = sb->log_block_size+10;

        pSbi->group_desc = ReadGroupDesc(pSbi);        

}

 

//딜렉토리 엔트리 삭제

BOOL DeleteDirEntry(SBINFO *pSbi, int idir, U32 inum){

    INODE inode;

    DIRENTRY *pPrevDir = NULL, *pCurDir = NULL;

    U8 *pBuf;

    int i = 0;

 

    pBuf = (U8*)malloc(pSbi->blocksize);

    InodeIO(pSbi, idir, &inode, FALSE);

    BlockIO(pSbi, inode.i_block[0], pBuf, FALSE);

    pPrevDir = pCurDir = (DIRENTRY *)pBuf;

 

    //블록 내를 돌며 같은 디렉토리를 찾는다.

    while( i < pSbi->inode_size){

        if(pCurDir->inode ==inum) {

            //삭제하고자 하는 inode 바로 앞의 inode 크기를 늘려서 다음 inode를 가리키도록 한다.

            pPrevDir->rec_len += pCurDir->rec_len;

            BlockIO(pSbi, inode.i_block[0], pBuf, TRUE);

            free(pBuf);

            return TRUE;

        }

        i += pCurDir->rec_len;

        pPrevDir = pCurDir;

        pCurDir = (DIRENTRY *)((char*)pCurDir + pCurDir->rec_len);

    }

    free(pBuf);

    return FALSE;

}

 

//Inode와 종속된 블록을 해제한다.

void DeleteInodeNBlocks(SBINFO *pSbi, int inum){

    SUPERBLOCK *sb = pSbi->sb;

    GROUP_DESC * gdp;

    INODE inode;

    U8 *pBitmap; //비트맵 버퍼

    U32 block_group ;

    int ipos, bpos;

    int bytes, bits, i;

    //Inode를 읽고 삭제 시간을 표시한 뒤 파일시스템에 씀

    InodeIO(pSbi, inum, &inode, FALSE);

    inode.i_dtime = time(NULL);

    InodeIO(pSbi, inum, &inode, TRUE);

 

    block_group = (inum -1) /sb->inodes_per_group;

    gdp = pSbi->group_desc + block_group ;

    pBitmap = (U8 *)malloc(pSbi->blocksize);

 

    //Inode 비트맵에서 위치를 계산하여 Inode를 해제한다.

    BlockIO(pSbi, gdp->inode_bitmap, pBitmap, FALSE);

    ipos = inum - (block_group * sb->inodes_per_group);

    bytes = ipos / 8;

    bits = ipos %8;

    pBitmap[bytes] &= ~(1<< bits);

    //수정한 Inode 비트맵을 파일에 씀

    BlockIO(pSbi, gdp->inode_bitmap, pBitmap, TRUE);

    gdp->free_inode_count++;

    sb->free_inodes_count++;

 

    //파일의 데이터를 담고 있는 블록 삭제

    //참고로, 간접 블록까지 삭제하는 로직은 더 구현해야 한다.

    BlockIO(pSbi, gdp->block_bitmap, pBitmap, FALSE);

    for(i = 0; i<EXT2_NDIR_BLOCKS && inode.i_block[i]; i++){

        bpos = inode.i_block[i] -(block_group * sb->blocks_per_group);

        bytes = bpos / 8;

        bits = bpos % 8 ;

        pBitmap[bytes] &= ~(1 <<bits);

        gdp->free_blocks_count++;

        sb->free_blocks_count++;

    }

    BlockIO(pSbi, gdp->block_bitmap, pBitmap, TRUE);

    free(pBitmap);

    //수정한 내용 파일시스템에 업데이트

    SyncGroupDesc(pSbi);

    SyncSuperBlock(pSbi);

}

 

//파일 데이터를 저장하는데 적학합 블록 그룹을 찾음

static int FindGroupForFile(SBINFO *pSbi, int parent_inum){

    SUPERBLOCK *sb = pSbi->sb;

    GROUP_DESC *desc;

    //Inode가 속한 Block Group을 알아냄

    int parent_group = (parent_inum -1)/sb->inodes_per_group;

    int ngroups = pSbi->group_count;

    int group, i;

    //상위 디렉토리가 속한 블록 그룹에 공간이있다면 우선적으로 선택

    group = parent_group;

    desc = pSbi->group_desc + group;

    if(desc && desc->free_inode_count && desc->free_blocks_count)

        goto found;

    //2중 해시를 이용한 블록 그룹 선택

    group = (group + parent_inum) % ngroups;

    for( i = 1 ; i < ngroups ; i <<= 1){

        group += i;

        if(group >=ngroups)

            group -= ngroups;

        desc = pSbi->group_desc + group;

        if(desc && desc->free_inode_count && desc->free_blocks_count)

            goto found;

    }

    //빈 Inode 공간이 있는 블록 그룹을 순차적으로 검색하여 선택

    group = parent_group;

    for( i = 0 ; i<ngroups ; i++){

        if(++group >= ngroups)

            group = 0;

        desc = pSbi->group_desc +group;

        if(desc && desc->free_inode_count)

            goto found;

    }

    return -1;

found:

    return group;

}

 

//Inode bitmap에 할당 후 번호 반환

int AllocInode(SBINFO *pSbi, int group){

    int *pBitmap; //Inode Bitmap

    int i, j;

    int int_per_block = pSbi->blocksize/4;

    pBitmap = (int*)malloc(pSbi->blocksize);

    BlockIO(pSbi, pSbi->group_desc[group].inode_bitmap, (U8*)pBitmap, FALSE);

    if(group ==0)

        j = EXT2_GOOD_OLD_FIRST_INO + 1;

    else

        j = 0;

    for(i = 0; i<int_per_block ; i++){

        if(pBitmap[i] != 0xFFFFFFFF){

            int value = pBitmap[i];

            for( ; j<32 ; j++){

                if( ((value >> j) & 0x01) ==0){

                    value |= (1 << j);

                    pBitmap[i] = value;

                    BlockIO(pSbi, pSbi->group_desc[group].inode_bitmap,(U8*) pBitmap, TRUE);

                    free(pBitmap);

                    //Inode 번호 반환

                    return (pSbi->inodes_per_group * group) + (i * 32 + j);

                }

            }

            j = 0;

        }

    }

    free(pBitmap);

    return 0;

}

 

//데이터를 저장할 블록을 할당하고, Bitmap을 업데이트

int AllocBlock(SBINFO *pSbi, int group, int alloc_cnt, int *pBlock){

    int *pBBitmap; //Inode Bitmap

    int i, j;

    int int_per_block = pSbi->blocksize/4;

    int cnt = 0;

    int offset = pSbi->blocks_per_group * group;

 

    pBBitmap = (int*) malloc(pSbi->blocksize);

    BlockIO(pSbi, pSbi->group_desc[group].block_bitmap, (U8*)pBBitmap, FALSE);

    for(i = 0 ; i < int_per_block ; i++){

block_alloc_start:

        if(pBBitmap[i] != 0xFFFFFFFF) {

            int value = pBBitmap[i];

            for(j = 0 ; j<32 ; j++) {

                if( ((value >> j ) & 0x01) == 0){

                    pBlock[cnt++] = offset + ( i * 32 + j);

                    value |= (1 << j);

                    pBBitmap[i] = value;

                    //필요한 블록을 모두 업데이트 했는지 체크

                    if(cnt == alloc_cnt)

                        goto block_alloc_end;

                    else

                        goto block_alloc_start;

                }

            }

        }

    }

    return 0;

 

block_alloc_end:

    BlockIO(pSbi, pSbi->group_desc[group].block_bitmap , (U8*)pBBitmap, TRUE);

    free(pBBitmap);

    pSbi->sb->free_blocks_count = cnt;

    pSbi->group_desc[group].free_blocks_count -=cnt;

    return cnt;

}

 

//Inode를 생성하는데 필요한 영역을 할당하고, 기본 항목 설정

int NewInode(SBINFO *pSbi, int dir_inum, int *pgroup, INODE *inode){

    int group = FindGroupForFile(pSbi, dir_inum);

    int ino = AllocInode(pSbi, group);

    *pgroup = group;

    memset(inode , 0, sizeof(INODE));

    inode->i_atime = inode->i_ctime = inode->i_mode = time(NULL);

    inode->i_mode = 0x81a4; //Inode 모드는 편의상 루트 권한으로 지정함

    inode->i_links_count = 1;

    pSbi->sb->free_inodes_count--;

    pSbi->group_desc[group].free_inode_count--;

    return ino;

}

 

//주어진 경로에 파일이 위치하도록 디렉토리 엔트리 추가

//편의상 디렉토리 내의 전체 엔트리 크기는 1Block 이하라고 가정

void AllocDirentry(SBINFO *pSbi, char *pszDest, int dir_inum , int inum, INODE *pInode){

    DIRENTRY direntry;

    DIRENTRY *pCurDir, *pPrevDir;

    U8 *pBuf;

    int slashmark = 0, new_rec_len, i;

    int pathlen = strlen(pszDest);

    //파일명을 가져오기 위해 파일 경로의 마지막 '/' 위치를 알아냄

    for( i = 0 ; i<pathlen ; i++){

        if(pszDest[i] == '/' )

            slashmark = i +1;

    }

    //디렉토리 엔트리 생성

    memset(&direntry , 0 , sizeof(DIRENTRY));

    direntry.inode = inum;

    direntry.file_type = 1;

    direntry.name_len = pathlen - slashmark;

    direntry.rec_len = 8 + ((direntry.name_len /4 ) +1 ) *4;

    strcpy_s(direntry.name,sizeof(direntry.name), pszDest + slashmark);

 

    //디렉토리 아이노드와 해당 엔트리 블록을 읽어 들인다.

    InodeIO(pSbi, dir_inum, pInode , FALSE);

    pBuf = (U8 *) malloc(pSbi->blocksize);

    BlockIO(pSbi, pInode->i_block[0], pBuf, FALSE);

    pCurDir = (DIRENTRY *)pBuf;

 

    i = 0;

    while( i < pSbi->blocksize){

        i += pCurDir->rec_len;

        pPrevDir = pCurDir;

        pCurDir = (DIRENTRY *)((U8*)pCurDir + pCurDir->rec_len);

    }

    new_rec_len = 8 + ((pPrevDir->name_len/4)+1)*4;

    //마지막 엔트리의 크기를 수정하고, 추가되는 엔트리 블록의 가장 끝을 가리킴

    direntry.rec_len = pPrevDir->rec_len = new_rec_len;

    pPrevDir->rec_len = new_rec_len;

    memcpy(((U8*)pPrevDir + pPrevDir->rec_len), &direntry, direntry.rec_len);

    BlockIO(pSbi, pInode->i_block[0], pBuf, TRUE);

    free(pBuf);

}

 

//로컬 영역의 파일을 Ext2 파일시스템으로 복사

void CopyFileLocalToExt(SBINFO * pSbi, char *pszSrcPath, char *pszDest){

    INODE inode;

    int block[EXT2_NDIR_BLOCKS] = {0,};

    int dir_inum = 0, inum = 0, group, file_size, blks;

    int write_size = 0, towrite, i;

    U8 *pFileData;

 

    printf("%s\n%s\n",pszSrcPath, pszDest);

 

    //로컬 파일을 읽어 들임

    FILE *pFile ;

    fopen_s(&pFile,pszSrcPath,"rb+");

    file_size = filelength(fileno(pFile));

    blks = (file_size / pSbi->blocksize) + ((file_size % pSbi->blocksize) > 0 ? 1 : 0);

    pFileData = (U8*)malloc(blks * pSbi->blocksize);

    fread(pFileData, 1, file_size, pFile);

    fclose(pFile);

 

    //간단한 테스트를 위해 간접 블록까지 사용하지 않는 파일 사용

    if(file_size > pSbi->blocksize * EXT2_NDIR_BLOCKS)

        return ;

 

    //디렉토리를 구하기위한 것이므로 반환 값은 필요 없다.

    SearchFile(pSbi, pszDest, &dir_inum);

    inum = NewInode(pSbi, dir_inum, &group, &inode);

    inode.i_size = file_size;

    AllocBlock(pSbi,group,blks,block);

    memcpy(inode.i_block, block, EXT2_NDIR_BLOCKS);

    InodeIO(pSbi, inum, &inode, TRUE);

 

    SyncSuperBlock(pSbi);

    SyncGroupDesc(pSbi);

 

    //읽어온 파일을 Ext2 파일시스템의 해당 블록에 씀

    for(i = 0 ; write_size < file_size ; i++){

        towrite = min(file_size - write_size, pSbi->blocksize);

        BlockIO(pSbi, block[i], pFileData + write_size, TRUE);

        write_size += towrite;

    }

    AllocDirentry(pSbi, pszDest, dir_inum, inum, &inode);

}

 

U32 main(int argc, char* argv[]){

    PARTITION PartitionArr[50];

    U32 nPartitionCnt = 0;

    SUPERBLOCK *sb;

    SBINFO sbi;

    int inum = 0, idir;

    U8 *pBuf = NULL;

    

    if(argc ==1)

        return 0;

 

    //파티션 정보 읽어오기

    memset(&PartitionArr, 0, sizeof(PARTITION) *50);

    nPartitionCnt = GetLocalPartition(1, PartitionArr);

    nPstart = PartitionArr[0].start;

    //슈퍼블록 얻기

    sb = ReadSuperBlock();

    InitSbInfo(sb, &sbi);

    

    if(strcmp(argv[1], "c" ) == 0){

        CopyFileLocalToExt(&sbi, argv[2], argv[3]);

    }

    else if(strcmp(argv[1], "d") == 0){

        inum = SearchFile(&sbi, argv[2], &idir);

        if(!inum)

            return 0;

        DeleteDirEntry(&sbi, idir, inum);

        DeleteInodeNBlocks(&sbi, inum);

    }

 

    if(pBuf)

        free(pBuf);

    free(sbi.group_desc);

    free(sb);

    return 0;

}

d드라이브에 test.txt라는 파일을 생성한 것을

Ext2 파일시스템으로 포맷한 USB /test 저장한 것이다.

 

 

 

 

 

 

 

복사한 파일을 삭제한 것이다.

 

 포맷을  상태에서 실행을 해야 수월하게 실행될 것이다.

 

파일시스템에서 버퍼 캐시를 이용하지 않고 있고, 모든 Inode 데이터 블록을 읽어 들이고자   메모리릉 할당하고, 저장장치의 I/O 통해 데이터를 읽어 들여야 한다.

 

그리고 동기화 처리가 되지 않아 멀티태스킹 시에 문제가 발생할 것이다. 또한 메타 데이터와 파일 데이터의 저장순서도 실제 커널 소스처럼 구현이 되지 않아있다.

 

위의 소스를 수정하여 탐색기나 응용프로그램을 제작하거나 파일시스템을 읽고 쓰는 디바이스 드라이버를 제작해보면 좋을 것이다.

내용

1. 저널링 기법(Journaling) 도입

- 의도치 않게 시스템이 멈추거나 정전으로 컴퓨터가 꺼지게 되는 경우가 종종 존재할  있게 되는데, 이때 파일시스템을 업데이트 하고 있었다면 파일시스템의 일관성 문제를 초래할  있게  것이다.

- 파일을 생성하고 데이터를 쓰는 도중에 시스템이 갑자기 멈춘다면 해당 파일은 신뢰할  없는 상태가  것인데  때문에 파일시스템의 일관성 체크를 위해 e2fsck 등의 유틸이 존재하게 되었다.

- e2fsck 경우 파일시스템을 마운트 하는 시점에 기존의 파일시스템이 정상적으로 Unmount 되지 않았다면 검사를 수행하게 되는데, 저장장치 내에 있는 모든 Inode 비트맵 데이터 등의 메타 데이터를 검사하여 일관성 문제를 해결한다. 그러나 요즘 하드디스크 용량은 엄청나게 늘어났기 때문에 모두 검사를 하게 되면 시간이 오래 걸리게  것이고 이런 문제로 인해 저널링 기술이 파일시스템에서도 사용하게 되었다.

 

2. 저널링 파일 시스템과 Ext3

- 저널링 기법은 데이터베이스의 기본적 특징을 구현하기 위한 방법의 하나로 일종의 로깅(Logging) 기법을 이용한 데이터 백업 체계라고   있을 것이다.

- 데이터베이스에서 트랜잭션 단위로 커밋과 롤백이 가능한게 데이터 업데이트  원본 데이터 영역에 바로 쓰는 것이 아니라 로그를 기록한  최종적용을 하거나 취소를 하기 때문인데 이를 파일시스템에 적용한 것이다.

- 파일을 쓰는 작업  파일시스템의 특정 영역 저널(Journal)이라 불리는 로그를 기록한  작업이 정상 완료  커밋(commit) 하면 실제 사용되는 영역에 기록 하는 것이다.

- 이렇게 되면 시스템이 갑자기 종료되더라도  위치가 어디인지 파악할  있고, 파일 시스템 전체를 검사할 필요가 없어지게  것이다. 따라서 파일시스템을 마운트 하는 시점에 일관성 검사  소요되는 시간이 굉장히 줄어들 것이다.

- 이런 기법은 다른 파일시스템에도 많이 사용되고, Ext3 장점은 Ext2 완벽한 호환을 가지게 된다. 따라서 저널링 기능을 사용하기 위해 파일시스템을 교체할 필요가 없으며, 일체의 손상 없이 저널링 기능만을 추가  삭제하는 것은 매우 쉽다.

- 어느 정도 검증된 Ext2 파일시스템에 저널링 기능만 부가한 파일시스템인 Ext3 리눅스의 기본 파일 시스템으로 자리잡게  것이다.

 

3. Ext3 파일시스템의 저널링 기법

- Ext3 저널링 방식에 로그 기록 옵션이 크게 3가지가 있는데 이를 알아보자.

 

 

1) Wirteback 모드

- 파일 시스템의 데이터 저널링을 사용하지 않고, 메타 데이터의 변경 이력만 로그로 남긴다. 변경 이력만 저장하고, 실제 메타 데이터는 저장하지 않아 파일이 쓰여지는 시점에서 시스템 Crash 발생 하면 해당 파일이 비교적 쉽게 손상된다. 그러나 파일시스템의 일관성을 유지하는 것은 충실하며, 저장 데이터가 적기 때문에 다른 모드에 비애 속도가 뛰어난 장점이 있어 다른 여러 저널링 파일시스템에서 유사하게 사용되는 방식이다.

 

2) Ordered 모드

- 파일시스템의 메타 데이터 저널링을 구현하는 방식으로 실제 로깅되는 데이터는 메타 데이터뿐 아니라 관련 데이터 블록을 함께 포함하여 트랜잭션 단위로 그룹화 한다. 따라서 전체 데이터 블록을 저널링하지 않더라도 파일시스템 손상  효과적으로 복구하며  속도 또한 빠르다. 효율과 안전성을 절충한 방식이기 때문에 가장 널리 사용되는 모드이기도 하다.

 

3) Journal 모드

- 메타데이터  아니라 데이터 블록에 대한 모든 변경 로그를 남긴다. 따라서 안전성을 보장 받지만 파일 I/O 2배로 일어나 가장 느린 모드이다.

- 모든 데이터가 쓰여질 때는 저널링 영역에 먼저 쓰여진  최종적으로 원래 쓰고자 했던 위치에 파일을 쓰게 된다.

저널링 기능이 파일시스템의 일관성  시스템 Crash 상황의 데이터 복구 기능까지 좋은 성능을 보이지만 이는 시스템  차원의 문제이다.  커널이 동작하고 상위 레벨 시스템 콜을 이용한 로깅 기법이기 때문에 파일시스템 입장에서 보면 일종의 유틸리티라고   있기 때문에 파일시스템을 직접 구현하고자 한다면 설계 시점부터 저널링 기능을 구현할 것인지 염두 하여야  것이다.

 

 

4. 저널 데이터 구조

- 저널 로그 기록 영역은 저널링을 위한 영역에 따로 저장되는데  영역을 찾기 위해서는 Super Block Journal Inode Number 항목을 읽어 들여 찾아갈  있게 된다.

- 일반적으로 저널링을 위한 Inode 8번으로 예약 되어 있다.

- Super Block 저널링 항목들은 저널링 기능을 지원하는 경우에만 관련 항목들을 사용하게 되며, 그렇지 않으면 Padding 위해 예약된 영역으로 비워두고 사용하면  것이다.

- 저널링  파일시스템의 동작에 있어 필수적인 요소는 아니라는 점이다.

- 저널링 기능을 사용할  없는 저장장치가 있음으로 저널링을 설치하였음에도 동작하지 않으면 Super Block incompatible feature flags 항목에 EXT3_FEATURE_INCOMPAT_JOURNAL_DEV(0x0008) 플래그가 ON되어 있는지 확인 해보자. 이때 Ext3 아닌 Ext2 인식할  파일시스템의 동작에 아무런 문제가 없다.

 

- Ext3 파일시스템의 저널링 기능으로 4가지 구조체를 사용한다.  데이터들은 다른 저널 데이터들과 구분할  있도록 Signature 가지며, Ext2 데이터가 저장되는 방식과 달리 Big Endian 방식을 사용하므로 데이터를 읽을  유의해야 한다.

 

1) Journal Header(12Byte)

 

이름

Signature

위치

(Offset)

0

크기

(Size)

4 Byte

일반적인 

(Value)

0xc03b3998U

설명

저널 헤더의 Signature 값으로 리눅스 커널 소스의 linux/include/linux/jdb.h 경로에 정의 되어 있다.

 

#define JFS_MAGIC_NUMBER 0xc03b3998U

 

이름

Block Type

위치

(Offset)

4

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

앞서 언급한 4가지 구조체(Journal Super Block, Descriptor, Commit, Revoke Block)들을 구분하기 위한 Block 타입의 고유 값이 기록되며 값들은 아래표를 확인 하자

 

[저널링 Descriptor Block 타입]

Block 타입

설명

JFS_DESCRIPTOR_BLOCK

1

Descriptor Block

JFS_COMMIT_BLOCK

2

Commit Block

JFS_SUPERBLOCK_V1

3

Journal Super Block version 1

JFS_SUPERBLOCK_V2

4

Journal Super Block version 2

JFS_REVOKE_BLOCK

5

Revoke Bolck

 

이름

Sequence Number

위치

(Offset)

8

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

트랜잭션(transaction) 관리를 위한 시퀀스 번호로  번호를 이용하여 트랜잭션이 시작되고 Commit 된다.

 

2) Journal Super Block

 

이름

Journal Header

위치

(Offset)

0

크기

(Size)

12 Byte

일반적인 

(Value)

가변적

설명

위에서 Journal Header 구조체

 

이름

Journal block size

위치

(Offset)

12

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

저널링에 사용되는 Block 크기로 고정되어 있진 않으나 일반적으로 0x400(1024) Byte 사용 된다.

 

이름

Number of journal blocks

위치

(Offset)

16

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

저널링에 사용되는 최대 Block 

 

이름

Journal start block

위치

(Offset)

20

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

저널 데이터가 기록된 최초 블록

 

이름

Sequence number of transaction

위치

(Offset)

24

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

 트랜잭션이 일어난 시퀀스 번호

 

이름

Journal block of first transaction

위치

(Offset)

28

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

 트랜잭션이 일어난 블록의 번호

 

이름

Error Number

위치

(Offset)

32

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

에러 코드로  값은 리눅스 커널 소스의 journal_abort( ) 함수에 의해 쓰여진다.

 

- 아래 항목들은 Journal Super Block Version 2, Version 1 이어서 추가로 기록되는 영역이다. 따라서 앞의 구조체의 뒤를 이어 추가하여야 한다.

 

이름

Compatible Teatures

위치

(Offset)

36

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

저널링 시스템과 호환 가능한 특징

 

이름

Incompatible Feature

위치

(Offset)

40

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

저널링 시스템과 호환 불가능한 특징으로 현재 정의된 플래그는 아래 항목 하나 뿐이다.

 

#define JFS_FEATURE_INCOMPAT_REVOKE 0x00000001

 

이름

Read only Compatible Features

위치

(Offset)

44

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

저널링 시스템에서 읽기 전용인 특징

 

- 위의 3 항목들은 현재까지 정의된 특징들이  가지밖에 없기 때문에 크게 신경 쓰지 않아도 되지만 앞으로 추가될 여지를 두고 있으니 필요시 그때그때 가장 최근 소스를 참고하길 바란다.

 

이름

Journal UUID

위치

(Offset)

48

크기

(Size)

16 Byte

일반적인 

(Value)

가변적

설명

저널링에 사용되는 128bit UUID

 

이름

Number of filesystems using jornal

위치

(Offset)

64

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

파일시스템에서 사용하는 저널의 개수

 

이름

Location of super block copy

위치

(Offset)

68

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

Super Block 복사본이 있는 블록 번호

 

이름

Max journal blocks per transaction

위치

(Offset)

72

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

트랜잭션당 저널 블록의 최대 개수

 

이름

Max filesystem blocks per transaction

위치

(Offset)

76

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

트랜잭션당 파일시스템 블록의 최대 개수

 

이름

Padding

위치

(Offset)

80

크기

(Size)

176 Byte

일반적인 

(Value)

가변적

설명

패딩 용도의 사용되지 않는 영역

 

이름

16-byte IDs

위치

(Offset)

256

크기

(Size)

768 Byte

일반적인 

(Value)

가변적

설명

파일시스템에서 사용하는 저널의 UUID(128bit) 기록하며, UUID 16Byte이므로 48개를 연속 기록   있다.

 

3) Journal Descriptor Block Entry(journal_block_tag_t)

- Block Tag 부르기도 하는 Journal Descriptor Ext2 파일시스템의 블록 위치와 상태  등을 나타내기 위한 작은 영역이다. 그러나  영역을 나타내기 위해 Journal Super Block 같이 Signature 필요하다.

- 아래 항목(실제 구조체)에는 생략되었지만  항목들에 앞서 Journal Header 위치하여 Descriptor 나타내고 있다. 모든 구조체 들은 Journal Header 포함해야 하는걸 잊지 말아야 한다.

 

이름

Block Number

위치

(Offset)

0

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

파일 시스템 내의 블록 번호

 

이름

Descriptor Entry Flags

위치

(Offset)

4

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

Descriptor Entry 상태 플래그. 아래  참조

 

[Descriptor Entry 타입]

Entry 타입

플래그

설명

JFS_FLAG_ESCAPE

0x01

저널 블록이 잘못된 경우

JFS_FLAG_SAME_UUID

0x02

앞선 Entry UUID 같은 경우

JFS_FLAG_DELETED

0x04

현재의 트랜잭션으로 인해 블록이 지워진 경우

JFS_FLAG_LAST_TAG

0x08

현재의 Entry 저널의 마지막 Entry  경우

 

- JFS_FLAG_ESCAPE 플래그가 사용되는 시점은 다음 읽어 들이는 파일시스템 블록에서 Journal Header Signature 읽혀진 경우로  경우 Journal 의해 해당 데이터가 초기화 된다.

 

4) Revoke Block

이름

Journal Header

위치

(Offset)

0

크기

(Size)

12 Byte

일반적인 

(Value)

가변적

설명

앞에서 나온 Journal Header

 

이름

Block Size

위치

(Offset)

12

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

저널 로그를 기록한 블록의 크기(Byte 단위)

 

이름

Revoked Data Block

위치

(Offset)

16

크기

(Size)

x Byte

일반적인 

(Value)

가변적

설명

 영역은 Block Size 만큼의 크기를 가지며, 로깅이 취소된 데이터 블록의 포인터를 가리키는 배열로 이루어져 있다. 따라서 4Byte 단위로 데이터를 읽어 들인다.

 

- 모든 저널 로그들은 트랜잭션 단위로 그룹화되며, 각각의 트랜잭션들은 시퀀스 번호를 부여 받게 되며, 저널 데이터는  트랜잭션을 관리할  있는 정보와 파일시스템의 변경 내역에 대한 데이터를 가지고 있어 이를 이용하여 비정상적으로 시스템이 정지한 상황에 수행했던 작업을 마무리 한다.

- commit  저널 데이터는 e2fsck에서 정상 데이터로 분류하여 파일시스템에 기록

- commit 되지 않은 경우 완료되지 않은 데이터로 무시하고 넘어감.

내용

 

1. Inode

 

1) Inode

- 파일시스템의 모든 파일이나 디렉토리는 각기 하나의 Inode 할당되어 있으며,  블록 그룹을 위한 Ext2Inode 어떤 Inode 할당되었는지 아닌지를 확인하기 위한 Inode Bitmap 함께 Inode Table 저장되게 됩니다.

 

2) Inode 구조 분석

 

[Inode 영역 출력한 ]

 

-  그림은 Inode Table에서  번째 블록 상위 256Byte 덤프한 내용 입니다.

- Inode 크기는 Super Block inode structure size 항목의 기록된 값으로 파악하며, 일반적으로 Inode 구조체 크기는 128Byte이게 됩니다.

- 0x00x70 아래 부분이 Inode사이의 경계가 되며 Super Block inode per group 항목의 개수만큼 연속적으로 기록되어 있기 "때문에 Inode Table 전체 크기를 쉽게 계산하여 블록 데이터 영역을 찾을  있게 됩니다.

 

[Inode 항목]

 

3) Inode  항목

 

이름

File Mode

위치

(Offset)

0

크기

(Size)

2 Byte

일반적인 

(Value)

가변적

설명

Inode 타입으로 파일 타입과 접근 권한을 나타냅니다.

유닉스 시스템에서는 모든 것을 파일로 표현하는 의미를 두고 File Mode에서 확인 가능 합니다.

 

[Inode 접근 권한]

Inode Owner

Flag

설명

소유자

0x100

읽기 권한

0x080

쓰기 권한

0x040

실행 권한

동일 그룹

0x020

읽기 권한

0x010

쓰기 권한

0x008

실행 권한

기타( )

0x004

읽기 권한

0x002

쓰기 권한

0x001

실행 권한

 

- File Mode 0~8비트까지를 나타내는 곳입니다.(하위 9비트이용)

- 리눅스에서 소유자 권한을 바꾸는 명령어인 chmod 생각 한다면 어렵지 않게 이해가능  것입니다.

- 만약 File Mode 하위 플래그가 0x1FF(1 11[소유자] 11 1[그룹]111[기타])라면 모든 소유자가 읽기,쓰기, 실행이 가능할 것입니다.

- 0x1C0(1 11[소유자]00 0[그룹] 000[기타]) 비트를 통해 확인하면 소유자만 읽기,쓰기, 실행이 가능하다는 것을 알게 됩니다.

 

[실행 파일  디렉토리 속성]

속성 타입

Flag

설명

STICKY BIT

0x200

Sticky bit

SGID

0x400

Set Group ID

SUID

0x800

Set User ID

- File Mode 9~11 비트 플래그로 3개의 비트를 이용합니다. (하위 10~12번째 비트)

- 실행 파일과 디렉토리에만 사용되는 필드이며,  비트가 ON이라면 실행  다르게 동작하거나 디렉토리 아의 파일들이 다른 속성을 지니게 됩니다.

 

[메타 데이터 타입값]

메타 데이터 타입

설명

FIFO

0x1000

명명된 파이프(named pipe)

Character device

0x2000

문자 장치

Directory

0x4000

디렉토리

Block device

0x6000

블록 장치

Regular file

0x8000

정규 파일

Symbolic link

0xA000

소프트(심벌릭)링크

Unix Socket

0xC000

소켓

- File Mode 13~16번째 비트에 저장되는 (최상위 4bit)

-  값들은 플래그처럼 여러 타입이 정의   없고 하나의 값만 저장됩니다.

- 이를 확인 하면 Inode 타입이 일반 파일  아니라 디렉토리, 소켓  다양한 것을   있습니다.

- 소켓의 경우 데이터 블록이 필요 없으며, Inode 내에 모두 기록되는 타입입니다.

 

 

이름

uid

위치

(Offset)

2

크기

(Size)

2 Byte

일반적인 

(Value)

가변적

설명

소유자 식별자(Owner ID) 32byte 하위 16bit 저장.

 

이름

size in bytes

위치

(Offset)

4

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

파일 크기를 Byte 단위로 나타냄

 

이름

access time

위치

(Offset)

8

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

파일의 마지막 접근 시간(last access time)

 

이름

change time

위치

(Offset)

12

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

Inode 마지막으로 변경한 시간(last corresponds time)

 

이름

modification time

위치

(Offset)

16

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

파일 내용을 마지막으로 변경한 시간(last Modification time)

 

이름

deletion time

위치

(Offset)

20

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

파일이 삭제된 시간(delete time)

 

이름

Group ID

위치

(Offset)

24

크기

(Size)

2 Byte

일반적인 

(Value)

가변적

설명

그룹 식별자(Group ID) 32Bit  하위 16bit

 

이름

link count

위치

(Offset)

26

크기

(Size)

2 Byte

일반적인 

(Value)

가변적

설명

하드 링크 

 

이름

block count

위치

(Offset)

28

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

파일 데이터를 저장하는  필요한 블록 개수로 size in bytes 경우 파일의 실제 크기를 나타내고, block count 파일의 데이터가 저장되는 블록의 개수이다.

 

이름

flags

위치

(Offset)

32

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

파일 플래그

 

[Inode 파일 플래그 항목]

Inode Flags

Flag

설명

EXT2_SECRM_FL

0x00000001

Secure Deletion(사용되지 않음)

EXT2_UNRM_FL

0x00000002

Undelete : 파일 삭제  데이터 영역을 초기화 하지 않는다.(사용되지 않음)

EXT2_COMPR_FL

0x00000004

파일 압축(사용되지 않음)

EXT2_SYNC_FL

0x00000008

Syncronous updates : 데이터가 쓰여질  저장장치에 바로 쓴다.

EXT2_IMMUTABLE_FL

0x00000010

파일의 내용을 변경할  없다.

EXT2_APPEND_FL

0x00000020

파일에 덮어 쓰기는 불가능하고, 파일의 뒤에 이어 쓰는 것만 가능

EXT2_NODUMP_FL

0x00000040

파일 덤프 기능을 사용하지 않음

EXT2_NOATIME_FL

0x00000080

A-time(파일 접근 시간) 업데이트 하지 않음.

-  외에도 압축 기능  추가로 여러 가지 플래그가 정의되어 있다.

- 이를 지속적으로 확인해야  것이다. (공식 루트가 아니더라도 최근 릴리즈된 커널 소스의 ext2_fs.h 참고하여도 된다.)

 

이름

OS description 1

위치

(Offset)

36

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

특정 운영체제 정보로 Inode 구조체에는 특정 OS 대한 예약 영역을 가지고 있으나 실제 사용되진 않는다.

 

이름

block pointers(i_block 변수)

위치

(Offset)

40

크기

(Size)

60 Byte

일반적인 

(Value)

가변적

설명

데이터 블록을 가리키는 포인터 배열, int (4byte) 배열 15개를 가진다. 따라서 60byte이다.

- i_block[0]~iblock[11] :  배열 값들이 블록을 가르킴

- i_block[12] 에서 가리키는 블록 내의  4Byte들이 데이터 블록을 가르킴(간접 포인터)

- i_block[13] : 이중 간접 포인터

- i_block[14] : 3 간접 포인터

 

- i_block 배열은 파일 데이터가 있는 실제 블록을 가리키기도 하며, 파일 데이터를 가리키는 위치 블록을 가리키기도 하여 , 간접적으로 파일 블록을 가리킨다.

- 커널이 파일을 저장장치에 기록할  i_block 배열에는 파일의 데이터가 기록되어 있는 블록의 번호가 저장되는 것이다.

 

[i_block 직접 포인터]

 

- i_block 배열이 파일 블록을 직접 가르키고 있는데 이렇게 되면 15개의 블록밖에 가리킬  없다.

 

[i_block 간접 포인터]

- 만약 파일 크기가 'Block Size X 15' 보다 크다면 i_block[0]~i_block[11]까지는 실제 데이터가 있는 블록을 가리키고 i_block[12] 넘어가는 데이터부터는 실제 데이터를 가르키는 것이 아니라 i_block[12] 가리키는 블록 데이터가 바로 실제 데이터를 가리키는 포인터 배열이 된다.

- 다시 정리하면 만약 Block size 1KB이고, 20KB 용량을 가진 파일을 가리킨다 했을  i_block[0~11]까지는 직접 파일의 12개의 블록을 가리키고, 13번째부터는 i_block[12] 가리키는 위치 블록에 4Byte 이용하여 가리키게 된다.  i_block[12] 가리키는 블록에는  256개의 파일 블록을 가리킬  있는데(Block Size : 1KB, 가리키는 블록 주소:4Byte) 이곳에서 나머지 블록들이 차례대로 가리키게 되는 것이다. 이렇게 되면 i_block[12] 통해 기록할  있는 데이터의 크기는 256KB 되는 것이다.

- 따라서 i_block[0~11]까지의 12KB 직접, 나머지 8KB i_block[12] 가리키는 블록의 사우이 32Byte(8) 블록 번호가 가리키는 파일 블록을 찾아가게 된다.

 

- i_block 상위 13 배열을 통해 '12+256'KB 가리킬  있다는 것을 확인 했는데  보다  파일의 경우는 i_block[13], i_block[14] 이용하여 가리킬수 있게 된다.

 

[i_block 이중 간접 포인터]

 

[i_block 삼중 간접 포인터]

 

- 위치 블록이 하나씩 늘어 남에 따라 2562 크기로 파일 블록을 가리킬  있게 되어 기록할  있는 파일의 크기가 커지게 된다.

- 이중의 경우 256 X 256KB = 64MB, 삼중인 경우 64MB X 256 = 16384(16GB) 거장이 된다. 따라서 블록 크기가 1KB 경우 하나의 Inode 저장할  있는 파일의 크기는 '12KB + 256KB + 64MB + 16GB' 되는 것이다.

- 물론 이론상이고, 실질적으로 파일시스템을 직접 접근해서 사용하는 커널의 역량에 따라 크기가 달라진다. 만약 32bit 변수를 이용한 주소 지정의 경우 최대 4GB까지만 사용 가능  것이며, 이도 부호 지정 가능한 변수(signed)라면 반이 줄어 4GB까지 사용 가능  것이다.

- 최신 커널의 경우 이런 제한이 줄었겠지만 파일 시스템  사용되는 시스템을 구현해야 하는 상황이라면 이런 부분을 고려하여야  것이다.

 

이름

generation number

위치

(Offset)

100

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

NFS(Network File System) 위한 파일 버전

 

이름

file acl

위치

(Offset)

104

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

파일 확장 속성(extended Attribute)

 

이름

directory acl

위치

(Offset)

108

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

단편 주소

 

이름

Os description 2

위치

(Offset)

116

크기

(Size)

12 Byte

일반적인 

(Value)

가변적

설명

특정 운영체제 정보 2

 

 

4) 예약된 Inode

- Inode 중에는 여러 용도로 미리 예약된 부분이 많다.

- 루트 디렉토리의 경우 미리 예약된 Inode 번호가 있다. 이는 파일이나 디렉토리를 찾아가기 위해 파일시스템을 읽을 경우 해당 파일의 Inode 알아야 하고  상위 디렉토리를 알고 있어야  것이다 따라서 루트 디렉토리에는 예약된 번호를 쓰게된다.

 

[예약된 Inode 리스트]

예약된 Inode

번호

설명

EXT2_BAD_INO

1

사용 불가능 데이터 블록 Inode

EXT2_ROOT_INO

2

루트 디렉토리 Inode

EXT2_BOOT_LOAER_INO

5

Boot Loader Inode

EXT2_UNDEL_DIR_INO

6

삭제 불가 디렉토리의 Inode

EXT2_GOOD_OLD_FIRST_INO

11

예약되지 않은  번째 Inode

 

- EXT2_BAD_INO 경우 배드 섹터의 이유로 문제 발생한 블록을 표시한다. e2fsck 같은 파일시스템 체크 프로그램에 의해 관리되며 배드 블록 발견시 i_block[] 추가 되며 이렇게 추가될  있는 배드 블록은 최대로 만들  있는 파일 크기와 같다. 또한 블록 비트맵(Block Bitmap)에서도 해당 블록의 Bit On으로 바꾸어 사용 중인 블록으로 표시해야 한다.

 

- EXT2_ROOT_INO 경우 루트 디렉토리에만 해당되는 Inode 이며, 시스템 부팅  커널이 루트 디렉토리에 파일 시스템을 마운트 한다.

 

- EXT2_BOOT_LOAER_INO  EXT2_UNDEL_DIR_INO 최근 사용되지 않는 Inode, 해당 Inode 초기화 되거나 쓰레기 값으로 채워져있다.

 

- EXT2_GOOD_OLD_FIRST_INO 예약되지 않은  Inode번호를 나타내며 파일시스템이 생성된 이후 할당되는 Inode 모두 11 이후 번호를 가진다. 일반적으로 11 Inode '/lost+found' 디렉토리가 할당되며,  디렉토리는 부모 디렉토리와 비정상적으로 연결이 끊어진 디렉토리나 파일을 하위로 연결시키기 위한 디렉토리이다.  커널 에서 사용하던 Inode 번호이기 때문에 최근에도 일반적으로 11번을 할당한다.

 

2. 데이터 영역

 

1) 디렉토리 엔트리

- 일반적으로 Ext2 파일시스템의 디렉토리와 파일은 동일하다고 하는데 실제로도 거의 동일하다. 다만 다른 점은 Inode 파일모드(File Mode항목) 플래그가 틀리다는 것이다.

- 디렉토리는 파일시스템이 파일 데이터 저장하는 방식과 같이 디렉토리 엔트리 구조를 Block  것을 표현한다. 디렉토리 엔트리의 경우 파일이나 데이터의 블록을   있도록 Inode 주소를 담는 구조체 이다.

 

- 모든 디렉토리의 경우 현재 디렉토리와, 상위 디렉토리를 의미하는 '.', '..' 엔트리로 시작하며,  아래로 하위 디렉토리나 여러 파일들이 위치한다.

- 루트 디렉토리의 Inode 번호는 2번으로 디렉토리를 찾아갈 때는 2 Inode 부터 검색하면  것이다.

 

- 파일 길이가 1~255까지 가변적이 듯이 디렉토리 엔트리 크기도 고정되지 않지만 구조체에서 파일명을 저장하는 버퍼 크기는 최대 사이즈(255Byte) 고정되어 있지만, 블록에 저장될 때는 가변적이다.

- 엔트리 구조체에는 파일명의 길이를 저장하는 필드와 현재의 엔트리 크기를 저장하는 필드  데이터 블록을 알아낼  있는 필드 등을 가지게 된다.

 

 

[디렉토리 엔트리 Trace]

 

-  그림을 보면 Group Descriptor에서부터 디렉토리 엔트리를 찾아가는 경로를 표현한 것으로 Group Descriptor Inode 테이블이 시작되는 블록을 가리키고, Inode 2번은 루트 디렉토리라는 것을 이미 알고 있고, 2 Inode 데이터 블록 번호를 i_block[0]에서 알아낸  해당 블록으로 이동하여 디렉토리 엔트리를 읽어오면 된다.

- 모든 엔트리에는 현재 디렉토리와 상위 디렉토리를 가리키는 '.'  '..' 엔트리가 있는데, 루트 디렉토리도 이걸 가지고 있지만 '..' 역시 '.' 엔트리와 동일한 Inode 가리키고 있다.

- 추가로 루트 디렉토리 하위의 디렉토리 엔트리들이 계속 기록된다.

 

- 디렉토리 엔트리는 파일명과 디렉토리명을 저장하는 영역으로  영역은 데이터 블록 영역의 사이사이에 기록되며, 파일이나 디렉토리와 관련된 Inode 주소를 가지며, 디렉토리 엔트리 구조체는 2가지 종류 있다.

- 모두 구조체의 크기는 같지만, 나중에 만들어진 구조체가 공간 활용에 있어 이점이 있다.

- 슈퍼블록(Super Block) compatible feature flags항목에서 EXT2_FEATURE_INCOMPAT_FILETYPE 보면 "디렉토리 엔트리가 파일 타입 속성을 포함하는지의 유무" 있는데  내용이 바로 디렉토리 엔트리 구조체 버전과 관련된 항목이다. 현재 리눅스에 탑재된 Ext3 파일 시스템에서는  번째 구조체를 기본적으로 사용하고 있다.

 

2) 디렉토리 엔트리 항목 1번째

 

이름

Inode

위치

(Offset)

0

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

디렉토리 엔트리의 하위 디렉토리 또는 파일 등을 가리키는 Inode

 

이름

Record Length

위치

(Offset)

4

크기

(Size)

2 Byte

일반적인 

(Value)

가변적

설명

현재 디렉토리 엔트리의 크기

 

- Record Length 항목은 파일명의 길이 외에도 영향을 받는  가지가 있는데 엔트리가 생서되는 시점에 엔트리의 크기를 4 배수로 맞추기 위해 패딩을 넣거나, 나중에 파일 삭제하는 경우 엔트리에도 제거하는데 이때 실제 해당 엔트리가 지워지는 것이 아니라  앞의 엔트리 크기를 지우려는 파일(or 디렉토리) 엔트리 크기만큼 늘려서  다음 엔트리를 가리키도록 수정한다.

- 파일이나 하위 디렉토리 개수가 적어 블록 내에 엔트리들이 별로 없다면 마지막에 추가된 엔트리 크기는 '블록크기  엔트리들의 크기'  것이다.

- 예로  디렉토리라면 '..' 엔트리의 크기는 최대 4096-12  것이다. 이런 요인으로 인해 디렉토리 엔트리의 크기는 자주 바뀐다.

 

 

이름

Name Length

위치

(Offset)

6

크기

(Size)

2 Byte

일반적인 

(Value)

가변적

설명

파일명의 길이

 

이름

Name

위치

(Offset)

8

크기

(Size)

255 Byte

일반적인 

(Value)

가변적

설명

파일명.

 

 

- Name Length 크기가 2Byte 것으로 보아 Ext2_NAME_LEN 매크로 값을 최대값만 변경하면 최대 길이를 65535까지 확장할  있을  하지만 새로운 엔트리 구조체가 발표되면서 이는 현실적으로 불가능해졌다.

 

3) 디렉토리 엔트리 항목2번째

 

이름

Inode

위치

(Offset)

0

크기

(Size)

4 Byte

일반적인 

(Value)

가변적

설명

디렉토리 엔트리의 하위 디렉토리 또는 파일 등을 가리키는 Inode

 

이름

Record Length

위치

(Offset)

4

크기

(Size)

2 Byte

일반적인 

(Value)

가변적

설명

현재 디렉토리 엔트리의 크기

 

이름

Name Length

위치

(Offset)

6

크기

(Size)

1 Byte

일반적인 

(Value)

가변적

설명

파일명의 길이

 

이름

File Type

위치

(Offset)

7

크기

(Size)

1 Byte

일반적인 

(Value)

가변적

설명

파일 타입. 아래  참고

 

[파일 타입]

타입

설명

EXT2_FT_UNKNOWN

0

알려지지 않은 타입

EXT2_FT_REG_FILE

1

정규 파일

EXT2_FT_DIR

2

디렉토리

EXT2_FT_CHRDEV

3

문자 장치

EXT2_FT_BLKDEV

4

블록 장치

EXT2_FT_FIFO

5

명명된 파이프(named pipe)

EXT2_FT_SOCK

6

소켓

EXT2_FT_SYMLINK

7

심벌릭 링크

 

이름

Name 

위치

(Offset)

8

크기

(Size)

255 Byte

일반적인 

(Value)

가변적

설명

파일명

 

- 디렉토리 엔트리 항목 2번째는 파일명의 길이를 반드시 255Byte 내로 맞추어야 한다. 그렇지 않으면 File Type 영역을 침범하게 되어 문제가 발생되는 것이다.

 

 

4. 실습

 

Visual Studio 2012 관리자 권한으로 실행한다.

     

관리자권한으로 실행

프로젝트 속성  구성 속성  일반 -> 프로젝트 기본값  문자집합 : 멀티 바이트 문자 집합 사용

     

 

 

 

 

 

1) 실행파일로 만들 것이기 때문에 Realse 컴파일 하자.

 

 

[Partition.h] 이전에 만들었던 파티션과 관련된 소스의 모음 이다.

시작섹터와, 디바이스 번호를 정확히 지정하기 바란다. 

HDD_read, HDD_write, GetLocalPartition 함수에서 자신의 PC의 연결된 디바이스 번호를 정확히 지정해야한다.

이전 소스들 참고​

무조건 복사 붙여넣기 하면 제대로 동작하지 않을 것이다.​

#include <Windows.h>

#include <stdio.h>

 

typedef unsigned char U8;

typedef unsigned short U16;

typedef unsigned int U32;

 

//섹터 내 파티션 테이블 번지

#define PARTITION_TBL_POS 0x1BE

//확장 파티션 타입

#define PARTITION_TYPE_EXT 0x0F

 

typedef struct _PARTITION

{

    U8 active;

    U8 begin[3];

    U8 type;

    U8 end[3];

    U32 start;

    U32 length;

}PARTITION, *PPARTITION;

 

U32 HDD_read (U8 drv, U32 SecAddr, U32 blocks, U8* buf);

 

//디스크 내의 모든 파티션 검색

U32 GetLocalPartition(U8 Drv, PPARTITION pPartition);

//확장 파티션 검색(재귀 호출)

void SearchExtPartition(U8 Drv, PPARTITION pPartition, U32 *pnPartitionCnt);

//디스크 내의 부트 레코드에서 파티션 테이블 저장

void GetPartitionTbl(U8 Drv, U32 nSecPos, PPARTITION pPartition, int nReadCnt);

//파티션 테이블의 속성 출력

void PrintPartitionInfo(PPARTITION pPartition, U32 nIdx);

 

void HexDump (U8 *addr, U32 len) ; //헥사 덤프

 

void GetPartitionTbl(U8 Drv,U32 nSecPos, PPARTITION pPartition, int nReadCnt)

{

    U8 pSecBuf[512];

    

    //물리적 디스크 Drv의 nSecPos번 섹터에서 1개의 블록을 읽어온다.

    if(HDD_read(Drv, nSecPos, 1, pSecBuf) ==0) {

        printf("boot sector read failed \n");

        return ;

    }

    memcpy(pPartition, (pSecBuf + PARTITION_TBL_POS), sizeof(PARTITION) * nReadCnt);

}

 

U32 GetLocalPartition(U8 Drv, PPARTITION pPartition)

{

    U32 i;

    U32 pPartitionCnt=0;

    PPARTITION pPriExtPartition = NULL;

 

    //주 파티션이므로 4개의 파티션 읽어옴

    GetPartitionTbl(Drv, 0, pPartition, 4);

    for(i=0;i<4 && pPartition->length !=0; i++){

        if(pPartition->type ==PARTITION_TYPE_EXT) {

            pPriExtPartition = pPartition;

        }

        pPartition++;        //다음 파티션으로 이동

        pPartitionCnt++;    //파티션 카운트 UP

    }

 

    if(!pPriExtPartition)

        return pPartitionCnt;

 

    SearchExtPartition(Drv, pPriExtPartition, &pPartitionCnt);

    return pPartitionCnt;

}

 

void SearchExtPartition(U8 Drv, PPARTITION pPartition, U32 *pnPartitionCnt)

{

    int nExtStart = pPartition->start;

    //데이터를 읽어오기 위해 포인터를 다음 파티션 번지로 이동

    pPartition++;

    //부 파티션과 확장 파티션이 있을 수 있으므로 2개의 파티션을 읽어옴

    GetPartitionTbl(Drv, nExtStart, pPartition, 2);

    while(pPartition->length !=0) {

        (*pnPartitionCnt)++;

        if(pPartition->type == PARTITION_TYPE_EXT) {

            SearchExtPartition(Drv, pPartition, pnPartitionCnt);

        }

        else

            pPartition++;

    }

}

 

U32 HDD_read (U8 drv, U32 SecAddr, U32 blocks, U8* buf){

    U32 ret;

    U32 ldistanceLow, ldistanceHigh, dwpointer, bytestoread, numread;

 

    char cur_drv[100];

    HANDLE g_hDevice;

 

    sprintf_s(cur_drv,sizeof(cur_drv),"\\\\.\\PhysicalDrive%d",(U32)drv);

    g_hDevice=CreateFile(cur_drv, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

    

    if(g_hDevice==INVALID_HANDLE_VALUE)    return 0;

 

    ldistanceLow = SecAddr << 9;

    ldistanceHigh = SecAddr >> (32 - 9);

    dwpointer = SetFilePointer(g_hDevice, ldistanceLow, (long *)&ldistanceHigh, FILE_BEGIN);

    

    if(dwpointer != 0xFFFFFFFF)    {

        bytestoread = blocks * 512;

        ret = ReadFile(g_hDevice, buf, bytestoread, (unsigned long*)&numread, NULL);

        if(ret)    ret = 1;        

        else        ret = 0;        

    }

    

    CloseHandle(g_hDevice);

    return ret;

}

 

U32 HDD_write(U8 drv, U32 SecAddr, U32 blocks, U8*buf)

{

    U32 ret = 0;

    U32 ldistanceLow, ldistanceHigh, dwpointer, bytestoread, numread;

    char cur_drv[100];

    HANDLE g_hDevice;

 

    sprintf_s(cur_drv,sizeof(cur_drv),"\\\\.\\PhysicalDrive%d",(U32)drv);

    g_hDevice = CreateFile(cur_drv, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

 

    if(g_hDevice == INVALID_HANDLE_VALUE) return 0;

 

    ldistanceLow = SecAddr << 9;

    ldistanceHigh = SecAddr >> (32-9);

    dwpointer = SetFilePointer(g_hDevice , ldistanceLow, (long*)&ldistanceHigh, FILE_BEGIN);

 

    if(dwpointer != 0xFFFFFFFF){

        bytestoread = blocks * 512;

        ret = WriteFile(g_hDevice , buf, bytestoread, (unsigned long*)&numread, NULL);

 

        if(ret)        ret = 1;

        else        ret = 0;

    }

 

    CloseHandle(g_hDevice);

    return ret;

}

 

void HexDump (U8 *addr, U32 len){

    U8        *s=addr, *endPtr=(U8*)((U32)addr+len);

    U32        i, remainder=len%16;

        

    printf("\n Offset Hex Value /t/t/t/t/t Ascii value\n");

    while (s+16<=endPtr){ // print out 16 byte blocks.

        printf("0x%08lx ", (long)(s-addr));// offset 출력.

        

        for (i=0; i<16; i++){ // 16 bytes 단위로 내용 출력.

            printf("%02x ", s[i]);

        }

        printf(" ");

        

        for (i=0; i<16; i++){

            if (s[i]>=32 && s[i]<=125)printf("%c", s[i]);

            else printf(".");

        }

        s += 16;

        printf("\n");

    }

 

    if (remainder){// Print out remainder.    

        printf("0x%08lx ", (long)(s-addr)); // offset 출력.

        for (i=0; i<remainder; i++){ // 16 bytes 단위로 출력하고 남은 것 출력.

            printf("%02x ", s[i]);

        }

        for (i=0; i<(16-remainder); i++){

            printf(" ");

        }

 

        printf(" ");

        for (i=0; i<remainder; i++){

            if (s[i]>=32 && s[i]<=125) printf("%c", s[i]);

            else    printf(".");

        }

        for (i=0; i<(16-remainder); i++){

            printf(" ");

        }

        printf("\n");

    }

    return;

}    // HexDump.

[ext2_fs.h]

#ifndef _LINUX_EXT2_FS_H

#define _LINUX_EXT2_FS_h

 

#include "Partition.h"

 

//#define min(a,b)        ( ( (a)<(b) ) ? (a) : (b) )

 

#define EXT2_BAD_INO        1

#define EXT2_ROOT_INO    2

#define EXT2_GOOD_OLD_FIRST_INO 11

 

typedef struct ext2_group_desc{

    U32 block_bitmap; //blocks bitmap block

    U32 inode_bitmap;

    U32 inode_table;

    U16 free_blocks_count;

    U16 free_inode_count;

    U16 used_dirs_count;

    U16 pad;

    U32 reserved[3];

}GROUP_DESC;

 

#define EXT2_NDIR_BLOCKS    12

#define EXT2_IND_BLOCK        EXT2_NDIR_BLOCKS

#define EXT2_DIND_BLOCK        (EXT2_IND_BLOCK +1)

#define EXT2_TIND_BLOCK        (EXT2_DIND_BLOCK +1)

#define EXT2_N_BLOCKS        (EXT2_TIND_BLOCK +1)

 

//Revision levels

#define EXT2_GOOD_OLD_REV 0

#define EXT2_GOOD_OLD_INODE_SIZE 128

 

//Inode 구조체

typedef struct ext2_inode{

    U16    i_mode;

    U16    i_uid;

    U32    i_size;

    U32    i_atime;

    U32    i_ctime;

    U32 i_mtime;

    U32    i_dtime;

    U16    i_gid;

    U16    i_links_count;

    U32    i_blocks;

    U32 i_flags;

    U32 l_i_reserved1;

    U32    i_block[EXT2_N_BLOCKS];

    U32    i_generation;

    U32    i_file_acl;

    U32    i_dir_acl;

    U32    i_faddr;

    U32    l_i_reserved2[3];

}INODE;

 

//Super Blocks 구조체

typedef struct ext2_super_block{

    U32 inodes_count;

    U32 blocks_count;

    U32 r_blocks_count;

    U32 free_blocks_count;

    U32 free_inodes_count;

    U32 first_data_block;

    U32 log_block_size;

    U32 log_frag_size;

    U32 blocks_per_group;

    U32 frags_per_group;

    U32 inodes_per_group;

    U32 mtime;

    U32 wtime;

    U16 mnt_count;

    U16 max_mnt_count;

    U16 magic;

    U16 state;

    U16 errors;

    U16 minor_rev_level;

    U32 lastcheck;

    U32 checkinterval;

    U32 creator_os;

    U32 rev_level;

    U16 def_resuid;

    U16 def_resgid;

 

    U32 first_ino;

    U16 inode_size;

    U16 block_group_nr;

    U32 feature_compat;

    U32 feature_incompat;

    U32 feature_ro_compat;

    U8 uuid[16];

    char volume_name[16];

    char last_mounted[64];

    U32 algorithm_usage_bitmap;

 

    U8 prealloc_blocks;

    U8 prealloc_dir_blocks;

    U16 padding1;

 

    //저널링 항목

    U8 journal_uuid[16];

    U32 journal_inum;

    U32 journal_dev;

    U32 last_orphan;

    U32 hash_seed[4];

    U8 def_hash_version;

    U8 reserved_char_pad;

    U16 reserved_word_pad;

    U32 default_mount_opts;

    U32 first_meta_bg;

    U32 reserved[190];

}SUPERBLOCK;

 

#define EXT2_NAME_LEN 255

//디렉토리 엔트리 1 구조체

typedef struct ext2_dir_entry{

    U32 inode;

    U16 rec_len;

    U16 name_len;

    char name[EXT2_NAME_LEN];

} DIRENTRY_1;

 

//디렉토리 엔트리 2 구조체

typedef struct ext2_dir_entry_2{

    U32 inode;

    U16 rec_len;

    U8 name_len;

    U8 file_type;

    char name[EXT2_NAME_LEN];

} DIRENTRY;

 

// 연산 결과를 미리 메모리에 저장하기 위한 구조체

typedef struct ext2_sb_info{

    U32 inodes_per_block; //블록당 inode 수

    U32 blocks_per_group; //Group당 block 수

    U32 inodes_per_group; //Group당 inode 수

    U32 itb_per_group; // group 당 inode table block 수

    U32 gdb_count; // group당 inode table block 수

    U32 desc_per_block; //block 당 group descriptor 수

    U32 group_count; //총 group 수

    SUPERBLOCK *sb; //super block 포인터

    GROUP_DESC * group_desc; //group descriptor table 포인터

    int addr_per_block_bits;

    int desc_per_block_bits;

    int inode_size; //Inode 크기(128 또는 sizeof(ext2_inode)

    int first_ino; //예약되지 않은 최초 Inode 번호

    int blocksize; //block 크기(1024, 2048, 4096)

    int lba_per_block; //1Block당 lba 개수

    int ext2_block_size_bits;

}SBINFO;

#endif

[ReadExt.cpp]

#include "ext2_fs.h"

#include <io.h>

 

int nPstart = 0;

SUPERBLOCK* ReadSuperBlock(){

    int offset =2;

    SUPERBLOCK *sb = (SUPERBLOCK *) malloc(sizeof(*sb));

    //물리적 디스크 drv의 nSecPos번 섹터에서 1개 블록을 읽어옴

    //sector * 2 = 1024(ext default block size)

 

    if(HDD_read(1, nPstart + offset, 2,(U8 *)sb) == 0) {

        printf("Boot sector read failed\n");

        free(sb);

        return NULL;

    }

    return sb;

}

 

GROUP_DESC* ReadGroupDesc(SBINFO* pSbi){ //Group Descriptor Table 을 읽어들임

    GROUP_DESC *pBlkGrps;

    int nBlkGrpNum = pSbi->group_count;

 

    int nBlks=0;

    while(1){

        nBlks++;

        nBlkGrpNum -= pSbi->desc_per_block_bits;

        if(nBlkGrpNum <=0)

            break;

    }

    pBlkGrps = (GROUP_DESC *)malloc(pSbi->blocksize * nBlks);

    if(HDD_read(1, nPstart+pSbi->lba_per_block, nBlks * pSbi->lba_per_block, (U8 *)pBlkGrps) == 0){

        return NULL;

    }

    pSbi->gdb_count = nBlks;

    return pBlkGrps;

}

 

//수정한 Super Blck 파일시스템에 업데이트

BOOL SyncSuperBlock(SBINFO *pSbi){

    int offset = 2;

    if(HDD_write(1, nPstart + offset, 2, (U8 *)pSbi->sb) == 0) {

        return FALSE;

    }

    return TRUE;

}

 

//수정한 Group Descriptor Table 파일시스템에 업데이트

BOOL SyncGroupDesc(SBINFO *pSbi){

    if(HDD_read(1,nPstart + pSbi->lba_per_block, pSbi->gdb_count * pSbi->lba_per_block, (U8 *)pSbi->group_desc) ==0){

        return FALSE;

    }

    return TRUE;

}

 

//Block Input/Output

BOOL BlockIO(SBINFO * pSbi, U32 block, U8 *pBlkBuf, BOOL bWrite){

    if(bWrite){

        if(HDD_write(1, nPstart+ pSbi->lba_per_block * block, pSbi->lba_per_block, pBlkBuf) ==0){

            printf("Block Wirte Failed \n");

            return FALSE;

        }

    }else{

        if(HDD_read(1,nPstart + pSbi->lba_per_block * block, pSbi->lba_per_block, pBlkBuf) == 0){

            printf("Block Read Failed \n");

            return FALSE;

        }

 

    }

    return TRUE;

}

 

//Inode Input/Output(Read or Wirte)

void InodeIO(SBINFO * pSbi, int nIno, INODE *pInode, BOOL bWrite){

    SUPERBLOCK *sb=pSbi->sb;

    GROUP_DESC * gdp;

    U32 nBlockGroup, nBlock, nOffset;

    U8 *pBlkBuf;

 

    //Inode가 속한 Block Group을 알아냄

    nBlockGroup = (nIno - 1) / sb->inodes_per_group;

    gdp = pSbi->group_desc+nBlockGroup;

    

 

    //Inode가 저장된 Block과 offset을 구함

    nOffset = ((nIno - 1) % sb->inodes_per_group) * pSbi->inode_size;

    nBlock = gdp->inode_table + (nOffset >> pSbi->ext2_block_size_bits);

    nOffset &= (pSbi->blocksize - 1);

 

    pBlkBuf=(U8*) malloc(pSbi->blocksize);

 

    BlockIO(pSbi, nBlock, pBlkBuf, FALSE);

    

    if(bWrite){ //inode 쓰기

        memcpy(pBlkBuf + nOffset, pInode, sizeof(INODE));

        BlockIO(pSbi, nBlock, pBlkBuf, TRUE); //읽거나 쓰기

    }else { //inode 읽기

        memcpy(pInode, pBlkBuf + nOffset, sizeof(INODE));

    }

    free(pBlkBuf);

}

    //Super Block의 보조 구조체로 Supber Block을 이용하여 자주 쓰이는 값 저장

void InitSbInfo(SUPERBLOCK * sb, SBINFO * pSbi){

        memset(pSbi, 0, sizeof(SBINFO));

        pSbi->sb = sb;

        pSbi->inode_size = sb->rev_level == EXT2_GOOD_OLD_REV ? EXT2_GOOD_OLD_INODE_SIZE : sb->inode_size;

        pSbi->first_ino = EXT2_GOOD_OLD_FIRST_INO;

        pSbi->blocksize = 1024 << sb->log_block_size;

        pSbi->blocks_per_group = sb->blocks_per_group;

        pSbi->inodes_per_group = sb->inodes_per_group;

        pSbi->inodes_per_block = pSbi->blocksize / pSbi->inode_size;

        pSbi->itb_per_group = pSbi->inodes_per_group/pSbi->inodes_per_block;

        pSbi->desc_per_block = pSbi->blocksize/sizeof(GROUP_DESC);

        pSbi->addr_per_block_bits = pSbi->blocksize / sizeof(U32);

        pSbi->desc_per_block_bits = pSbi->blocksize/ sizeof(GROUP_DESC);

        pSbi->group_count= (sb->blocks_count - sb->first_data_block + sb->blocks_per_group -1) /sb->blocks_per_group;

        pSbi->lba_per_block = pSbi->blocksize/512; //블록 사이즈를 섹터 단위 (4K/512 = 8) 8개섹터가 1블럭

        pSbi->ext2_block_size_bits = sb->log_block_size+10;

        pSbi->group_desc = ReadGroupDesc(pSbi);        

}

 

int SearchFile(SBINFO * pSbi, char *pszPath, int *pidir){ //주어진 경로의 파일을 Ext2(또는 Ext3) 파일시스템에서 찾는다.

    char aszTmp[256];

    char *pszStr = NULL;

    U8 *pBuf = NULL;

    INODE TmpInode;

    DIRENTRY *pTmpDir = NULL;

    int i, inum;

    char* type;

 

    strcpy_s(aszTmp,sizeof(aszTmp), pszPath);

    pszStr = strtok_s(aszTmp, "/",&type);

        

    //ROOT DIR

    if(pszStr == NULL)

        return EXT2_ROOT_INO;

    inum = EXT2_ROOT_INO;

    

    pBuf = (U8 *)malloc(pSbi->blocksize);

 

    while(pszStr)

    {

        InodeIO(pSbi, inum, &TmpInode, FALSE);

        if(TmpInode.i_mode & 0x4000 == 0){ //디렉토리인지 검사

            free(pBuf);

            return 0;

        }

 

        *pidir = inum;

        BlockIO(pSbi, TmpInode.i_block[0], pBuf, FALSE);

        pTmpDir = (DIRENTRY *)pBuf;

        i = inum = 0;

 

        while( i < pSbi->blocksize){ //블록내를 돌며 같은 디렉토리 찾기

            if(strncmp(pszStr, pTmpDir->name, pTmpDir->name_len) == 0){

                inum = pTmpDir->inode;

                break;

            }

 

            i += pTmpDir->rec_len;

            pTmpDir = (DIRENTRY *)((char*) pTmpDir + pTmpDir->rec_len);

        }

        

        if(!inum){ //해당 디렉토리를 찾지 못했다면 리턴

            free(pBuf);

            return 0;

        }

        pszStr = strtok_s(NULL, "/",&type);

    }

    free(pBuf);

    return inum;

}

 

//파일 데이터를 읽어들이기 위해 pnPtr i_block 배열(pnPtr)의 Block 데이터를 읽어 드림

int ReadBlocks(SBINFO * pSbi, char *pData, int * pnPtr, int nLimitBlock, int nLimitSize){

    int nReadSize = 0, nToRead, i;

    U8 *pBuf = (U8*) malloc(pSbi->blocksize);

    for(i = 0 ; i<nLimitBlock && nReadSize < nLimitSize ; i++)

    {

        BlockIO(pSbi, pnPtr[i], pBuf, FALSE);

        nToRead = min(nLimitSize - nReadSize, pSbi->blocksize);

        memcpy(pData+nReadSize, pBuf, nToRead);

        nReadSize += nToRead;

    }

    free(pBuf);

    return nReadSize;

}

 

//Ext2 파일시스템의 파일을 읽어들임

char * ReadExtFile(SBINFO *pSbi, INODE *pInode){

    char *pData = (char *)malloc(pInode->i_size);

    U8 *pBuf, *pBuf2;

    U32 nReadSize = 0, int_per_block, i;

 

    int_per_block = pSbi->blocksize/4;

    nReadSize = ReadBlocks(pSbi, pData, (int*)(pInode->i_block), EXT2_NDIR_BLOCKS,pInode->i_size );

    if(nReadSize == pInode->i_size)

        return pData;

 

    //간접 블록

    pBuf = (U8*) malloc(pSbi->blocksize);

    BlockIO(pSbi, pInode->i_block[EXT2_IND_BLOCK], pBuf, FALSE);

    nReadSize += ReadBlocks(pSbi, pData+nReadSize, (int*)pBuf, int_per_block, pInode->i_size-nReadSize);

    if(nReadSize == pInode->i_size){

        free(pBuf);

        return pData;

    }

 

    //이중 간접 블록

    BlockIO(pSbi, pInode->i_block[EXT2_DIND_BLOCK], pBuf, FALSE);

    pBuf2 = (U8*)malloc(pSbi->blocksize);

    for(i=0; i<int_per_block ; i++){

        BlockIO(pSbi, pBuf[i], pBuf2, FALSE);

        nReadSize += ReadBlocks(pSbi, pData+nReadSize, (int*)pBuf, int_per_block, pInode->i_size - nReadSize);

 

        if(nReadSize == pInode->i_size){

            free(pBuf);

            free(pBuf2);

            return pData;

        }

    }

    free(pBuf);

    free(pBuf2);

    return NULL;

}

 

void WriteLocalFile(char *pszFile, char *pBuf, U32 nSize){

    FILE *pFile ;

    fopen_s(&pFile,pszFile, "wb+");

    fwrite(pBuf, 1, nSize, pFile);

    fclose(pFile);

}

 

U32 main(int argc, char *argv[]){

    PARTITION PartitionArr[50];

    U32 nPartitionCnt = 0;

    SUPERBLOCK *sb;

    SBINFO sbi;

    INODE Inode;

    int inum = 0, idir;

    char *pBuf = NULL;

    

    if(argc == 1 || argc== 2){

        printf("using : \"파일명\" \"대상 경로\" \n");

        return 0;

    }

    //파티션 정보 읽어오기

    memset(&PartitionArr, 0, sizeof(PARTITION) * 50);

    nPartitionCnt = GetLocalPartition(1, PartitionArr);

    nPstart = PartitionArr[0].start;

    

    //super Block 얻기

    sb = ReadSuperBlock();

    InitSbInfo(sb, &sbi);

 

    inum = SearchFile(&sbi, argv[1], &idir);

 

    if(!inum) return 0;

    InodeIO(&sbi, inum,&Inode, FALSE);

    pBuf = ReadExtFile(&sbi, &Inode);

    if(pBuf)

        WriteLocalFile(argv[2], pBuf, Inode.i_size);

    

    //자원 해제

    if(pBuf)

        free(pBuf);

    free(sbi.group_desc);

    free(sb);

    

    return 0;

}

2) Ext2 포맷   하나의 파일을 생성 하도록 하자. (USB 이용한 경우)

포맷 하는 방법은 http://blog.naver.com/bitnang/70186337222

에서 Ext2 포맷을 하고 파일을 생성하는 방법을 이용하거나 각자의 방법으로 포맷을 하도록 하면 된다.

 

아래 그림 처럼 USB ext2 포맷을 하였다.

그리고 그림에는 없지만 마운트를   hello.txt 리눅스 환경에서 작성하였다.

 

 

윈도우에서 ExtFS for Windows 이용하여 열어보았다.

 

 

 

CMD 실행할  관리자 권한으로 실행하여야 한다.

컴파일한 파일 [ext2 저장된 파일명] [저장할 위치(디렉토리는 만들어 두어야 )]

하게 되면 정상적이라면 아래와 같이 Ext2 파일을 NTFS파일 시스템인 D드라이브로 복사가  것을   있다.

 

 

+ Recent posts