[GetNextLine] 삽질의 기록

이대현·2020년 4월 11일
9

42SEOUL

목록 보기
8/27

0. Program Source

Get Next Line은 읽어온 파일의 한 줄을 반환하는 프로그램이다. 자세한 코드는 아래 저장소에.

1. 선행지식

1.1. 파일 디스크럽터(fd)

  • 운영체제가 만든 파일 또는 소켓의 지칭을 편히 하기 위해서 부여된 숫자이다.
  • 기본적으로 파일 디스크립터는 정수형으로 차례로 넘버링 되고 0,1,2는 이미 할당되어 있어서 3부터 파일 디스크립터를 부여한다.
    0 : 표준입력 (Standard Input)
    1 : 표준출력 (Standard Output)
    2 : 표준에러 (Standard Error)

1.2. read() 함수

size_t read(int fd, void *buf, size_t bytes)
  • bytes 수 만큼 fd를 읽어 buf에 저장한다.
  • 리턴값 : 읽어 온 바이트 수. 실패 시 -1.
    • 파일을 끝까지 읽었으면, 다음 번에는 더 이상 읽을 바이트가 없기 때문에 0을 반환한다.

1.3. gcc -d 플래그

  • 외부에서 #define을 정의한다.

  • 이 문제에서 컴파일은 다음과 같이 진행된다.

      $ gcc -Wall -Wextra -Werror -D BUFFER_SIZE=32 get_next_line.c get_next_line_utils.c

    즉, BUFFER_SIZE를 컴파일할 때 정하게 된다.

1.4. static 변수

  • 데이터 영역에 저장되어 프로그램 종료 시 까지 남아있기 때문에, 다음 line을 읽을 시작 주소을 계속 저장할 수 있도록 backup 버퍼를 static 변수로 선언해야 한다.
  • 자세한 내용은 다음 포스트 참고

2. 목표

  • GNL 함수를 loop 안에서 호출하면 fd의 텍스트를 EOF가 올 때 까지 한 번에 한 줄씩 읽을 수 있다.
  • GNL 함수를 처음 호출 했을 때 파일을 끝까지 읽었다 하더라도, 두 번째 호출했을 때는 두 번째 line부터 시작해야한다.
  • file로부터, redirection으로부터, stdin으로부터 읽었을 때 함수가 제대로 동작해야 한다.
  • BUFFER_SIZE가 1일 때도, 9999일 때도, 10000000 (1000만)일 때도 함수가 제대로 동작해야 한다.

3. 아이디어

  1. 파일을 read 할 임시 버퍼를 만든다.
    char buf[BUFFER_SIZE];

  2. read한 버퍼를 백업할 static 버퍼를 만든다.

    static char *backup

  3. read(fd, buf, BUFFER_SIZE);를 해서 라인을 읽은 다음,

  4. buf를 static 변수 backup에 백업한다.

  5. backup 안에 개행문자가 있는지 없는지 검사한다.

  6. 개행문자가 있으면 다음 단계로 넘어가고, 없다면 개행 문자가 있을 때 까지 3번으로 돌아가 파일을 계속 읽으면서

    1. 기존에 백업한 것과 계속 합쳐나간다. -> append_backup 함수
  7. 개행문자가 있는 backup을 개행문자 전과 후로 잘라서, \n 전까지는 line 에다가 주고 \n 후는 다시 static 변수 backup에 백업한다. -> split_line 함수

4. 고민한 지점들

4.1. line 변수에 대한 고민

  • ** line은 왜 이중포인터인가?

    -> 읽어들인 라인(char *)의 주소를 저장하기 위해서다

  • GNL을 호출할 때 마다 GNL 내부에서 line을 재할당 해줘야 되는가?

    -> get_next_line() 에서는 한 줄 라인의 길이가 얼마나 길어질지 모르기때문에 항상 line에 동적으로 할당해야한다.

  • 아무것도 적혀있지 않은 빈 파일을 읽었을 때 line에 할당을 해야하나?

    -> line에 빈 문자열을 할당하고 0을 반환한다.

4.2. static 변수를 사용해야 하는 이유

버퍼 사이즈가 10이라고 가정하고

abcd
1234

를 읽는다고 생각하면 한번 부르면 abcd를, 한번 더 부르면 1234 를 *line에 넣어줘야 한다.

그런데 버퍼 사이즈가 10이라 한번에 파일을 끝까지 다 읽어버리기 때문에 처음 함수가 호출 됐을 때 나머지 1234 를 따로 저장해둬야 한다. 그래서 따로 저장할 때 함수가 끝나도 날라가지 않게 하려고 static(정적) 변수를 쓰게 된다.

4.3. read함수가 0을 반환하는 경우

  1. 파일을 끝까지 다 읽어서 0 반환

    line = 0으로 메모리 해제가 된 상황. 남은 backup을 line에 넣어준다. main에서 다른 파일을 read할 수 있으니 backup = 0; 해준다.

    free(buf)를 해버리면 *line이 담고있는 주소값에 대한 메모리가 해제돼버려서 올바른 값이 들어가지 않고 쓰레기값이 남게된다.

  2. 빈 파일을 읽어서 0 반환

    *backup이 아무 값도 가리키고 있지 않기 때문에 위와 같은 경우로 묶었을 때 segmentation fault가 뜬다.

    read_size == 0 && *backup == 0 인 경우를 새로 추가해서

    *line에 1만큼의 공간을 동적할당하고 널문자를 넣어줘야 한다.

4.4. 언제 메모리가 누수 되는가?

char buf = malloc(10) 와 같이 동적할당 된 메모리 주소를 buf가 가리킬 때, free(buf) 와 buf = 0; 의 차이는?

  • 전자는 할당받은 메모리를 해제하는 것.
  • 후자는 할당받은 메모리 주소를 담고있는 buf 변수가 0 값을 갖게 되는 것.

후자는 할당받은 메모리 주소를 잃어버린 게 돼서 해제가 불가능 하게 된다. 이런 상황을 memory leak, 메모리 누수라고 한다.

4.5. 스태틱 변수 backup에 두 번째 줄 주소를 어떻게 저장해 둘것인가?

  1. read할 buf와 backup를 static으로 선언한 뒤 개행문자 + 1 메모리 주소를 backup에 저장한다.
    • buf까지 static으로 선언하는 이유는 gnl을 호출할 때마다 buf의 메모리 주소가 달라지기 때문이다.
  2. backup만 static으로 선언한 뒤 개행문자 + 1메모리 주소의 값을 backup에 copy한다.
    • backup이 갖는 값이 주소가 아니기 때문에 buf는 일회성이어도 상관 없다.

4.6. 이중포인터 backup의 세로 축 크기를 몇으로 정적할당 할 것인가?**

backup의 세로 축은 fd가 들어간다. *backup이 가리키는 값을 fd에 따라서 따로 관리하고 싶었다. 한 파일이 끝나기 전에 gnl로 다른 파일을 호출할 수도 있으니까.

"고민한 부분은 사용자가 한번에 파일을 몇 개나 열 수 있도록 할 것인가? " 였다. 처음에는 그냥 50개 정도로 설정했었는데, 나중에 알고보니 파일 디스크럽터는 0 ~ OPEN_MAX 까지의 값을 가질 수 있으며, OPEN_MAX 값은 플랫폼에 따라 다르다고 한다.

프로세스 하나가 동시에 open할 수 있는 최대 파일 갯수는 다음의 명령을 사용하여 알아볼 수 있다.

$ getconf OPEN_MAX
65536

혹은 sysconf() 함수를 사용할 수도 있다.

sysconf(_SC_OPEN_MAX); 
>>> return 65536
  • 원래 유닉스는 <limits.h> 헤더파일에 OPEN_MAX가 정의되어있는데, 내 컴퓨터가 윈도우여서 그런지, 정의되지 않은 값이라고 나온다. 그래서 <stdio.h>의 FOPEN_MAX를 사용했다.

4.7. BUFFER_SIZE의 최대 크기는?

100만 까지는 되는데 1000만 부터는 Segmentation fault (core dumped)가 뜬다.

-> 자동변수는 stack 영역에 저장되는데, 보통 스택 사이즈가 윈도우는 1메가, 리눅스는 8메가로 설정되어있다.만약에 char buf[BUFFER_SIZE + 1];라고 선언하고 여기에 스택 사이즈보다 큰 수를 받으면 스택 오버플로우가 생길 수 있다

스택 오버플로우 피하기

  1. 정적 변수로 선언하여 데이터 영역에 잡는다.
  2. 전역 변수로 선언하여 데이터 영역에 잡는다.
  3. malloc 등을 사용, 동적 할당하여 힙 영역에 잡는다.
  4. 시스템 설정 스택 영역 사이즈를 늘린다.

5. 테스트 프로그램

profile
삽질의 기록들 👨‍💻

0개의 댓글