2013년 9월 12일 목요일

fflush 에 대한 고찰

C언어에서 fflush 함수는 stdio.h 에 선언되어 있는 C 표준 함수이다.
형식은 다음과 같다.

int fflush( FILE *stream );

stream 에 대한 버퍼의 데이터를 꺼내어 해당 스트림에 쏟아붓는(?) 함수이며 return 값은 작업에 성공하면 0, 버퍼에 아무것도 없는 상태라면 EOF, 실패하면 그 실패에 대한 errno 값이다.

이 fflush 함수의 매뉴얼을 잘 살펴보면, 표준 C 라이브러리에서는 출력 스트림에 대해서만 정의되어 있다. 즉, 우리가 자주 사용하는 출력 스트림인 stdout 의 버퍼에 있는 데이터를 꺼내어 출력한 후 버퍼를 비우는 데에도 사용할 수 있다. 이런 경우 통상적으로 "출력 스트림의 버퍼를 비운다." 라고 이야기하기도 하는데 이 뜻은 정확하게는 출력 스트림 버퍼에 남아 있는 데이터를 꺼내어 모두 출력해 버린다는 것이다.

출력 스트림의 버퍼가 비워지는 시점은 OS마다 다를 수 있다.  즉 어떤 OS에서는 출력 스트림 버퍼에 데이터가 들어오자 마자 꺼내어 출력하는가 하면, 또 다른 OS에서는 줄바꿈 캐릭터가 들어와야 출력을 하기도 한다.

C 언어에서 정의하는 대표적인 출력 스트림은 stdout, stderr 가 있다.

본인이 기억하기로는 Windows 에서는 stdout와 stderr가 동일하며 출력 스트림에 데이터가 들어오면 바로 출력하지만 (즉 버퍼를 즉시즉시 비우지만), Linux 는 stdout 과 stderr가 별도의 버퍼를 가지고 있으며, stderr는 즉시 비우는 반면, stdout는 개행을 의미하는 문자가 나타날 때까지, 혹은 표준 입출력 버퍼에 다른 이벤트가 벌어질 때까지 데이터를 출력하지 않고 버퍼에 그대로 쌓는다.

fflush의 사용 예는 다음 코드로 확인해본다.

#include <stdio.h>

int main()
{
    int i;

    printf("12345");
    for(i=0; i>=0; i++);
    printf("67890\n");

    return 0;
}

소스를 그대로 해석해 보면 우선 12345 를 출력하고 i에 대한 for 루프를 돈 후 67890 을 출력하는 것을 예상할 수 있다 (위의 for 루프는 무한루프가 아니다!!! 왜일까?). 그런데 일부 Linux에서는 for 루프를 모두 돈 다음 1234567890 을 한꺼번에 출력한다.

이럴 때, 소스의 의도 대로 12345 출력 후 루프를 돌고 67890을 출력하게끔 하는 것이 fflush 함수이다.

#include <stdio.h>

int main()
{
    int i;

    printf("12345");
    fflush(stdout);
    for(i=0; i>=0; i++);
    printf("67890\n");

    return 0;
}

이렇게 fflush 를 추가하면 된다. 12345가 출력 버퍼에 담긴 후 fflush 함수에 의해 12345가 버퍼에서 나와 출력된다.

한편...

fflush가 C 표준 라이브러리에 정의된 함수임은 분명하나, 함정이 하나 있다.
fflush에 인자로 들어가는 stream의 값은 출력 스트림에 한정된다는 것이다.
즉 stream 에 입력 스트림이 들어갈 경우 C 표준에 정의되어 있지 않기 때문에 컴파일러마다, OS 마다 동작하는 양상이 전혀 다르다.

입력 스트림을 비운다는 뜻은, 입력 스트림 버퍼에 남아 있는 데이터를 깔끔히 청소하고 없앤다는 뜻으로 해석할 수도 있으며, Windows 계열과 POSIX UNIX의 일부 계열에서 이렇게 컴파일되어 동작한다.

하지만 Linux 등 다른 OS에서는 위와 같이 동작하지 않는다. Linux에서는 fflush의 인자로 표준 입력 스트림이 들어오면 아무런 동작도 하지 않는다.

아래의 예를 보자.

#include <stdio.h>

int main()
{
    char c[100];
    int i;

    scanf("%d", &i);
    fflush(stdin);
    fgets(c, sizeof(c), stdin);

    printf("[%d][%s]\n", i, c);
    return 0;
}

이를 실행하고 기대하는 것은
처음에 1234 를 입력하고 Enter, 그 다음 ABCDEFG 를 입력하고 Enter 를 누르면 다음과 같은 결과를 도출할 것인데

[1234][ABCDEFG
]

이를 Linux에서 실행하면 fflush 가 아무런 동작을 하지 않으므로, 실행한 후 1234 만 입력해도

[1234][
]

이렇게 출력하고 끝나버린다. 즉 Linux에서는 fflush에 의하여 '\n' 이 버퍼에서 비워지지 않는다. fflush라는 뜻이 버퍼를 꺼내어 미루었던 작업을 모두 수행해버리는 것이므로, 그냥 버퍼를 비우는 작업은 약간 다른 작업일 수도 있다. 즉, 추후에 합당한 동작이 정의될 때까지 stdin에 대한 fflush 기능은 일단 정의하지 않는다는 의미로 해석해 본다.

Linux에서는 대신 __fpurge, 혹은 fpurge 라는 함수를 소개하고 있다. __fpurge는 Solaris 라는 OS에서 소개되었으며 리눅스의 glibc에서 채택하였다. fpurge는 BSD에서 소개되었다. (하지만 리눅스에서는 아직 지원하지 않는다.) 이 함수는 말 그대로 해당 버퍼를 비워 없앤다는 뜻이다. stdio.h (혹은 stdio_ext.h) 에 다음과 같이 선언되어 있다.

void __fpurge( FILE *stream );   // 현재 리눅스(glibc) 에서 지원함. fpurge 앞에 _ 가 두개임.
int fpurge( FILE *stream );   // 현재 리눅스에서 지원하지 않음.

리눅스에서는 위의 예에서

fflush(stdin);



__fpurge(stdin);

으로 바꾸면 기대했던 결과를 볼 수 있다.

하지만, fflush와 __fpurge는 분명 전혀 다른 동작을 하는 함수이다.
__fpurge 함수는 말 그대로 해당 스트림 버퍼를 말끔히 비우는 기능을 한다. 즉, fflush(stdout) 과 __fpurge(stdout) 은 완전히 다르게 동작한다. fflush(stdout) 이 표준 출력 버퍼에 있는 데이터를 꺼내어 출력을 하는 반면, __fpurge(stdout) 은 표준 출력 버퍼에 있는 데이터를 그냥 없애버린다는 사실을 주의해야 한다.

다시 stdin 으로 돌아와서... 그렇다면 Windows 든 Linux 든 같은 결과를 얻을 수 있게 하는 방법은 없을까? ( __fpurge 함수는 Windows 에서 사용할 수 없다. )

표준 입력 버퍼에 데이터가 채워지는 순간이 키보드의 '\n' (Enter) 가 입력되는 때임을 감안한다면 다음과 같은 방법으로 버퍼에 남아 있는 데이터를 없앨 수 있다.

while( getchar() != '\n' );

보기에 깔끔한 방법은 좀 아닌 것 같지만, OS 호환성을 감안해야 한다면 저 방법이 최선인 듯 하다.

----------------------

여담이지만 C 프로그래밍 교재로 매우 유명한 책인
윤성우 저 열혈(강의) C 프로그래밍의 초판에서는
fflush(stdin) 이 표준 라이브러리로서 표준 입력 스트림 버퍼를 비워 청소한다고 설명하였다가 뭇매를 맞았다. 2010년 발간된 개정판에서는 이에 대해 수정하여 기술되어있다 (개정판 430~432 페이지 및 508 페이지 참고). 그만큼 fflush에 대한 기능은 프로그램의 고수들도 실수하기 쉬운 부분이기도 하다.


댓글 9개:

  1. 아 ㅜㅜㅜ 감사합니다 ㅜㅜ 어쩐지 아무리 해도 안되더니 저자님의 실수가 있었군요...

    답글삭제
  2. 잘 읽었습니다. 감사합니다

    답글삭제
  3. 안녕하세요. 게시글의 내용에서 for(i=0; i>=0; i++); 가 무한 루프가 아니라는 것이 왜 그런지 궁금합니다. 작동방식이 어떻게 되는 것인지요...?

    답글삭제
    답글
    1. 뒤늦게 답을 드립니다. 일반적으로 int 타입은 -2147483648 ~ +2147483647 사이의 값을 표현할 수 있습니다. 만일 위의 i 를 계속 증가시켜 2147483647 이 된 후 또 i++ 이 된다면 i가 어떤 수가 될지를 잘 생각해 보시면 궁금하신 부분이 풀리지 않을까 생각합니다.

      삭제
  4. "stdout는 개행을 의미하는 문자가 나타날 때까지, 혹은 표준 입출력 버퍼에 다른 이벤트가 벌어질 때까지 데이터를 출력하지 않고 버퍼에 그대로 쌓는다."고 하셨는데, 혹시 다른 이벤트에 함수 종료도 포함될까요?

    답글삭제
    답글
    1. 단순한 함수 종료는 포함이 되지 않는 것으로 알고 있습니다. 입출력 버퍼에 이벤트가 발생하는 것과 함수 종료와는 별개이지 않을까 합니다.

      삭제
  5. 재미난 글 감사합니다.

    답글삭제
  6. 참고되었습니다. 감사합니다. ^^

    답글삭제