# 파일과 디렉터리[^ypilseong]
[^ypilseong]: [양필성]()

## 파일과 디렉터리

저장 장치의 가상화에 대한 두 가지 주요 개념인 파일과 디렉토리에 대해 알아봅시다.

1. 파일

    - 읽거나 쓸 수 있는 순차적인 바이트의 배열
    - *아이노드(inode)*: 각 파일이 갖는 *저수준의 이름(low-level name)*, 이는 보통 숫자로 표현되지만 사용자는 그 이름에 대해서 알지 못합니다.
    - 대부분 시스템에서 운영체제는 파일의 구조를 모르고, 데이터가 요청되면 처음 저장했던 데이터를 돌려줄 뿐입니다.

2. 디렉터리

    - 파일과 마찬가지로 디렉터리도 저수준의 이름(예, 아이노드 번호)을 갖습니다.
        - 디렉터리는 `<사용자가 읽을 수 있는 이름, 저수준의 이름>` 쌍으로 이루어진 목록을 갖고있습니다.
            - ex) <"foo", "10"> <- 사용자는 foo를 읽음
    - 디렉터리의 각 항목은 파일 또는 다른 디렉터리를 가리킵니다.
    - 디렉터리 내에 다른 디렉터리를 포함함으로써 *디렉터리 트리(directory tree, 또는 디렉터리 계층(directory hierarchy))*을 구성할 수 있습니다.
        - 디렉터리 계층은 루트 디렉터리(root directory) 부터 시작합니다.
        - 구분자(separator) 를 사용하여 하위 디렉터리를 명시합니다.
        (절대경로: /foo/bar.text)
        - 파일 이름에서 점(.) 뒤에 오는 부분은 관용적으로 파일의 종류를 나타냅니다.

![fileimg1](./figs/fileimg1.png)

## 파일 시스템 인터페이스

이번에는 기본적인 파일 시스템 인터페이스를 좀 더 상세하게 논의해 볼 것입니다.

```{note}
파일 시스템 인터페이스

파일 생성
파일 접근
파일 삭제
```

## 파일의 생성

open 시스템 콜을 사용하여 파일을 생성할 수 있습니다.
```c
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC);
```
- `open()`을 호출하면서 `O_CREAT` 플래그를 전달하면 프로그램은 새로운 파일을 만들 수 있습니다.
- `open()`은 다수의 플래그를 받으며, 이 예제에서는 `O_WRONLY`와 `O_TRUNC` 플래그도 추가로 전달하였습니다.
    - `O_WRONLY`: 파일이 열렸을 때 쓰기만 가능하게끔 허용
    - `O_TRUNC`: 파일이 이미 존재할 때는 기존 내용을 모두 삭제
- 리턴 값으로 *파일 디스크립터(file descriptor)*를 반환합니다.
    - *파일 디스크립터(file descriptor)*: 파일 디스크립터는 프로세스마다 존재하는 정수로서 Unix 시스템에서 파일을 접근하는데 사용됩니다.
    - 파일 디스크립터로 파일을 읽고 쓸 때 권한이 필요하고, 이러한 측면에서 파일 디스크립터는 *capability*입니다.

## 파일의 읽기와 쓰기

파일이 있으면, 그 파일들을 당연히 읽거나 쓰고 싶을 것입니다.

커맨드 라인을 사용 중이라면 `cat` 명령어로 파일을 읽을 수 있습니다.
```c
> echo hello > foo
> cat foo 
hello # 화면에 hello 출력
```

```
cat 프로그램은 어떻게 파일 foo에 접근할까요?
```

- 리눅스에는 *strace*라는 도구가 있는데, strace는 프로그램이 실행되는 동안에 호출된 모든 시스템 콜을 추적하고, 그 결과를 화면에 보여줍니다.
- cat의 동작 과정 예시를 통해 어떻게 파일에 접근하는지 알아봅시다. (strace를 사용)

```c
> strace cat foo
 ...
 open("foo", O_RDONLY|O_LARGEFILE) = 3 	# foo 파일 열기
 read(3, "hello\n", 4096) = 6 			# foo 파일 읽기
 write(1, "hello\n", 6) = 6 			# stdout에 파일 쓰기
 hello 									# 화면에 hello 출력
 read(3, "", 4096) = 0 					# foo 파일 읽기
 close(3) = 0 							# foo 닫기
 ...
 ```

1. 파일을 읽기 위해 먼저 파일을 엽니다.
    - `O_RDONLY` 플래그 줘서 읽기만 가능
    - `O_LARGEFILE` 플래그 줘서 64bit 오프셋 사용
    - open 성공 후 3이라는 값을 파일 디스크립터로 리턴(표준 입력/출력/오류가 파일 디스크립터 0,1,2를 사용하고 있기 때문)

2. 파일 읽기 성공시 `cat`은 `read()` 시스템 콜을 사용하여 파일에서 몇 바이트씩 반복적으로 읽습니다.
    - 첫 번째 인자는 `fd`가 들어갑니다
    - 두 번째 인자는 결과를 저장할 버퍼가 들어갑니다. 위의 경우엔 읽은 결과 `"hello\n"`가 들어간 것을 표현한 것입니다.
    - 세 번째 인자는 버퍼의 크기입니다.
    - 리턴 값은 읽은 바이트 수를 반환합니다. (`"hello\n"`는 6바이트)

3. 표준 출력에 읽은 파일을 출력하기 위해 `write(1,buf,size)`을 해줍니다.
    - 파일 디스크립터 1은 표준 출력입니다.

4. 화면에 파일 내용 출력 (hello 띄움)

5. 파일을 더 읽으려고 시도 (더 없으므로 0 리턴)

6. 파일을 다 읽었으므로 `foo`를 `close`

파일에 쓰는 것도 비슷한 단계를 거칩니다.

- 파일 열기 - write 시스템콜 호출 (반복) - 파일 닫기

## 비 순차적 읽기와 쓰기

지금까지 파일을 읽고 쓰는 과정을 논의하였는데, 모든 접근은 *순차적*이었습니다. 그렇지만 때로는 파일의 특정 오프셋(위치)부터 읽거나 쓰는 것이 유용할 때가 있습니다.

*임의*의 오프셋에서 읽기/쓰기 수행

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

- 첫 번째 인자: 파일 디스크립터
- 두 번째 인자: 파일 오프셋 (파일의 특정 위치)
- 세 번째 인자: 탐색 방식
    - `SEEK_SET`: 오프셋은 `offset` byte로 설정
    - `SEEK_CUR`: 오프셋은 현재 위치에 `offset` byte를 더한 것으로 설정
    - `SEEK_END`: 오프셋은 파일 크기에 `offset` byte를 더한 것으로 설정
- "현재" 오프셋을 추적하여 다음 읽기 또는 쓰기 위치를 결정합니다.
    - 오프셋 갱신
        1. `N` 바이트를 읽거나 쓸때, `offset+N` 으로 암묵적 갱신
        2. `lseek`를 사용하여 명시적으로 오프셋 갱신

## fsync()를 이용한 즉시 기록

`write()` 호출은 성능상 이유로 파일 시스템은 쓰기들을 일정 시간동안 메모리에 모아(버퍼링) 한 번에 저장 장치에 전달됩니다.

어떤 프로그램은 쓰기에 있어서 좀 더 강력한 보장을 필요로 합니다 (ex. DBMS의 복원 모듈). 이럴 땐 강제적으로 즉시 디스크에 기록할 수 있는 기능이 필요합니다.

### fsync(int fd)

- 특정 fd에 대해 파일의 모든 더티(dirty 즉, 갱신된) 데이터를 디스크로 강제로 내려보내는 유닉스 API
- 파일 생성과 같이 특정 상황에서는 파일 foo가 존재하는 디렉터리도 fsync() 해주어야 합니다.

## 파일 이름 변경

커맨드 라인에서는 `mv` 명령으로 파일명을 변경할 수 있습니다.

```c
> mv foo bar # foo를 bar로 rename
```

- strace를 사용하면 `mv`가 `rename(char *old, char *new)` 라는 시스템 콜을 호출하는 것을 확인할 수 있습니다. (각각 옛날 이름, 새 이름)
- `rename()`은 시스템 크래시에 대해 원자적으로 구현되었습니다.
    - 이름 변경 중 시스템 크래시가 발생했을 때, 파일 이름은 원래의 이름이나 새로운 이름, 둘 중의 하나를 갖게 되며 그 사이의 중간 상태는 발생하지 않습니다.
- 예시 (파일 중간에 한 줄 삽입)
    - 파일의 이름은 `foo.txt` 입니다.
- 새로운 파일이 원래의 내용과 추가된 줄을 모두 포함합니다는 것이 보장되도록 갱신하는 방법은 다음과 같을 것입니다.

```c
int fd = open("foo.txt.tmp", O_WRONLY|O_CREAT|O_TRUNC);
write(fd , buffer , size); // 파일의 새로운 버전 쓰기
fsync(fd);
close(fd);
rename("foo.txt.tmp", "foo.txt");
```

- 임시 파일 `foo.txt.tmp` 생성 후 여기에 기록
- `foo.txt.tmp`를 `foo.txt`로 rename

## 파일 정보 추출

파일 시스템은 각 파일에 대한 정보를 보관하고, 이를 *메타 데이터*라고 합니다.

### 메타 데이터(metadata)

- 파일(데이터)에 대한 정보
- 어떤 파일의 메타 데이터를 보려면 `stat()`이나 `fstat()` 같은 시스템 콜을 사용합니다.
- stat의 구조

    ```c
    struct stat {
        dev_t st_dev; 			/* ID of device containing file */
        ino_t st_ino; 			/* inode number */
        mode_t st_mode; 		/* protection */
        nlink_t st_nlink; 		/* number of hard links */
        uid_t st_uid; 			/* user ID of owner */
        gid_t st_gid; 			/* group ID of owner */
        dev_t st_rdev; 			/* device ID (if special file) */
        off_t st_size; 			/* total size, in bytes */
        blksize_t st_blksize; 	/* blocksize for filesystem I/O */
        blkcnt_t st_blocks; 	/* number of blocks allocated */
        time_t st_atime; 		/* time of last access */
        time_t st_mtime; 		/* time of last modification */
        time_t st_ctime; 		/* time of last status change */
    };
    ```
    - 파일의 크기, 아이노드 번호, 소유권, 파일 접근/변경 시간 등 다양한 정보를 포함합니다.
- 일반적으로 파일 시스템은 *아이노드*에 이 메타 데이터를 보관합니다.

## 디렉터리 생성

디렉터리 관련 시스템 콜들은 디렉터리를 생성하고, 읽고, 삭제하지만, 디렉터리에는 절대로 직접 쓸 수 없습니다.

- 디렉터리는 파일 시스템의 메타데이터로 분류되며, 항상 간접적으로만 변경됩니다.
- 왜냐하면 파일 시스템은 디렉터리의 내용이 항상 예상과 일치하도록 보장해야 되기 때문입니다.

대표적인 디렉터리 생성 명령어는 `mkdir`이 있습니다.

- `mkdir` 동작 과정 (strace)
    ```c
    > strace mkdir foo
    ...
    mkdir("foo", 0777) = 0
    ...
    ```
- 디렉터리가 처음 생성되면 빈 상태이지만, 사실 자기 자신을 나타내기 위한 dot(.) 디렉터리, 부모 디렉터리를 가리키기 위한 dot-dot(..) 디렉터리를 포함합니다.

## 디렉터리 읽기

디렉터리 읽기는 `ls`와 같은 명령어가 있습니다. `ls`와 유사한 도구를 직접 만들어 어떻게 동작하는지 알아봅시다.

```c
int main(int argc , char *argv[]) {
    DIR *dp = opendir("."); // 디렉터리 열기
    assert(dp != NULL);
    struct dirent *d;
    while ((d = readdir(dp)) != NULL) { // 디렉터리 반복적으로 읽기
    	printf("%d %s\n", (int) d−>d_ino , d−>d_name);
    }
    closedir(dp); // 디렉터리 닫기
    return 0;
}
```

- 간단한 반복문을 사용하여 디렉터리 항목을 하나씩 읽은 후에 디렉터리의 각 파일의 이름과 아이노드 번호를 출력하는 프로그램입니다.
- `opendir()`, `readdir()`, 및 `closedir()` 시스템 콜을 사용합니다.
디렉터리에는 많은 정보가 있지 않기 때문에 (단순하게 이름과 아이노드 번호를 매핑하는 것 이외에 몇 가지만 제공함) 프로그램은 각 파일에 `stat()`을 호출하여 파일 크기와 같은 구체적인 정보를 얻습니다.

- 디렉터리 항목에 저장된 정보

```c
struct dirent {
    char d_name[256]; 			/* filename */
    ino_t d_ino; 				/* inode number */
    off_t d_off; 				/* offset to the next dirent */
    unsigned short d_reclen; 	/* length of this record */
    unsigned char d_type; 		/* type of file */
};
```

## 디렉터리 삭제하기

마지막으로 `rmdir()` 시스템 콜을 사용하여 디렉터리를 삭제할 수 있습니다.

- 디렉터리 삭제는 하나의 명령으로 아주 많은 양의 데이터를 지울 수 있기 때문에 좀 더 위험합니다.
- 때문에 `rmdir()`은 디렉터리를 지우기 전에 디렉터리가 비어 있어야 합니다는 조건이 붙습니다.

## 하드 링크

파일 삭제 시 왜 `unlink()`를 사용하는지를 이해하기 위해서 이제 파일 시스템 트리에 항목을 추가하는 새로운 시스템 콜 `link()`를 알아봅시다.

하드 링크 <- `link()` 시스템 콜 사용

- `link(char *old, char *new)`: 원래 경로명, 새 경로명을 인자로 받습니다.
- 원래 파일 이름에 새로운 이름을 “link(연결)”하면 동일한 파일을 접근할 수 있는 새로운 방법을 만들게 됩니다.
- 명령어 `ln`이 그 일을 합니다.

    ```c
    > echo hello > file1
    > cat file1
    hello
    > ln file1 file2 # linking file2 to file
    > cat file2
    hello
    ```
    - file2는 file1의 하드 링크이므로 해당 파일의 내용(hello)를 보려면 file1이나 file2를 열면 됩니다.
- link는 새로이 링크하려는 이름 항목을 디렉터리에 생성하고, 원래 파일과 같은 아이노드 번호를 가리키도록 합니다.
- 단, 파일을 복사되지 않지만, 대신 같은 파일을 가리키는 두 개의 이름(file1과 file2)이 생성됩니다.
    ```c
    > ls −i file1 file2
    67158084 file1
    67158084 file2 # inode가 같다
    ```
- 즉, link는 동일한 아이노드 번호(이 예제에서는 67158084)에 대한 새로운 링크를 생성합니다.
- 하드 링크를 생성한 후에는 file1과 file2는 차이가 없습니다. 사실 그 두 개의 파일 이름은 아이노드 번호 67158084에서 찾을 수 있는 파일의 메타데이터에 대한 연결일 뿐입니다.

```
unlink()가 왜 unlink()가 되었는지 이제 이해되기 시작했을 것입니다.
```

파일을 생성할 때 사실은 두 가지 작업을 하게 됩니다.

1. 파일 관련 거의 모든 정보를 관리하는 자료 구조(아이노드)를 만드는 것입니다. 파일 크기와 디스크 블럭의 위치 등이 포함됩니다.
2. 해당 파일에 사람이 읽을 수 있는 이름을 연결하고 그 연결 정보를 디렉터리에 생성하는 것입니다.

`unlink()` 시스템 콜

- 파일 삭제 시 `unlink()`를 호출합니다.
- 위의 예제에서 file1을 제거합니다고 하더라도, 여전히 file2를 통해 해당 파일을 어려움 없이 접근할 수 있습니다.

```c
> rm file1
removed 'file1'
> cat file2
hello
```

- 파일을 `unlink`하면 아이노드 번호의 *참조 횟수(reference count)*를 검사합니다.
    - 이 참조 횟수 (때로는 *연결 횟수(link count)*라고도 불린다)가 특정 아이노드에 대해 다른 이름이 몇 개나 연결되어 있는지 관리합니다.
    - *unlink()*가 호출되면 이름과 해당 아이노드 번호 간의 "연결"을 끊고 참조 횟수를 하나 줄입니다.
    - 참조 횟수가 0에 도달하면 파일 시스템은 비로소 아이노드와 관련된 데이터 블럭을 해제하여 파일을 진정으로 "삭제"합니다.
    
    ```c
    > echo hello > file1
    > stat file1
    ... Inode: 67158084 	Links: 1 ...
    > ln file1 file2
    > stat file1
    ... Inode: 67158084 	Links: 2 ...
    > stat file2
    ... Inode: 67158084 	Links: 2 ...
    
    > ln file2 file3
    > stat file1
    ... Inode: 67158084 	Links: 3 ...
    > rm file1
    > stat file2
    ... Inode: 67158084 	Links: 2 ...
    > rm file2
    > stat file3
    ... Inode: 67158084 	Links: 1 ...
    > rm file3
    ```

## 심볼릭 링크

하드 링크의 제한

- 디렉터리에 대해서는 하드 링크를 만들 수 없습니다.(디렉터리 트리에 순환 구조가 생길 수도 있습니다)
- 다른 디스크 파티션에 있는 파일에 대해서도 하드 링크를 걸 수 없습니다.(아이노드 번호는 하나의 파일 시스템 내에서만 유일합니다)

### 심볼릭 링크 (또는 소프트 링크)

- `ln -s` 명령어 사용

    ```c
    > echo hello > file1
    > ln −s file1 file2
    > cat file2
    hello
    ```

- 심볼릭 링크 file2를 통해 원래 파일 file1에 접근할 수 있습니다.
- 하드 링크와 다르게 심볼릭 링크는 다른 형식의 독립된 파일입니다.
    - 파일, 디렉터리과 같이 심볼릭 링크는 파일 시스템에 존재하는 세 번째 종류의 유형입니다.
    - 심볼릭 링크에 대해 `stat` 명령어를 실행시키면 일반 파일이나 디렉토리와 다른 "symbolic link" 유형이라고 출력됩니다.
    - `ls` 명령어를 통해서도 다르다는 것을 알 수 있습니다. (첫 글자가 l임)
- *dangling reference*: 원래의 파일을 삭제하면 심볼릭 링크가 가리키는 실제 파일은 더 이상 존재하지 않게 되는 현상

```c
> echo hello > file1
> ln −s file1 file2
> cat file2
hello
> rm file1
> cat file2
cat: file2: No such file1 or directory
```

## 파일 시스템 생성과 마운트

```
다수의 파일 시스템들이 존재할 때 이들을 묶어서 어떻게 하나의 큰 디렉터리 트리를 구성할까요?
```

- 여러 개의 파일 시스템 파티션들이 모여서 하나의 큰 디렉터리를 구성합니다.
- 각각의 파일 시스템을 생성하고, 이들을 "마운트"함으로써 단일 디렉터리 트리를 구성합니다.

### `mkfs` 명령어

- 파일 시스템을 생성하는 명령어
- 장치명과 파일 시스템 타입을 전달하면, 해당 파티션에 전달된 파일 시스템 형식으로 구성된 빈 파일 시스템을 생성합니다.
    - 예) 디스크 파티션 `/dev/sda1`에 시스템 타입 `EXT3`의 형식으로 구성된 파일 시스템을 생성할 수 있습니다.
- 생성된 파일 시스템은 자체적인 디렉터리 구조로 구성되어 있고, 비어있습니다. 즉, 루트 디렉터리만 존재합니다.

### `mount` 명령어

- *마운트*: 새로이 생성된 파일 시스템을 루트 디렉터리에서 시작하는 기존의 디렉터리 구성을 통해 접근할 수 있도록 해주는 것

- `mount()` 시스템 콜을 사용하여 기존의 디렉터리 중 하나를 *마운트 지점(mount point)*으로 지정합니다.

- 그리고 나서 마운트 지점에 생성된 파일 시스템을 "붙여 넣습니다".

- 예)

    - 파티션 `/dev/sda1`에 `EXT3` 파일 시스템 존재합니다 가정 (아직 마운트 되지 않음)
    - 해당 파일 시스템의 루트 디렉터리에 `a`와`b` 라는 두 개의 하위 디렉토리가 있고, 각 디렉터리에 `foo` 라는 파일이 들어있습니다.

        ```sh
        /
        ├── a
        │   └── foo
        └── b
            └── foo
        ```
    - 이 파일 시스템을 `/home/users/` 위치에 마운트 하기 위해 다음 명령어를 실행합니다.
        ```c
        > mount −t ext3 /dev/sda1 /home/users
        ```
    - 마운트 성공시, 새로운 파일 시스템을 기존의 디렉터리 경로를 통해 접근할 수 있습니다.
        ```c
        > ls /home/users/
        a b
        > ls /home/users/a/
        foo
        ```
- 여러 개의 개별 적인 파일 시스템을 갖는 대신에 마운트는 모든 파일 시스템들을 하나의 트리 아래에 통합시킵니다.
    ```sh
    /
    ├── home
    .   ├── users
    .   .   ├── a
    .   .   │   └── foo
        .   └── b
                └── foo
    ```
## 요약
