GET_NEXT_LINE - Bonus (포인터 배열)
GET_NEXT_LINE - Bonus (포인터 배열)
개요
get_next_line은 하나의 fd만 읽을 수 있었습니다. 보너스 파트에서는 여러 개의 fd가 들어와도 전부 읽을 수 있어야 합니다. 이전 과제를 잘 풀어냈고 포인터 배열의 개념을 알게 되면 매우 간단히 풀어낼 수 있습니다.
아마 대부분의 보너스 시도는 포인터 배열로 시작할 거로 생각됩니다. 운이 나쁘면 동료 평가에서 크게 문제가 될 수 있는 부분을 맞닥뜨리게 될 것이고, 보너스를 포기하거나 다른 방법으로 과제를 또 풀어야 할 수도 있습니다.
포인터 배열의 풀이를 보고 어떤 부분이 문제가 되는지를 중점으로 제 생각을 풀어나가며 연결리스트의 풀이 방식으로 다시 돌아오겠습니다.
목표
Develop get_next_line() using only one static variable.
정적 변수를 하나만 사용하여 get_next_line()을 개발하세요.
Your get_next_line() can manage multiple file descriptors at the same time. For example, if you can read from the file descriptors 3, 4 and 5, you should be able to read from a different fd per call without losing the reading thread of each file descriptor or returning a line from another fd.It means that you should be able to call get_next_line() to read from fd 3, then fd 4, then 5, then once agin 3, once again 4, and so forth.
당신의 get_next_line()이 여러 개의 파일 descriptor를 한번에 관리할수 있어야 합니다. 예를 들어, 파일 디스크립터 3, 4, 5에 접근 가능한 경우, descriptor나 다른 줄에서의 fd의 reading thread를 잃지 않은 채로 각 호출당 다른 fd를 읽을수 있어야 합니다.이는 get_next_line()을 호출하여 fd 3, fd 4, 다음에 5 그 다음에 다시 3, 4, 등등을 읽을 수 있어야 합니다.
구현 전 사전 지식
1. 포인터 배열
2. fd의 최댓값
1. 포인터 배열
포인터 배열은 배열 포인터와 다릅니다. 만약 이 두 가지 개념을 혼용하고 있었다면 바로 잡으시길 바랍니다. 이전 과제에서는 포인터 하나로 문자열을 기억했습니다. 그럼 fd가 여러개라면? 그만큼의 포인터를 만들어 내면 됩니다!
2. fd의 최댓값
그럼 fd를 몇개까지 만들어야 할까요? 이것에 대해서는 하단의 Defense에서 다루도록 하겠습니다.
구현
pseudo code
char *get_next_line(int fd)
{
문자열이 저장될 포인터 배열선언 //backup[몇개의 포인터를 만들 것인가?]
읽을 문자열을 가리킬 포인터 선언 //buff
반환할 문자열을 가리킬 포인터 선언 //result
if 동작할 수 없는 조건을 확인한다.
널을 반환한다.
읽을 글자수 + 1 만큼 버퍼를 할당한다.
파일을 읽는 함수를 실행한다.
버퍼를 해제한다.
한 줄이 완성되었으면 반환한다.
}
이전 mandatory와 크게 다르지 않습니다 static 포인터로 선언했던 backup을 포인터 배열로 선언하면 됩니다. 이제 backup은 배열이며, 배열의 요소들은 포인터 입니다. 인자로 전달받은 fd값을 배열의 인덱스로 활용하여 fd당 하나의 포인터를 갖게 구현할 수 있습니다.
동료 평가
1. 포인터 배열의 개념을 설명할 수 있어야 합니다.
2. 각각의 fd가 포인터 배열과 어떻게 상호작용하는지 설명할 수 있어야 합니다.
Defense
포인터 배열이 문제가 되는 이유는 fd값이 무엇이 들어올지 모른다는 것입니다. 포인터 배열로 구현한 함수는 fd값을 몇 개나 받을지 고려할 수 없습니다. 따라서 우리는 fd의 최댓값(파일을 최대로 열 수 있는 개수)을 찾게 될 것이고, 높은 확률로 OPEN_MAX(limits.h)를 알게 될겁니다. OPEN_MAX는 파일 디스크립터의 최댓값으로써 fd는 OPEN_MAX를 넘을 수 없고, 그 이상 파일을 열려고 시도하면 fd는 -1값을 갖게 됩니다. 따라서 헤더에 limits.h를 선언하고 OPEN_MAX를 배열의 개수로 정하여 풀었을 겁니다.
하지만, 이 fd의 최댓값은 운영체제에 따라 다를 수 있습니다. 또한 OPEN_MAX는 실제로 열 수 있는 파일의 개수를 정확하게 나타내는 것도 아니며, 사용자가 최댓값을 임의로 조정할 수 있습니다. 더구나 OPEN_MAX값이 정해져 있지 않은 환경도 있습니다.
또한, fd는 제한된 범위 내에서 임의로 설정할 수 있습니다. 이때 우리는 구경도 못한 과제에서 등장하는 dup2 함수를 듣게 됩니다.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("test.txt", O_RDWR);
dup2(fd, 455);
write(455, "123", 3);
return (0);
}
현재 파일을 하나 열었으니 fd는 값은 3입니다. dup2 함수를 이용해서 생뚱맞은 int값으로 write 함수를 실행하면 정말 test.txt파일에 "123"이 써집니다. 시스템에 설정되어 있는 fd의 최대값을 넘지만 않으면 fd값은 음수만 아니라면 무엇이든 될 수 있습니다. 심지어 0으로 실행해봐도 써지는 것을 확인할 수 있습니다!
-(추가).
pipex의 과제를 하면서 클러스터의 일부 아이맥 환경에서는 dup2() 함수의 두 번째 인자로, 설정된 fd값이 들어가야만 올바르게 동작함을 확인 했습니다.
예를 들어 파일 두 개를 open() 했을 때, fd는 각각 3과 4로 정해질 것입니다. 이때 dup2(fd1(3), fd2(4)) 함수를 실행하면 fd2에 write()를 사용하여도 fd1 파일에 출력이 됩니다.
그래서 파일을 새롭게 오픈할 때 마다 노드를 추가해서 파일의 정보를 담는 것이 올바른 풀이라고 포인터 배열을 무조건 터트리는 방법이 있다며 풀이를 하나로만 귀결시키는 사람들이 있는데 정말 그러지 마시길 바랍니다.
그럼 어떻게 디펜스할 수 있을까요?
먼저, OPEN_MAX의 취약점은 인정할 수밖에 없습니다. 함수를 사용하는 환경마다 결과가 달라질 수 있다는 것은 좋지 못한 코드인 것도 인정해야 합니다.
중요한 것은, gnl과제를 풀어내는 학습자 관점에서 평가를 진행해야 하는 것이 옳습니다. 자신은 해당 과제를 끝마쳤다고 과제와 관련 없는 개념을 끌고 와 괴롭히는 것은 좋지 못한 행위입니다. 정도가 지나칩니다.
dup2 함수를 사용하지 않았더라도 파일을 많이 열면 언젠가는 OPEN_MAX를 넘을 것입니다. dup2를 사용하면 가뿐히 넘을 것이구요. 하지만 그 단위가 적게는 천, 많게는 몇만 단위입니다. "누가 파일을 몇천에서 몇만 개를 열겠냐"고 말할 수 있습니다. 또한, 파일 디스크립터 값은 운영체제에서 파일을 관리하기 위한 고유 식별자입니다. 이 디스크립터 값을 임의로 바꾸는 것은 문제가 될 수 있는 것으로, 권장되지 않는 행위입니다. 따라서 평가자가 dup2를 들먹인다면 파일 디스크립터를 바꾸어야만 하는 상황 제시를 반드시 요구하셔야 합니다. 그리고 피평가자의 수준과 입장에서 충분히 납득할 수 있어야 할 것입니다.
fd가 추가될 때마다 구조체를 하나씩 생성하고 연결하는 연결리스트와는 다르게, 포인터 배열은 미리 받을 수 있는 fd 개수를 정하고 시작하기 때문에 fd의 한계치를 넘기 전까지는 파일을 열면 열수록 연결 리스트보다 탐색과 생성 면에서 더 빠르게 처리할 수 있다는 장점을 가지고 있습니다. 반대로 한계치를 넘어간다면 안정성 면에서 연결리스트보다 뒤처진다는 단점이 있습니다.
마치며
포인터 배열은 풀이보다 디펜스가 중요한 부분입니다. 풀이가 하나로만 되어야 한다는 것은 스스로가 학습할 기회와 범위를 제한하는 것입니다. 두 가지 풀이에 대해 흥미가 있다면, 더욱 완벽한 디펜스를 하실 수 있을 겁니다.
다음은 연결리스트 풀이로 gnl을 마무리 하려고 합니다.