get_next_line


get_next_line 목적은 파일 디스크립터로부터 읽혀진, 개행으로 끝나는 한 줄을 반환하는 함수를 코드화 하는 것입니다.

이번 과제는 처음부터 bonus기반으로 작성하였습니다.

  • norm규칙을 준수할 것
  • 뮬리넷을 통과해야지 컴파일 된다.
  • 메모리 관련 누수는 허락하지 않는다.
  • 전역변수사용 금지
  • libft는 프로젝트에 사용금지(따로 utils.c 에 작성)

get_next_line의 함수를 반복문내에서 호출하게 되면 한 번에 한줄씩 읽으며 EOF를 만다면 종료하게 된다.

만약 문자열 한줄을 읽어 반환 할때 읽어 올 것이 없거나 에러가 발생한다면 null을 반환환다.

lseek함수는 사용금지..! 따라서 파일 읽기는 한번만 행해져야 한다.

자세한 문제 설명은 아래에서!

시작하기 전 필요한 정보들

  1. 시스템콜에 대한 전박적인 이해가 필요!
  2. 이 문제는 메모리에 대한 공부에 도움이 될 수 있도록 설계되었다.. 따라서 메모리 관련 공부!
  3. 문제가 최근에 변형되었으니 확인하고 공부할 것!
  4. 문제 자체에 대한이해가 어려울 수 있음 (나만?)
섹션 분류
1 실행 가능한 프로그램이나 셸 명령어
2 시스템 콜
3 라이브러리 함수
4 특별한 파일들(디바이스 파일)
5 파일 포맷
6 게임
7 규격 등
8 시스템 관리용 명렁어

파일디스크립터나 시스템콜 정보

파일 디스크럽터


앞서 fd(파일 디스크럽터)에 대한 정보를 설명했지만 한번 더 기재한다.(libft)

파일 디스크립터(File Descriptor)란 리눅스 혹은 유닉스 계열의 시스템에서 프로세스가 파일을 다룰때 사용하는 개념으로 일종의 파일에 접근하는 키로 생각할 수 있다.

파일 디스크립터는 0이 아닌 정수값을 가지며 할당 시에는 이미 할당된 값을 제외한 가장 작은 값을 할당한다.

사실 유닉스 시스템에서는 모든 것을 파일이라고 한다.(파이프,소켓,디렉토리..)
자세한 내용은 상단의 유닉스 정리를 참고

프로그램이 프로세스로 메모리에서 실행될 때 기본적으로 할당되는 표준입력, 표준 출력, 표준 에러출력으로 나뉘고 0,1,2의 값이 할당된다.

시스템 콜

시스템 콜
시스템콜에대한 설명

사용하게 되는 시스템 콜 함수는 open, read, write를 사용한다.

open()함수는 fcnt.h에 정의 되어 있지만 write(), read(), close()는 unistd.h에 정의되어 있다.

  • open

int open(const char *filepath, int flag);
int open(const char *filepath, int flag, mode_t mode);
  • const char *filepath 열고자 하는 파일의 경로를 적는다.

    예시 “test.txt”

  • int flag 파일을 open할 때 사용할 옵션을 넣는다.(OR의 경우 |를 사용하여 구분한다.)
flag 설명
O_RDONLY 읽기 모드
O_WRONLY 쓰기 모드
O_RDWR 읽기/쓰기 모드
O_CREAT 파일 생성
O_APPEND 파일 쓰기모드로 열어 기존 파일의 맨 뒤에서 부터 이어쓰기
O_TRUNC 파일 초기화

성공시 해당 파일 디스크립터를 반환, 실패 시 -1값을 반환

  • mode_t mode사용을 잘하지 않지만 파일을 생성할 때의 권한을 주는 옵션!

심볼릭 상수로 사용하거나 리눅스 권한설정과 똑같이 0777로 권한을 줄 수 있다.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    int fd;
    if((fd = open("testtxt.txt", O_WRONLY)) < 0){
        printf("error");
    }
    write(fd, "fkdl\n", 5);
    return 0;
}

해당 경로에 있는 txt파일이 open이 성공하면 해당 파일디스크립터로 접근하여 해당 파일을 수정할 수 있다.
컴퓨터 환경에 따라 다르겠지만 표준 출력,입력,에러를 제외한 다음 가장 작은값인 3이 들어간걸 확인 할 수 있다.

  • read

ssize_t read (int fd, void *buf, size_t nbytes);

read함수는 해당 파일 fd를 nbytes만큼 읽어서 buf에 저장하는 함수이다.

반환값은 읽어들인 데이터의 길이를 반환하고 실패한 경우 똑같이 -1를 반환한다.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFF_SIZE 1024
int main()
{
    int fd;
    char buff[BUFF_SIZE];
    if((fd = open("testtxt.txt", O_RDWR)) < 0){
        printf("error");
    }
    read(fd, buff, BUFF_SIZE);
    puts(buff);
    close(fd);
    return 0;
}

read()는 파일의 끝을 알려주기 위해 0을 반환하는데 이는 더이상 읽은 바이트가 없는 경우에 해당되며 에러로 처리하지 않는다.

read함수의 경우 여러번 실행하는 경우 우리가 생각하기에 당연하게 파일의 맨 처음부분부터 읽어들인다고 생각되지만 오프셋의 개념이 적용되기에 read함수는 읽었던 다음부터 연속적으로 읽어들인다.

즉, 마지막 읽었던 위치부터 읽기 때문에 따로 해당 사이즈만큼 증가 시킬 필요가 없다.

앞서 다룬 파일디스크립터의 확장개념이지만 fd값마다 fdTable에는 해당 오프셋이 저장되어 각각의 저장위치를 다룰 수 있다.

  • write

size_t write(int fd, const void *buf, size_t nbytes);

write함수는 fd로 접근하여 buf에 있는 데이터를 nbytes만큼 쓴다(흘려보낸다).

마찬가지로 데이터의 형태 즉, 리눅스 개념으로 어떤 파일이 들어 올지 모르고 해당 파일을 수정하면 안되기 때문에 const void한정자가 붙는다.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFF_SIZE 1024
int main()
{
    int fd, n;
    char buff[BUFF_SIZE];
    if((fd = open("testtxt.txt", O_RDWR)) < 0){
        printf("error");
    }
    n = read(0, buff, BUFF_SIZE);
    write(fd, buff, n);
    return 0;
}

사용자 표준 입력 파일 디스크립터(0)로 접근하여 사용자의 입력값을 받고 반환값으로 길이를 반환한다.

해당 길이만큼 write함수를 통해 fd에 작성

  • close

파일을 open했다면 close는 필수!

EOF


EOF란 End Of File의 약어이며 대체적으로 -1의 값을 가진다.

말 그대로 파일의 끝을 말하며 운영체제마다 파일의 끝을 알아내는 방법은 다르지만 c언어에서는 운영체제와 상관없이 EOF를 반환한다.

유닉스 시스템에서는 Ctrl + D를 누르면 EOF를 발생시킬 수 있고 윈도우명령차에서는 Ctrl + Z를 누르고 Enter를 통해 발생시킬 수 있다.

#include <stdio.h>
int main()
{
    char ch;
    printf("EOF체크");
    while((ch = getchar()))
    {
        if(ch == EOF)
        {
            printf("EOF눌림");
            break;
        }
        putchar(ch);
    }
    return 0;
}

줄넘김과 EOF는 다르다!

static


static키워드는 변수나 함수앞에 사용되게 되는데 이름 그대로 정적의 성격을 가지게 된다.

앞서 libft에서 다룬 정적영역/정적할당/동적영역/동적할당의 개념을 제대로 이해하고 있다면 어렵지 않게 이해 가능하다.

staic은 정적영역(데이터 영역)에 정적할당되는 개념으로 프로그램의 시작과 동시에 할당되고 프로그램이 종료되면 메모리에서 사라진다.

그렇다면 전역변수와 다른 점이 뭘까?

static은 범위가 지정되어 있고 해당 영역밖에서는 접근이 불가능하다.

따라서 전역변수보단 좁은 의미로 바라볼 수 있고 해당 함수내에서는 지속적으로 접근 가능하다.

따라서 정적.전역.변수로 선언한 경우에는 해당 영역을 .c로 제한한 것이기 때문에 다른 파일에서 extern키워드로 접근 불가능하다.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
void count()
{
    static int num = 0;
    printf("%d\n", num);
    num++;
}
int main()
{
    count();
    count();
    count();
    return 0;
}

static변수의 경우 자동으로 초기화 되지만 명시적으로 초기화를 써주는 것이 좋다

해당 코드는 static키워드가 없다면 동적변수이기 때문에 0이 연속적으로 출력되어야 하지만 static키워드로 정적 선언했기 때문에 프로그램이 시작되는 순간(컴파일 단계)부터 종료시점까지 메모리가 잡혀있다.

동적변수?

동적 변수란, 프로그램 시작과 동시에 변수 선언부분 ex)int a = 4;를 만나면 해당 메모리가 스택영역(동적영역)에 자동으로 정적할당되며 자동으로 해제된다.

  • 정적할당인 이유..?

애초에 코드자체가 하드코딩으로 메모리크기를 명시하기 때문에 컴파일시 해당 메모리의 크기가 결정되는 것.

동적할당과 다르게 메모리 릭(누수)와 같은 걱정은 필요없다.

조금 길어진 감이 없지않지만.. 좀더 정리된 내용은 libft의 메모리 영역? 할당?부분을 참고!

메모리 영역? 할당?

이번 과제에서 활용되는 방식은 단일연결리스트 형식으로 풀어 head에 해당하는 노드를 static으로 선언하여 활용

gcc -D


gcc -D는 전처리기에서 사용될 매크로를 정의하는 플래그이다..!

# <- 이걸 전처리기라고 하는데 전처리기에 사용되는 값을 사전에 정의할 수 있다.

#include <stdio.h>

int main()
{
	printf("GCC -D 체크\n");

	printf("%d", BUFFER_SIZE);
	
	return 0;
}

다음코드에서 gcc -D BUFFER_SIZE=42 test.c으로 컴파일하게 되면 BUFFER_SIZE에 값을 설정할 수 있다.

42출력

버퍼(buffer)


버퍼란.. 스트림으로 연결된 통로에 실어보내는 마차같은 개념으로 생각한다.(임시메모리공간)

임시로 사용되는 공간같은 느낌으로 사용되는데 버퍼에 정보를 넣다가 가득 차게 되면 스트림으로 흘려보낸다.

따라서 버퍼는 키보드입력시 바로바로 출력되는 기능보다는 버퍼크기만큼 채우고 연속된 입력을 처리하지 않고 성능을 높이는 용도로 사용된다.

따라서 사용여부를 잘 판단하여 넣어야한다.

  • 완전 버퍼링: 버퍼의 크기만큼 차게되면 버퍼에 담겨있는 내용을 전달하는 것(파일 입출력)
  • 라인 버퍼링: 키보드입력시 자주 사용되며 개행문자를 만나는 경우 목적지로 가게된다.

지금까지 우리가 사용한 입출력함수들 또한 내부적으로 버퍼를 사용하여 데이터를 처리한다.

이번 과제에서는 버퍼 크기가 유동적이기 때문에 임시로 버퍼만큼 반복해서 읽는 과정이 필수적이다.

단일연결리스트


단일연결리스트란 노드(구조체)로 이루어진 리스트형 자료형인데, 배열과 다른 점은 각 노드들이 다음 노드의 주소를 가지고 있는 형태이기 때문에 값에 접근할 때 순차적으로 접근한다는 차이점이 있다.

장점으로는 구조체이기 때문에 요소를 추가하기 간편하고 값 삭제 추가에 용이하다는 특징이 있다.

그에 반해 배열은 값을 순회하는데 있어서 이점이 크지만 삭제나 추가에 단점이 있다.

단일연결리스트로 구현한 이유는 bonus의 문제를 해결하기 위해 들어오는 값에 대한 fd값으로 즉, key를 설정하여 값을 찾는다.

해당 노드는 버퍼만큼 짤린 문자열을 저장하고 있어 언제든지 쉽게 접근할 수 있다.

OPEN_MAX < fd일 경우 작동되지 않기 때문에 연결리스트로 구현해야한다.

이중포인터


앞서 설명한 const char *와 마찬가지로 이중포인터라고 해서 다른 점이 크게 있는 것이 아니다..!

이중 포인터라고 함은 똑같이 메모리 공간의 주소를 가리키지만 포인터의 주소를 가리킨다는 점이 다르다.

int a = 10;
int *p = &a;
int **dp = &p;

구현 방법

이 과제를 접했을 때 이해하는 부분이 가장 오래걸린 것 같다.

가장 쉽게 풀어서 설명하자면 이 함수의 목적은 정말 말 그대로 한줄을 읽어서 반환하는 것!

어려웠던 이유는 읽는 과정에서의 조건이 붙기 때문이다.

(처음부터 보너스로 풀려고해서..?)

  1. 한줄을 읽을 때 정해진 버퍼 사이즈만큼 읽어서 딱 알맞게 한줄을 반환 (동적할당이 필수)
    • 다시 함수 호출 시 똑같은 한줄이 아닌 다음줄 읽음(read의 기본특성이지만 버퍼사이즈만큼 읽기 때문에 줄넘김을 넘어가는 문제가 생김)
  2. 반복문안에서 이 함수를 실행시켰을 경우 EOF를 만나거나 에러가 나면 종료
    • 종료가 된다면 올바르게 free하여 메모리를 비워야함
  3. 파일디스크립터와 스트림, 커널, 버퍼등 전반적인 이해 필요
  4. 제일 어려웠던 버퍼 사이즈에 짤린 문자열 처리…

사용한 util함수

char	*ft_strjoin(char *s1, char *s2)

libft의 strjoin함수를 사용했다 gnl에 유리한 구조로 사용하기 위해서 변형을 했는데 이 함수는 버퍼사이즈만큼 읽은 경우 읽었던 버퍼에 합치는 과정을 수행한다.

따라서 새로운 동적할당을 했을 경우 원본의 free부분이 추가됨

  • 이후 s1의 값이 null값일 경우에 예외 처리추가

내부에서 strlen, strlcpy또한 사용된다.

fd추가 및 검색 방법

bonus를 생각하고 풀었기 때문에 애초에 여러가지 fd값이 들어올 경우를 생각해 새로운 fd값, 원래 존재하는 fd값에 대한 함수가 필요했다.

여기서는 이중포인터로 해당 주소값 자체를 참조 가능하게 매개변수로 받는다.

temp노드로 fd값을 비교하며 순회한 뒤 값이 존재하지 않다면 동적할당을 통해 head에 삽입하면 된다.

버퍼사이즈 만큼 read하기

우선 인자 자체를 해당 연결리스트의 str를 받아서 해당 str에 값이 존재하는지 검사한다.

  • 함수를 이전에 실행시켜 한줄을 읽고 over된 라인은 해당 노드에 값이 저장되고 free하지 않는다. 따라서 head에 값이 연결되어 있는 모습

만약 null일 경우 버퍼를 동적할당 후 read한다 앞서 작성한 strjoin()함수로 해당 문자열 합치고 마지막에 반환한다.

반환된 문자열에서 줄넘김까지 동적할당하여 반환하게 되면 한줄 읽기 끝!

마찬가지로 반환된 문자열은 free, 반환된 문자열에서 줄넘어간 문자열은 노드에 저장!

배운점

버퍼 초기화에 관하여

버퍼는 malloc을 사용하여 초기화하게 된다면 해당 메모리에는 쓰레기 값이 존재할 수 있다.

따라서 calloc을 사용하여 비워주거나 memset을 사용하여 한번더 초기화 해주는 과정이 필요하다.

태그: ,

카테고리:

업데이트:

댓글남기기