get_next_line 구현 전 정리

sameul__choi·2022년 5월 10일
0

42Seoul 살아남기

목록 보기
1/1
post-thumbnail

Get_Next_Line

GNL 이란 ?

gnl 함수 한 번 호출에 한 줄 씩 읽어서 파일의 끝까지 읽어내는 함수

표준 입력으로도 동작해야함

int get_next_line(int fd, char **line)

이 함수의 리턴은 1, 0, -1

returndescription
1A line has been read
0EOF has been reached
-1An error happened

GNL 은 open 함수로 파일을 열어 받은 fd를 사용하여 한 줄씩 line에 저장하는 함수

  • 잘못된 파일 디스크립터나 line, buffer_size가 들어오면 에러처리 한다.
  • get_oneline에서 read로 문자열을 buffer에서 읽어온다.
  • 읽어온 문자열 중 한 줄에 해당하는 만큼 포인터 temp에 동적 할당 하고
  • 다음 줄의 첫 원소의 주소를 스태틱 포인터인 next에 할당한다.
  • get_online에서 error 발생 시엔 (read or malloc 실패) 리턴 -1 (error)
  • 포인터 temp에 동적할당한 한줄의 문자열 주소를 *line에 할당
  • get_oneline이 파일 끝까지 읽었으면 리턴 0 (end)
  • 0, -1이 아니면 리턴 1

그럼 이제부터는 GNL을 구현하기 위해 필요한 개념들에 대해서 알아보자! !

(1) 파일 디스크립터, 컴파일 옵션

FD란 ?

정한 파일에 접근하기 위한 추상적인 키를 말한다. 쉽게 말하자면 파일을 관리하기 위해 매겨놓은 숫자이다.

프로세스가 실행 중에 파일을 Open 하면 커널은 해당 프로세스의 파일 디스크립터 숫자 중에 사용하지 않는 가장 작은 값을 할당해 준다. 그 다음 프로세스가 열려있는 파일에 시스템 콜을 이용해서 접근 할 때, FD 값을 이용해 파일을 지칭 할 수 있다.

유닉스 시스템에 존재하는 모든 것은 파일이라고 한다. 일반적인 정규파일에서부터 디렉토리, 소켓, 파이프, 블록 디바이스, 캐릭터 디바이스 등 모든 객체들은 파일로써 관리된다. 유닉스 시스템에서 프로세스가 위와 같은 파일들을 접근할 때에 파일 디스크립터라는 개념을 이용한다.

  • 표준 입력(Standard Input) : File Descriptor 0
  • 표준 출력(Standard Output) : File Descriptor 1
  • 표준 에러 출력(Standard Error) : File Descriptor 2

(위 값들은 매크로로써 정의되어 있으며, unistd.h에서 확인할 수 있다.)

때문에 우리가 생성하는 FD는 3번부터 차례대로 할당받게 된다. 다시 말하자면 fd는 파일을 다루기 위해 해당 파일의 주소를 참조하여 접근하는 형태라고 보면 된다.

0 ~ OPEN_MAX(fd의 최대값) 까지의 값을 가질 수 있다. OPEN_MAX는 단일 프로그램에 허용되는 최대 열린 파일 수를 정의하는 상수이다. 각 실행환경마다 다른데 이걸 확인하려면 OPEN_MAX 값을 확인하고 싶다면, 터미널에서 getconf OPEN_MAX라고 쳐보면 그 값을 얻을 수 있다. 혹은 터미널에서 sysconf(_SC_OPEN_MAX)를 통해서도 알 수 있다.

하지만 운영체제마다 fd에 대한 설정이다르고 fd 제한과 유저에 따라 한 프로세스당 설정된 fd 제한이 각각 달라서 OPEN_MAX를 너무 맹신해서는 안된다.

스토리 비유하기

(출처 : mintnlatte.tistory.com/266)
전화 한 통만 하면 필요한 논문을 복사해 주는 곳이 있다. 그리고 그곳의 단골손님 영수가 있다. 그런데 이 녀석은 매번 똑같은 논문의 일부분을 복사해 달라고 한다. “아저씨~ ‘고도의 정보화 사회가 되어 가면서, 인간의 삶의 질과 관계된 문제들이 점점 더 그 중요성이 더해짐에 따라 감각, 지각, 사고, 성격, 지능, 적성 등의 인간적 특징들이 고려됐을 때의 인간의 원리에 대한 연구’ 라는 논문 26쪽부터 30쪽까지 복사해 주세요” 이 녀석은 보통 이런 식으로 하루에도 여러 번 주문을 한다. 설상가상으로 말하는 속도도 느린 편이다. 그래서 아저씨가 말씀하시길 “그 논문은 이제부터 너의 18번이다! 그냥 저의 18번 논문 26쪽부터 30쪽까지 복사해 주세요 라고 해라!”영수는 그 이후로도 최소 50자가 넘는 제목의 논문만 복사 주문을 한다. 그 때 마다 아저씨는 논문에 새로운 번호를 할당해 준다(중복되지 않는). 그래야 영수와의 대화 속에서 스트레스를 덜 받을 수 있기 때문이다.

여기서 아저씨는 시스템이고, 영수는 개발자를 의미한다. 그리고 숫자는 파일 디스크립터이고, 논문은 소켓이나 파일을 의미한다. 파일(혹은 소켓)을 생성할 때마다 시스템은 그러한 숫자를 생성해서 건네줄 것이다. 그것이 시스템과 개발자가 편하게 대화하는 방법이 된다.

결국 파일 디스크립터란 시스템이 만들어 놓은 것을 가리키기 좋게 하기 위해 시스템이 우리들에게 건네주는 숫자에 지나지 않는다. 쉽게 말하자면 fd는 번호표이다.

그럼 파일디스크립터를 확인해보자

open(pathname, flag, mode) //return fd

open함수는 pathname이 가르키는 파일을 열고, 열린 파일을 이후 호출에서 참조할 때 사용하는 파일 디스크립터를 return 한다. 현재 가용 가능한 숫자 중 가장 작은 값으로.

file.cfile.txt 파일을 생성한 뒤 프로세스가 파일 열때 얻는 fd를 출력해보자.

// file.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
	int fd;

	fd = open("test.txt", O_RDONLY);
	if (fd < 1)
	{
		printf("open() error");
		exit(1);
	}
	printf("FD : %d\n", fd);
	close(fd);
	return (0);
}

결과는 3이 나왔다. 여기서의 3은 어떤 의미일까 ? 아래의 그림을 보면 정수값으로 된 파일 디스크립터를 통하여 원하는 파일에 접근하는지 이해할 수 있다.

fd는 프로세스가 유지하고 있는 fd table의 인덱스이다. 그림에서 보듯이 우리는 3을 이용하여 fd table의 3번째 인덱스로 접근하고 해당 칸이 가르키고 있는 file table로 가서 원하는 행동을 할 수 있다.

  • fd table의 각 칸들은 fd FlagFile table pointer를 가지고 있다.

  • file table의 각 칸들은 modeinode Table Pinteroffset을 가지고 있다.

    • offset -> 컴퓨터 과학에서 배열이나 자료 구조 오브젝트 내의 오프셋은 일반적으로 동일 오브젝트 안에서 오브젝트 처음부터 주어진 요소나 지점까지의 변위차를 나타내는 정수형이다.
  • inode Table은 소유자 그룹, 접근 모드(읽기, 쓰기, 실행 권한), 파일 형태, 고유 번호 (inode number, i-number) 등 해당 파일에 관한 정보를 가지고 있다.

    • inode -> 파일을 기술하는 디스크 상의 데이터 구조로서 파일의 데이터 블록이 디스크 상의 어느 주소에 위치하고 있는가와 같은 파일에 대한 중요한 정보를 갖고 있다. 각각의 inode들은 고유 번호(inode number)를 가지고 있어서 파일을 식별할때 사용한다. 터미널에서 ls -i 옵션으로 inode number를 확인할 수 있다.

    근데 여기서 잠깐 파일 디스크립터는 복제할 수 있을까 ?

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
	int fd;
	int fd2;

	fd = open("test.txt", O_RDONLY);
	fd2 = open("test.txt", O_RDONLY);
	if (fd < 1 || fd2 < 1)
	{
		printf("open() error");
		exit(1);
	}
	printf("fd\t: %d\n", fd);
	printf("fd2\t: %d\n", fd2);

	printf("fd2 = dup(fd)\n");
	fd2 = dup(fd); //dup() : 파일 디스크립터 복제 함수.

	printf("fd\t: %d\n", fd);
	printf("fd2\t: %d\n", fd2);

	close(fd);
	close(fd2);
	return (0);
}

결과를 보니 기존 fd는 3을 fd2는 4를 할당받았다. dup을 활용하여 fd를 복사한 뒤 fd2에 넣었더니 fd2가 5가 되었다. 즉 복제하면 새로운 fd를 생성한다고 생각하면 된다.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
	int fd;
	int fd2;

	fd = open("test.txt", O_RDONLY);
	fd2 = open("test.txt", O_RDONLY);
	if (fd < 1 || fd2 < 1)
	{
		printf("open() error");
		exit(1);
	}
	printf("fd\t: %d\n", fd);
	printf("fd2\t: %d\n", fd2);

	printf("fd2 = dup(fd)\n");
	fd2 = dup(fd); //dup() : 파일 디스크립터 복제 함수.

	printf("fd\t: %d\n", fd);
	printf("fd2\t: %d\n", fd2);

	close(fd);
	close(fd2);
	return (0);
}

malloc / free

#include <stdlib.h>

//void *malloc(size_t size);

int *ip;
ip = (int *)malloc(sizeof(int) * 5);

//void free(void *ptr);
free(ip);

malloc 함수는 메모리의 크기 바이트를 할당하고 할당 된 메모리에 대하나 포인터를 반환한다.

free 함수는 ptr이 가르키는 메모리 할당을 해제한다 ptr이 NULL이면 작업이 수행되지 않는다.

포인터가 여전히 해제된 메모리 영역을 가르키고 있는 것으로 인해 다양한 문제를 야기할 수 있기에 메모리 해제후에 포인터를 NULL로 설정해야 한다.

(2) File Control을 위한 함수

1) open, create, close

Linux, Unix 계열의 시스템에서 Process(프로세스)가 File(파일)을 열 때 open 함수 혹은 openat 함수를 사용할 수 있다.

#include <fcntl.h> // open 함수가 있는 헤더파일이다.

int open(const char *pathname, int flag);

int open(const char *pathname, int flag, mode_t mode);

int openat(int dirfd, const char *pathname, int flag);

int openat(int dirfd, const char *pathname, int flag, mode_t mode);

매개 변수로 File Path(절대경로 or 상대경로), flag, mode 등을 받고 File Descriptor(fd)값을 반환한다. 이때 에러가 나면 -1을 반환한다.

즉, open 관련 함수는 path에 명시된 파일을 flag에서 설정한 모드로 열어서 파일 디스크립터를 반환하는 함수이다.

openat 함수와 open 함수의 차이

open은 해당 경로의 파일을 flag 옵션을 적용하여 FD Table의 인덱스인 FD를 리턴한다면, openat은 open과 동일한 작업을 수행하지만 dirfd값을 추가로 받아 경로를 이용하는 방식에 open과 차이가 있다.

openat 함수가 도입된 데에는 다음과 같은 문제를 해결하기 위함이다.

  • 멀티 쓰레드 환경에서 상대 경로를 다루기 쉽게 해준다. 같은 프로세스에 있는 쓰레드들은 CWD(Current Working Directory)를 공유한다.

  • TOCTTOU(time-of-check-to-time-of-use) 문제를 해결하기 위해서 사용된다.

openat 함수는?

관건은 path가 절대 경로인지 상대 경로인지에 따라 달라진다.

  1. 경로로 주어진 path인자가 절대 경로라면 dirfd는 무시된다.

  2. 경로로 주어진 path인자가 상대 경로라면, FD Table의 dirfd 인덱스에 해당하는 항목을 찾아 나온 디렉토리를 기준으로 path를 붙여 찾아간다.

  3. 경로로 주어진 path인자가 상대 경로라면, dirfd 값이 현재 작업 디렉토리를 의미하는 (Current Working Directory) AT_FDCWD로 되어 있다면 현재 디렉토리를 기준으로 path를 붙여 찾아간다.

    이 때, dirfd의 AT_FDCWD라는 특수 값이 존재하며 이 때는 open과 동일하게 동작한다. (open 역시 CWD 기준으로 동작함)

    (openat의 매개 변수 dirfd의 이름에서도 유추할 수 있지만, 현재 FD Table에 기록되어 있는 디렉토리를 이용하기 위함이므로 해당 디렉토리의 fd를 이용한다. 매개 변수로 사용하는 dirfd 값을 얻기 위해 디렉토리 구조체를 인자로 사용하는 int dirfd(DIR *dirp) 함수를 주로 이용한다.)

열기 옵션설명
O_RDONLY읽기 전용응로 열기
O_WRONLY쓰기 전용으로 열기
O_RDWR읽기와 쓰기가 모두 가능
O_CREAT해당 파일이 없으면 생성합니다. O_CREAT E 가 아니라 끝에 E가 없는 O_CREAT 입니다. O_CREAT로 파일을 생성하게 된다면 파일의 접근권한을 지정하기 위해 접근 권한 값을 추가해야 합니다. open( "jwmx", O_WRONLY
O_EXCLO_CREAT를 사용했을 때, 파일이 이미 있어도 열기가 가능하여 쓰기를 하면 이전 내용이 사라집니다. O_CREAT를 사용할 때, O_EXCL를 함께 사용하면, 이미 파일이 있을 때에는 open() 되지 않아 이전 파일을 보존할 수 있습니다. fd = open( "./test.txt", O_WRONLY
O_TRUNC기존의 파일 내용을 모두 삭제합니다.
O_APPEND파일을 추가하여 쓰기가 되도록 open 후에 쓰기 포인터가 파일의 끝에 위치하게 됩니다.
O_NOCITTY열기 대상이 터미널일 경우, 이 터미널이 플로그램의 제어 터미널로 할당하지 않습니다.
O_NONBLOCK읽을 내용이 없을 때에는 읽을 내용이 있을 때까지 기다리지 않고 바로 복귀합니다.
O_SYNC쓰기를 할 때, 실제 쓰기가 완료될 때 까지 기다립니다. 즉, 물리적으로 쓰기가 완료되어야 복귀하게 됩니다.

create 함수

새로운 파일 생성은 creat 함수를 이용할 수 있다.

#include <fcntl.h> 
int creat(const char *path, mode_t mode);
  • open 함수와 마찬가지로 성공하면 File Descriptor(fd)를, 실패하면 -1을 반환한다.

  • creat 함수는 open 함수로도 구현할 수 있다.

open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); // mode는 생성될 파일의 퍼미션 정보!

creat 함수의 최대 단점은 write 모드로만 열린다는 것이다. 다시 읽기 위해서는 creat 함수로 파일을 만든 후, close 함수로 닫고 O_RDONLY로 읽는 과정이 필요한 것이다. 따라서 아래의 코드처럼 사용하는 것이 더 좋은 선택일 수 있다.

open(path, O_RDWR | O_CREAT | O_TRUNC, mode);

close 함수

open 함수로 연 파일은 close 함수로 닫을 수 있다.

#include <unistd.h> 
int close(int fd);

정상적으로 종료되면 0을, 실패하면 -1을 반환한다.

파일을 닫으면, 프로세스가 파일에 설정했던 Record Lock(레코드 잠금)도 자동으로 잠금 해제된다. 또 한 프로세스가 종료되면 프로세스가 열어놨던 파일들은 close 함수로 닫기게 된다.

Record Lock(레코드 잠금)이란?

멀티 쓰레드 프로그램에서, 여러 쓰레드가 하나의 파일에 동시에 접근할 경우 파일 잠금이 필요할 수 있다. 한번에 하나의 쓰레드만 파일에 읽기및 쓰기를 해야 하는 경우가 있을 수 있기 때문이다.

레코드 잠금에 대한 자세한 내용은 여기를 참고하면 된다. 요약하자면 잠금이라는 행동의 주체는 운영체제이다. 잠금에 대한 기록을 운영체제에서 관리, 기록을 한다고 보면 된다. (Record Lock 자체는 fcntl.h의 fcntl 함수를 통해 이뤄지며, 이 때 flock이라고 하는 잠금을 위한 구조체를 이용한다.)

fcntl은 file control의 줄임말이다! 추가적인 정보는 여기를 참고하면 된다.

2) Read 함수

#include <unistd.h>

ssize_t read (int fd, void *buf, size_t nbytes)
  • int fd : 파일 디스크립터
  • void *buf : 파일을 읽어 들일 버퍼
  • ssize_t nbytes : 버퍼의 크기
  • 반환 : 실패 시 -1, 성공하면 읽어들인 바이트 수

size_t란?

size를 나타내기 위한 type으로 보통의 32 bit machine에서는 32 bit, 즉 unsigned int로 되어있다. 가장 유명한 sizeof라는 연산자가 반환하는 값을 담기 위한 type으로 보면 되는데 이 역시 크기를 의미하므로 많은 I/O 함수에서 사용된다. ssize_t는 signed size type으로 보통의 32 bit machine에서는 간단히 말해 int다. I/O 함수의 반환값으로 많이 사용되는데 그 이유는 해당 IO 함수의 실패를 알려주기 위해서이다. 따라서 수행도중 오류가 발생했을 경우 -1을 반환하면서 해당 I/O 함수의 실패를 알려줄 수 있다. 추가적인 내용은 여기서 확인할 수 있다.

참고한 사이트

https://80000coding.oopy.io/4d3eba5f-5d2d-4bec-b0a2-fa058d67c643
https://go-it.tistory.com/3
https://dev-ahn.tistory.com/96
https://lowlevel.tistory.com/3
https://www.cyberciti.biz/faq/linux-increase-the-maximum-number-of-open-files/

0개의 댓글