이 글은 993일 전에 작성되었습니다.
최신 정보가 아닐 수 있습니다.
GNL
오늘은 42 과제 중 하나인 get_next_line 을 하며 배운 것들에 대해 작성하고자 합니다.
과제 해결은 오래전에 했으나, 리액트 맛좀 보느라 평가가 늦어졌습니다.
과제 설명
함수 프로토타입 : char *get_next_line(int fd)
fd 를 매개변수로 입력받고 fd 에서 한 줄씩 읽어 출력하는 함수입니다.
만약 읽을 것이 없거나, 오류가 발생하면 NULL
을 반환합니다.
함수 이름 | get_next_line |
---|---|
프로토타입 | char *get_next_line(int fd) |
제출할 파일 | get_next_line.c, get_next_line_utils.c, get_next_line.h |
매개변수 | 읽어들일 파일의 디스크립터 (서술자) |
반환값 | 읽혀진 라인 : 한 줄이 제대로 읽힘 <br/> NULL : 읽을 라인이 더이상 없거나 에러 발생 |
사용가능한 외부 함수 | `read, malloc, free` |
설명 | 파일 디스크립터로부터 한 줄을 읽고, 반환하는 함수를 작성하시오. |
사전 지식
File Descriptor
간단히 설명하자면 프로세스
내에서 열린 파일을 구분하기 위한 정수입니다.
여기서 프로세스는 하나 이상의 스레드에서 실행되는 프로그램 인스턴스의 단위로, 2개의 프로그램을 실행했을 때 2개의 프로세스가 존재한다고 할 수 있습니다.
더 자세히 들어가자면 파일 디스크립터는 프로세스 단위로 할당되며, 한 프로세스에 0 부터 OPEN_MAX
까지 존재할 수 있습니다.
파일을 닫으면 해당 파일 디스크립터는 추가로 할당이 가능합니다.
또한, fd 는 프로세스 내에서 독립적이며 고유합니다.
만약에 a
라는 프로그램과 b
라는 프로그램을 실행시킨 후 파일을 읽는다면 읽은 파일의 fd
는 어떻게 될까요?
// a.c
#include <fcntl.h>
int main(void)
{
int fd;
//atest.txt 를 읽음
fd = open("./atest.txt", O_RDONLY);
while (1){
}
return (0);
}
// b.c
#include <fcntl.h>
int main(void)
{
int fd;
//btest.txt 를 읽음
fd = open("./btest.txt", O_RDONLY);
while (1){
}
return (0);
}
해당 파일을 읽고 프로세스가 종료되지 않게 무한루프를 걸어놓은 상태입니다.
- 우선
ps
명령어를 이용해 각각의PID
를 확인합니다. - 그 후
lsof -p PID
명령어를 통해 해당 프로세스에서 열린 파일들의 정보,fd
를 확인할 수 있습니다.
상기 사진을 통해 File Descriptor
는 각각의 프로세스에서 독립적이란 것을 확인할 수 있습니다.
참고 (OPEN_MAX 는 터미널에 ulimit -n
명령어로 확인할 수 있습니다.)
더불어 프로그램을 실행하면 fd 0, 1, 2
는 예약이 되어있습니다.
fd | Represents | POSIX Name | stdio stream | Examples |
---|---|---|---|---|
0 | Standard input | STDIN_FILENO | stdin | Keyboard, file, terminal |
1 | Standard output | STDOUT_FILENO | stdout | Screen, database |
2 | Standard error | STDERR_FILENO | stderr | File, terminal |
요약
- 음이 아닌 정수.
- 프로세스 내에서 열린 파일을 고유하게 나타내는 값.
- 해당 프로세스에 할당되지 않은 가장 작은 fd 부터 순차적으로 할당.
- get_next_line 에선 파일을 열고 해당 파일을 기억한다고 생각하면 이해가 쉬울 것 같습니다.
static
간단 설명
static
은 말 그대로 정적 그 자체이며 아래와 같은 특징을 갖고 있습니다.
static 변수
는 함수 호출 사이에 값을 잃지 않습니다. 즉, 전역 변수이지만 해당 변수가 정의된 지역 함수로 범위가 지정됩니다.static 전역 변수
는 정의된 C 파일 외부에서 볼 수 없습니다.static 함수
는 정의된 C 파일 외부에서 볼 수 없습니다.static
변수는 명시적으로 초기화하지 않은 경우0
으로 초기화됩니다.
#include <stdio.h>
void print_number ()
{
int i = 10;
static int s_i = 10;
i += 10; // int
s_i += 10; // static int
printf("i = %d\ts_i = %d\n\n", i, s_i);
return;
}
int main() {
print_number(); // i = 20 s_i = 20
print_number(); // i = 20 s_i = 30
print_number(); // i = 20 s_i = 40
return 0;
}
상기 코드의 실행 결과를 보았을 때 static
으로 변수를 선언하면 함수를 재호출 하여도 값을 잃어버리지 않고 더해가는 것을 확인할 수 있습니다.
요약
- static 변수는 함수 호출 사이에 값을 잃지 않는다.
- static 변수, 전역 변수, 함수 는 정의된 C 파일이 아닌 외부에선 값을 볼 수 없다.
read
함수 : ssize_t read(int fd, void *buf, size_t count)
파일 디스크립터 fd
를 count
만큼 읽고 buf
에 저장하는 함수로 정상적으로 읽었다면 읽은 바이트 수, 실패하였다면 -1, eof 에 도달하였다면 0 을 반환합니다.
GCC -D
gcc -D name=definition
GCC 컴파일러의 -D 플래그
는 컴파일 할 때 name 을 predefine 하며, definition
을 적어주지 않으면 1 을 저장합니다(?)
#include <stdio.h>
int main()
{
printf("%d", TEST);
return (0);
}
gcc -D TEST=20 test.c
커맨드로 컴파일 후 실행시키면 20 이 출력됩니다.
요약
gcc -D name=definition
name 에 원하는 변수명, definition 에 값을 적고 컴파일하면 코드 실행 전 해당 변수를 미리 정의 후 값을 할당해준다.
구현
주의사항
- 유효한 fd 인지 검증
- malloc null-guard 해주기
- 댕글링 포인터 처리해주기
- 정적 변수인
static char *backup
을 활용해 함수 호출 사이에서도 값이 누적되도록 하여 개행문자 이후의 값들을 여전히 참조할 수 있도록 활용.
우선 제가 생각한 로직은 아래와 같습니다.
ft_read_line
파일을 읽고 개행문자 줄을 추출하는 과정
- BUFFER_SIZE 만큼 buffer 배열에 메모리 할당
- fd 에 할당된 파일을 읽고 buffer 배열에 데이터 저장
- backup 배열 생성 후 데이터가 저장되어있는 buffer 배열을 연결
- 연결된 backup 배열에서 개행문자를 탐색, 있으면 2번째 섹션, 없으면 다시 읽고 backup 배열에 이어서 붙임
ft_extract
ft_read_line 작업을 거치고 넘어온 한 줄에서 개행문자의 전후를 분리 및 추출하는 작업
- 읽어들인 한 줄에서 개행문자의 위치를 확인
- 만일 개행문자 말고 EOF 가 발견된다면 0 반환 (출력하는 줄은 그대로)
- 개행문자 위치 이후로부턴 backup 에 저장
backup
은 개행문자 이후 데이터로,backup
의 첫 문자가 EOF 라면 메모리 해제 후 NULL 반환(정확하게 개행문자까지 읽음 or 실제 파일 마지막일 수도 있기에)- 읽어들인 한 줄의 개행문자 위치 이전은 실질적으로 출력해야할 데이터로, 해당 줄의 개행문자 + 1 위치에
\0
저장
예시를 들어보자면
// BUFFER_SIZE = 100
char *origin = "안녕하세요!\n감구마입니다!\n감사합니다!\0"
// 상기 로직을 거친 후 line 과 backup 은 각각
char *line = "안녕하세요!\0";
char *backup = "감구마입니다!\n감사합니다!\0"
// 상태일 것입니다.
#include "get_next_line.h"
static char *ft_read_line(int fd, char *buf, char *backup)
{
int count;
char *temp_pointer;
count = 1;
while (count)
{
count = read(fd, buf, BUFFER_SIZE);
if (count == -1)
return (0);
else if (count == 0)
break ;
buf[count] = '\0';
if (!backup)
backup = ft_strdup("");
temp_pointer = backup;
backup = ft_strjoin(temp_pointer, buf);
if (!backup)
return (NULL);
free(temp_pointer);
temp_pointer = NULL;
if (ft_strchr(buf, '\n'))
break ;
}
return (backup);
}
static char *ft_extract(char *line)
{
int i;
char *result;
i = 0;
while (line[i] != '\n' && line[i] != '\0')
i++;
if (line[i] == '\0')
return (0);
result = ft_substr(line, i + 1, ft_strlen(line) - i);
if (!result)
return (NULL);
if (result[0] == '\0')
{
free(result);
result = NULL;
return (NULL);
}
line[i + 1] = '\0';
return (result);
}
char *get_next_line(int fd)
{
char *line;
char *buf;
static char *backup;
if (fd < 0 || BUFFER_SIZE <= 0)
return (NULL);
buf = (char *)malloc(sizeof(char) * (BUFFER_SIZE + 1));
if (!buf)
return (NULL);
line = ft_read_line(fd, buf, backup);
free(buf);
buf = NULL;
if (!line)
return (NULL);
backup = ft_extract(line);
return (line);
}
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int fd;
fd = 0;
fd = open("./test", O_RDONLY);
char *line = get_next_line(fd);
printf("%p\n", line);
printf("%s", line);
return (0);
}
Bonus
- 보너스 문제는 간단하게 여러 fd 에 대응하게끔. 즉, 1번 파일 한 줄 읽고 2번 파일 한 줄 읽은 후 1번 파일로 다시 돌아가도 이어서 읽을 수 있어야 한다.
- 정적 변수를 하나만 선언하여 해결하는 것이 통과 조건입니다.
기존 정적 변수인 backup 을 모든 fd 에 대응할 수 있게끔 OPEN_MAX 만큼의 포인터 배열을 생성해줍니다. static char *backup[OPEN_MAX]
물론 이와 같은 방법은 연결리스트로 구현한 것보다 메모리 낭비가 심하다고도 합니다. 이유는 char *
포인터의 크기가 8byte 이며 클러스터의 맥의 OPEN_MAX
값은 10240, 그러므로 8 * 10240 = 80kb 의 크기로 음,, 80kb 의 낭비랄까?
처음엔 char **
처럼 이중 포인터로 했으나 해당 방식은 이중포인터 값 자체를 잃어버리지 않지만 이중포인터가 가르키고 있는 배열들의 데이터를 잃어버리기에 적용되지 않는 듯 하다. lldb 를 통해 알았습니다,,
메모리 누수
상기 코드 + 필요한 유틸 함수들을 gcc -Werror -Wextra -Wall -g -BUFFER_SIZE=100
로 컴파일,
leaks -atExit -- ./a.out
명령어로 메모리 누수를 확인하면 누수 목록에 ROOT LEAK 이 존재한다. 물론 test 파일에 내용이 있어야한다.
main 함수에 있는 line 변수의 메모리를 해제하지 않았기 때문이며, 값만 printf 로 출력해도 메모리 릭이 발생한다.
물론 이건 line 변수에 할당된 get_next_line 의 결과가 malloc 을 통한 메모리 할당이어서 그런 것 같다,,! 너무 어렵다 C!!!
처음엔 왜 누수가 발생하지,, 생각했는데 lldb 로 디버깅과 동시에 메모리 누수를 검사하니 어디서 발생하는지 쉽게 확인할 수 있었다!
결론
- 메모리 누수는 여러 케이스를 테스트하여 확인하자.
- 눈과 뇌로 하는 것보다 디버깅 툴을 사용하자.
- 이 코드에서 누수는 발견되지 않았다,,!(?)
마무리
이번 과제를 하면서 배운 것은 크게 정적변수, read, fd, 디버거 등등 여러가지를 배울 수 있었습니다.
역시 뭐든 사용하기 전은 어렵고 이해가 힘들지만 직접 사용해보면서 깨닫는 것이 많은 것 같습니다.
특히 lldb,, 이거 없었으면 보너스 문제 해결도 못 했을 거야,,
제가 과제를 해결했을 당시의 테스터와 다르게, 현재 유행하는 테스터를 돌리면 많은 부분에서 fail 받습니다!