Skip to content

Latest commit

 

History

History
140 lines (108 loc) · 13.3 KB

파일 입출력.md

File metadata and controls

140 lines (108 loc) · 13.3 KB

파일 입출력

파일은 읽거나 쓰기 전에 반드시 열어야한다.

커널은 파일 테이블이라고 하는 프로세스별로 열린 파일 목록을 관리한다.

  • 이 파일은 음이 아닌 정수 값으로 저장되며 파일 디스크립터 (fd) 로 인덱싱 되어 있다.
  • 이 테이블의 각 항목은 열린 파일에 대한 정보를 담고 있다. 여기에는 메모리에 복사된 inode 를 가리키는 포인터와 각종 메타데이터 (파일 위치와 접근 모드)가 포함되어 있다.
    • 파일 오프셋, 파일 상태 (읽기/쓰기 모드), 접근 권한 등이 있다.

파일을 열어서 파일 데이터를 읽는 과정을 생각해보면 이렇다.

  • 파일을 열면 운영체제는 파일 디스크립터를 생성하고 파일 테이블에 이를 인덱싱한다. 그리고 이를 이용해서 파일 정보 (파일 오프셋, 상태, inode 에 대한 참조) 들을 읽을 수 있음.
  • 데이터를 읽으려면 파일 디스크립터를 통해서 inode 를 찾아서 데이터 블록의 위치를 파악하고, 파일 오프셋 위치부터 읽을 바이트의 수 만큼 데이터를 읽어온다.
  • 데이터를 읽어오면 파일 오프셋을 갱신한다. 오프셋은 읽은 바이트 수만큼 증가한다.

파일 디스크립터는 C 의 int 형 자료이다. 그리고 리눅스 프로세스가 열 수 있는 최대 파일 개수는 정해져있다.

  • 그래서 0 부터 이 값까지 파일을 열 수 있다. 기본적 값으로 최대 1,024 이지만 1,048,576 까지 설정할 수 있다.
  • 파일 디스크립터는 음수가 될 수 없기 때문에 -1 이 나온다면 이는 에러를 나타낸다.

프로세스가 시작 될 때 기본적으로 3가지의 파일 디스크립터가 생성된다. 이건 표준 입출력과 연관되어있다.

  • 0 은 표준 입력 (Standard Input)
    • 키보드와 같은 사용자 입력 장치와 연결되어있다.
  • 1 은 표준 출력 (Standard Output)
    • 터미널 콘솔 창과 같은 출력 장치와 연관되어있다.
  • 2 은 표준 오류 출력 (Standard Error Output)
    • 터미널 콘솔 창과 같은 출력 장치와 연관되어 있지만 에러메시지와 같은 에러 상황에서의 출력을 담당한다.
  • 내가 잘못 생각했는데 프로세스에서 파일을 열면 3 이상의 파일 디스크립터가 생성될 것이다. 0,1,2 는 기본적으로 프로세스가 시작될 때 열리는 파일 디스크립터다.

파일 디스크립터는 단순히 일반 파일만 나타내는 것이 아니다. 유닉스에서는 모든 것이 파일이므로 파이프, 디렉터리, 장치 파일, 퓨텍스, FIFO, 소켓 접근 등과 같은 모든 곳에 파일 디스크립터가 사용된다.

기본적으로 자식 프로세스는 부모 프로세스가 소유한 파일 테이블의 복사본을 상속받는다.

  • 열린 파일, 접근 모드, 파일 오프셋 등의 목록은 동일하다.
  • 자식 프로세스가 파일을 닫는 등의 변화는 다른 프로세스의 파일 테이블에 영향을 미치지 않는다. (복사본을 받으니까 영향이 없는거인듯.)

파일에 대한 설명

우리가 부르는 파일은 일반 파일을 의미하며 바이트 스트림 형식으로 기록되어 있다. 바이트 배열이라고 생각하면 된다.

  • 리눅스에서는 파일에 대한 특별한 자료구조가 없다.

파일은 바이트를 읽고 쓰는 것이 가능한데 이를 위해서는 바이트의 위치를 알아야 하며 이것과 관련된 게 파일 오프셋 (file offset or file position) 이라고 한다.

파일 오프셋은 열린 파일에서 관리하는 주요한 메타 데이터 중 하나이다.

  • 파일이 처음 열리면 파일 오프셋은 0 이다.
  • 보통 파일은 바이트 단위로 읽고 쓰기 때문에 바이트 단위의 숫자로 증가하거나 감소한다.
  • 파일 오프셋은 0부터 시작하며 음수가 될 수 없다.
  • 파일 오프셋은 직접 지정할 수 있으며 파일의 끝을 지정할 수도 있다. 파일의 끝을 넘어서는 지정도 가능하며 이 경우에는 파일의 끝에서부터 해당 위치까지는 바이트가 0 으로 기록된다.

파일 중간에 데이터를 기록하면 이미 존재하는 데이터는 덮어쓰여진다. 따라서 파일의 중간에 데이터를 쓰는 것으로 파일을 확장하는 것보다 파일의 끝에서 주로 데이터를 쓴다.

파일 오프셋의 최대 크기는 C언어의 off_t 타입의 크기로 결정되며 최신 리눅스 시스템에서는 64bit 값이다.

파일의 길이는 바이트배열의 길이이다.

하나의 파일은 다른 프로세스에서도 열 수 있고, 동일 프로세스에서도 한 번 이상 열 수 있다. 그러나 파일은 열릴 때마다 고유한 파일 디스크립터를 반환한다.

그렇지만 프로세스는 파일 디스크립터를 공유할 수 있다. 하나의 파일 디스크립터는 하나 이상의 프로세스에서 사용될 수 있다. 커널은 파일에 대한 동시 접근을 막지 않는다.

여러개의 프로세스에서 동시적으로 파일을 읽거나 쓰는 것도 가능하다.

  • 그렇지만 동시 접근은 연산 순서에 대해서 다른 결과를 낼 수 있기 때문에 사용자 영역의 프로그램은 이러한 것들을 회피하도록 신경 써야한다.

일반적으로 파일 이름을 통해서 파일을 접근하지만 파일에 대한 접근은 파일 이름과 직접적인 연관은 없다.

  • 파일은 inode (information node) 라고 하는 파일 시스템 내에서 고유한 정수 값으로 참조된다.
  • 이 값은 inode 번호 라고 하며 ino 라고 줄여 쓰기도 한다.
  • inode 는 변경된 날짜, 소유자, 타입, 길이, 데이터 저장 위치 등과 같이 파일에 대한 메타 정보를 관리한다. 하지만 여기에는 파일 이름은 포함되어 있지 않다.
  • inode 는 디스크에 저장되는 물리적인 객체임과 동시에 리눅스 커널에서 자료구조로 표현되는 논리적인 개념이기도 하다.
  • inode 에는 파일의 실행중인 정보인 파일 오프셋과 같은 정보는 저장되어 있지 않다.
  • 파일 테이블에서 inode 에 대한 참조를 포함하고 있는 것.

디렉터리와 링크

inode 번호로 파일에 대한 접근을 하면 귀찮고 보안 문제가 있기 때문에 일반적으로 파일 이름을 통해서 접근한다.

디렉터리는 파일에 대한 접근을 위해서 이름을 제공한다. 이렇게 파일 이름과 inode 의 쌍을 링크라고 한다.

파일과 디렉터리는 유사하지만 디렉터리는 파일 이름과 inode 매핑을 저장한다는 점에서 차이가 있다.

사용자 영역 어플리케이션에서 파일을 열겠다고 한다면 커널은 파일 이름을 통해서 디렉터리를 열고 파일을 찾는다.

  • 여기서 파일 이릉므로 inode 번호를 찾고 이렇게 얻은 inode 번호로 inode 를 찾는다.
  • Inode 에서는 디스크에 저장된 파일 위치와 같은 정보가 들어 있어서 찾을 수 있다.

리눅스에서 /home/blackbeard/concorde.png 파일을 찾는 과정을 보면 다음과 같다.

  • 루트에서 시작해서 home 의 inode 를 찾고, home 으로 가서 blackbeard 의 inode 를 찾고 blackbeard 로 가서 concorde.png inode 를 찾는다.
  • 커널에서 디렉터리 같은 경우는 dentry 라고 부르며 이것도 캐시가 되기 때문에 탐색 속도가 더 빠르다.

2.1 파일 열기

파일에 대한 접근하는 가장 기본적인 방법은 read() 와 write() 시스템 콜이다.

하지만 파일에 대해 접근하기 전에 open() 이나 create() 와 같은 시스템 콜을 이용해서 파일을 열어두고 다 쓴 다음에는 close() 시스템 콜로 닫아야한다.

open() 시스템 콜

#include <sys/type.h> 
#include <sys/stat.h> 
#include <fcntl.h> 

int open (const char *name, int flags); 
int open (const char *name, int flags, mode_t mode); 
  • 경로 네임인 name 을 지정해주고, 파일 디스크립터를 얻는다.
  • flags 는 접근 모드를 말한다.
    • flags 인자는 O_RDONLY, O_WRONLY, O_RDWR 중 하나를 포함해야한다.
    • 각 모드는 읽기 전용모드, 쓰기 모드, 읽기/쓰기 모드로 파일을 열도록 요청한다.
    • 쓰기 전용 모드로 열면 읽는게 불가능하다.
    • flags 매개변수는 비트 OR 연산으로 다음 값 중 하나를 추가해서 열기 동작을 변경할 수 있다.
      • O_APPEND: 덧붙이기 모드. 파일 오프셋이 마지막을 가리키도록 한다 .
      • O_ASYNC: 특정 파일에서 읽기나 쓰기가 가능해질 때 시그널 (SIGIO) 가 발생한다. 이 파일은 일반 파일이 아니라 터미널과 소켓에서만 사용할 수 있다.
        • 입출력 작업이 비동기로 된다. 해당 작업이 완료되면 시그널을 통해서 알려줌.
        • 일반적으로 파일은 동기식으로 처리됨. 이렇게하면 비동기 식으로 될 수 있음.
      • O_CLOEXEC: 열린 파일에 close-on-exec 플래그를 설정한다. 새 프로세스를 실행하면 (exec() 와 같은 시스템콜) 이 파일은 자동으로 닫힌다.
        • 이걸 쓰면 자식 프로세스는 부모 프로세스의 파일 테이블을 상속받지 않는다.
      • O_CREATE: name 에 적힌 파일이 없으면 파일을 새로만든다.
      • O_DIRECT: 직접 입출력을 수행하기 위해서 파일을 연다.
      • O_DIRECTROY: name 이 디렉터리가 아니면 open() 이 실패한다.
      • O_EXCL: O_CREATE 와 함께 이 플래그를 쓴다면 name 에 적힌 파일이 있다면 open() 이 실패하게 된다. 경쟁 상태를 피하려고 씀.
      • O_LARGEFILE: 2기가 바이트를 초과하는 파일을 열기 위해서 64비트 오프셋을 사용한다. 이 플래그는 64비트 아키텍처를 내재한다.
        • 일부 운영체제는 2기가 이상의 파일을 열 수 없다. 최신 운영체제는 이 기능을 당연히 지원하지만 하위 호환성을 위해서 쓰는게 낫다.
      • O_NOATIME+: 읽기를 하더라도 파일의 마지막 접근 시간이 업데이트 되지 않는다. 이 플래그는 백업이나 인덱싱 또는 시스템 내에 존재하는 모든 파일을 읽어야 하는 프로그램에서 파일을 읽을 때마다 해당 inode 를 갱신하는 쓰기 작업을 방지할 수 있어서 좋다. 이 플래그는 리눅스 커널 2.6.8 이상에서 사용가능하다.
      • O_NOCTTY: name 이 터미널 디바이스 (/dev/tty) 를 가리킨다면 프로세스에 현재 제어중인 터미널이 없더라도 프로세스의 제어 터미널이 되지는 않는다. 자주 쓰이지 않는 옵션이다. 일반적으로 터미널 장치를 열 떄 해당 터미널이 제어 터미널이 된다. 그러나 이렇게 되면 원치 않는 터미널 입출력을 받게될 수 있으므로 이를 방지하기 위해 이 옵션을 쓴다. 이 옵션으로 터미널을 열면 해당 파일이 프로세스의 제어 터미널로 되지 않는다. 따라서 원치 않는 터미널 입출력을 막을 수 있다.
      • O_NOFOLLOW: name 이 심벌릭 링크라면 open() 이 실패한다. 일반 파일이라면 링크를 따라가서 파일을 연다.
      • O_NONBLOCK: 가능한 경우 파일을 논블록킹 모드로 연다. 그리고 읽기와 쓰기 연산에서도 논블로킹 모드로 설정한다.
        주로 파일이나 소켓에 쓰인다. 기본적으로는 파일이나 소켓에서 입출력 작업이 생기면 블로킹 되지만 이 옵션을 쓰면 블로킹 되지 않는다.
        • 논블로킹 되기 때문에 작업이 완료되는 시점을 직접 확인해야한다. 여기에는 여러가지 기법이 있다.
          • select() 함수 사용
            • 이 함수를 사용하면 여러 파일 디스크립터를 동시에 모니터링 할 수 있다. 그리고 여러 파일 디스크립터 중 준비가된 녀석을 알 수 있다.
            • 준비가 된 애가 반환된다. 이때 어떤 파일 디스크립터가 작업을 완료했는지 알 수 있따.
          • poll() 함수 사용
            • select() 함수와 유사하다. 다만 파일 디스크립터를 관리하는 방식이 다르다.
            • poll() 를 사용하면 이것도 여러 파일 디스크립터를 모니터링하고 완료된 애를 알 수 있다.
          • epoll() 함수
            • linux 전용 입출력 함수로 하는 일이 poll() 과 select() 와 유사하다. 다만 더 효율적이다.
        • O_NONBLOCKO_ASYNC 를 같이 사용하는 경우도 있다고 한다. 아무래도 O_NONBLOCK 을 쓰면 완료되는 시점을 알기 어려우니까. 다만 시그널이 왔을 때 핸들링하는 로직이 필요로한다.
      • O_SYNC: 파일을 동기식 입출력으로 연다. 데이터를 물리적으로 디스크에 쓰기 전까지 쓰기 연산이 완료되지 않는다. 읽기 연산은 이미 동기식이니까 읽기와 연관해서는 이 플래그가 영향을 미치지 않는다.
      • O_TRUNC: 파일이 존재하고 일반 파일이며 flags 인자에 쓰기가 가능하도록 명시되어 있으면 파일 길이를 0 으로 자른다.
        • 파일을 열 때 기존 존재하는 파일 데이터를 모두 삭제하는 옵션이다. 파일이 존재하는 경우에 파일 길이가 0 이 되므로. 주로 파일에 새로운 데이터를 덮어쓰고 싶을 때 쓴다.