🥨

[GetNextLine] 삽질의 기록

Category
GNL
Tags
Empty
Day learned
2020/04/01
Property
Empty
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)
Plain Text
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
Plain Text
즉, 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
Plain Text
를 읽는다고 생각하면 한번 부르면 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
Plain Text
혹은 sysconf() 함수를 사용할 수도 있다.
sysconf(_SC_OPEN_MAX); >>> return 65536
Plain Text
원래 유닉스는 <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. 테스트 프로그램