파일 접근 프리미티브
파일
: 연속된 바이트의 나열. 특정한 포맷이 정해져 있지는 않음(각 응용 프로그램에서 해석)
: 보조저장장치 뿐만 아니라 외부 입출력 장치에 대한 인터페이스 (e. 장치 파일)

시스템 호출(system call)
: 커널에 서비스 요청을 위한 인터페이스. 응용 프로그램은 시스템 호출을 통해 커널에 서비스를 요청

파일 접근 프리미티브(file access primitives)
: 커널에서 파일을 다루기 위해 제공되는 I/O 설비에 직접 접근을 제공하는 시스템 호출 함수들의 집합
- 리눅스 I/O를 위한 기초적 요소를 형성. 다른 파일 접근 방식은 이에 기반함
| 시스템 호출 함수 | 용도 |
| open | 파일을 열거나 빈 파일 생성 |
| creat | 빈 파일 생성 |
| close | 열린 파일을 닫음 |
| read | 파일로부터 정보를 추출 |
| write | 파일에 정보를 기록 |
| lseek | 파일 안에 지정된 바이트로 이동 |
| unlink | 파일을 제거 |
| remove | 파일을 제거 |
| fcntl | 한 파일에 연관된 속성 제어 |
파일 열기: open()
: 파일을 사용하기 위해서는 먼저 open() 시스템 호출 함수를 이용해 접근할 파일을 열어야 함
- 파일 디스크립터(file descriptor) : 열린 파일에 대한 식별 번호. 0 이상의 양의 정수값
#include <sys/types.h> // 다양한 자료형 정의
#include <sys/stat.h> // 파일의 상태 및 정보
#include <fcntl.h> // file control header
int open(const char* path, int olfag, [mode_t mode]);
// 파일 열기에 성공하면 파일 디스크립터를, 실패하면 -1을 반환
open()의 기본 플래그
: open()의 두 번째 인수 oflag는 <fcntl.h>에 정의된 정수 상수 사용
- O_RDONLY : 읽기 전용으로 파일 열기. read() 호출 사용 가능
- O_WRONLY : 쓰기 전용으로 파일 열기. write() 호출 사용 가능
- O_RDWR : 읽고 쓰기용으로 파일 열기. read(), write() 호출 사용 가능
파일 닫기: close()
: 파일 사용을 마치면 close() 시스템 호출 함수를 이용해 열린 파일을 닫아야 함
- why) ① 한 프로세스 내에서 동시에 열 수 있는 파일 수에 제한이 있음 / ② 안전하게 파일의 사용을 마침
#include <unistd.h>
int close(int fd);
// 파일 닫기에 성공하면 0, 실패하면 -1을 반환
실습(파일 열고 닫기): open.c, mycat.c
open.c
#include <fctl.h> // open(), O_WRONLY
#include <unistd.h> // close()
#include <stdio.h>
#include <stdlib.h> // exit()
int main(int arg, char* argv[])
{
int fd;
if ((fd = open(argv[1], O_RDONLY)) == -1) {
printf("파일 열기 오류\n");
exit(-1);
}
printf("파일 %s 열기 성공: %d\n", argv[1], fd);
close(fd);
exit(0);
}
실습 결과
~/fio$ gcc -o open open.c
~/fio$ ls -F
cmd_arg* cmd_arg.c open* open.c readme
~/fio$ ./open abcd
파일 열기 오류
~/fio$ ./open readme
파일 readme 열기 성공: 3
~/fio$
mycat.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define BUFSIZE 1024
int main(int argc, char* argv[]) // ./mycat readme
{
char buf[BUFSIZE];
int fd;
ssize_t nread;
if ((fd = open(argv[1], O_RDONLY)) == -1 ) {
perror(argv[1]);
exit(1);
}
while ((nread = read(fd, buf, BUFSIZE) > 0)) { /* fd로부터 buf에 읽어들이기 */
/* buf의 내용을 표준출력에 쓰기 */
write(1, buf, nread);
}
/* 파일 닫기 */
close(fd);
exit(0);
}
open()의 추가 플래그
| 플래그 | 설명 |
| O_CREAT | 해당 파일이 없는 경우에 생성 |
| O_TRUNC | 파일이 이미 있는 경우 내용을 지움 |
| O_EXCL | O_CREAT와 함께 사용. 해당 파일이 이미 있으면 오류 발생 |
| O_APPEND | 데이터를 쓰면 파일 끝에 추가 |
| O_NONBLOCK | 넌-블로킹 모드로 입출력 |
| O_SYNC | write() 호출시 디스크에 물리적으로 쓴 후 반환 |
블로킹(Blocking) vs. 논블로킹(Non-blocking)
기본모드(Blocking) : 데이터를 읽으려는데 데이터가 없으면 들어올 때까지 프로세스를 재우고 대기 ➡ I/O가 완료될 때까지 함수(read, write)에서 빠져나오지 못함
O_NONBLOCK (non-blocking) : 데이터가 없으면 대기하지 않고 즉시 에러(-1)를 반환. 즉, 함수 호출 시 즉시 결과(성공 혹은 실패)를 받고 다음 코드를 실행
파일의 생성
: open()을 이용해 쓰기 모드로 연다
| 플래그 | 설명 |
| O_WRONLY | O_CREAT | 파일 존재 시 시작 위치부터 overwrite |
| O_WRONLY | O_CREAT | O_TRUNC | 파일 존재 시 대상 파일의 내용을 지우고 크기를 0으로 |
| O_WRONLY | O_CREAT | O_EXCL | 파일이 존재하면 오류 발생 |
| O_WRONLY | O_CREAT | O_APEND | 기존 파일 끝에 추가 |
open()의 모드
: open()의 세 번째 인수 mode는 선택적으로 사용
- 생성되는 파일의 접근 권한 지정할 때(O_CREAT 플래그와 함께 사용됨)
- 설정값: <sys/stat.h>에 정의된 접근권한에 대한 상수 이용 (8진수 표기법 e. 0644:-rw-r--r--)
파일 생성의 다른 방법
: 일반적인 저장용 파일 생성 후 열기
- open(path, O_WRONLY|O_CREAT|O_TRUNC, mode); 를 이용한 경우와 동일
#include <sys/types.h> // 다양한 자료형 정의
#include <sys/stat.h> // 파일의 상태 및 정보
#include <fcntl.h> // file control header)
int creat(const char* path, mode_t mode);
// 파일 열기에 성공하면 파일 디스크립터를, 실패하면 -1을 반환
실습(파일생성): create.c
#include <fcntl.h> // open(), creat(), O_WRONLY
#include <unistd.h> // close()
#include <stdio.h>
#include <stdlib.h> // exit()
#define PERMS 0644
int main(int argc, char* argv[])
{
int fd;
if ((fd = creat(argv[1], 0600)) == -1) {
printf("파일 열기 오류\n");
exit(-1);
}
printf("파일 %s 열기 성공: %d\n", argv[1], fd);
close(fd);
exit(0);
}
파일 읽기: read()
: fd가 나타내는 파일에서 nbytes 만큼 한번에 읽어 buf에 저장
- 읽을 데이터가 nbytes보다 적으면 있는 만큼만 읽음
- read() 호출 때마다 파일 위치 포인터는 읽은 바이트 만큼 이동함
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t nbytes);
// 파일 읽기에 성공하면 읽은 바이트 수 반환
// 파일 끝을 만나면 0을, 실패하면 -1을 반환
파일 위치 포인터(file position pointer)
: 파일 내에 읽거나 쓸 현재 파일 위치(current file position)를 가리킴
- 기본적으로 파일 열기 시 시작점을 가리킴 (O_APPEND 플래그 사용시 마지막 위치로 자동 갱신)
- 읽기 쓰기 동작 시 자동으로 갱신

파일 읽기: write()
: fd가 나타내는 파일에 buf에 있는 nbytes 만큼의 데이터를 저장.
- write() 호출 때마다 파일 위치 포인터는 마지막으로 쓰인 바이트 바로 뒤로 이동됨
#include <unistd.h>
ssize_t write(int fd, void* buf, size_t nbytes);
// 파일 쓰기가 성공하면 실제 저장된 바이트 수 반환
// 실패하면 -1을 반환
실습: mycp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define BUFSIZE 1024
#define PERMS 0644
int main(int argc, char* argv[])
{
char buf[BUFSIZE];
int fd_r, fd_w;
ssize_t nread;
if (argc != 3) {
fprintf(stderr, "usage: %s file1 file2\n", argv[0]);
exit(1);
}
if ((fd_r = open(argv[1], O_RDONLY))/* argv[1]을 읽기 모드로 열기 */ == -1) {
perror(argv[1]);
exit(2);
}
if ((fd_w = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC))/* argv[2]를 쓰기 모드로 열기 */ == -1) {
perror(argv[2]);
exit(3);
}
while ((nread = read(fd_r, buf, BUFSIZE)) > 0) {
if (write(fd_w, buf, nread) < nread) {
close(fd_r);
close(fd_w);
perror("File write error");
exit(4);
}
}
/* 모든 파일 닫기 */
close(fd_r);
close(fd_w);
if (nread == -1) {
perror("File read error");
exit(5);
}
exit(0);
}
파일 디스크립터의 복제: dup(), dup2()
: 열려 있는 한 파일을 공유하기 위해 사용. 추후 파일 입출력 리다이렉션 시 사용
#include <unistd.h>
int dup(int oldfd);
// oldfd에 대한 복제본인 새로운 파일 디스크립터를 생성하여 반환
// 실패하면 -1을 반환
int dup2(int oldfd, int newfd);
// oldfd을 newfd에 복제하고 복제된 새로운 파일 디스크립터를 반환
// 실패하면 -1을 반환
실습: dup.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define FILENAME "dup_test"
int main(void)
{
int fd, fd_cp;
if ((fd = creat(FILENAME, 0600)) == -1)
perror(FILENAME);
write(fd, "Hello", 6);
fd_cp = dup(fd);
write(fd_cp, "World\n", 6);
exit(0);
}
실행결과
minjeong@ubuntu:~/Documents/test$ gcc -o dup dup.c
minjeong@ubuntu:~/Documents/test$ ./dup
minjeong@ubuntu:~/Documents/test$ ls
dup dup.c dup_test helloworld helloworld.c
minjeong@ubuntu:~/Documents/test$ cat dup_test
HelloWorld
파일 임의 접근
파일 위치 포인터의 이동: lseek()
: 파일 위치 포인터를 whence를 기준으로 offset만큼 이동한 곳으로 변경
- offset : 양수이면 파일 끝 쪽으로, 음수이면 파일 앞쪽으로
- whence:
SEEK_SET(파일의 시작),SEEK_CUR(현재 위치),SEEK_END(파일의 끝)

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
// 이동에 성공하면 현재 위치를, 실패하면 -1을 반환
파일 위치 포인터의 이동 예(1/3) : 기본 이동
lseek(fd, 0L, SEEK_SET);: 파일 시작 위치로 이동(rewind)lseek(fd, 100L, SEEK_SET);: 파일 시작 위치에서 100바이트 떨어진 위치로 이동- l
seek(fd, 0L, SEEK_END);: 파일 끝 위치로 이동(append)
파일 위치 포인터의 이동 예(2/3) : 레코드 단위 이동(구조체 변수 record 가정)
lseek(fd, n * sizeof(record), SEEK_SET);: n + 1 번째 레코드 시작 위치로- l
seek(fd, sizeof(record), SEEK_CUR);: 다음 레코드 시작 위치로 - l
seek(fd, -sizeof(record), SEEK_CUR);: 전 레코드 시작위치로
파일 위치 포인터의 이동 예(3/3) : 파일의 끝 너머로 이동
lseek(fd, sizeof(record), SEEK_END);: 파일의 끝에서 한 레코드 다음 위치로

실습: lseek_test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
int main() {
char* filename = "test.txt";
char* data = "Hello, this is a test file for lseek!";
// 파일 생성 및 데이터 쓰기
int fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
printf("Failed to open file: %s\n", strerror(errno));
exit(1);
}
// 데이터 쓰기
write(fd, data, strlen(data));
/* 파일의 시작으로 이동 (SEEK_SET) */
printf("1. Moved to the beginning of the file: %ld\n", (long) lseek(fd, 0L, SEEK_SET));
/* 현재 위치에서 5바이트 앞으로 이동 (SEEK_CUR) */
printf("2. Moved 5 bytes from the current position: %ld\n", (long) lseek(fd, -5L, SEEK_CUR));
char buff;
read(fd, &buff, 1);
printf("3. Read data at current position: %c\n", buff);
/* 파일 끝에서 10바이트 뒤로 이동 (SEEK_END) */
printf("4. Moved 10 bytes back from the end of the file: %ld\n", (long) lseek(fd, 10L, SEEK_END));
// 현재 위치에서 데이터 읽기
char buffer[100] = { '\0' };
read(fd, buffer, 10);
printf("5. Read data at current position: %s\n", buffer);
// 파일 닫기
close(fd);
return 0;
}
실습하기: 임의 접근을 이용한 학생 레코드 검색
개요
: 학생 정보를 레코드 단위로 파일 입출력
- 임의 접근 프리미티브를 이용해 각 레코드에 접근
- 학번(id), 이름(name), 점수(score) 저장 : 구조체 타입으로 정의해 사용 (student.h)
#ifndef _STUDENT_H_
#define _STUDENT_H_
#define MAX 24
#define START_ID 1001001
typedef struct student {
char name[MAX];
int id;
double gpa; // Grade Point Average
} STUD_T;
#endif
레코드 수정 과정 (in stud_create.c)
: 파일에서 해당 레코드를 읽어 화면에 표시 후 원래 위치에 수정된 레코드 기록
- lseek()를 이용해 레코드 크기 만큼 이동 필요

makefile
CC = gcc
CFLAGS = -Wall
TARGETS = stud_create stud_list stud_search stud_update
all: $(TARGETS)
stud_create: stud_create.c student.h
$(CC) $(CFLAGS) -o stud_create stud_create.c
stud_list: stud_list.c student.h
$(CC) $(CFLAGS) -o stud_list stud_list.c
stud_search: stud_search.c student.h
$(CC) $(CFLAGS) -o stud_search stud_search.c
stud_update: stud_update.c student.h
$(CC) $(CFLAGS) -o stud_update stud_update.c
clean:
rm -f $(TARGETS) mydb
stud_create.c : 키보드로 입력(^d로 종료)한 데이터를 파일에 저장
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include "student.h"
int main(int argc, char* argv[]) // ./create mydb
{
int fd;
STUD_T rec;
if (argc < 2) {
fprintf(stderr, "usage: %s file\n", argv[0]);
exit(1);
}
/* 첫번째 명령행 인자로 전달된 이름으로 쓰기 용도의 파일 열기 */
if ((fd = open(argv[1], O_WRONLY | O_CREAT, 0644)) == -1) {
perror(argv[1]);
exit(2);
}
printf("%-9s %-13s %-4s\n", "ID", "NAME", "GPA");
while (scanf("%d %s %lf", &rec.id, rec.name, &rec.gpa) == 3) {
/* 해당 학번의 학생 정보 위치로 파일 포인터 이동 */
lseek(fd, (rec.id - START_ID) * sizeof(rec), SEEK_SET);
/* fd가 가리키는 파일에 rec 저장 */
write(fd, &rec, sizeof(rec));
}
/* 파일 닫기 */
close(fd);
return 0;
}
stud_list.c : 파일에 저장된 전체 학생 정보 일괄 조회
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include "student.h"
int main(int argc, char* argv[])
{
int fd;
STUD_T rec;
if (argc < 2) {
fprintf(stderr, "usage: %s file\n", argv[0]);
exit(1);
}
/* 첫번째 명령행 인자로 전달된 이름으로 읽기 용도의 파일 열기 */
if ((fd = open(argv[1], O_RDONLY)) == -1) {
perror(argv[1]);
exit(2);
}
while ( 1 ) {
/* fd가 가리키는 파일로부터 하나의 레코드를 rec에 읽어 들이기 */
if (read(fd, &rec, sizeof(rec)) <= 0)
break;
/*rec의 id필드가 0이면*/
if (rec.id == 0)
continue;
printf("ID: %d\t NAME: %-10s\t GPA: %.1f\n", rec.id, rec.name, rec.gpa);
}
/* 파일 닫기 */
close(fd);
return 0;
}
stud_search.c : 학번을 기준으로 특정 학생 정보 조회
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include "student.h"
int main(int argc, char* argv[])
{
int fd;
STUD_T rec;
char c;
if (argc < 2) {
fprintf(stderr, "usage: %s file\n", argv[0]);
exit(1);
}
/* 첫번째 명령행 인자로 전달된 이름으로 읽기 용도의 파일 열기 */
if ((fd = open(argv[1], O_RDONLY)) == -1) {
perror(argv[1]);
exit(2);
}
do {
int id;
printf("\nEnter student ID to search: ");
if (scanf("%d", &id) == 1) {
/* 해당 학번의 학생 정보 위치로 파일 포인터 이동 */;
lseek(fd, (id - START_ID) * sizeof(rec), SEEK_SET);
/* fd가 가리키는 파일로부터 하나의 레코드를 rec에 읽어 들이기 */
if ((read(fd, &rec, sizeof(rec)) > 0) && (rec.id != 0))
printf("ID: %d\t Name: %-10s\t GPA: %.1f\n", rec.id, rec.name, rec.gpa);
else
printf("Record %d not found\n", id);
}
else
printf("Input error");
printf("Continue? (Y/N) ");
scanf(" %c", &c);
} while (c == 'Y' || c == 'y');
/* 파일 닫기 */
close(fd);
return 0;
}
stud_update.c : O_RDWR 플래그로 파일을 열어서 → 학번을 기준으로 특정 학생의 정보를 보여준 후 → 새 성적을 입력받아 그 학생의 성적 정보를 갱신
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include "student.h"
int main(int argc, char* argv[])
{
int fd;
STUD_T rec;
char c;
if (argc < 2) {
fprintf(stderr, "usage: %s file\n", argv[0]);
exit(1);
}
/* 첫번째 명령행 인자로 전달된 이름으로 읽기/쓰기 용도의 파일 열기 */
if ((fd = open(argv[1], O_RDWR)) == -1) {
perror(argv[1]);
exit(2);
}
do {
int id;
printf("\nEnter student ID to modify: ");
if (scanf("%d", &id) == 1) {
/* 해당 학번의 학생 정보 위치로 파일 포인터 이동 */
lseek(fd, (id - START_ID) * sizeof(rec), SEEK_SET);
/* fd가 가리키는 파일로부터 하나의 레코드를 rec에 읽어 들이기 */
if ((read(fd, &rec, sizeof(rec)) > 0) && (rec.id != 0)) {
printf("ID: %d\t Name %s\t GPA %.1f\n", rec.id, rec.name, rec.gpa);
printf("Enter new gpa: ");
scanf("%lf", &rec.gpa);
/* 이전 레코드 위치로 파일 포인터 이동 */
lseek(fd, (long) - sizeof(rec), SEEK_CUR);
/* fd가 가리키는 파일에 rec 저장 */
write(fd, &rec, sizeof(rec));
}
else
printf("Record %d not found\n", id);
}
else
printf("Input error\n");
printf("Continue? (Y/N) ");
scanf(" %c", &c);
} while (c == 'Y' || c == 'y');
/* 파일 닫기 */
close(fd);
return 0;
}