혹시나 문제가 된다면 바로 비공개 처리하겠습니다. 지적이나 댓글 환영합니다!
과제 중에 동료평가용 설명 정리 차원으로 블로그 글을 작성하기 때문에, 통과 완료 전까지는 계속 글을 수정할 수도 있습니다. 양해바랍니다. 감사합니다.
참고로, 이 포스팅의 중요한 본론은 (5), (6), (7) 부터 나옵니다.
libft가 조금 늦게 끝난 감이 있지만, get_next_line을 지금이라도 구현하게 되어서 다행이라고 생각한다. 처음에 get_next_line을 보고 좀 난감하다는 생각을 했다.
예를 들자면, "1줄씩 자른다" 라는 것은 문자열 포인터로 전체 문단이 주어진 경우에는 쉽다. 문자 "\n"를 기준으로, while문을 처음부터 문자열 끝까지 돌며 strdup() 함수 등을 사용하며 문자열을 분리해주면 된다.
그러나, 이번 문제는 주어진 BUFFER_SIZE만큼 fd를 이용해, 문자열(char *)이 아닌 "파일"을 읽어온다.
그러면 이때 어떤 처리를 해야, BUFFER_SIZE가 어떻게 주어지던 간에, 파일을 1줄씩 읽어오는 함수를 구현할 수 있는지 생각해보았다.
(1) 과제의 목표 / 구현해야할 사항
* 이번 과제에서는 C에서 사용되는 "정적 변수"란 무엇인지를 배울 수 있다.
* 과제에 명시된 고려사항 (번역)
- get_next_line()을 반복문을 통해 실행할 때, 한번 루프가 돌때마다, fd의 텍스트를 1문장씩 읽는다(EOF 전까지)
- 파일이나 표준입력으로부터 읽어왔을 때 함수가 잘 동작되도록 해야한다
- libft 는 이 프로젝트에 허용되지 않는다. get_next_line_utils.c에 get_next_line에 필요한 함수들을 포함시킬 수 있다.
- 프로그램을 -D BUFFER_SIZE=xx 이라는 컴파일 옵션과 함께 컴파일한다. 이것은 get_next_line에서 실행되는 read()의 버퍼 사이즈로 사용될 것이다.
- 컴파일은 이런 형식으로 이루어진다 : gcc -Wall -Wextra -Werror -D BUFFER_SIZE=32 get_next_line.c get_next_line_utils.c
- read()는 파일이나 표준입력으로부터 읽어올 때 컴파일할 때 정의된 BUFFER_SIZE를 사용해야한다.
- get_next_line.h 헤더파일은 최소한 함수 get_next_line의 프로토타입을 포함해야한다.
- 당신의 함수가 BUFFER_SIZE 값이 9999일 때 여전히 작동하는 가? BUFFER_SIZE 값이 1일 때는? 또 10000000일 때는? 그렇게 되는 이유를 아는가?
- get_next_line 호출 시마다 최대한 적게 읽을 수 있도록 노력해야한다. 만약 당신이 개행을 만나면, 현재 문장을 리턴해야한다. 파일 전체를 읽고 각 문장을 처리하는 것은 하는 것은 금지된다.
- 당신의 프로젝트를 테스트 없이 제출하지 말도록 한다. 당신의 모든 경우에 대비하여 실행할만한 많은 테스트들이 있다. 파일, redirection 그리고 표준입력으로부터 읽는 것을 시도해보라. 당신의 프로그램이 당신이 표준출력으로 개행을 보냈을 때는 어떻게 동작하나? 그리고 CTRL-D 를 할 경우는 또 어떤 동작을 보이는가?
- 우리는 get_next_line이 정의되지 않은 동작을 가진다고 가정한다. 만약, 2번의 호출 사이에, 첫번째 fd가 EOF에 도달하기 전에 같은 파일 디스크림터가 다른 파일로 바뀌는 경우에 말이다.
- lseek()는 허용되지 않는 함수이다. 파일을 읽어오는 것은 한번으로 끝나야한다.
- 마침내 우리는 get_next_line은 binary 파일을 읽어오는 중에 정의되지 않은 동작을 가진다고 가정한다. 그렇지만, 당신이 바란다면, 당신은 이러한 동작을 일관적으로 만들 수 있다.
- 전역 변수의 사용은 금지된다.
- 정적 변수란 무엇인지 알아보자 : https://en.wikipedia.org/wiki/Static_variable
* 보너스 구현에 관한 고려사항 (번역)
- 기본 파트가 완벽하지 않으면 보너스는 채점되지 않는다.
- 보너스 파트를 위해서는 3개의 초기 파일에 모두 _bonus를 붙여야한다
- get_next_line을 1개의 static 변수를 사용하여 성공한다.
- get_next_line에 여러 개의 fd가 사용될 수 있도록 한다. 예를 들자면, fd 3, 4, 5가 읽을 수 있는 파일 디스크립터라고 하자, 이때 get_next_line을 fd를 3으로 하여 호출하고, 또 한번은 fd를 4로 하여 호출하고, 또 한번은 fd 를 5로 하여 호출하거나 할 수 있다. 이때 각각의 fd에서 읽던 스레드를 잃지 않아야한다.
(2) static 변수(정적 변수)란 무엇인가?
- 정적으로 할당되는 변수
- 프로그램이 종료 될 때까지 메모리 유지 : 프로그램이 종료되지 않으면 함수가 종료되어도 값이 유지됨
- 초깃값을 지정하지 않아도 자동으로 0으로 초기화된다 : 초기화는 처음 한번만 수행하고 이후는 무시
- 자료형 앞에 static 키워드를 붙여 선언한다.
참고) 정적 전역 변수는 자신이 선언된 소스 파일 안에서만 사용할 수 있고, 외부에서는 가져다 쓸 수 없다.
즉, 전역 변수에 static을 붙이면 변수의 범위를 파일 범위로 제한하는 효과를 낸다.
참고) 정적 메모리 할당은 일반적으로 관련 프로그램을 실행하기에 앞서 컴파일 시간에 메모리를 할당한다.
참고) 정적 변수는 함수의 매개변수로 사용할 수 없다. 매개변수에 static을 붙이더라도 매개변수는 정적 변수가 되지 않으며 값이 유지되지 않는다.
(3) 파일 디스크립터 (fd)란 무엇인가?
1) 시스템으로부터 할당 받은 파일을 대표하는 0이 아닌 정수 값
2) 프로세스에서 특정 파일에 접근할 때 사용하는 추상적인 값
3) 프로세스에서 열린 파일의 목록을 관리하는 테이블의 인덱스
- 유닉스 시스템에서 모든 것은 "파일"이다. 모든 객체들은 파일로써 관리된다.
- 유닉스 시스템에서 프로세스가 이 파일들을 접근할 때에 파일 디스크립터라는 개념을 이용한다.
- 파일 디스크립터는 '0이 아닌 정수' 값이다. 즉, 음수가 아닌 0과 양수인 정수 값을 갖는다.
프로세스가 실행 중에 파일을 Open 하면 커널은 해당 프로세스의 파일 디스크립터 숫자 중에 사용하지 않는 가장 작은 값을 할당해 준다.
그 다음 프로세스가 열려있는 파일에 시스템 콜을 이용해서 접근할 때, FD 값을 이용해 값을 지칭할 수 있다.
프로그램이 프로세스로 메모리에서 실행을 시작할 때, 기본적으로 할당되는 파일 디스크립터들이 있다. 바로 표준 입력, 표준 출력, 표준 에러이다. 이들에게 각각 0, 1, 2 라는 정수가 할당된다.
0이 아닌 정수로 표현된느 파일 디스크립터는 0 ~ OPEN_MAX까지의 값을 가질 수 있으며, OPEN_MAX 값은 플랫폼에 따라 다르다.
파일 디스크립터는 위에서 볼 수 있듯이, 단순히 숫자인 이유는 프로세스가 유지하고 있는 FD 테이블의 인덱스이기 때문이다. FD 3번이라는 의미는 FD 테이블의 3번 항목이 가리키는 파일이라는 의미이다.
프로세스는 이런 FD 테이블과 파일 테이블의 정보를 직접 고칠 수 없으며, 반드시 커널을 통해서 수정해야 한다.
(4) open, read 함수
* open() : 파일을 여는 함수
헤더 : #include <fcntl.h>
형태 : int open(const char *filepath, int flag);
int open(const char *filepath, int flag, mode_t mode);
인수 :
- char *FILENAME : 열고자하는 파일의 경로
- int flags : 파일 열 때 사용할 옵션 (자세한 옵션은 더보기에)
O_RDONLY : 읽기 모드 (Read Only)
O_WRONLY : 쓰기 모드 (Write Only) - 읽지 않고 쓰기만 하는 경우는 크게 많지 않음
O_RDWR : 읽기/쓰기 모드
O_CREAT : 파일 생성
O_APPEND : 파일을 쓰되 기존 파일의 맨 끝부터 이어 쓰는 기능
O_TRUNC : 파일을 초기화
O_EXCL : O_CREAT 와 함께 사용되며, 이미 파일이 존재한다면 에러를 리턴
- mode_t mode : O_CREAT 옵션을 쓸 때 필수적으로 사용해야하는 옵션으로, 파일의 접근 권한을 명시
기본 값 ( 파일 : 0666 / 디렉토리 : 0777)
반환 : 성공적으로 수행 시, 음이 아닌 정수형의 값이 반환(= file descripter), 실패하면 -1 반환
- open()과 fd
: 정상적으로 open한 파일의 위치를 가리키는 번호가 저장된다. 0, 1, 2 는 시스템적으로 건들면 안되는 플래그이므로 3부터 번호를 부여한다. 만약 하나의 C 파일에서 2개의 파일을 open했고, 두 파일 모두 정상적으로 open되었다면 처음 open한 파일의 fd는 3, 두 번째 open한 파일의 fd는 4다. 이렇게 순서대로 번호가 부여된다. 추가적으로 동일한 파일을 두번 open 해서 서로 다른 fd1과 fd2에 할당한다면, 그것 또한 3과 4로 별개의 번호가 부여된다.
* read() : 파일을 읽어오는 함수
헤더 : #include <unistd.h>
형태 : ssize_t read(int fd, void *buf, size_t nbytes);
인수 : int fd : 파일 디스크립터
void *buf : 파일을 읽어들일 버퍼
size_t nbytes : 버퍼의 크기
반환 : 실패 시 -1, 정상적으로 실행되었다면 읽어들인 바이트 수
참고) close 함수 : open된 해당 fd 를 가진 file을 close 함
헤더 : #include <unistd.h>
형태 : int close(int fd)
인수 : int fd : 파일 디스크립터
반환값 : 정상적으로 close() 했다면 0을, 실패했다면 -1을 반환
참고) lseek 함수 : 읽기 및 쓰기를 위해서 파일의 위치를 재지정
헤더 : #include <sys/types.h>
#include <unistd.h>
형태 : off_t lseek(int fildes, off_t offset, int whence);
설명 : lseek() 는 열린 파일 지정자 fildes로부터 offset만큼 위치를 변경한다. 위치 변경 시 기준점을 정할 수 있는 데 whence를 이용해서 지정할 수 있다. 실수로 파일의 마지막을 초과해서 lseek을 사용했을 경우, lseek에서 리턴을 하지는 않지만, write() 혹은 read()에서 에러를 발생하게 되므로 주의해야 한다.
- SEEK_SET : 파일의 처음을 기준으로 offset 계산
- SEEK_CUR : 파일의 현재 위치를 기준으로 offset을 계산
- SEEK_END : 파일의 마지막을 기준으로 offset을 계산
반환값 : 성공했을 경우 파일의 시작으로부터 떨어진 byte만큼의 offset을 리턴. 실패했을 경우 -1을 리턴.
에러 :
- EBADF : Fildes가 열린 파일 지정자가 아니다
- ESPIPE : Fildes가 파이프, 소켓 혹은 FIFO이다
- EINVAL : Whence가 유효한 값이 아니다
(5) read 함수로만 파일을 읽고, 그대로 출력할 경우 어떻게 되는지
-> 여기서 한문장씩 잘라서 읽게 하려면 어떤 고려사항을 생각해봐야하는 지
read()만 사용하여 파일을 읽고 읽은 부분을 바로 출력할 경우 어떤 일이 일어나는 지, 직접 open()과 read()를 실행해서 알아보았다.
위 코드와 컴파일 명령어로 메인함수를 돌려서, 어떤 일이 일어나는 지 알아보았다. (BUFFER_SIZE를 컴파일 옵션을 통해 정의해줌)
(우선, open() 함수를 위해 <fcntl.h> 를, read() 함수를 위해 <unistd.h> 를 include 하였다)
맨 먼저 open()을 통해 파일의 fd 를 받아, 그 fd를 인자로 read()를 수행해서 버퍼 line에 읽어온 부분을 저장하였다.
그리고 읽어온 부분을 printf로 출력하기 위해서, 읽어온 바이트의 끝에 "\0"을 붙여 문자열처럼 null로 끝나게 해주었다.
그다음 읽어온 부분을 printf()로 출력하여 읽어온 부분의 결과를 확인하였다.
EOF까지 읽은 후에 read()를 다시 실행하면, 아무것도 버퍼에 읽어오지 않으며 리턴값으로 받은 read_size(read()를 통해 읽어온 바이트 수)는 0이 된다. 그 결과로 위의 main 함수에서는 while 문의 조건을 벗어나 while문을 빠져나오고 main 함수를 종료하게 된다.
주의 : read()를 통해 버퍼에 읽어온 파일에 다시 read()를 사용하면, 전에 읽은 부분의 뒤부터 읽기 시작한다는 점을 기억하자
주의 : 만일 open()을 다시 실행하고 받아온 fd로 read()을 실행할 경우, 처음부터 파일을 읽는다.
참고로 test.txt에는 팝송 lemon tree 의 가사 일부가 있다.
위의 결과를 통해 경우를 나눠 분석해 볼 수 있다.
1) BUFFER_SIZE 가 1문장의 길이보다 긴 경우
먼저 0번째 출력을 보자, 첫번째 문장은 "~ i wonder why"에서 끝나야 하는데, (\n)yester 까지 출력이 되버렸다. 즉, 뒤에 EOF가 나오지 않는 이상, 개행과 함께 뒷 문장까지 출력이 되버리는 결과가 나올 수 있다.
2) BUFFER_SIZE가 1문장의 길이보다 짧은 경우
문장이 전부 출력되지 않고 잘린다. 다음 read()에서 이어서 읽힌다.
"I'm turnig, turnig, turnig,turnig, turnig around" 라는 문장이 4번째, 5번째, 6번째의 세 차례에 걸쳐 읽힌 것을 보면 알 수 있다.
3) 읽어온 부분에서 문장의 끝이 개행('\n')일 경우
문장에 이어서 개행까지 같이 출력되며, BUFFER_SIZE의 길이가 넉넉한 경우 다음 문장의 일부까지 출력될 수 있다.
4) 읽어온 부분에서 문장의 끝이 EOF 일 경우
BUFFER_SIZE가 넉넉하다고 하더라도, 남아있는 문장을 읽은 후에는 파일에 남아있는 것이 없으므로 더이상 읽지 않는다.
(6) get_next_line은 어떤 구조로 실행되야할까?
과제에서 제시한 get_next_line 함수의 프로토타입은 다음과 같다.
* PROTOTYPE
형식 : int get_next_line(int fd, char **line);
인자 :
- int fd : 파일을 읽어오기 위한 파일 디스크립터
- char **line : 읽어온 값
반환 :
1 : 문장 1개를 읽고나서 반환
0 : EOF까지 도달했을 때 반환
-1 : 오류가 발생했을 때 반환
허용 함수 : read, malloc, free
설명 : fd로 부터 문장 하나를 읽고, 개행없이 그 문장을 반환해주는 함수를 작성하여라.
과제에서 제시한 고려사항을 충족하는 get_next_line을 구현할 경우, get_next_line을 반복문을 통해 실행하면 read()를 사용하여 1문장씩 받아와야 한다. 그리고 EOF에 도달하면 읽기를 중단해야 한다. 이때, get_next_line() 함수는 1, 0, -1 를 리턴해야한다.
따라서 이런 형식으로 함수를 실행할 수 있을 것이라 생각하고 main을 짜보았다.
참고로, get_next_line.h 헤더파일에는 read()를 위한 <unistd.h>와 get_next_line()속 malloc()을 위한 <stdlib.h> 헤더가 포함되어 있어야 한다.
- get_next_line()이 -1 을 리턴한 경우는 에러가 발생한 경우이므로 처리해주었다.
- get_next_line()이 1을 리턴한 경우는 문장 1개를 읽어왔다는 뜻이므로 받아온 문장을 출력해주고 계속 loop를 진행한다
- get_next_line()이 0을 리턴한 경우는, EOF에 도달했다는 뜻이므로 while문을 빠져나온다.
(7) gnl 함수의 역할과 구조 설계
get_next_line()을 통해 read()된 값에 대한 처리를 해줘서, 함수 호출 시 1 문장씩 (char *) 포인터에 받아올 수 있게 하는 것이 이 과제의 목표이다.
먼저, 0번째 출력에서 1개의 문장만 뽑아오려면, 받아온 문장에서 개행이 있을 경우, 개행과 그 뒷부분을 제외하고 문장 1개를 (char *) 포인터에 담아주면 된다. "i wonder how i wonder why(\n)yester"에서 "(\n)yester" 부분을 제외하고 "i wonder how i wonder why"로 만들어준다는 뜻이다.
그런데 이때, get_next_line()을 다시 실행하여 다음 문장을 받아오려는 경우 문제가 생긴다!
다음의 read()에서는 지난번의 read()로 읽어온 부분 이후부터 읽어오기 때문에, 앞서 읽혔던 "yester" 부분 없이 "day you told me ' bout the blue blue sky"밖에 읽어올 수가 없다. 문장의 일부를 잃어버리게 되는 셈이다.
그렇다면 어떻게 "yesterday you told me ' bout the blue blue sky" 라는 문장을 만들 수 있을까?
혹시, 저번에 get_next_line()을 실행했을 때 문장을 자르느라 남겨진 뒷부분의 "yester"를 어딘가에 저장해놓고, 이번에 get_next_line()에서 read된 문자열의 앞에 이걸 다시 붙여주면 어떨까? 그러면 문장의 일부를 잃어버리는 일을 방지할 수 있을 것 같다.
그런데 이런 경우, 전역함수는 과제에서 허용되지 않고, 지역 변수는 함수가 종료될 때마다 메모리가 소멸된다. 따라서 두가지 방법으로는 다음 번에 함수를 호출할 때 이번에 저장한 메모리를 이어서 사용할 수가 없다. 그럼 어떻게 해야할까?
정답은 이번 과제의 핵심인 static 변수를 활용하는 것이다!
static 변수는 프로그램(main)이 종료되기 전까지 메모리가 소멸되지 않고 유지되는 특징을 가진다.
따라서 get_next_line()을 몇번 호출하던 간에, 저번에 함수를 호출했을 때 저장해놓은 값을 가져다 쓸 수 있다.
이때, 저번에 남겨진 문자열을 저장해놓을 변수, static char *backup을 선언한다고 가정하자.
개행으로 구분된 문자열의 뒷부분을 backup이라는 변수에 저장해놓고 다음번 read된 문자열에 붙여주면, BUFFER_SIZE 가 문장의 길이보다 긴 경우 나타나는 문제를 해결할 수 있을 것이다.
만일 BUFFER_SIZE 가 문장의 길이보다 짧은 경우는 어떻게 해야할까? 이때는 한번의 read로 문장을 다 읽어들이지 못한다.
이때는 문장의 끝(개행이나 EOF)이 나올 때까지 read()를 반복적으로 수행하여, 반환한 문자열을 저장한 별도의 버퍼에 read()된 메모리를 연속적으로 저장해주면 될 것이다. (read의 버퍼는 매번 read할 때마다 새로운 값으로 채워지므로, 문장을 저장하는 데는 별도의 변수를 사용해야함)
이때 결국 읽어온 문장의 끝이 개행('\n')이라면 개행을 기준으로 앞뒤로 잘라주고 (개행은 반환될 문자열에 포함시키지 않음), 반환한 문자열을 저장한 버퍼에 개행('\n') 전의 앞부분을 붙여주면 된다.
만일 읽어온 문장의 끝이 EOF라면 이번 read()로 읽어온 문자열 전체를 반환한 문자열을 저장한 버퍼에 붙여주면 된다.
마지막으로, 인자로 받은 문자열 포인터에, 반환할 문자열을 저장할 버퍼의 주소를 할당해주고 0, 1 중 하나를 리턴하면 마무리된다.
이때 읽어온 문장이 EOF라면 0을, 읽어온 문장이 EOF가 아니라면 1을 리턴하여 get_next_line() 함수를 종료해준다.
읽어온 문장이 EOF인지 처리해주기 위해서는, read()의 리턴값으로 받은 읽은 바이트 수가 0으로 나오는 경우를 처리해주면 된다.
리턴값 중 -1은 오류가 발생하였을 때 리턴하는 것이므로, 함수 호출 시 line의 null 여부와 read할 BUFFER_SIZE 그리고 fd의 범위(0 ~ OPEN_MAX)에 대한 예외처리를 할 때 리턴하면 된다.
참고 자료 링크 :
https://dev-ahn.tistory.com/96
리눅스 - 파일 디스크립터
File Descriptor (파일 디스크립터) [출처: http://dev.plusblog.co.kr/22] 1. 파일 디스크립터 - 시스템으로부터 할당 받은 파일을 대표하는 0이 아닌 정수 값 - 프로세스에서 열린 파일의 목록을 관리하는 테
dev-ahn.tistory.com
C/C++ open 함수 - 파일 생성 / 읽기 / 쓰기
Open 함수 기능 파일을 열거나 생성 후 열어주는 함수 함수원형 #include #include #include int open(const char *filepath, int flag); int open(const char *filepath, int flag, mode_t mode); 매개변수 const..
bubble-dev.tistory.com
https://jeongchul.tistory.com/368
리눅스 open - 리눅스 시스템 프로그래밍
리눅스 open - 리눅스 시스템 프로그래밍 open은 이미 존재하는 파일을 읽기 또는 쓰기용으로 열거나 새로운 파일을 생성하여 연다. #include 헤더 파일을 사용 int open(const char* pathname, int flags, [mode_..
jeongchul.tistory.com
C언어 파일 읽기 함수 read()
C함수 파일 읽기 read() open() 함수로 열기를 한 파일의 내용을 읽기를 합니다. 헤더: unistd.h 형태: ssize_t read (int fd, void *buf, size_t nbytes) 인수: int fd 파일 디스크립터 void *buf 파일을 읽어 들..
badayak.com
https://mong9data.tistory.com/111
open, read, close 함수 정리
open 헤더 : fcntl.h open은 두 가지 형태의 시스템 콜을 가지고 있다. 형태는 아래와 같다. 참고로 rush 및 bsq에서는 첫 번째 시스템 콜을 이용할 것이다. int open(const char *pathname, int flags); int open..
mong9data.tistory.com
https://www.joinc.co.kr/w/man/2/lseek
linux man page : lseek - 파일의 위치를 재지정한다.
www.joinc.co.kr
http://blog.naver.com/PostView.nhn?blogId=rbdi3222&logNo=220732695910
정적변수란?
전역 변수는 프로그램의 모든 영역에서 접근이 가능하고, 프로그램이 종료되지 않는 한 메모리가 소멸되지 ...
blog.naver.com
https://dojang.io/mod/page/view.php?id=690
C 언어 코딩 도장: 79.2 정적 변수 선언하기
정적 변수를 알아보기 전에 먼저 자동 변수로 예제를 작성해보겠습니다. 다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요. variable.c #include void increaseNumber() { int num1 = 0; // 변수 선언 및
dojang.io
https://www.notion.so/Get-Next-Line-c7a311e63bd2483ab5bf404791e917c6
Get Next Line
단축 주소 : [ http://bit.ly/gnljs ]
www.notion.so
'IT > 42Seoul' 카테고리의 다른 글
[born2beroot] 개념 정리 - 프로젝트 개요 (0) | 2021.11.15 |
---|---|
[GET_NEXT_LINE] 구현 예시 (0) | 2021.06.11 |
Libft 과제 시 유의점 (테스터기, protected, Makefile) (0) | 2021.05.31 |
malloc/calloc 관련 정리 (0) | 2021.05.30 |
[Libft] C 언어 라이브러리 구현_BONUS_보너스 함수 구현2 (0) | 2021.05.30 |