리눅스 파일 시스템
리눅스에서 파일은 단순한 데이터의 집합이 아니라, 운영체제가 효율적으로 관리할 수 있도록 설계된 데이터 구조이다.
파일은 바이트의 연속적인 시퀀스이며, 다양한 입출력(I/O) 방식과 파일 관리 구조를 통해 운영된다.
1. 리눅스의 파일 I/O 방식
리눅스에서는 다양한 방식으로 파일을 처리할 수 있으며, 대표적인 방법은 다음과 같다.
다중화된 I/O (Multiplexed I/O)
여러 개의 파일을 동시에 처리할 때, 운영체제는 다중화된 I/O 기법을 사용하여 성능을 최적화한다. select(), poll(), epoll() 등의 시스템 콜을 활용하면 여러 파일을 동시에 감시하고, 준비된 파일에 대해 데이터를 읽거나 쓸 수 있다.
메모리 매핑 I/O (Memory Mapped I/O)
파일을 물리적인 디스크에서 읽어 들이는 대신, 메모리에 매핑하여 직접 접근할 수 있도록 한다. mmap() 함수를 활용하면 파일을 메모리에 매핑할 수 있으며, 이를 통해 성능을 높일 수 있다.
파일 공유 (File Sharing)
리눅스에서는 여러 프로세스가 동일한 파일을 공유할 수 있다. 파일 테이블과 글로벌 파일 테이블을 통해 프로세스 간 파일 접근이 조율되며, 이를 통해 데이터 일관성이 유지된다.
I/O 리다이렉션 연산 (IO Redirection Operators)
쉘에서 <, >, >>, 2> 등의 연산자를 이용하여 표준 입출력을 재지정할 수 있다. 이를 통해 파일과 프로세스 간 데이터 흐름을 유연하게 관리할 수 있다.
표준 입출력 함수 (Standard I/O)
C 언어의 stdio.h 라이브러리를 활용하면 표준 입출력 스트림을 활용할 수 있다. fopen(), fwrite(), fread() 등의 함수가 제공되며, 이를 통해 파일을 쉽게 다룰 수 있다.
2. 리눅스 파일의 유형
각 파일은 특정한 타입을 가지며, 이는 파일 시스템에서 다르게 처리된다.
- Regular file (일반 파일): 텍스트 파일, 실행 파일, 이미지 파일 등.
- Directory (디렉터리): 파일과 하위 디렉터리를 포함하는 컨테이너.
- Socket (소켓 파일): 프로세스 간 통신(IPC)을 위한 파일.
- Named Pipes (FIFO): 두 프로세스 간의 데이터를 주고받을 수 있는 파이프.
- Symbolic links (심볼릭 링크): 다른 파일을 가리키는 링크 파일.
- Character and Block Device (문자 및 블록 장치 파일): 터미널, 하드 디스크 등 하드웨어 장치와 연결된 파일.
3. 디렉터리 구조
디렉터리는 파일의 주소값(포인터)의 배열로 구성되며, 각 항목은 파일 이름과 해당 파일의 inode(파일 메타데이터를 담고 있는 데이터 구조)를 가리킨다.
디렉터리 기본 구성 요소
- . : 현재 디렉터리를 가리킨다.
- .. : 상위 디렉터리를 가리킨다.
이러한 구조 덕분에 파일 시스템 탐색이 효율적으로 이루어질 수 있다.
4. 파일 테이블과 파일 디스크립터
리눅스에서는 프로세스가 파일을 열면 운영체제의 파일 테이블(File Table)에서 파일 정보를 관리한다.
파일 테이블 (OS)
파일 테이블은 프로세스별로 열려 있는 파일 목록을 저장하는 자료구조이다. 각 항목은 파일의 inode 정보, 파일 오프셋(file offset), 접근 모드(access mode) 등을 포함한다.
파일 디스크립터 (File Descriptor, fds)
파일 디스크립터는 파일을 참조하는 정수형 ID이다. 프로세스가 파일을 열면 커널은 해당 파일에 대한 file descriptor(0부터 시작하는 정수 값)를 반환한다.
- 0 : 표준 입력(stdin)
- 1 : 표준 출력(stdout)
- 2 : 표준 에러(stderr)
파일 디스크립터는 해당 프로세스에서만 유효하며, 프로세스 간 공유되지 않는다.
5. Inode와 데이터 블록
inode란?
inode는 파일의 메타데이터를 저장하는 자료구조이다. 각 파일은 inode를 가지며, inode number(고유한 정수 ID)를 통해 식별된다.

inode에 포함된 정보
- 파일 타입
- 접근 모드 (읽기, 쓰기, 실행 권한)
- 소유자 정보
- 데이터 블록의 위치 정보
데이터 블록과 인덱스 블록
- Direct Block: 파일의 실제 데이터가 저장되는 블록.
- Single Indirect Block: 여러 개의 데이터 블록을 가리키는 포인터 블록.
- Double Indirect Block: 포인터를 저장하는 블록을 가리키는 포인터 블록.
이를 통해 작은 파일은 direct block에 저장되고, 큰 파일은 계층적인 포인터 구조를 통해 관리된다.
6. 파일 메타데이터 (File Metadata)
파일 메타데이터는 파일 자체의 내용이 아니라, 파일에 대한 정보를 저장하는 데이터이다. stat 또는 fstat 명령어를 사용하면 파일 메타데이터를 확인할 수 있다.
파일 정보 조회 예제
struct stat {
dev_t st_dev;
ino_t st_ino;
mode_t st_mode;
nlink_t st_nlink;
uid_t st_uid;
gid_t st_gid;
dev_t st_rdev;
off_t st_size;
unsigned long st_blksize;
unsigned long st_blocks;
time_t st_atime;
time_t st_mtime;
time_t st_ctime;
};
int main(int argc, char *argv[]){
struct stat st;
char *type, *readok;
stat(argv[1], &st);
if(S_ISREG(st.st_mode))
type = "regular";
else if(S_ISDIR(st.st_mode))
type = "directory";
else
type = "other";
if((st.st_mode & S_IRUSR))
readok = "yes";
else
readok = "no";
printf("type: %s, read: %s\n", type, readok);
return 0;
}
7. 파일 오프셋 (File Offset)
파일 오프셋은 read(), write() 연산이 수행될 때 파일 내 현재 읽기/쓰기 위치를 나타내는 값이다.
파일 오프셋 조정 (lseek)
off_t lseek (int fd, off_t pos, int origin)
lseek(fd, 10, SEEK_CUR); // 현재 위치에서 10바이트 앞으로 이동
lseek(fd, -5, SEEK_CUR); // 현재 위치에서 5바이트 뒤로 이동
파일 끝을 넘어 write하면 파일 hole이 생길 수 있으며, 이는 물리적으로 저장되지 않는 공간이다.

8. Positional Read/Write
기본적인 read(), write() 함수는 파일 오프셋을 변경하지만, pread()와 pwrite()는 파일 오프셋을 변경하지 않고 지정된 위치에서 읽기/쓰기를 수행한다.
Positional Read/Write 예제
ssize_t pread (int fd, void *buf, size_t count, off_t pos);
ssize_t pwrite (int fd, const void *buf, size_t count, off_t pos);
9. 공유된 파일 디스크립터

부모 프로세스와 자식 프로세스는 fork()를 호출하면 동일한 파일 디스크립터를 공유하며, 이는 global file table을 통해 관리된다.
File Table

각 프로세스가 관리하는 fd를 담은 테이블
아래는 부모, 자식 프로세스의 독자적인 file Table을 볼 수 있다.
global file table

시스템 안에서 열려있는 파일들을 OS가 관리하는 것이다.
몇개의 fd(몇개의 해당 fd다르는 프로세스 수)가 가리키고 있는지(차수) ref에 저장되고, offset도 관리된다.
해제되면 offset도 초기화 된다.
시험 예제 코드
int main(){
int fd = open("file1.txt", O_RDONLY);
int rc = fork();
if(rc == 0){//child
rc = lseek(fd, 10, SEEK_SET);
} else if (rc > 0){//parent
wait(NULL);
printf("P: offset = %d\n", (int)lseek(fd, 0, SEEK_CUR));
}
return 0;
}
결과 : 자식 프로세스가 offset을 10 만큼 옮겼는데, 부모 프로세스에서 offset이 10만큼 이미 옮겨져 있다고 나왔다.
같은 offset을 global file table을 통해서 공유
offset을 공유하기 때문에 나올수 있는 이슈 해결 예제
두 프로세스가 같은 global file table의 같은 file entry를 공유하면 offset이 공유가 되기 때문에, 만약에 한 프로세스가 해당 파일을 전부 읽으면 offset은 파일의 끝으로 이동하기 때문에 다른 프로세스는 파일을 읽을수가 없다.
offset을 다시 파일의 처음으로 이동한다음에 읽을수 있도록 해야된다.
int main(){
char str[BUF_SIZE], read_buf[BUF_SIZE];
size_t len;
ssize_t ret;
int fd;
char *bufp = read_buf;
printf("input: ");
fgets(str, BUF_SIZE, stdin);
len = strlen(str);
// fork 이전에 open()
fd = open("file1.txt", O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR);
if(fd == -1){
printf("open error\n");
exit(1);
}
if(fork() == 0){
// child
// write하면서 offset 변경
write(fd, str, len);
} else {
// parent
// read하면서 offset 변경이미 돼서 파일의 끝에서 읽음
// 아무것도 못읽음
wait(NULL);
lseek(fd, 0, SEEK_SET); // offset 초기화해주면 다시 읽을수 있음
while(len){
ret = read(fd, bufp, len);
if(ret < 0){
printf("read error\n");
exit(1);
}
len -= ret;
bufp += ret;
}
*bufp = '\0';
printf("output: %s\n", read_buf);
}
close(fd);
return 0;
}
10. 파일 링크 (File Link)
하드 링크 (Hard Link)
- 같은 inode를 공유하며, 파일이 삭제되더라도 다른 링크는 유지된다.
- ln oldfile newfile 명령어로 생성 가능.
심볼릭 링크 (Symbolic Link)
- 원본 파일을 가리키는 별도의 파일.
- ln -s oldfile newfile 명령어로 생성 가능.
11. 파일 디스크립터 복제 (file sharing)
dup(int oldfd) 함수는 기존 파일 디스크립터(oldfd)를 복제하여 새로운 파일 디스크립터를 생성한다. 새로 생성된 파일 디스크립터는 사용 가능한 가장 작은 번호를 반환하며, 오류가 발생하면 -1을 반환한다.
만약 특정 예약된 번호(예: 0, 1, 2)를 사용하려면 먼저 해당 fd를 close(fd)를 통해 닫아야 한다.
int dup(int oldfd);
dup2(int oldfd, int newfd) 함수는 dup()과 유사하지만, 복제된 fd를 newfd 값에 맞춰 생성한다. 만약 newfd가 이미 열려 있다면 자동으로 close(newfd)를 수행한 후 oldfd를 복제한다.
int dup2(int oldfd, int newfd);
이러한 dup 함수들은 기존 파일 디스크립터와 같은 Open(Global) File Table을 가리키므로 ref++가 증가하고, 파일 오프셋이 공유된다.
dup() 함수 예제
아래 코드를 실행하면 dup()을 통해 복제된 파일 디스크립터가 같은 Global File Table을 가리킨다는 것을 확인할 수 있다.
int main(){
int fd1, fd2;
char c;
fd1 = open("file1.txt", O_RDONLY);
if(fd1 == -1){
perror("open");
exit(1);
}
read(fd1, &c, 1);
fd2 = dup(fd1);
read(fd2, &c, 1);
printf("c: %c\n", c);
return 0;
}
예제 실행 결과 (file1.txt 내용: "konkuk")
c: o
첫 번째 read(fd1, &c, 1); 실행 후 offset이 1 증가하여 fd2에서 읽을 때 "o"가 출력되었다. 즉, fd1과 fd2가 같은 Global File Table을 공유하기 때문에 offset이 함께 이동한다.
Shared File Descriptor (공유된 파일 디스크립터)
fork()를 호출하면 부모 프로세스의 파일 디스크립터 테이블이 그대로 복사되며, 같은 Global File Table을 공유하게 된다. 이때, 참조 카운트(ref++)가 증가한다.
File Sharing (파일 공유)
파일이 열리는 방식에 따라 같은 파일이라도 서로 다른 Global File Table을 가리킬 수 있다.
open()을 두 번 호출할 경우
dup()이 아니라 open()을 두 번 호출하면, 같은 파일을 열더라도 서로 다른 Global File Table을 가리킨다. 이 경우 같은 파일을 가리키더라도 파일 오프셋이 공유되지 않는다. (결국 같은 v-node를 가리킴, 같은 파일의 entry)
코드 예제
int main(){
int fd1, fd2;
char c;
fd1 = open("file1.txt", O_RDONLY);
fd2 = open("file1.txt", O_RDONLY);
if(fd1 == -1 || fd2 == -1){
perror("open");
exit(1);
}
read(fd1, &c, 1);
read(fd2, &c, 1);
printf("c: %c\n", c);
return 0;
}
첫 번째 read(fd1, &c, 1);이 실행된 후에도 fd2의 offset이 그대로 유지되어 k가 출력되었다. 즉, fd1과 fd2가 서로 다른 Global File Table을 가리키므로 offset을 공유하지 않는다.
서로 다른 두 파일에서 fork()이 불릴경우
fork()를 호출하면 부모 프로세스의 Open File Table이 복사되며, 같은 Global File Table을 공유한다. 따라서 파일 오프셋과 ref count가 함께 공유된다.
fork() 호출 전
부모 프로세스가 두 개의 파일 디스크립터를 보유
fork() 호출 후
부모와 자식이 같은 Global File Table을 공유
'CS 지식 > 시스템 프로그래밍' 카테고리의 다른 글
[C언어] 다중화된 입출력(Multiplexed IO) 관련 시스템 콜(System call) (0) | 2025.03.02 |
---|---|
[C언어] File I/O (Unix vs Standard IO)관련 시스템 콜(System call) (0) | 2025.03.02 |
[C언어] pthread 라이브러리를 이용해서 스레드에게 signal 보내기 (0) | 2025.01.28 |
[C언어] Deadlock(교착상태) 원인과 해결법 (0) | 2025.01.28 |
[C언어] pthread 라이브러리를 이용한 스레드 동기화 (0) | 2025.01.28 |
댓글