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 함수를 이용해 연 파일의 내용을 읽는 함수.
fd
를n 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한다.
이 포스팅은 이대현님의 블로그를 많이 참고하였습니다.
깔끔하고 좋은 코드와 자세한 설명이 나와있으므로 이대현님의 블로그도 참고해 보시는 것을 추천드립니다!
링크텍스트
Author And Source
이 문제에 관하여(get next line), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@yeunjoo121/getnextline구현저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)