Notice
Recent Posts
Recent Comments
Today
Total
04-16 07:26
Archives
관리 메뉴

Jeongchul Kim

파일 시스템 본문

Linux

파일 시스템

김 정출 2015. 12. 19. 01:53

Chapter 5 파일 시스템


5.1 개요

파일 시스템은 디스크나 입출력 장치에 정보를 저장하거나 입/출력 및 검색 등을 하기 위한 운영체제의 구성 요소를 말한다.

파일 시스템은 장치로의 입출력이나 저장 형태 등, 장치에 의존적인 복잡한 부분을 감추고 사용자에게 논리적이고 장치에 독립적인 쉬운 사용 인터페이스를 제공하기 위해 존재한다. 예를 들면, 디스크 파일 같은 경우, 실제 입출력을 위한 접근은 트랙(track)이나 섹터(sector) 번호 등으로 해야 하지만 이처럼 복잡한 부분은 커널(kernel)이나 라이브러리가 담당하고, 사용자에게는 디렉토리, 파일 이름 및 상대적 접근 위치(read-write offset)에 의한 논리적인 순차적 접근 방법을 제공한다.


또한, Sun사의 NFS 같은 경우는 원격 호스트의 파일에 대해 통일된 디렉터리 구조를 제공하여 일반 국부(local) 파일과 같이 접근할 수 있도록 한다. 이와 같은 파일 시스템의 일반적인 기능은 다음과 같다.


⚫ 논리적 입출력 접근 방법의 제공

⚫ 파일의 보호

⚫ 계층 구조적 디렉토리 구조의 제공

⚫ 파일 시스템 형태에 독립적인 인터페이스의 제공(Virtual File System)

⚫ 버퍼링 ( S/W caching )에 의한 빠른 접근


리눅스에서 제공되는 파일의 형태에는 다음과 같다.

⚫ 정규 파일(regular file) 텍스트나 이진 파일 등, 커널에서는 구분이 없다.

⚫ 디렉터리 파일(directory file) 어떤 디렉터리에 속한 파일의 이름과 정보를 갖는 파일

(실제 디렉터리는 영역이 아닌 디렉터리에 속한 파일들의 정보 수록 파일이다)

⚫ 문자 특수 파일(character special file) 문자 단위로 입 출력하는 장비에 접근하기 위한 특수 파일로 일반 파일과 같은

논리적인 접근 방법을 제공하기 위한 파일

⚫ 블록 특수 파일(block special file) 블록 단위로 입출력 하는 장비에 접근하기 위한 특수 파일(논리적 접근 방법 제공)

⚫ FIFO 파일 : 프로세스들 사이에서 파이프(pipe) 통신을 하는 데 사용되는 파일.

부모 자식 프로세스 간에 사용하는 unnamed pipe와 혈통 관계가 없는 프로세스 간에 사용하는 named pipe가 있다.

⚫ 간접 링크 파일(symbolic line or soft link file) 다른 파일로의 포인터를 갖는 파일


본 장에서는 이러한 파일 시스템의 내부 구조와 커널 수준에서 제공하는 file descriptor 입출력에 관해 설명한다. 표준 입출력 라이브러리와 파일 구조체에 의한 입출력들도 내부적으로는 file descriptor 입출력을 사용하고, 추가로 사용자 수준의 버퍼링을 제공한다.




5.2 파일시스템 inode

정규 파일(regular file)을 포함한 모든 리눅스의 파일은 그 파일에 관한 모든 정보를 수록하는 일종의 파일 헤더인 inode를 가진다.

리눅스 운영체제는 실질적으로 inode 정보에 의해 모든 파일에 대한 접근을 해결한다.

이러한 inode에는 일련 번호가 주어지고, 디스크 파일 시스템의 특정 위치에 배열(array) 형태로 존재하므로,

inode의 번호만 알면 파일에 접근하기 위한 모든 정보를 얻을 수 있다. 따라서 사용자가 지정한 모든 파일명(path name)은

커널 내부에서의 접근을 위해 inode 번호로 바뀌어야 한다.


사용자에게는 디렉토리, 서브 디렉토리 및 파일 이름에 의한 계층 구조적 파일 체계가 제공되지만, 커널 내부에서는 단일 계층 구조인 inode array를 사용하는 것이다. 이러한 inode 정보는 파일 시스템 유형(ext2, ufs, sysv, nfs, 등)에 따라 약간 씩 차이가 나는데 다음은 일반적인 inode 내의 주요 정보 요약


inode 내의 정보

inode 번호;

파일 생성자 정보(owner, group user id);                       파일 시간 정보(생성, 수정, 접근 시간)

입출력 장치 정보;                                                    파일 크기 정보;

파일 lock 정보;                                                       파일 type;

파일 위치 정보;(디스크 파일인 경우, 각 데이터 블록의 디스크 위치 정보);

기타;        






5.3 파일 시스템과 마운트(mount)

디스크 파일 시스템의 경우, 일반적으로 고정된 크기로 나누어진 디스크의 파티션(partition)은 하나의 독립된 파일 시스템을 구성한다.

이러한 파일 시스템은 부팅 과정에서 계층 구조적 디렉토리 구조의 한 디렉토리에 마운트(mount)될 수 있다.

즉 어떤 디스크 파티션의 root 디렉토리는 부팅 시, root 파일 시스템의 /usr 디렉토리에 연결될 수 있는데 이는 마운트를 통해 가능하다. 따라서 mount는 독립적인 파일 시스템을 하나의 계층 구조로 연결하는데 사용되는 커널 함수이다.

그러나 부팅 시에 기본적으로 커널 이미지를 저장하고 있는 root file 시스템은 제공되어야 한다.

<그림> 파일 시스템과 마운트의 개념




5.4 파일 시스템(파티션) 구조

하나의 파일 시스템을 구성하는 파티션은 다음과 같은 정보 블록들로 구성된다.


Boot Block(Bootsrtap Loader)


Super Block

              free inode 정보

              free disk block 정보

inode Area

              이 파일 시스템에 속한 file들의 inode array


Data Area

              디스크 데이터 블록들(각 블록의 크기는 파일 시스템 초기 생성 시 줄 수 있다.) 일반적으로 4k Byte


⚫ Block block : 부팅 시 kernel을 메모리로 적재하기 위한 bootstrap loader가 저장된다.

⚫ Super block : inode area 내에 사용하지 않은 inode에 관한 정보와 사용하지 않는 data block에 관한 정보를 갖고 있다.

                    free inode 정보는 파일의 생성이나 삭제 시에 사용되고 수정된다. 또한, free data block 정보는 파일의 데이터

                    블록 생성이나 삭제 시에 사용되고 수정된다.

⚫ inode Area : inode의 배열로 구성된다.

⚫ Data Area : 실제 파일의 데이터 블록이 할당되는 주소이다.


5.4.1 inode 내의 파일 데이터 블록 정보

각 파일의 inode에는 파일에 관한 여러 정보가 저장되는데 이 중에는 이 파일을 구성하는 데이터 블록들의 위치 정보가 저장된다.

파일의 각 데이터 블록들은 디스크 데이터 영역에서의 자유로운 할당과 반환을 위해 물리적으로 흩어진 위치에 할당되는 것이 보통이다(uncontiguous allocation. 따라서 여러 블록으로 구성되는 디스크 파일에 접근하려면 파일을 구성하는 모든 블록의 디스크 블록 주소를 알아야 한다.


즉 inode는 이러한 디스크 블록 주소를 저장해야 하는데, 문제는 각 파일들의 크기(블록의 개수)가 일정치 않다.

(inode area는 배열로 구성되므로 각 inode의 크기는 일정해야 한다)

이러한 문제를 해결하기 위해서 유닉스 및 리눅스 계열의 운영체제 들은 inode 내의 디스크 블록 주소 테이블은 일정한 개수의 포인터를 갖게 하고(13~15개), 이들 중 일부는 또 다른 디스크 블록 주소 테이블을 포인트(가리키다) 하는 indirection을 사용한다.

indirection은 일반적으로 3 level까지 사용하는 것이 보통이다

.

<그림> inode 데이터 블록 포인터 테이블 구조


위와 같은 디스크 블록 주소 테이블 구조는 inode의 크기를 일정하게 유지하면서 크기가 큰 파일의 뒤 부분에 있는 블록까지의 주소를 모두 알 수 있도록 하는 구조이다.

Indirection에 의한 블록의 주소 찾기에서 필요한 포인터 블록들은 시스템 차원에서 메모리에 캐싱은 되지만, 뒷 부분에의 접근 시에 디스크 입출력의 확률이 커지는 것은 당연한 결과이다.

특히 대형 파일에서 순차적 접근이 아닌 임의 접근을 사용하는 경우 캐싱으로도 많은 포인터 블록을 감당하기 힘들어지고,

결과적으로 많은 디스크 입출력이 발생하여 속도의 저하를 가져오게 된다. 즉 위와 같은 구조는 임의 접근을 많이 사용하는

대형 데이터베이스의 기본 파일 구조로는 적합하지 않다.

근래에는 대형 파일을 지원하기 위해 64bit offset을 지원하는 large file sysem도 제공되고 있다.





5.5 디렉토리 파일과 파일 접근

일반적으로 사용자에 의해 주어지는 파일의 이름은 path name이라 한다. 이는 파일의 이름 앞에 일련의 디렉토리 계층 구조가 붙기 때문이다. 예를 들면 “/Home/program.c”와 같은 경우이다.

파일 접근 시 앞의 directory path가 생략되는 것은 current directory 개념이 있기 때문이다.

모든 파일로의 접근을 위해서는 path name이 inode 번호로 바뀌어야 하는데 이는 디렉토리의 파일을 이용함으로써 가능하다.

(디렉토리 파일은 자기 디렉토리에 소속된 파일의 이름과 inode 번호를 갖는 특수한 파일이다.


원하는 파일의 inode 번호를 찾는 과정은 다음과 같다. (“/Home/program.c” 찾는 과정)


⚫ root 디렉토리 파일은 inode 2번이고, 이에 의해 root 디렉토리 파일의 내용을 찾을 수 있다.

⚫ root 디렉토리 파일의 내용 중에서 Home 파일의 inode 번호를 찾는다.

⚫ Home의 inode 내의 블록 부속 테이블에 의해 Home 파일의 내용을 읽는다.

⚫ Home의 파일 내용 중 program.c 의 inode 번호를 찾아 이를 읽는다.

⚫ program.c의 inode 내의 블록 주소 테이블의 내용으로 program.c의 내용에 접근할 수 있다.

<그림> 디렉토리 탐색에 의한 파일 접근

이와 같이 접근을 원하는 파일의 inode를 찾기 위한 과정을 path name traverse 또는 inode lookup이라고 한다.

그러나 매번 파일을 오픈 할 때마다 이러한 과정을 반복하는 것은 디스크 입출력을 유발하게 된다.

이러한 과정을 축소하고 디스크 입출력을 줄이기 위해서 리눅스는 current working directory 정보를 가지고 있고,

또한, 자주 사용되는 파일 이름과 inode 번호를 저장하는 name cache를 사용하며, 메모리에 적재된 inode 자체를 버퍼링하는

inode cache 및 directory cache도 사용한다.




5.6 파일 입출력

파일에 대한 입출력은 standard I/O 라이브러리나 파일 시스템 호출 인터페이스에 의해 가능하다.

커널이 직접 제공하여 이들의 기반이 되는 file descriptor I/O에 대해 살펴본다.


모든 파일에 대한 입출력은 밑의 과정을 거쳐야 한다.

⚫ 파일 생성 또는 open

⚫ 파일 입출력

⚫ 파일 close

파일 입출력 이전에 반드시 파일을 open 해야 하는 이유를 이해하기 위해 open의 기능을 살펴본다.



Open의 기능

⚫ 파일의 존재 여부 또는 파일에 대한 올바른 접근 권한이 있는지 검사한다.

⚫ 파일 접근 시에 항상 필요한 inode를 매 접근 시마다 디스크에서 참조하면 큰 오버헤드(Overhead)이므로,

              inode를 번호를 알아 inode의 내용을 메모리에 가져오고 파일의 사용 기간 중에 메모리에 유지한다.


파일을 open 하게 되면 inode가 메모리에 적재되고 이를 프로세스와 연결하기 위한 커널 자료구조의 링크가 형성되는데, 이들은 태스크 구조체 내의 files 포인터에 의한 files_struct(프로세스 별 open file table) 정보와 이 테이블의 각 open file table pointer array에 의한 file 구조체(system-wide open files) 정보 및 file 구조체가 포인트하는 inocore inode 정보(system-wide)로 구성된다.


*fs_sruct

    ⚫ close_on_exec :

          일반적으로 exec 계열의 시스템 호출을 하면 새로 적재되는 프로그램은 open된 파일들을 그대로 유지한다.

          그러나 이 flag가 set 되어 있으면 exec 실행 시 모든 파일을 close 한다.

    ⚫ fd[i] :

          0부터 시작되는 file descriptor table이다. 0,1,2의 경우는 fork의 상속으로 stdin, stdout, stderr 파일로

          이미 open 되어 있는 것이 보통이다.

* files 구조체

    ⚫ f_pos : 파일 접근을 위한 논리적 read/write offset

    ⚫ f_count : 여러 프로세스가 파일을 공유할 때의 reference count

    ⚫ f_inode : inode inode pointer


<그림> 파일 open 시 관련 커널 자료구조

5.6.1 파일 입출력 함수


OPEN(2)

#include <sys/types.h>

#include <sys/stat.h>

#include <fnctl.h>


int open(const char *path, int flags);

int open(const char *path, int flags, mode_t mode);


입력 값

     path : 열고자 하는 파일의 이름 (경로명 포함)

     flags : 여는 파일에 적용될 선택사항

     mode : 파일 생성 시의 access permission 모드

반환 값

    정상 : 파일 식별자

    에러 : -1

flags의 주요 옵션

    O_RDONLY : 읽기 전용 모드

    O_WRONLY :  쓰기 전용 모드

    O_RDWR : 읽기 쓰기 모드

    O_CREAT : 파일 부재 시, 파일 생성

    O_TRUNC : 파일이 있으면 모든 내용 삭제 후 처음부터 접근

    O_APPEND : 파일의 기존 내용 뒷 부분 부터 쓰기



CLOSE(2)

#include <unistd.h>


int close(int fildes);

입력 값

     fildes : 닫고자 하는 파일 식별자

반환 값

     정상 : 0, 에러: -1



OPEN(2)

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>


int creat(const char *path, mode_t mode);


입력 값

      path : 생성하고자 하는 파일 이름(경로명 포함)

      mode : 파일 생성 시 모드

반환 값

      정상 : 파일 식별자, 에러 :  -1


READ(2)

#include <unistd.h>


ssize_t read(int files, void *buf, size_t nbyte);

입력 값

     fildes : 읽어 들이고자 하는 파일의 식별자

     buf : 읽어 들일 버퍼

     nbyte : 읽어 들일 바이트 수

반환 값     정상 : 읽어 들인 바이트 수, 에러 : -1

위의 read 함수에서 읽어 들인 byte의 수가 0으로 return 되면 이는 EOF(End Of File)을 의미


WRITE(2)

#include <unistd.h>


ssize_t wirte(int fides, const void *buf, size_t nbyte);

입력 값

     fildes : 기록하고자 하는 파일의 식별자

     buf : 기록하고자 하는 내용을 담고 있는 버퍼

     nbyte : 기록하고자 하는 바이트 수

반환 값

     정상 : 기록한 바이트 수, 에러 : -1


파일을 다른 파일로 복사하는 프로그램의 예제

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdlib.h> /* exit() */


#define BUFFER_SIZE 1024


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

{

    int fdin, fdout, n;

    char buf[BUFFER_SIZE];

    

    if(argc != 3) {

         perror(argv[0])

         exit(1);

    }

  

    if((fdin = open(argv[1], O_RDONLY)) == -1) {

           perror(argv[1]);

           exit(1);

    }

    if((fdout = open ( argv[2], O_WRONLY | O_CREAT, S_IRUSR | S_IMUSR)) == -1 ) {

            perror(argv[2]);

            exit(1);

    }

 

    while((n = read(fdin,buf,BUFFER_SIZE)) != 0)

            write(fdout,buf,n);


    close(fdin);

    close(fdout);

}


위의 파일 복사 프로그램에서 한 번에 읽는 사용자 버퍼의 크기는 1024로 되어 있다. 커널 차원에서 디스크 버퍼링을 제공하므로 이 버퍼의 크기와 디스크 입출력의 횟수는 대부분 무관하다. 그러나 시스템 호출 자체를 적게하고 버퍼링에 의한 커널 내부의 디스크 입출력이 복잡해지지 않도록 한 번에 읽어 오는 크기는 가능하면 물리적 블록 크기의 배수로 하는 것이 프로그램의 실행 속도를 빠르게 한다.
















5.7 파일에의 임의 접근(Random Access)

read나 write 시스템 호출은 기본적으로 read/write offset에 의한 순차적 접근 방식을 제공한다. 리눅스는 이와 함께 어떤 파일을 같은 크기의 레코드 단위로 레코드 순번(위치)에 의해 접근하는 임의 접근 방식(random access)도 제공하는데 이때 사용되는 함수가 lseek이다.  lseek 함수는 파일의 현재 read/write offset을 원하는 위치로 바꾸어 그 위치에서 부터 파일에 접근할 수 있도록 한다.



LSSEK(2)

#include <sys/types.h>

#include <unistd.h>


off_t lseek(int fildes, off_t offset, int whence);

입력 값

      filedes : 열린 파일 식별자

      offset : 설정하고자 하는 위치

      whence : 초기 위치에 대한 값

반환 값

      정상 : 새로운 파일 offset , 에러 :  -1

whence

       - SEEK_SET : 주어진 offset 값 과 같은 위치에 파일 위치 설정

       - SEEK_CUR : 주어진 offset 값과 현재 offset의 합을 파일 위치로 설정

       - SEEK_END : 주어진 offset 값과 파일의 현재 크기의 합을 파일 위치로 설정



record 구조체에 의해 정의 되는 레코드(record) 1024개를 가진 파일에 대해 레코드의 순번으로 임의 접근을 수행하고 읽기/삽입/수정을 실행하는 프로그램

#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>


#define NUM_RECORDS 1024


struct record {

   int id;

   char name[20];

};


void input_record_contents(struct record *current);


int main() {

    struct record current;

    int record_no;

    int fd, pos, i, n;

    char yes;

    fd = open(“testdata”, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);

    current.id = -1;   //empty record

    // initialize with empty record

    for(i=0; i<NUM_RECORDS; ++i)

            wrtie(fd, &current, sizeof(struct record));


    printf(“enter record number\n”);

    scanf(“%d%*c”, &record_no);


    while( record_no >= 0 && record_no < NUM_RECORDS) {
          pos = record_no * sizeof(struct record);

          lseek(fd, fos, SEEK_SET);


          n = read(fd, &current, sizeof(struct record));


          if(current.id == -1)

                 printf(“record empty\n”);

          else {

                 printf(“record id = %d\n”,current.id);

                 printf(“name=%s\n”,current.name);

           }


           printf(“update or insert ? yes  = y\n”);

           scanf(“%c*c”, &yes);


           if(yes == ‘y’) {

                 printf(“enter new contents\n”);

                 input_record_contents(&current);

                 lseek(fd, pos, SEEK_SET);

                 write(fd, &current, sizeof(struct record));

           }

           printf(“enter next record number, -1 = EXIT \n”);

           scanf(“%d%*c,&record_no);

     }

     close(fd);

     return 0;

}


void input_record_contents(struct record *current)

{

       printf(“record id = “);

       scanf(“%d%*c”, &(current->id));

       printf*”name = “);

       scanf(“%s%*c”, &(current->name));

}


위의 예에서 레코드의 번호를 입력받아 lseek와 read 후, 이 레코드에 다시 수정된 내용을 입력할 때에는 다시 lseek를 하여야 함에 주의하여야 한다. 이는 read에 의해 offset이 레코드의 크기만큼 앞으로 나갔기 때문이다. 또한, 입력하는 레코드 번호가 마지막에 저장된 레코드의 번호보다 클 때에 lseek와 read를 수행하면 EOF로 반환되므로 통상 최대 레코드의 수를 정하고 마지막에는 마지막 표시 레코드를 미리 삽입해두어야 한다.




5.8 파일의 제어

open된 파일에 대해 여러가지 상태를 설정하거나 조사할 때에 대표적으로 사용되는 함수로 fcntl


FCNTL(2)

#include <unistd.h>

#include <fcntl.h>


int fcntl(int fd, int cmd):

int fcntl(int fd, int cmd, long arg);

int fcntl(int fd, int cmd, struct flock *lock); // file lock

입력 값

        fd : 제어하고자 하는 파일 descriptor

        cmd : 원하는 파일 제어

        arg : cmd 인수에서 필요한 값

반환 값

       정상 : cmd에 따른 값 , 에러 : -1

cmd

        - F_DUPFD : dup, arg로 주어진 번호보다 크거나 같은 값에서 dup, 새로운 file descriptor 반환

        - F_DUPFD : close_on_exec flag 조사, flag의 값 return

        - F_DUPFD : close_on_exec flag를 arg에 따라 set, 0을 return

        - F_DUPFD : 파일 상태 flag return

        - F_DUPFD : 파일 상태 flag set(O_APPEND, O_NONBLOCK, O_SYNC, O_DIRET flag), 0을 return







다음 예제 프로그램은 fnctl을 이용하여 이미 오픈된 stdout 파일을 다른 식별자로 복사하는 예제이다.

이처럼 일반적으로 fcntl은 대부분의 다음 파일 관련 함수의 기능을 수행할 수 있는 일반적 API이다.

#include <stdio.h>

#incdlue <sys/types.h>

#include <fcntl.h>

#include <unistd.h>

#include <stdlib.h>

#include <string.h>


#define MESSAGE “message to stdout \n”


int main(void)

{

      int newfd;


      if(( newfd = fcntl(STDOUT_FILENO, F_DUPFD, 3)) == -1) {

             perror(“fcntl”);

             exit(1);

      }


      close(STDOUT_FILENO);

      write(3, MESSAGE, strlen(MESSAGE) ); // 실제 모니터로 출력됨

      return 0;

}



다음 프로그램은 fcntl을 이용하여 파일의 open된 상태 중 CLOSE_ON_EXEC flag를 조사하는 프로그램이다.

CLOSE_ON_EXEC 이 설정되어 있으면 exec 계열 함수 호출 시, 이미 열려 있던 파일들은 모두 close된다.



#include <stdio.h>

#include <sys/types.h>

#include <fcntl.h>

#include <unistd.h>

#include <stdlib.h>


int main(void)

{

      int flag;


      //read the close-on-exec flag

      if(( flag = fcntl( STDIN_FILENO, FGETFD)) == -1) {

             perror(“fcntl”);

             exit(1);

      }


      printf(“close on exec flag = %d\n”,flag);

      return 0;

}







































































'Linux' 카테고리의 다른 글

ch02 시스템 구조  (0) 2016.02.04
메모리 관리  (0) 2015.12.19
리눅스 스케줄링  (0) 2015.12.19
프로세스와 쓰레드  (0) 2015.12.19
리눅스 활용을 위한 기본 지식  (0) 2015.12.19
Comments