커널연구회

리눅스 시스템 프로그래밍 2

리눅스 임베디드보드 구조 이해를 바탕으로 개발환경을 구축하고 디바이스 드라이버 작성 및 리눅스 시스템 프로그래밍 방법을 익혀서 리눅스 임베디드보드에서 영어학습기를 구현해 보도록 하자.  필자가 관련기술들을 체계적으로 학습하고 실습한 내용으로 임베디드보드에 영어학습기를 구현한 사례를 독자 여러분들과 공유하고자 한다. 내용을 되도록이면 정확하게 전달하고자 노력했으며, 독자 여러분들의 많은 관심과 격려 있기를 바란다.  필자의 작은 노력이 관련 기술분야에서 한줌의 민들레 꽃씨가 되었으면 한다.

글: 정재준 / rgbi3307@nate.com
커널연구회(www.kernel.bz)

연재순서
01회. 임베디드보드 구조
02회. 부트로더 이해
03회. 리눅스 커널포팅
04회. 디바이스 드라이버 이해1
05회. 디바이스 드라이버 이해2
06회. 디바이스 드라이버 이해3
07회. 리눅스 시스템 프로그래밍1
08회. 리눅스 시스템 프로그래밍2
09회. 리눅스 시스템 프로그래밍3
10회. 영어학습기 구현1
11회. 영어학습기 구현2
12회. 영어학습기 구현3

필 / 자 / 소 / 개
필자는 학창시절 마이크로마우스를 만들었고, 10년 동안 쌓아온 IT관련 개발 경험을 바탕으로 『오라클실무활용SQL튜닝(혜지원)』이라는 책을 집필하였다. 서울대병원 전산실에서 데이터베이스 관련 일을 하면서, 학창시절부터 꾸준히 해온 리눅스 연구를 계속하고 있다. 특히, 컴퓨터구조 및 자료구조 효율성 연구를 통한 기술서적 집필에 노력하고 있다. 또한, 온라인 상에서 커널연구회(http://www.kernel.bz/) 웹사이트를 운영하며 관련기술들을 공유하고 있다. 

개요

이번 연재에서는 스트림 파일 포인터를 사용하는 버퍼 입출력(Buffer I/O)에 대해서 기술한다.  버퍼 입출력은 그동안 표준화 되었고 스트림 파일 포인터(File Pointers)를 사용한다.  따라서, 스트림 파일 열기(open), 스트림 파일 읽기(read), 스트림에 쓰기(write), 스트림 탐색(seek)에 대해서 자세히 알아보고 간단한 예제코드를 사용하여 실습도 해보도록 하자.

버퍼 입출력(Buffer I/O)

입출력(I/O) 수행은 블럭(block)에 영향을 많이 받는다.  입출력 크기의 경계를 블럭 크기의 정수배로 하면 수행력이 향상된다. 예를 들면 읽기를 위한 시스템 호출을 1바이트 단위로 한 번씩 1024번 호출하는 것은 1024바이트를 한번에 호출하는 것보다 수행력이 현저하게 떨어진다.  만약 블럭 크기가 1킬로바이트(1024바이트)라면 1130바이트를 입출력 수행하는 것은 1024바이트에 비해서 속도가 저하된다.

블럭의 크기는 보통 512, 1024, 2048, 4096 바이트이다.  이러한 블럭 크기를 곱하거나 나누어서 정수형태로 떨어지는 크기로 입출력을 수행하면 효율이 좋아진다. 왜냐하면 커널과 하드웨어는 블럭 단위로 동작하기 때문에 데이터 입출력이 블럭 경계치 단위로 이루어지면 불필요한 동작이 줄어들어 수행력이 놓아진다.  stat() 시스템 호출을 사용하면 현장치의 블록 크기를 알아낼 수 있다.  표준 입출력 라이브러리 함수는 블럭 버퍼링 방식으로 동작하는 기능을 제공한다.

표준 입출력(Standard I/O)

표준 C 라이브러리는 장착되는 위치에 상관없이 독립적인 표준 입출력 라이브러리를 제공한다. 이것을 간단히 stdio라고 부르며, 사용하기에 간편하고 user-buffering 솔루션에 해당한다.  FORTRAN과는 다르게 C 언어는 내장형 함수들을 내부에 포함하지는 않지만 C 언어 사용자들은 핵심적으로 반복되는 루틴들을 표준 함수형 집합으로 개발했다. 대표적인 것으로 문자열 조작, 수학적인 루틴, 시간과 날짜 함수, 입출력 함수 등이 있다. 이러한 것들은 점차적으로 성숙되어 안정화 되었고, 1989년에 ANSI C에서 표준 C 라이브러리(C89)로 채택하였다.

파일 입출력에 해당하는 사용자 버퍼 I/O는 표준 C 라이브러리로 구현되었으며, 이것을 통하여 파일을 열고, 닫고, 읽고, 쓴다. 응용프로그램이 표준 입출력을 사용할 것인지, 스스로 만든 사용자 버퍼 솔루션을 사용할 것인지, 직접적으로 시스템 호출을 사용할 것인지의 결정은 어플리케이션의 필요와 요구사항을 고려하여 주의 깊게 선택한다. 지금부터 설명하는 버퍼 입출력은 현대 리눅스 시스템의 glibc에 구현되어 있는 것이다.

파일 포인터(File Pointers)

표준 입출력 루틴은 file descriptor들을 직접적으로 동작 시키지 않는다. 대신에 자신이 식별할 수 있는 file pointer을 사용하며, 이것은 C 라이브러리 안쪽에 있고 file descriptor에 연결된다. file pointer는 에 정의되어 있는 FILE typedef 되어 있는 포인터로 표현된다.

FILE *stream;
표준 입출력에서 파일을 스트림(stream)이라 한다.  스트림들은 읽기용(input streams), 쓰기용(output streams) 혹은 두가지 모두(input/output streams)로 열려 질 수 있다.

스트림 파일 열기(open)

fopen()을 사용하여 파일을 읽거나 쓰기용으로 파일을 오픈한다. path에 해당하는 파일을 mode 방식으로 오픈한다.  mode는 파일을 어떤 방식으로 오픈할 것인지 결정하며 아래와 같은 종류가 있다.

·r : 파일을 읽기용으로 오픈.  스트림은 파일의 시작에 위치한다.
·r: 파일을 읽기 및 쓰기용으로 오픈.  스트림은 파일의 시작에 위치한다.
·w : 파일을 쓰기용으로 오픈.  파일이 존재한다면 길이를 0으로 함.  파일이 존재하지 않는다면 생성함.  스트림은 파일의 시작에 위치한다.
·w: 파일을 읽기 및 쓰기용으로 오픈.  파일이 존재한다면 길이를 0으로 함.  파일이 존재하지 않는다면 생성함.  스트림은 파일의 시작에 위치한다.
·a : 파일을 추가(append) 목적의 쓰기용으로 오픈.  파일이 존재하지 않는다면 생성함.
·스트림은 파일의 끝에 위치한다.
·a: 파일을 추가(append) 목적의 쓰기 및 읽기용으로 오픈.  파일이 존재하지 않는다면 생성함.
·스트림은 파일의 끝에 위치한다.

아래 코드는 fopen() 함수의 사용 예이다.

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

FILE *stream;
stream = fopen ("/etc/manifest", "r");
if (!stream) /* error */
 
File Descriptor를 통하여 스트림 열기:
fdopen()은 이미 열려진 file descriptor(fd)을 스트림으로 변환한다. mode는 fopen()과 동일하며 file descriptor(fd)을 열때 사용한 것과 호환이 되어야 한다. w와 w도 사용할 수 있으나 파일을 0으로 자르지는 않는다. 스트림은 file descriptor와 연관된 파일 위치로 지정된다. fd가 스트림으로 변환 되었기 때문에 I/O는 더이상 fd와 직접적으로 수행되지 않는다. fd는 중복되지 않으며, 새로운 스트림과 연관된다. 스트림을 닫으면 이것과 연관된 fd도 또한 닫힌다.
아래의 코드는 open() 시스템 콜을 통하여 /home/kidd/ map.txt 파일을 오픈 한 후 fd를 fdopen()를 사용하여 스트림에 연관시킨다.

#include
FILE * fdopen (int fd, const char *mode);

FILE *stream;
int fd;
fd = open ("/home/kidd/map.txt", O_RDONLY);
if (fd == -1) /* error */

stream = fdopen (fd, "r");
if (!stream) /* error */

스트림 닫기:
fclose() 함수는 스트림을 닫는다. 버퍼에 있는 데이터나 아직 쓰여지지 않은 데이터가 먼저 완료된다. fclose()는 성공하면 0을 반환하고, 실패하면 EOF를 반환하고 적당한 errno을 설정한다.

#include
int fclose (FILE *stream);

모든 스트림 닫기:
fcloseall() 함수는 현재 프로세스와 연관된 모든 스트림을 닫는다.  닫히기 전에 모든 스트림들은 완료(flush)되고, fcloseall()는 항상 0을 반환한다; 리눅스 맞춤형 함수이다.

#define _GNU_SOURCE
#include
int fcloseall (void);

스트림 파일 읽기(read)

스트림에서 읽기:
표준 C 라이브러리는 열려진 스트림에서 데이터를 읽기 위한 여러 개의 함수들을 구현했다. 가장 대표적인 것으로 한번에 하나의 문자를 읽는 것과 한번에 한 라인을 읽는 것, 그리고 이진 데이터를 읽는 것이 있다. 스트림에서 데이터를 읽기 위해서는 적절한 mode로 스트림이 열려져 있어야 한다. 이때, w나 a모드로는 읽을 수 없다.

한번에 한 문자 읽기:
fgetc( ) 함수는 스트림에서 한 문자을 읽는다. 이 함수는 스트림에서 unsigned char을 읽어서 int로 형변환 한다. 

#include
int fgetc (FILE *stream);

int c;
c = fgetc (stream);
if (c == EOF)
/* error */
else
printf ("c=%c¥n", (char) c);

아래의 예제는 스트림에서 하나의 문자를 읽어서 오류여부를 체크하고, 읽은 결과를 문자로 출력하는 것이다.  이때 스트림은 읽기용으로 열려져 있어야 한다.

int c;
c = fgetc (stream);
if (c == EOF)
/* error */
else
printf ("c=%c¥n", (char) c);

문자를 되돌려 놓기:
표준 I/O는 문자를 스트림쪽으로 되돌려 넣는 함수 ungetc()를 제공한다. 스트림에 있는 데이터를 잠시 들여다 보고, 원하지 않는 값이면 다시 스트림에 되돌려 놓을 수 있다. 각각의 호출마다 c를 unsigned char로 형변환하여 스트림에 되돌려 넣는다. 성공하면 c가 반환되고, 실패하면 EOF이 반환된다. 만약 여러 개의 문자들이 되돌려 지면, 역방향 순서로 반환된다. 즉 마지막으로 push된 데이터가 처음으로 반환된다. POSIX에서는 단지 하나만 되돌려 넣는 것이 보장된다고 한다. 리눅스에서는 메모리가 허용하는 한 무한히 되돌려 넣을 수 있다.

#include
int ungetc (int c, FILE *stream);

한 라인 읽기:
fgets() 함수는 스트림에서 문자열을 읽는다.  이 함수는 스트림에서 size보다 하나 작은 바이트을 읽어서 str에 저장한다. 그리고 null 문자(₩0)를 읽은 버퍼의 끝에 추가한다. EOF 이나 개행(newline) 문자를 만나면 읽기 중지한다. 만약 개행(newline) 문자를 읽으면 str에 ₩n이 저장된다. 성공하면 str을 반환하고, 실패하면 NULL을 반환한다.

#include
char * fgets (char *str, int size, FILE *stream);

예를들면:
POSIX는 LINE_MAX을 에 정의해 두고 있다. 이것은 최대로 다룰 수 있는 입력 행의 최대 크기이다. 리눅스의 C 라이브러리는 이러한 최대 라인 크기 제약사항이 없으나 호환성의 문제로 LINE_MAX을 사용하는 것이 안전하다. 리눅스 맞춤형 프로그램에서는 이러한 라인 제약사항을 걱정하지 않아도 된다.

char buf[LINE_MAX];
if (!fgets (buf, LINE_MAX, stream)) /* error */

임의의 문자열 읽기:
fgets() 함수는 행 단위로 읽으므로 유용하다. 가끔 개발자들은 개행(newline) 문자가 아닌 다른 구분문자를 원하는 경우가 있다. 혹은 구분문자를 전혀 원하지 않는 경우도 있으며, 드물게는 버퍼에 구분문자를 저장하는 경우도 있다. 반환되는 버퍼에 개행문자를 저장할 것인지는 선택사항이다. fgetc() 함수를 사용하여 fgets()와 같은 역할을 하는 코드를 어렵지 않게 작성할 수 있다. 예를 들면, 아래 코드는 스트림에서 n - 1 바이트을 읽어서 str에 저장한뒤 널(₩0) 문자를 끝에 추가한다.

char *s;
int c;
s = str;
while (n > 0 && (c = fgetc (stream)) != EOF)
        *s = c;
*s = ′¥0′;

또한, 특정 구분문자 d을 추가하여 이 문자를 읽었을 때, 읽기 중지 하도록 위의 코드를 아래와 같이 수정할 수 있다.(여기서 d는 널 문자가 아니다) d를 개행문자(₩n)로 지정하면 fgets()와 동일한 역할을 수행하지만, 개행문자를 버퍼에 저장하지는 않는다.

char *s;
int c = 0;
s = str;
while (n > 0 && (c = fgetc (stream)) != EOF && (*s = c) != d)
;
if (c == d)
*s = ′¥0′;
else
*s = ′¥0′;

위와 같이 수정된 코드는 fgetc() 함수를 반복적으로 호출하므로 fgets()에 비해서 느릴 수 있다. 위 코드는 함수 호출 부하가 발생할 수 있으나, 시스템 호출 부하를 초래하지는 않는다. 시스템 호출 부하가 더 큰 문제이다.

이진 데이터 읽기:
몇몇 어플리케이션에서 문자형으로 데이터를 읽는 것이 부적절한 경우가 있다. 가끔, 개발자들은 C 구조체와 같이 복잡한 이진 데이터를 읽고 쓰기를 원한다. 이것을 지원하기 위해, 표준 I/O 라이브러리는 fread() 함수를 제공한다.

#include
size_t fread (void *buf, size_t size, size_t nr, FILE *stream);

fread() 함수는 stream에서 각각 size 바이트 만큼 데이터의 nr 요소씩 읽어서 buf가 지정하는 버퍼에 저장한다.  파일 포인터는 읽은 바이트 수만큼 증가되면서 진행된다.  읽은 요소의 수(읽은 바이트 수가 아님)가 반환된다. 반환값이 nr보다 작으면 읽기 실패나 EOF을 의미한다. 읽기 실패는 ferror(), EOF는 feof() 함수를 사용하여 판단해야 한다.  변수 크기, 경계치, 패딩, 바이트 순서 등의 차이로 인해, 이진 데이터는 어플리케이션간에 호환되지 않을 수 있다.  또한, 같은 어플리케이션이지만 머신이 다르다면 이같은 현상이 발생할 수 있다.
아래 코드는 스트림에서 하나의 요소를 읽는 간단한 예제이다.

char buf[64];
size_t nr;
nr = fread (buf, sizeof(buf), 1, stream);
if (nr == 0) /* error */

스트림에 쓰기(write)


읽기와 마찬가지로 표준 C 라이브러리는 스트림에 쓰기위한 함수들을 많이 정의했다. 쓰기용으로 가장 대표적인 3가지 종류가 있는데, 하나의 문자 단위로 쓰기, 문자열 쓰기, 이진 데이터 쓰기이다. 이러한 쓰기 작업은 버퍼 입출력으로 동작한다. 스트림에 쓰기 위해서는 출력용 스트림으로 오픈되어 있어야 하며, r 모드는 적합하지 않다.

하나의 문자 쓰기:
fgetc()의 반대 개념이 fputc()이다. fputc() 함수는 c에 저장되어 있는 데이터를 unsigned char로 형변환한 바이트를 스트림에 쓴다. 성공하면 c을 반환하고, 실패하면 EOF를 반환하고 적절한 errno를 설정한다.

#include
int fputc (int c, FILE *stream);

아래의 간단한 예제는 문자 ′p′를 쓰기용으로 열려져 있는 스트림에 쓴다.

if (fputc (′p′, stream) == EOF)
        /* error */

문자열을 스트림에 쓰기:
fputs() 함수는 스트림에 문자열을 쓴다. str은 널(null)로 종료되는 문자열이며 이것을 스트림에 쓴다. 성공 시 양수를 반환하고 실패 시 EOF를 반환한다.

#include
int fputs (const char *str, FILE *stream);

아래의 예제는 추가적(append) 쓰기용으로 파일을 오픈 한 스트림에 문자열을 쓰는 코드이다.

FILE *stream;
stream = fopen ("journal.txt", "a");
if (!stream)
        /* error */
if (fputs ("The ship is made of wood.¥n", stream) == EOF)
        /* error */
if (fclose (stream) == EOF)
        /* error */

이진 데이터 쓰기:
C 변수와 같이 이진 데이터를 직접적으로 저장하고자 할 때는 fwrite() 표준 I/O 함수를 사용한다. fwrite() 함수는 각각의 size 바이트 길이만큼 스트림에 nr 요소를 쓴다. 파일 포인터는 쓰여진 바이트 수만큼 이동한다. 쓰기 성공 시 요소들의 수(바이트 크기가 아님)를 반환하고, 실패 시 nr 보다 작은 값을 반환한다.

#include
size_t fwrite (void *buf,
               size_t size,
               size_t nr,
               FILE *stream);

아래는 Buffered I/O를 사용하는 간단한 프로그램 예제이며, 출력 결과는 다음과 같다.

name="Edward Teach" booty=950 beard_len=48

이진 데이터는 다른 어플리케이션에서 읽혀지지 않을 수도 있다. fwrite()로 쓰여진 데이터는 어플리케이션이 다르거나 혹은 동일한 어플리케이션이지만 기계가 다른 경우 읽혀지지 않을 수 있다. unsigned long의 크기가 바뀌거나 패딩의 량이 변화되면 이런 현상이 발생한다. 이러한 경우에 특정 기계 형식과 ABI(Application Binary Interface)를 참조 하도록 한다.

스트림 탐색(seek)

스트림의 현재 위치를 제어하는 것은 유용하다. 어플리케이션은 파일을 기반으로 하여 복잡한 레코드를 읽으며 스트림의 위치를 건너 뛰거나 0값으로 초기화한다. 표준 입출력은 lseek() 시스템 호출을 통하여 이러한 역할을 수행한다. fseek() 함수는 가장 일반화된 표준 입출력 함수로서 스트림의 위치를 조정한다.

#include
int fseek (FILE *stream, long offset, int whence);

whence 에 전달된 값에 따라서 fseek() 함수는 아래와 같이 동작한다.

·SEEK_: 파일 스트림의 위치가 offset으로 설정된다.
·SEEK_CUR: 파일 스트림의 위치가 현재의 위치에 offset을 더한 값으로 설정된다.
·SEEK_END: 파일의 끝에 offset를 더한 만큼 스트림의 위치를 설정한다.

성공적으로 완료되면 fseek()는 0을 반환하고, 실패하면 -1을 반환한다. 또한, 표준 입출력은 fsetpos() 함수를 제공한다. 이 함수는 스트림 위치를 pos로 설정하며, fseek() 함수의 whence 파라미터에 SEEK_을 전달한 것과 동일한 역할을 한다. 성공 시 0을 반환하고, 실패 시 -1을 반환하고 적절한 오류번호를 설정한다.

#include
int fsetpos (FILE *stream, fpos_t *pos);

위 함수는 Unix 계열이 아닌 다른 플랫폼을 위한 것이다. 리눅스 맞춤형 어플리케이션에서는 이 함수를 사용하지 않는다. 모든 플랫폼에서 사용 가능한 표준 입출력 함수는 rewind()이다.

#include
void rewind (FILE *stream);

위 함수는 스트림의 시작으로 위치를 설정하며, fseek(stream, 0, SEEK_)와 동일한 역할을 한다. rewind() 함수는 반환 값이 없어서 오류 상황에 직접적으로 대처하지 못하고, 아래 코드와 같이 errno 변수를 통하여 점검한다.

errno = 0;
rewind (stream);
if (errno) /* error */

현재의 스트림 위치 가져오기:
ftell() 함수는 스트림의 현재 위치를 알려준다. 오류 시 -1을 반환한다.

#include
long ftell (FILE *stream);

또 다른 것으로 표준 입출력은 fgetpos() 함수를 제공한다. 성공하면 0을 반환하고, 스트림의 현 위치를 pos로 설정한다. 실패하면 -1을 반환한다. fsetpos() 함수와 마찬가지로 fgetpos() 함수도 리눅스 플랫폼이 아닌 환경에서 동작한다.

#include
int fgetpos (FILE *stream, fpos_t *pos);

표준 입출력 라이브러리는 사용자 버퍼에 있는 데이터를 커널에 출력하는 함수를 제공하여, 스트림에 있는 모든 데이터가 write()를 통하여 모두 쓰기 완료 되도록 해준다. fflush() 함수가 이런 역할을 한다. fflush() 함수가 실행되면 스트림에서 쓰여지지 않은 모든 데이터에 커널에 출력된다. 성공하면 0을 반환하고, 실패하면 EOF를 반환한다.

#include
int fflush (FILE *stream);

fflush()의 효과를 이해하기 위해서는 C 라이브러리에서 관리되는 버퍼와 커널 버퍼 사이의 차이점을 이해해야 한다. 대부분의 어플리케이션에서 사용하는 C 라이브러리 함수는 커널영역이 아닌 사용자 영역의 버퍼를 사용한다. fflush()는 사용자 버퍼의 데이터를 커널 버퍼에 출력한다.  write() 함수는 데이터가 물리적인 매체에 모두 쓰기 완료했다고 보장하지 않기 때문에, fsync() 함수 사용과 마찬가지로 fflush() 함수를 사용하면 사용자 버퍼의 내용을 커널에 쓰기 완료한다. 그런 다음, 커널 버퍼의 내용이 디스크에 쓰기 완료된다.

오류와 End-of-File:
fread() 처럼 몇몇 표준 입출력 함수들은 오류와 EOF 사이의 차이점을 구별하지 못하는 경우가 있다. 이 경우, 오류가 발생했는지, 파일의 끝에 도달했는지를 판단하는 함수가 필요한데 표준 입출력 라이브러리가 이것을 제공해준다. ferror()는 스트림상에 오류가 발생했는지 알려준다. 오류가 발생하면 0이 아닌 값을 반환하고, 오류가 아니면 0을 반환한다.

#include
int ferror (FILE *stream);

feof( ) 함수는 파일의 끝에 도달하여 EOF가 발생했는지 여부를 알려준다. 이 함수는 EOF가 발생하면 0이 아닌 값을 반환하고 그렇지 않으면 0을 반환한다.

#include
int feof (FILE *stream);

clearerr() 함수는 스트림의 오류 표시와 EOF를 깨끗하게 지운다.

#include
void clearerr (FILE *stream);

clearerr() 함수는 반환 값이 없다. 아래 코드처럼 오류와 EOF를 점검한 후 clearerr() 함수를 호출 하도록 한다.

if (ferror (f))
printf ("Error on f!¥n");
if (feof (f))
printf ("EOF on f!¥n");
clearerr (f);

File Descriptor 가져오기:
가끔씩 스트림에서 file descriptor를 가져오는 것이 필요 할 때가 있다. 표준 입출력 함수가 존재하지 않을때 file descriptor를 통하여 시스템 호출해야 한다. 스트림에서 file descriptor를 가져오기 위해서 fileno()를 사용한다. 성공 시 fileno()는 스트림과 연관된 file descriptor를 반환하고, 실패 시 -1을 반환한다.

#include
int fileno (FILE *stream);

버퍼링 제어


표준 입출력은 3가지 형태의 사용자 버퍼링을 제공한다. 이러한 사용자 버퍼는 서로 다른 목적으로 사용되며 다음과 같은 선택사항이 있다:

Unbuffered:
사용자 버퍼링이 수행되지 않는다. 데이터는 직접적으로 커널에 제출된다. 이 선택사항은 일반적으로 사용되지 않는다.

Line-buffered:
행 단위 기반으로 버퍼링이 수행된다. 버퍼의 내용이 각각의 개행 문자마다 커널에 제출된다. 행 단위 버퍼는 화면에 데이터를 출력하는 스트림에 유용하다. 따라서 터미널에서는 행 단위 버퍼가 기본으로 설정되며, 표준 출력은 기본적으로 행 단위 버퍼를 사용한다.

Block-buffered:
블럭을 기반으로 버퍼링이 수행된다. 이 방식은 파일을 다룰 때 적합하다. 파일과 관련된 모든 스트림들은 블럭 버퍼이다. 표준 입출력은 블럭 버퍼를 전체 버퍼링으로 사용한다. 대부분의 경우 기본 버퍼링은 적절히 최적화된다. 그러나표준 입출력은 버퍼링 형태를 선택하도록 아래와 같은 함수를 제공한다.

#include
int setvbuf (FILE *stream, char *buf, int mode, size_t size);

setvbuf() 함수는 mode에 전달되는 값에 따라서 스트림의 버퍼 형태를 설정한다. mode는 다음과 같은 종류가 있다.

_IONBF(Unbuffered), _IOLBF(Line-buffered), _IOFBF(Block-buffered)

_IONBF가 아닌 경우 buf와 size는 무시된다. buf는 표준 입출력에서 스트림용으로 사용하기 위한 size 바이트 만큼의 버퍼를 가르킨다. buf가 NULL이라면 버퍼는 glibc에 의해서 자동으로 할당된다. setvbuf() 함수는 스트림이 열려진 후에 호출할 수 있고, 다른 동작들은 이 스트림상에서 수행된다. setvbuf() 함수는 성공 시 0을 반환하고, 실패 시 0이 아닌 값을 반환한다. 사용된 버퍼는 스트림이 닫혀져도 존재한다. 일반적으로 범하기 쉬운 실수는 자동 변수로 버퍼를 선언하는 경우인데, 이 경우 스트림이 닫히기 전에 영역을 벗어날 수 있다.
예를 들면 아래 코드에서 버퍼로 정의한 buffer[BUFSIZ]를 내부의 자동변수가 아닌, 외부의 전역변수로 정의하여 발생할 수 있는 버그를 없애도록 한다.

#include
int main (void)
{
    char buf[BUFSIZ];

    /* set stdin to block-buffered with a BUFSIZ buffer */
    setvbuf (stdout, buf, _IOFBF, BUFSIZ);
printf ("Arrr!¥n");
    return 0;
}

일반적으로 터미널은 행 단위 버퍼를 사용하고, 파일은 블럭 버퍼를 사용한다. 블럭 버퍼의 크기는 기본적으로 에 BUFSIZ로 정의되어 있으며 이것은 일반적으로 최적화된 크기이다.

쓰레드 안전:
쓰레드는 단일 프로세스에서 다중으로 실행된다. 쓰레드들을 개념화하는 방법 중에 하나로, 하나의 주소 공간을 같이 공유하는 다중 프로세스로 취급하는 것이 있다. 쓰레드는 어느 때나 실행될 수 있고 공유된 데이터를 겹쳐 쓸 수 있는데, 이때 데이터를 동기화하거나 쓰레드를 지역적으로 만드는 것이 필요하다. 쓰레드를 지원하는 운영체제는 잠금체계(상호배제 보장)를 제공하여 쓰레드가 다른 영역을 침범하지 않도록 한다.

표준 입출력도 이러한 잠금체계를 사용하지만, 항상 안전한 것은 아니다. 예를 들면 가끔씩 사용자는 호출 그룹을 잠가두고 싶을 때가 있다. 이때 다른 쓰레드에 의해서 간섭 받지 않아야 하는 코드 묶음이 있을 수 있다. 그러나 다른 상황에서는 효율을 향상 시키기 위해서 잠금상태를 해제해야 하는 경우도 있다. 이처럼 잠금상태를 설정하고 해제하는 기법에 대해서 알아보자.

표준 입출력 함수들은 내재적으로 쓰레드에 안전하며, 각각 열려진 스트림을 소유한 쓰레드와 잠금 회수를 내부적으로 가지고 있다. 쓰레드는 잠금을 획득하여 어떤 입출력 요청이 수행되기 전에 쓰레드를 소유해야 한다. 동일한 스트림에서 수행되고 있는 2개 이상의 쓰레드는 표준 입출력 동작을 간섭할 수 없다. 따라서, 단일 함수 호출 내에서 표준 입출력은 독립된 형태로 동작한다.

실제적으로 많은 어플리케이션들은 각각의 함수 호출보다 독립된 형태의 동작을 요구한다. 예를 들면 여러 개의 쓰레드들이 쓰기 요청을 했다면 각각의 쓰기 동작들이 상호 간섭되지 않고 수행되도록 요구한다. 이것을 달성하기 위해 표준 입출력은 스트림과 연관된 잠금 동작을 독립적으로 수행할 수 있는 함수들을 제공한다.

수동으로 파일 잠그기:
flockfile() 함수는 스트림이 잠금에서 해제 될 때까지 기다린 후 잠금을 요청하고 잠금 회수를 증가시킨 후 스트림을 소유하게 된다.

#include
void flockfile (FILE *stream);

funlockfile() 함수는 연관되어 있는 스트림의 잠금 회수를 감소시킨다. 잠금 회수가 0에 도달하면 현재의 쓰레드는 스트림의 소유를 반환하고, 또 다른 쓰레드는 잠금을 요구할 수 있게 된다. 이러한 호출들은 충첩 되어 발생할 수 있다.  단일 쓰레드는 여러번 flockfile() 함수를 호출할 수 있고, 프로세스가 funlockfile() 함수를 호출 할 때까지 해당 스트림은 잠금 해제되지 않는다.

#include
void funlockfile (FILE *stream);

ftrylockfile() 함수는 flockfile()의 잠금 없는(nonblocking) 버젼이다.

#include
int ftrylockfile (FILE *stream);

스트림이 현재 잠겨져 있다면 ftrylockfile()은 아무 일도 하지 않고 곧바로 0이 아닌 값을 반환한다. 스트림이 현재 잠겨져 있지 않다면 ftrylockfile()은 잠금을 요청하고 잠금 회수를 증가시킨 후 스트림의 쓰레드를 소유하고 0을 반환한다. 아래는 flockfile()과 funlockfile()의 사용 예를 보여준다.

flockfile (stream);
fputs ("List of treasure:¥n", stream);
fputs ("    (1) 500 gold coins¥n", stream);
fputs ("    (2) Wonderfully ornate dishware¥n", stream);
funlockfile (stream);

맺음말

표준 입출력은 그 동안 널리 사용되었으나, 몇몇 전문가들은 이것의 결함들을 지적한다.  fgets()와 같이 몇 가지 함수들은 가끔 부적당하다. gets()와 같은 함수들은 너무나 부족하여 표준에서 퇴출되어 왔다. 표준 입출력에 대한 가장 큰 불만은 중복 복사에 의한 수행능력 저하이다. 데이터를 읽을 때, 표준 입출력은 read() 시스템 호출을 커널에 보내고 커널로부터 데이터를 가져와 표준 입출력 버퍼에 복사한다. 어플리케이션이 표준 입출력 함수인 fgetc()를 사용하여 읽기 요청을 하게 되고, 이때 데이터가 표준 입출력 버퍼에서 실제 사용되는 버퍼쪽으로 다시 복사된다.

쓰기 동작은 위의 읽기 동작과 반대 방향으로 발생한다.  데이터가 사용되는 버퍼에서 표준 입출력 버퍼로 복사된 후 다시 write()를 통하여 표준 입출력 버퍼에서 커널쪽으로 복사된다. 각각의 읽기 요청마다 포인터를 표준 입출력 버퍼에 전달하여 위의 중복 복사가 발생하지 않도록 구현하는 방법이 있다. 이렇게 하면 데이터는 표준 입출력 버퍼 안쪽에서 직접적으로 읽혀지게 되어 불필요한 중복 복사가 발생하지 않는다. 어플리케이션은 자신의 영역 내부 버퍼에서 데이터가 처리 되는 것을 원하기 때문에 수작업으로 복사가 수행된다. 이러한 구현은 자율적으로 구현할 수 있으며, 읽기 버퍼에서 작업이 완료되면 신호를 보내도록 하는 방법이 있다.

쓰기 동작은 좀더 복잡하지만 중복 복사는 피할 수 있도록 한다. 쓰기 요청이 발생 할 때, 그 포인터를 기억해 두었다가 데이터를 커널에 보내줄 준비가 되었을 때 저장된 포인터 리스트를 활용하여 데이터를 출력할 수 있다. 이러한 동작은 writev()를 사용한 scatter-gather 입출력으로 구현한다. 이것은 한번의 시스템 호출로 동작되며 후반부에 소개할 것이다. 또한, 위에서 언급한 중복 복사 문제를 해결한 사용자 버퍼링 라이브러리가 존재한다. 몇몇 개발자들은 자기만의 사용자 버퍼를 사용하여 문제를 해결하기도 한다. 위와 같이 몇 가지 대안이 있음에도 불구하고 표준 입출력은 널리 사용되고 있다.

다시 정리하면, 표준 입출력은 표준 C 라이브러리에서 제공하는 사용자 버퍼 라이브러리이다. 몇 가지 단점이 남아 있지만 여전히 널리 애용되고 있는 솔루션이다. 사실상 많은 C 개발자들은 표준 입출력만 알고 있다. 라인 단위의 버퍼를 사용하는 터미널 입출력은 이상적인 경우이다. 표준 출력에 인쇄하기 위해 write()를 직접적으로 사용하는 사람이 있는가? 

표준 입출력은 아래와 같은 사항을 만족하는 상황에서 유용하다.

(1)많은 시스템 호출이 발생하거나 많은 호출을 적게 묶을 때 발생하는 부하를 최소화 하고자 할 때
(2)수행능력이 중요할 때, 모든 입출력이 블럭 크기로 묶여서 수행되도록 할 때
(3)데이터 접근 형태가 문자 혹은 행 단위로 이루어져서 다른 형 변환이 필요하지 않을 때
(4)low-level의 리눅스 시스템 호출보다 higher-level의 인터페이스가 더 필요할 때
다음 연재에서는 Scatter/Gather 입출력 방식에 대해서 알아보고, 프로세스, 메모리, 신호 와 시간에 대해서 기술할 예정이오니 독자 여러분들의 꾸준한 관심 바란다.

참고문헌
[1]  Computer Organization and Design (c)2007
[8] by David A. Patterson, John L. Hennessy
[2] Embedded Linux Primer (c)2006
[8] by Christopher Hallinan
[3] Essential Linux Device Drivers (c)2008
[8] by Sreekrishnan Venkateswaran
[4] LINUX System Programming (c)2007
[8] by Robert Love
[5] DATA STRUCTURES (c)2005
[8] by Richard F. Gilberg, Behrouz A. Forouzan
[6] (주)에프에이리눅스 포럼
[8] http://forum.falinux.com/zbxe/
[7] RMI사의 Au1200 User's Guide
[8] http://www.rmicorp.com/
[8] Linux Kernel In a Nutshell (c)2007 O'Reilly
[8] by Greg Kroah-Hartman


회원가입 후 이용바랍니다.
개의 댓글
0 / 400
댓글 정렬
BEST댓글
BEST 댓글 답글과 추천수를 합산하여 자동으로 노출됩니다.
댓글삭제
삭제한 댓글은 다시 복구할 수 없습니다.
그래도 삭제하시겠습니까?
댓글수정
댓글 수정은 작성 후 1분내에만 가능합니다.
/ 400
내 댓글 모음
저작권자 © 테크월드뉴스 무단전재 및 재배포 금지