본문 바로가기
Activities/공부

[42Seoul] get_next_line 과제 - 배경지식 정리

by 코드포휴먼 2021. 1. 8.

42Seoul Subject를 수행하면서 필요했던 지식을 정리해본다.

이전까지는 과제에서 알아야할 주제 별로 나누어서 포스팅 했는데, get_next_line 과제에서 조금 애매한 것들은 한번에 모아 기록하게 됐다.

 

get_next_line은 읽은 파일의 한 줄을 반환하는 프로그램이다. 

시스템이 파일을 열고, 일정한 크기씩 읽어들여서 줄바꿈을 구분하여 파일내용을 출력하는 함수를 짜야한다.

유사한 질문이 있는 어느 게시판이 있었는데, kldp.org/node/156440을 읽어보면 대충 감이 올 것이다. 

 

사용한 헤더

  • <limits.h> : 파일 디스크립터의 최대값인 OPEN_MAX가 정의돼 있다. 사용하는 컴퓨터 환경의 OPEN_MAX를 직접 구해서 상수로 사용한다면 이 헤더는 include 하지 않아도 된다.
  • <unistd.h> : 파일을 읽고 쓰고 닫는 read(), write(), close() 함수가 정의돼 있다. (⚠️fread() 등의 표준 입출력 함수는 <unistd.h>가 아닌 <stdio.h>에 정의돼있다. 사용방식이 조금 다르다.)
  • <fcntl.h> : 파일을 여는 open() 함수와 O_RDONLY 등의 파일 접근모드가 정의돼있다.
  • <stdlib.h> : 힙 메모리에 동적할당하는 malloc() 함수가 정의돼 있다.

 

 

main 함수 코드

get_next_line은 main.c와 Makefile을 제출하지 않는다.

과제로 제출하지 않지만 프로그램을 돌리기 위해 사용할 main 함수 견본코드는 다음과 같다.

슬랙에서 yohlee님이 공유해주셨다.  

#include "get_next_line.h"
#include <stdio.h>
#include <fcntl.h>

int	main(void)
{
	int		temp;
	int		fd;
	char	*line;

	fd = open("test.txt", O_RDONLY);
	while ((temp = (get_next_line(fd, &line)) > 0))
	{
		printf("%s\n", line);
		free(line);
	}
	printf("%s\n", line);
	free(line);
	close(fd);
	return (0);
}

get_next_line을 이용해서 main 함수는 파일내용을 한줄씩 출력한다.

파일의 한 줄(줄바꿈되기 전까지)을 담는 line 변수는 main 함수에서 선언하고, get_next_line 함수에 line 변수을 넘겨줌으로써

get_next_line 함수가 끝나면 main 함수는 값이 담긴 line을 출력한다.

get_next_line 함수는 line을 직접 리턴하지 않으며, 로직에 의해 동적할당해서 담을 뿐이다.

(get_next_line의 리턴값은 1, 0, -1이다.)

 

main 함수에서는 파일을 여는 open() 함수와 파일을 읽는 read() 함수가 사용된다.

유닉스 파일시스템을 미리 훑어보는 걸 추천한다.

 

파일 관련 함수

int open(const char *FILENAME, int FLAGS[, mode_t MODE])
헤더 <fcntl.h>
형태 int open (const char *FILENAME, int FLAGS[, mode_t MODE])
인자 char *FILENAME 대상파일이름
int FLAGS 파일 열기 옵션
[, mode_t MODE] O_CREAT 옵션 사용에 의해 파일이 생성될 때 지정되는 파일 접근 권한
리턴값 - 정상적으로 파일을 열었다면 : 파일 디스크립터의 양의 정수 값
- 파일 열기에 실패하면 : -1

MODE의 여러 옵션을 잘 정리한 블로그를 찾아볼 수 있었다.

 

⚠️open()과 fopen()의 차이

open() 함수는 사용하는 파일을 구별하기 위해 파일 디스크립터라는 정수를 반환하고 FD 번호로 지정하여 사용한다.

반면 fopen()함수는 FILE 구조체 정보, 즉 파일 스트림 정보를 구한다.

즉, 정수값만 구하는 open()함수 보다도 fopen()은 FILE 의 다양한 정보를 구하여 편리함을 제공한다.

subject에서는 표준 입출력함수인 fopen, fread 등을 사용하지 않고 open, read를 사용한다.

 

 

ssize_t read (int fd, void *buf, size_t nbytes)
헤더 <unistd.h>
형태 ssize_t read (int fd, void *buf, size_t nbytes)
인자 int fd 파일 디스크립터
void *buf 파일을 읽어들일 버퍼
(read 함수가 끝나면 버퍼에 파일 내용이 담긴다.)
size_t nbytes 버퍼의 크기
리턴값 - 정상적으로 파일을 읽었다면 : 읽어들인 바이트 수
- 파일 읽기에 실패했다면 : -1
- 파일을 끝까지 읽었다면 : (더 이상 읽을 바이트가 없다면 0을 반환한다. EOF(End of file)을 의미한다.)

버퍼(buffer) 개념

파일을 읽을 때 사용할 임시 공간으로 버퍼 변수를 사용한다.

버퍼(buffer)란 컴퓨터의 주기억장치와 주변 장치 사이에서 데이터를 주고받을 때 둘 사이의 전송속도 차이를 해결하기 위해 전송할 정보를 임시로 저장하는 고속 기억장치다.

버퍼에는 용량이 한정되어 있기 때문에, 오버 플로우 현상이 발생할 수 있다.

표준 입출력함수(scanf, printf 등)는 버퍼를 사용한다.

 

offset 개념

read 함수와 관련해서 offset을 이해할 필요가 있다.

read(fd, buf, buffsize)를 수행하면 데이터를 읽은 만큼 offset를 증가시킨다.

다시 파일을 읽으려하면 마지막 읽은 파일 위치부터 읽어들인다.

read 함수는 자동으로 끊어진 부분부터 쭉쭉 읽는데, 이유는 파일 디스크립터 테이블(FD Table)에서 읽은 위치(offset)를 저장하기 때문이다.

offset은 각 프로세스가 같은 FD를 참조하여 같은 i-node를 참조하더라도, 각자 다른 부분을 읽을 수 있도록 할당한다.

사진 출처 https://dev-ahn.tistory.com/96

 

 

참고로 ab만 담긴 파일을 버퍼크기 4 로 읽으면

read 함수의 리턴값은 2 이고, 버퍼에는 a, b, 쓰레기값, 쓰레기값 이 담긴다. (By jko님)

 

만약 버퍼크기가 4일 때 abcdefghij가 담긴 파일을 읽는다면

첫 번째 read() 호출 후 리턴값은 4 이고, 버퍼에는 abcd 이 담긴다.

두 번째 read() 호출 후 리턴값은 4 이고, 버퍼에는 efgh 이 담긴다.

세 번째 read() 호출 후 리턴값은 2 이고, 버퍼에는 ijgh 이 담긴다. (By jolim님)

 

 

파일 디스크립터(File Descriptor)

파일 디스크립터 개념은 La Piscine과 Libft 과제에서 접했던 개념이다.

파일 디스크립터는 시스템으로 부터 할당받은 파일이나 소켓을 대표하는 정수다.

정수값은 사용하는 컴퓨터에 따라 각각 다른 상한값인 OPEN_MAX까지 할당된다.

OPEN_MAX 값은 <limits.h>에서 include 해서 쓸 수도 있고, 

개인 컴퓨터에서 한번에 열 수 있는 파일 개수인 OPEN_MAX 값을 확인해서 헤더파일에 직접 상수로 define할 수도 있다. 

파일 디스크립터에 대해서는 이 포스팅에 정리한 바 있었다. 

더보기

+) Norm에는 '표준 라이브러리의 매크로는 프로젝트에서 사용이 허가되었을 경우에만 사용 가능합니다.'라는 문구가 있어서 <limits.h의 OPEN_MAX로 통과할 수 있지만 상수를 추천한다는 글이 있었다. (By hyeonski님)

 

+) Linux 시스템에서는 OPEN_MAX가 더 이상 사용되지 않는다는 stackoverflow의 답변도 있었다.

만약 wsl 리눅스 환경에서 문제를 푼다면 아예 헤더파일에서 상수로 define하는 것도 괜찮을 것 같다. 

stackoverflow.com/questions/14042824/where-is-open-max-defined-for-linux-systems

 

 

컴파일 옵션 -D

조건부 컴파일을 할때 유용하게 사용되는 것이 -D 옵션이다.

gcc -D <매크로이름>

GNL 과제에서 gcc -D 플래그로 컴파일 시 외부에서 #define 하여 버퍼사이즈를 정의한다.

subject PDF에서 안내하는 컴파일 방식은 다음과 같다.

버퍼사이즈를 외부에서 정의하기 때문에 코드에서는 BUFFER_SIZE라는 변수를 별도의 선언 없이 바로 사용할 수 있다.

gcc -Wall -Wextra -Werror -D BUFFER_SIZE=32 get_next_line.c get_next_line_utils.c

 

 

정적변수(Static Variable)

함수 내에서 사용하는 변수로서 지역변수, 전역변수, 정적변수가 있다. 본 과제에서는 정적변수를 사용할 필요가 있다. 

get_next_line은 버퍼사이즈 단위로 파일을 읽고, line변수에 한 줄에 해당하는 문자열을 동적할당해야 한다. (한 줄이 얼마나 길지 모르기 때문에 동적할당)

한 줄이 완성되기까지 문자열을 보관할 필요가 있으며, 개행문자 ‘\n’을 기준으로 이후의 문자열도 보관해야 한다.

따라서 함수 안에서 값을 유지시키는 static 변수를 사용한다. 

위의 과정을 파일 디스크립터별로 한다면 OPEN_MAX만큼 관리하기 위해서 포인터 배열 변수를 선언해야할 것이다.

 

한편 static 변수는 스택이 아닌 데이터 영역에 저장되는데, 경우에 따라 스택오버플로우를 피하는 용도로도 사용될 수 있다.

정적변수에 대한 자세한 내용은 이 포스팅에 별도로 정리했다.

정적변수는 메모리의 데이터(Data) 영역에 저장되는데(비초기화시 BSS 영역에 저장), 그 이유를 이 포스팅에서 더 자세히 다뤘다. 

 

 

GNL Tester

내가 사용했던 테스터들은 아래와 같다.

 

 

GNL Convention

 

 

추가 Tips

  • 과제에서 읽는 파일에 NUL 문자('\0')는 없다고 간주한다. subject PDF에 binary file일 때 undefined behavior 라고 써있는데 NUL 문자가 들어있는 파일은 binary file이므로 신경 안 써도 된다.
  • bonus part의 점수를 받으려면 정적 변수를 한 개만 선언해야 한다. 테스터 중 Get_Next_Line_Tester은 두 개 이상의 static 변수를 선언 했을 시에 CHECK ONE STATIC VARIABLE : KO를 출력한다. 
    -> 나의 경우 여러 정적 변수를 사용했다가 뒤늦게 발견해서, 구조체 자료형으로 변경한 뒤 하나의 static 변수만 선언했다.
  • mandatory part의 get_next_line.c가 bonus part까지 충족하게 코드를 작성하고 get_next_line_bonus.c로 복사해도 된다. bonus part는 get_next_line_bonus.h를 include 해야함을 주의하자. 처음부터 bonus part를 충족하게끔 여러 FD를 사용하도록, static 변수를 한 개만 사용도록 짜는게 편할 수도 있다. 

 


출처

42Seoul의 get_next_line

hi, daehyunlee velog.io/@hidaehyunlee/GetNextLine-%EC%82%BD%EC%A7%88%EC%9D%98-%EA%B8%B0%EB%A1%9D

SEOHLOG ohseyong.github.io/study/Get-Next-Line/

 

open()

FF 포럼 http://forum.falinux.com/zbxe/index.php?document_srl=408448&mid=C_LIB

open()과 fopen()의 차이

티스토리 랄라라 https://unabated.tistory.com/entry/fopen-open-차이 

read()

FF 포럼 http://forum.falinux.com/zbxe/index.php?mid=C_LIB&document_srl=466628

snowdeer.github.io/c++/2017/09/10/linux-control-file-as-low-level/

 

fgets()

KLDP 프로그래밍 QnA kldp.org/node/156440

 

버퍼 개념

네이버블로그 else if goodjob blog.naver.com/PostView.nhn?blogId=paydma&logNo=50179473286&proxyReferer=https:%2F%2Fwww.google.com%2F

 

gcc -D 플래그

네이버블로그 NeoWizard blog.naver.com/beyondlegend/110029042755

댓글