get next line

1. get_next_line 용도

* file descriptor을 통해 텍스트에서 eof까지 한 라인을 읽어 반환하는 함수. 
('\n'을 기준으로, '\n'이 나오기 전까지의 문자열을 line에 할당)

* 보너스 파트의 경우, `한 개의 static variable`만을 이용해 여러 쓰레드를 이용할 수 있게 한다. 
즉, 여러 개의 파일 디스크립터를 통해 여러 파일의 라인을 각각 읽을 수 있도록 한다.

2. get_next_line 프로토타입

int get_next_line(int fd, char **line);
int fd : 읽을 파일의 파일 디스크립터. fd를 통해 어떤 파일을 읽을 것인지 알 수 있다
char **line : 파일의 한 줄을 읽어 저장할 line변수.
이중 포인터로 받은 이유는 문자열의 주소값을 나타내기 때문이다.
반환값 : int형식의 반환값.

- 에러 발생 시 -1
- eof까지 읽었으면 0
- 한 줄을 반환했으면 1을 반환한다.

3. 구현 전 알아야 할 요소

3.1 파일 디스크립터

  • file descriptor는 리눅스 혹은 유닉스 계열의 시스템에서 프로세스가 파일을 다룰 때 사용하는 개념으로, 프로세스에서 특정 파일에 접근할 때 사용하는 추상적인 값이다. 음수가 아닌 정수값이다.

  • 파이프, FIFO, 소켓, 터미널, 디바이스, 일반파일 등 종류에 상관없이 모든 열려있는 파일을 참조할때 이용한다.

  • 파일 디스크립터 0, 1, 2는 각각 표준 입력, 표준 출력, 표준 에러를 의미하고 미리 할당되어 있다. 따라서 3부터 차례로 부여된다.

3.2 read함수

  • read 함수의 용도 : open 함수를 이용해 연 파일의 내용을 읽는 함수.
    fdn bytes만큼 읽어 buf에 저장한다.

  • 헤더 : unistd.h

  • 프로토타입 :

ssize_t read (int fd, void *buf, size_t nbytes)
int fd : 읽을 파일의 파일 디스크립터. fd를 통해 어떤 파일을 읽을 것인지 알 수 있다.
void *buf : 읽어온 파일의 내용을 저장할 배열.
size_t nbytes : 파일을 얼마나 읽을지 나타내는 변수.
반환값 : read함수의 ssize_t 타입의 반환값. 읽어들인 데이터의 크기를 의미. 에러 발생 시 -1 반환.

<주의> 무조건 n바이트가 반환되는 것은 아님.

eof까지 읽을 때나 읽을 때 오류가 났을 때 n바이트보다 작을 수 있음.
ex) 파일의 길이가 20바이트이고, n이 100바이트일 때는 20바이트만 읽으므로 반환값은 100이 아닌 20이다.
ex) 읽을 때 오류가 생겼다면 반환값은 -1이다.

3.3 static 변수와 지역 변수의 차이

  • static 변수 : static 변수는 메모리의 data영역에 저장된다. 지역변수와는 다르게 함수 호출과 종료 시에 값이 초기화되거나 제거되지 않는다. 즉 함수 블록을 벗어나도 값이 제거되지 않는다.

  • 지역변수 : 지역변수는 스택 영역에 저장된다. 이 영역은 함수 내부에서 선언된 지역변수, 리턴값 등등이 저장되고, 함수 호출 시 기록되고 종료되면 제거된다.

우리가 구현할 get_next_line 함수에서는 이전까지 읽었던 문자열을 저장하기 위해 'static 변수'를 이용한다.

3.4 포인터 배열

포인터 배열 : 포인터들의 배열이다. 배열의 요소로 포인터 변수를 가진다.

<간단한 예제>
아래의 코드는 포인터 배열이 어떠한 역할을 하는 지 테스트해 볼 간단한 코드이다.

#include <stdio.h>

void test()
{
    char    *array[3];
    
    array[0] = "hello~";
    array[1] = "this is a";
    array[2] = "test!";
    for(int i = 0; i < 3; i++)
    {
        printf("%s\n", array[i]);
    }
    return ;
}

int main(void)
{
    test();
    return 0;
}

실행결과 :

포인터 배열 array의 인덱스가 각각 "hello~", "this is a", "test!" 이 3개의 문자열의 첫번째 문자의 주소값을 저장하고 있다.

4. 구현 시 유의사항

(1) 메인 함수 내부에서 get_next_line()을 반복해서 호출할 수 있어야 한다.

즉 get_next_line()을 한번 호출하면, line에는 \n을 기준으로 한 라인이 저장되어 있어야 하고, 두번째 호출해도 line에 한 라인이 저장되어야 한다.

(2) 이전에 읽었던 값이 저장되어 있어야 하기 때문에 static 변수를 이용한다.

ex) BUFFER_SIZE가 10이고, 파일에는 아래와 같은 텍스트가 저장되어 있다고 하자.

hellohi
thisisa
getnextline

get_next_line() 첫번째 호출 시 BUFFER_SIZE가 10이니 \n을 포함하여 hellohi\nth까지 read했고(\n은 문자 하나로 취급한다), line에 hellohi가 저장된 후 1을 반환한다.

두번째로 get_next_line()을 호출하면 BUFFER_SIZE가 10이니 \n을 포함하여 isisa\ngetn까지 read했고, line에 thisisa가 저장된 후 1을 반환한다.

세번째로 get_next_line()을 호출하면 \n을 포함하여 extline까지 read했고, line에 getnextline이 저장된 후 eof까지 읽었으니 0을 반환한다.

중요한 점은 read한 데이터를 저장할 변수가 필요하다는 점이다.
그러나 이 저장할 변수를 지역변수로 저장하면 함수 호출과 종료 때마다 기록되고 제거되기 때문에 이전에 호출했었을 때의 값을 저장할 수 없다.

따라서 이 read한 데이터를 저장할 변수를 static 변수로 저장한다.

(3) line을 동적으로 할당 해 주어야 한다.

\n을 기준으로 하기 때문에 line의 길이가 일정하지 않다. 따라서 동적으로 할당한다.

(4) 에러 처리 부분

fd는 음수가 될 수 없고, line은 문자열의 주소를 저장하는 char* 변수이다. 따라서 line이 NULL이면 주소를 저장할 공간이 할당되지 않았다는 것이다.
또, BUFFER_SIZE가 0보다 작거나 같으면 읽을 데이터가 0보다 작거나 같다는 것이기에 말이 되지 않는다. 이 3가지 조건에 대해 에러 처리를 해 준다.

(5) 보너스 파트 구현

보너스 파트는 여러 개의 fd로도 한 라인을 반환할 수 있게 하는 것이다.
이 부분을 구현하기 위해 지금까지 읽었던 data를 백업하는 변수를 static 포인터 배열로 선언한다.
이렇듯 static 포인터 배열로 선언하면, 각 fd마다 백업된 문자열을 backup[fd]를 통해 처리할 수 있게 되므로 보너스 처리가 가능해진다.

백업된 문자열의 첫번째 인덱스의 주소값은 backup[fd]에 저장이 되어 있다.

즉 backup[fd]는 하나의 문자열을 의미하므로, backup여러 개의 문자열 처리가 가능해진다.

(6) ft_strjoin() 구현 시 널가드 필수!!

이전 libft과제를 수행했을 때에는 ft_strjoin()을 구현할 때 널가드가 필요치 않았다. 따라서 ft_strjoin()의 파라미터 s1과 s2 둘 중 하나만이라도 널이면 Segfault가 떴었는데, 이 get_next_line 과제를 진행할 때는 널가드를 꼭 진행해야 한다.

처음 ft_strjoin()을 실행할 때가 바로 get_next_line()을 처음 호출했을 때다. 이 경우, buf에는 읽은 문자열이 들어 있지만 backup[fd]에는 NULL이 들어 있다.

따라서 널가드를 하지 않은 ft_strjoin()을 실행하게 되면 segfault가 뜨게 된다.

이런 상황을 방지하고자 이전에 구현한 libft가 널가드가 되어 있지 않다면 꼭 널가드를 해 주는 것을 추천한다. 널가드를 한 ft_strjoin()은 아래에 구현되어 있다.

(7) ft_strdup()을 이용해 line과 backup[fd]에 문자열을 넣게 될 때, malloc 실패에 유의하자.

내 코드에서는 ft_strdup()을 이용해 line과 backup[fd]에 문자열을 넣는다. 이 때 주의해야 할 점이 ft_strdup()내부에서 malloc에 실패하게 되었을 때다.

내 코드에서는 ft_strdup에서 malloc이 실패했을 때에 널을 반환했는데, 이 때 malloc에 실패한 것은 get_next_line 함수에서 '에러'에 해당한다.

따라서 malloc에 실패한 backup[fd]를 할당 해제하고, -1을 반환해야 한다. 이를 코드로 구현한 게 하단의 ft_error()함수이다.

(8) read했을 때 0이 반환되더라도(읽은 게 없을 때도) backup[fd]에 값이 남아 있다면 주의한다.

read했을 때 읽은 값이 없더라도 backup[fd]에 값이 남아 있다면 이에 유의한다.
만약 backup[fd]에 '\n'이 있으면 이 '\n'전까지를 line에 할당하고, '\n'이후부터를 backup[fd]에 넣는다.

만약 backup[fd]에 '\n'이 없으면 backup[fd]을 line에 할당하고 backup을 free한다.

5. 구현 아이디어

(1) 보너스 파트 구현을 위해 static char *backup[OPEN_MAX]선언.

OPEN_MAX는 단일 프로그램에 허용되는 `최대 열린 파일 수`를 정의하는 상수다. 
Unix 시스템에서 C언어의 OPEN_MAX는 limits.h에 정의돼있다.
그러나 허용되지 않은 헤더를 import하면 안되므로 나는 헤더에 OPEN_MAX를 정의해 놓았다.

최대 파일 수만큼의 인덱스를 가지는 포인터 배열을 선언함으로써, OPEN_MAX만큼의 백업 문자열을 다룰 수 있게 되었다.

(2) read를 통해 읽은 데이터를 저장할 char buf[BUFFER_SIZE+1]을 선언한 후 buf에 읽은 데이터 저장.

크기가 'BUFFER_SIZE + 1'인 이유는 
'ft_strjoin()'을 이용해 이전에 읽었던 데이터가 저장된 backup[fd] 문자열과 buf 문자열을 합하는데,
문자열을 합치는 기준이 '\0'이기 때문에 buf의 맨 뒤에도 '\0'을 넣는다.

(3) 읽은 데이터를 저장한 buf를 ft_strjoin()을 통해 backup[fd]와 합하고, 합한 문자열을 다시 backup[fd]에 넣는다.

이 과정을 통해 지금까지 읽었던 문자열이 backup[fd]에 저장되어 있다.

(4) '\n'을 찾고, 찾은 '\n'을 '\0'으로 바꿔 ft_strdup()을 이용해 line에 \n까지를 넣는다. 개행이 없으면 (2)로 돌아간다.

(5) 아까 찾은 \n의 다음 index의 주소값을 ft_strdup()에 넣어, \n이후의 문자열을 static char *변수 backup[fd]에 넣는다.

이 과정을 통해 \n이후의 문자열이 backup[fd]에 저장된다.

즉, \n을 기준으로 \n이전까지의 문자열은 line에 저장되고, \n이후의 문자열은 backup[fd]에 저장되어 이후 읽을 buf와 합쳐진다.

6. get_next_line_utils 코드

#include "get_next_line.h"

size_t		ft_strlen(const char *str)//문자열의 길이를 반환
{
	size_t		index;

	index = 0;
	while (str[index] != '\0')
	{
		index++;
	}
	return (index);
}

char			*ft_strdup(const char *s1)//s1의 문자열을 복사한 새 문자열 반환
{
	char		*p;
	size_t		slen;
	size_t		index;

	index = 0;
	slen = ft_strlen(s1);
	if (!(p = (char*)malloc(sizeof(char) * (slen + 1))))
	{
		return (0);
	}
	while (index < slen)
	{
		p[index] = s1[index];
		index++;
	}
	p[index] = '\0';
	return (p);
}

char	*ft_strjoin(char *s1, char *s2)//문자열 2개를 합하는 함수. get_next_line 과제 진행 시 NULL가드는 필수!!
{
	size_t	sindex1;
	size_t	sindex2;
	size_t	index;
	size_t	strindex;
	char	*str;
	
	if (!(s1) && !(s2))//문자열 둘 다 널이면
		return NULL;
	else if (!(s1) || !(s2))//문자열 둘 중 하나만 널이면 널이 아닌 문자열의 사본 반환
		return (!(s1) ? (ft_strdup(s2)) : ft_strdup(s1));
	sindex1 = ft_strlen(s1);
	sindex2 = ft_strlen(s2);
	index = 0;
	strindex = 0;
	if (!(str = (char*)malloc(sizeof(char) * (sindex1 + sindex2 + 1))))
		return (NULL);
	while (index < sindex1)
		str[strindex++] = s1[index++];
	index = 0;
	while (index < sindex2)
		str[strindex++] = s2[index++];
	str[strindex] = '\0';
	free(s1);//get_next_line진행 시 필요한 코드. s1과 s2 합했을 때 s1은 이제 필요가 없으므로 free시킴.
	return (str);
}

7. get_next_line 코드

#include "get_next_line.h"

int		isin_newline(char *str)//str에서 '\n'이 있는 index를 찾는 함수. \n이 없으면 -1리턴
{
	int		index;

	index = 0;
	while (str[index] != '\0')
	{
		if (str[index] == '\n')//'\n'발견시 발견한 index 리턴
		{
			return (index);
		}
		index++;
	}
	return (-1);//'\n'발견하지 못했을 시 -1 리턴
}

int		ft_error(char **backup)//에러 발생 시 backup을 할당 해제하기 위한 함수 ft_error()
{
	while (*backup != 0)
	{
		free(*backup);
		*backup = 0;//프리하기 전 0 넣으면 메모리 주소가 사라지게 되므로 free할 수 없음.
	}
	return (-1);
}

int					get_one_line(char **backup, char **line, int cut)//'\n'을 기준으로 '\n'전까지의 하나의 문자열을 line에 저장하기 위한 함수.
{
	char			*temp;

	(*backup)[cut] = '\0';//'\n'을 '\0'으로 바꾼다.
	if (!(*line = ft_strdup(*backup)))
	{
		return (ft_error(backup));//malloc에서 할당 실패했을 때 ft_error를 통해 backup을 할당 해제해 준다.
	}
	if (!(temp = ft_strdup(*backup + cut + 1)))
	{
		return (ft_error(backup));//malloc에서 할당 실패했을 때 ft_error를 통해 backup을 할당 해제해 준다.
	}
	free(*backup);
	*backup = temp;
	return (1);
}

int					get_last(char **backup, char **line)
{
	int				cut;

	if (!(*backup))//file이 비어있을 때. backup[fd]가 아무것도 할당이 되지 않았으므로 여기에 들어간다.(ft_strjoin을 한번도 실행하지 않았을 때)
	{
		*line = ft_strdup("");
		return (0);
	}
	else
	{
		if ((cut = isin_newline(*backup)) >= 0)//newline이 있으면
		{
			return (get_one_line(backup, line, cut));
		}
		//newline이 없으면
		if (!(*line = ft_strdup(*backup)))//line에 *backup을 복사한 것을 넣되, malloc에 실패하면 ft_error 실행
		{
			return (ft_error(backup));
		}
		free(*backup);
		*backup = 0;
		return (0);
	}
}

int					get_next_line(int fd, char **line)
{
	static char		*backup[OPEN_MAX];
	char			buf[BUFFER_SIZE + 1];
	int				readsize;
	int				cut;

	if ((fd < 0) || (line == 0) || (BUFFER_SIZE <= 0))//에러 처리
		return (-1);
	while ((readsize = read(fd, buf, BUFFER_SIZE)) > 0)//읽어들인 크기
	{
		buf[readsize] = '\0';//buf의 맨 뒤에 '\0'을 넣어 ft_strjoin()이 가능하게 함.
		backup[fd] = ft_strjoin(backup[fd], buf);
		if ((cut = isin_newline(backup[fd])) >= 0)//newline이 있으면 get_one_line()실행
			return (get_one_line(&backup[fd], line, cut));
	}
	if (readsize < 0)//read시 에러났을 경우
	{
		return (ft_error(&backup[fd]));
	}
	return (get_last(&backup[fd], line));//읽어들인 게 0일 경우, backup에 남아있는 라인을 line에 저장.
}

8. 구현 방법

(1) 이전에 읽었던 데이터를 저장할 변수 static char *backup[OPEN_MAX]와 읽은 데이터를 저장할 변수 buf[BUFFER_SIZE + 1]을 선언한다.

(2) read()를 통해 BUFFER_SIZE만큼 fd를 읽고, 읽은 데이터를 buf에 저장한다.

(3) readsize를 통해 읽은 데이터의 맨 끝에 '\0'을 넣는다.(ft_strjoin 실행 위해)

(4) backup[fd] = ft_strjoin(buf, backup[fd])로 buf와 backup[fd]의 문자열을 합한 최종 문자열을 다시 backup[fd]에 넣는다.

(5) backup[fd]에 '\n'이 있으면 get_one_line()을 통해 line에 하나의 문자열을 할당하고, '\n'이후부터를 다시 backup[fd]에 넣고 1을 반환한다.

(6) backup[fd]에 '\n'이 없으면 (2)로 돌아간다.

(7) 만약 ft_strdup()을 진행하는 도중 malloc이 실패했다면 ft_error를 통해 backup[fd]를 할당 해제한다.

(8) EOF까지 읽었다면 get_last()를 이용해 backup[fd]에 '\n'이 있는지 없는지를 비교한 후 '\n'이 있으면 get_one_line()으로 line에 한 라인을 할당 후 0을 반환한다.

'\n'이 없으면 backup[fd]를 line에 복사해 넣고 backup[fd]를 free한다.

이 포스팅은 이대현님의 블로그를 많이 참고하였습니다.

깔끔하고 좋은 코드와 자세한 설명이 나와있으므로 이대현님의 블로그도 참고해 보시는 것을 추천드립니다!
링크텍스트

좋은 웹페이지 즐겨찾기