Linux에서 표준 파일 입출력

|

표준 입출력 라이브러리 및 스트림

초창기 유닉스(Unix) 시스템에서는 모니터 대신 프린터, 저장 장치로는 자기 테이프 등이 이용되었는데 입출력 속도가 너무 느렸습니다. 그러다보니 조금이라도 속도를 향상시키기 위해서 표준 입출력 라이브러리(Standard Input/Output Library)가 등장하게 되었습니다.

표준 입출력 라이브러리는 입출력을 위해 데이터를 바로 사용하지 않고, 버퍼(Buffer)를 이용해서 데이터를 한꺼번에 처리하도록 했습니다. 그래서 파일의 입출력에 스트림(Stream)을 사용할 수 있도록 했습니다.


FILE 구조체

FILE 구조체라는 파일 포인터를 이용해서 파일을 제어할 수 있습니다. FILE 구조체는 <stdio.h> 헤더 파일에 정의되어 있습니다.

typedef struct {
  int _cnt;
  unsigned char *_ptr;
  unsigned char *_base;
  unsigned char _flag;
  unsigned char _file;
} FILE;

FILE 구조체는 스트림을 다루기 위한 파일 디스크립터, 버퍼 공간에 대한 포인터, 버퍼 크기, 버퍼에 남아 있는 문자 수, 에러 Flag 등의 정보를 갖고 있습니다.


fopen()

파일을 읽거나 쓰기 위해 파일을 여는 기능을 합니다.

FILE *fopen(const char *path, const char *mode);

두 번째 인자인 mode에는 다음과 같은 값들이 들어갈 수 있습니다.

인자 읽기 쓰기 파일 생성
r O X X
r+ O O X
w X O O
w+ O O O
a X O O
a+ O O O


fclose()

사용한 파일을 닫습니다.

int fclose(FILE *fp);


fread() / fwrite

파일을 읽거나 쓰는 기능을 수행합니다.

int fread(void *ptr, size_t size, size_t nmemb, FILE *fp);
int fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp);

첫 번째 인자 ptr은 데이터를 위한 버퍼 공간이며, 두 번째 인자는 데이터의 크기, 세 번째 인자는 반복 횟수입니다.


fseek() / rewind()

파일 스트림의 offset을 변경할 수 있는 함수들입니다.

int fseek(FILE *fp, long offset, int whence);
int rewind(FILE *fp);

fseek() 함수는 lseek() 함수와 사용법이 같습니다.

rewind() 함수는 offset을 파일의 처음으로 이동시키며 fseek(fp, 0, SEEK_SET)와 같은 작업을 수행합니다.


ftell() / fgetpos() / gsetpos()

long ftell(FILE *fp);
int fgetpos(FILE *fp, fpos_t *pos);
int fsetpos(FILE *fp, const fpos_t *pos);

ftell() 함수는 파일의 현재 offset을 리턴하며, fgetpos() 함수는 현재 offset를 두 번째 인자인 pos 포인터로 리턴합니다. fsetpos() 함수는 파일의 offset을 두 번째 인자인 pos 값으로 이동시킵니다.


fflush()

현재 버퍼에 있는 내용을 즉시 사용하며, 버퍼를 비우는 기능을 수행합니다.

int fflush(FILE *fp);

함수가 성공적으로 수행하면 0을 리턴하고, 실패할 경우 -1을 리턴합니다. 만약 fclose() 함수를 수행하는 경우는 내부적으로 flush 작업을 수앻하기 때문에 별도로 fflush() 함수를 수행할 필요는 없습니다.

Linux의 저수준 파일 처리

|

파일 처리

리눅스에서는 디렉토리 및 디바이스들도 모두 파일로 취급됩니다. 리눅스는 POSIX 기반으로 파일에 대한 시스템 콜(System Call)을 처리합니다.


저수준 파일 입출력 명령어

리눅스에서 제공하는 POSIX 기반 파일 시스템 제어 함수들은 다음과 같습니다. 저수준 파일 제어 함수들은 <unistd.h>에 선언되어 있습니다.

함수 설명
open() 파일을 읽기/쓰기 등을 위해 파일을 열거나 생성
creat() 파일을 생성한다. open()으로 대체 가능
close() 열려있는 파일을 닫는다.
read() 파일로부터 데이터를 읽는다.
write() 파일에 데이터를 기록
lseek() 파일 포인터를 특정 위치로 이동
unlink() 파일을 삭제
remove() 파일이나 디렉토리를 삭제
fcntl() 파일 속성을 설정하거나 조정함. ioctl()로 대체 가능
dup() 파일 디스크립터(descriptor)를 복사. dup2()로 대체 가능


open()

파일을 읽거나 쓰기 위해서는 먼저 파일을 열어야 합니다.

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

파일을 열 떄 flags로는 다음과 같은 값들을 사용할 수 있습니다.

Flag 설명
O_RDONLY 읽기 전용
O_WRONLY 쓰기 전용
O_RDWR 읽기/쓰기 모두 가능
O_APPEND 쓰기 작업시 파일 끝에 내용 추가
O_CREAT 파일이 없을 경우 생성
O_EXCL 파일이 존재할 경우 에러 발생
O_TRUNC 파일이 존재할 경우 삭제
O_NONBLOCK Non-Blocking 모드로 전환
O_SYNC 쓰기 연산마다 버퍼(Buffer)없이 디스크에 바로 저장


creat()

새로운 파일을 생성하는 함수입니다. 보통 open() 함수로 대체해서 쓸 수 있기 때문에 그리 많이 사용되지는 않습니다.

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


close()

읽기/쓰기 등의 작업을 끝낸 후에는 반드시 파일을 닫아줘야 합니다.

int close(int fd);


read()

파일로부터 데이터를 읽어들이며, 데이터를 읽은만큼 offset를 증가시킵니다.

ssize_t read(int fd, void *buf, size_t count);

보통 읽어들인 데이터의 바이트 수를 리턴하지만, 파일의 끝(EOF, End of file)을 만나면 0을 리턴하고, 읽기에 실패하면 -1을 리턴합니다.


write()

파일에 데이터를 쓰며, 데이터를 쓴만큼 offset를 증가시킵니다.

ssize_t write(int fd, const void *buf, size_t count);

기록한 데이터의 크기만큼 리턴하며, 쓰기 실패시 -1을 리턴합니다.


lseek()

현재 열려 있는 파일의 원하는 위치로 offset을 이동시킵니다.

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

마지막 인자인 whence에는 기준점을 넣습니다. 기준점으로는 다음과 같은 값을 사용할 수 있습니다.

인자 설명
SEEK_SET 파일의 처음부터 상대적인 거리
SEEK_CUR 파일의 현재 offset로부터 상대적인 거리
SEEK_END 파일의 끝에서부터 상대적인 거리

lseek() 함수가 성공적으로 실행되면 해당 위치의 offset를 리턴하며, 실패할 경우에는 -1을 리턴합니다.


ioctl() / fcntl()

ioctl() 함수는 일반 파일, 네트워크 통신 모듈 등의 디바이스와 관련된 속성 연산을 위해 만들어졌습니다.

int ioctl(int fd, int cmd, ...);

일반적으로 ioctl() 함수가 성공적으로 실행되면 0 또는 0 보다 큰 수가 리턴되고 실패할 경우 -1을 리턴합니다.

fcntl() 함수는 일반 파일의 연산을 위해 만들어졌는데, 이미 열려 있는 파일의 속성을 변경할 수 있습니다. 마찬가지로 실패시 -1을 리턴합니다.

int fcntl(int fd, int cmd, ...);

fcntl() 함수의 옵션들은 다음과 같습니다.

옵션 설명
F_DUPFD 파일 디스크립터를 복사할 때 사용되며, 세 번째 인수보다 크거나 같은 값 중에 가장 작은 미사용 값을 반환
F_GETFD 파일 디스크립터의 Flag를 리턴
F_SETFD 파일 디스크립터의 Flag를 설정
F_GETFL 파일 테이블에 저장되어 있는 파일 상태 Flag(O_APPEND, O_NONBLOCK, O_SYNC 등)를 리턴
F_SETFL 파일 상태 Flag를 설정
F_GETOWN SIGSO, SIGURG 시그널을 받는 프로세스 ID 및 프로세스 그룹 ID를 리턴
F_SETOWN SIGSO, SIGURG 시그널을 받는 프로세스 ID 및 프로세스 그룹 ID를 설정

복수의 프로세스들이 같은 파일에 접근할 때 다른 프로세스들과의 충돌 방지를 위해 파일을 잠궈야 하는 경우가 있습니다. fcntl() 함수를 이용해서 이러한 작업이 가능하며, 다른 프로세스들은 파일이 잠긴 동안 대기하도록 할 수 있습니다. fcntl() 함수를 이용해서 파일을 잠그는 경우는 3번째 인자로 flock 구조체를 사용합니다.

gdb 사용법

|

gdb를 이용한 디버깅

gdb는 프로그램 실행 도중 지정된 브레이크 포인트(Breakpoint)에서 멈출 수 있으며, 해당 지점에서 각 변수들이나 포인터 들의 값을 확인할 수 있는 디버깅 툴입니다.

다음과 같이 사용할 수 있습니다.

$ gdb [실행파일명]

gdb를 실행하면 명령어를 입력받을 수 있는 콘솔 프롬프트(prompt)가 실행됩니다. help를 입력해서 도움말을 확인할 수 있고 종료하고 싶은 경우는 quit, 또는 Ctrl + D를 입력하면 됩니다.


Breakpoint 설정

gdb 콘솔에서 break 명령어(단축 명령어 : b)를 입력하면 됩니다. 다음과 같이 설정할 수 있습니다.

  • break [파일명:]함수명
  • break [파일명:]라인 번호

설정된 Breakpoint는 info break 명령어를 입력해서 조회할 수 있습니다. 또한 clear 또는 delete 명령어를 이용하여 Breakpoint를 해제하거나 삭제할 수 있습니다.

명령어 단축 명령어 설명
break b Breakpoint 설정
clear   설정된 Breakpoint를 해제
delete d 설정된 Breakpoint를 삭제
info break info b Breakpoint 정보 조회


프로그램 실행

Breakpoint를 설정한 다음 프로그램 실행은 run(r) 명령어로 할 수 있습니다. 또한 다음 명령어들을 통해서 각각의 명령을 내릴 수 있습니다.

명령어 단축 명령어 설명
continue c 다음 Breakpoint 까지 코드를 수행
step s 한 줄씩 코드를 수행하며, 함수 내부까지 들어감
next n 한 줄씩 코드를 수행하며, 함수는 건너 뜀
kill   프로그램의 수행을 종료

sn을 입력한 다음부터는 Enter 키만 누르면 이전에 실행했던 명령이 다시 실행됩니다. 현재 시점에서 소스 코드를 확인하기 위해서는 list(l) 명령어를 이용하면 됩니다.


변수 값 확인

현재 정지된 위치에서 각 변수들의 값을 확인하기 위해서는 print [변수명] 명령어를 이용하면 됩니다. print의 단축 명령어는 p 입니다. 라인을 이동하면서 계속 변수 값을 확인할 때는 display [변수명] 명령어를 이용하면 됩니다. 또한 delete display [변수명] 명령어를 이용해 해당 변수의 display 명령을 해제할 수 있습니다.


변수 값 변경

print str="abc"와 같은 형태의 명령을 통해 특정 변수의 값을 변경할 수 있습니다.

커맨드라인에서 파일로 출력하기

|

커맨드라인에서 파일로 출력하기

커맨드라인에서 파일로 출력하려면 > 명령을 사용하면 됩니다. 또한 파일 복사에도 활용할 수 있습니다. 예를 들면 다음과 같이 활용할 수 있습니다.

$ ls > files.txt

$ more files.txt
Desktop
hello.txt
snowdeer.zip


파일 병합

> 명령어를 이용하면 여러 개의 파일들을 하나의 큰 파일로 합칠 수도 있습니다. 예를 들면 다음과 같습니다.

$ cat file1.txt file2.txt file3.txt > total_file.txt

make를 이용한 빌드 방법

|

make 사용법

gcc를 이용한 빌드 방법

일반적으로 gcc를 이용한 빌드는 다음과 같은 형태의 명령을 이용합니다.

$ gcc -o helloworld helloworld.c

이 때 -o 옵션 뒤에는 실행 파일의 이름을 지정합니다.

하지만 실제로 프로젝트를 진행하면 수 많은 소스 코드를 사용하는데, 일일이 gcc를 이용해서 빌드하는 것은 번거롭기 때문에 make 유틸리티를 활용하면 보다 효율적으로 빌드를 할 수 있습니다.

make를 사용하기 위해서는 makefile이 필요합니다. makefile이 있는 경우에는 콘솔창에서 make만 입력해도 자동으로 빌드가 되며, 별도로 --makefile 또는 -f 옵션을 이용하여 makefile의 이름을 지정할 수도 있습니다.


makefile의 구성

makefile은 크게 목표(Target), 의존성(Dependency), 명령(Command)의 3 부분으로 이루어집니다.

target:         dependencyList
                commandList
요소 설명
targetLit 명령이 수행되어 생성될 결과 파일 지정
dependencyList target(목표)을 수행하기 위해 필요한 의존 관계 설정
commandList target을 수행하기 위해 여기에 정의된 명령이 실행됨

Command(명령)의 경우 다양한 유틸리티를 사용할 수 있습니다. 예를 들어 gcc 외에도 clear, cp 등의 대부분의 명령어를 사용할 수 있습니다. 명령의 앞 부분은 반드시 Tab을 이용해서 공백을 만들어주어야 합니다.


make 예제

다음과 같은 파일들이 존재한다고 가정해봅시다.

  • main.c
  • input.c
  • output.c

위의 파일들은 gcc를 이용했을 때 다음과 같이 빌드할 수 있습니다.

$ gcc -o snowdeerApp main.c input.c output.c

이 경우 make를 위한 makefile로 만들어보면 다음과 같습니다.

snowdeerApp :
    gcc -o snowdeerApp main.c input.c output.c

기본적으로 위와 같은 makefile을 이용하면 되지만, 향후 확장을 위해 다음과 같이 좀 더 유연하게 만들어 줄 수 있습니다.

snowdeerApp : main.o input.o output.o
    gcc -o snowdeerApp main.o input.o output.o

main.o : main.c
    gcc -c main.c

input.o : input.c
    gcc -c input.c

output.o : output.c
    gcc -c output.c


makefile의 매크로 사용

위에서 작성한 makefile에 ‘$(매크로)’ 같은 형식의 매크로 기능을 사용할 수 있습니다.

OBJECTS = main.o input.o output.o

snowdeerApp : $(OBJECTS)
    gcc -o snowdeerApp $(OBJECTS)

main.o : main.c
    gcc -c main.c

input.o : input.c
    gcc -c input.c

output.o : output.c
    gcc -c output.c

makefile을 좀 더 수정하면

OBJECTS = main.o input.o output.o
SRC = main.c input.c output.c

CFLAGS = -g
TARGET = snowdeerApp

$(TARGET) : $(OBJECTS)
    $(CC) -o $(TARGET) $(OBJECTS)

clear :
    rm -f $(OBJECTS) $(TARGET) core

main.o : main.c
input.o : input.c
output.o : output.c

와 같은 형태로 사용할 수 있습니다.


make에 이미 정의되어 있는 키워드들

위에서 $(CC)CFLAG 등의 키워드들은 make에 이미 정의되어 있는 매크로입니다.

매크로 설명
ARFLAGS ar 아카이브 관리 프로그램의 Flag
ASFLAGS as 어셈블러의 Flag
CFLAGS C 컴파일러의 Flag
CXXFLAGS C++ 컴파일러의 Flag
CPPFLAGS C 언어 전처리기(Preprocessor)의 Flag
LDFLAGS ld linker의 Flag
COFLASG co 유틸리티의 Flag
FFLAGS 포트란 컴파일러의 Flag
PFLAGS 파스칼 컴파일러의 Flag
LFLAGS lex의 Flag
YFLAGS yacc의 Flag
명령어 매크로 명령어 설명
AR ar ar 아카이브 관리 프로그램
AS as as 어셈블러
CC cc C 언어 컴파일러
CXX g++ C++ 컴파일러
CO co co 유틸리티
CPP $(CC) -E C 언어 전처리기(Preprocessor)
FC f77 포트란 컴파일러
PC pc 파스칼 컴파일러
LEX lex lex 프로세서
YACC yacc yacc 프로세서
TEX tex tex 프로세서
RM rm -f 파일 삭제


자동 변수

먼저 다음과 같은 예제를 살펴보겠습니다.

.SUFFIXES : .c .o

OBJECTS = main.o input.o output.o
SRC = main.c input.c output.c

CC = gcc
CFLAGS = -g
TARGET = snowdeerApp

$(TARGET) : $(OBJECTS)
    $(CC) -o $@ $(OBJECTS)

.c.o :
    $(CC) $(CFLAGS) -c $< -o $@

clear :
    $(RM) -f $(OBJECTS) $(TARGET) core

main.o : main.c
input.o : input.c
output.o : output.c

위에서 $(TARGET) 명령에서 빌드 옵션에 보면 $@가 있습니다. 이 항목은 현재 target으로 하는 대상의 이름을 지칭하는 옵션입니다. 그 외의 주요 자동 변수들은 다음과 같습니다.

변수 설명
$* 확장자가 없는 현재 목표 파일의 이름을 지칭
$@ 현재 목표 파일의 이름을 지칭
$< 현재 목표 파일보다 더 최근에 갱신된 파일명으로, 첫 번째 종속물의 이름
$? ’$<’와 동일

목표로 선언한 .c.o.o에 대응하는 .c를 발견하면 해당 command가 수행됩니다. $<는 확장자를 제외한 파일의 이름과 target(.c.o)의 앞 부분인 .c를 붙여서 사용되며, 뒤의 $@는 확장자를 제외한 파일의 이름과 target 뒤의 .o를 붙여서 사용됩니다.


매크로 치환

매크로 치환은 다음과 같은 명령어로 할 수 있습니다.

$(매크로 이름:이전 내용=새로운 내용)

예를 들어, 다음과 같은 형태로 사용할 수 있습니다.

# 생략
OBJECTS = main.o input.o output.o

# 생략
SRC = $(OBJECTS:.o=.c)

# 생략


다중 타켓

실행 파일을 여러 개 생성할 때 사용하는 방법입니다. makefile 안에 all이라는 target을 만들고 그 뒤에 필요한 target들을 나열하면 됩니다. 단, all의 앞에 다른 target을 넣으면 안됩니다.

# 생략
# TARGET = snowdeerApp

all : snowdeerApp helloWorld

snowdeerApp : $(OBJECTS)
    $(CC) -o $@ $(OBJECTS)

helloWorld : helloworld.c
    $(CC) -o $@ $<

# 생략