IT
-
[KOCW] 운영체제 1차시 - 강의소개2021.10.26
-
[GET_NEXT_LINE] 구현 예시2021.06.11
-
[GET_NEXT_LINE] 구현 준비2021.06.04
-
Libft 과제 시 유의점 (테스터기, protected, Makefile)2021.05.31
-
malloc/calloc 관련 정리2021.05.30
-
[Libft] C 언어 라이브러리 구현_BONUS_보너스 함수 구현22021.05.30
[KOCW] 운영체제 1차시 - 강의소개
친구와 함께 운영체제에 대한 강의를 매일 1강씩 듣고, 블로그에 정리하기로 내기를 했습니다 ㅎㅎ
패널티로 벌금 내기 싫으니까 매일 열심히 하게 되겠죠?
무료대학강의 플랫폼인 KOCW에서 유명한 강의 중 하나입니다. 매일 매일 성실히 공부해서 정리하겠습니다~
그럼 시작합니다.
운영체제 강의
- 경성대 컴퓨터공학부 / 2학년 2학기 / 학부기초 / 3학점 (강의 3, 실습 0)
- 양희재 교수
- 교재 : Silberschatz et al, Operating System Concepts with java, 8th ed, 2011
- 수업목표
1) Windows, Unix/Linux, MacOS X, IOS, ...
2) 운영체제의 정의, 역할, 역사
3) 프로세스 관리, 주기억장치 관리, 파일 관리, ...
4) 운영체제 이해부터 설계까지
15주간 수업 계획
- 1~2주 : 개요, 역사, 현대운영체제
- 3~7주 : 프로세스 관리 (스케쥴링, 동기화)
- 8~11주 : 메모리 관리 (페이징, 가상메모리)
- 12~15주 : 파일 관리 (파일할당, 디스크스케쥴링)
1, 2 Introduction : Operating System? (오퍼레이팅 시스템이란)
3 Process Management (프로세스 관리)
4 Process Synchronization (프로세스 동기화)
5 Classical Problems (전통적인 문제)
6, 7 Deadlock (결착상태 -> 어떻게 해결할 것인가?) / 중간고사
8 Main Memory Management (주기억장치 관리)
9 Paging & Segmentation
10 Virtual Memory (가상메모리 -> 어떻게 만들 것인가)
11 File Allocation (파일 할당)
12 Disk Scheduling (디스크 스캐쥴링)
13~15 Summary & Final Exam (이 과목에 대한 전체적인 정리, 복습)
'IT > CS 공부' 카테고리의 다른 글
[KOCW] 운영체제 3차시 - 운영체제 서비스 (0) | 2021.11.08 |
---|---|
[KOCW] 운영체제 3차시 - 이중모드, 하드웨어 보호 (0) | 2021.11.08 |
[KOCW] 운영체제 2차시 - 고등운영체제, 인터럽트 기반 운영체제 (0) | 2021.10.27 |
[KOCW] 운영체제 2차시 - 운영체제 역사 (0) | 2021.10.27 |
[KOCW] 운영체제 2차시 - 운영체제 서론 (0) | 2021.10.26 |
[GET_NEXT_LINE] 구현 예시
혹시나 문제가 된다면 바로 비공개 처리하겠습니다. 지적이나 댓글 환영합니다!
그저께 get_next_line의 구현을 끝내고 어제오늘 평가를 받아, gnl 과제를 보너스 점수 포함 115점으로 통과했다.
코드에 대한 이해가 낮아지기 전에 어서 코드리뷰를 하도록 해야겠다.
우선, get_next_line 과제에서는 3개의 파일을 구현하게끔한다.
get_next_line.c, get_next_line_utils.c, get_next_line.h 를 구현해야한다.
보너스 파트에서는 여러개의 fd를 사용하여도 각 fd의 내용에 대한 스레드가 유지될 수 있도록 해야하는데, 이 경우에는 static 변수를 2차원 배열로 만들면 해결된다. 따라서 보너스 파트와 기본 파트의 함수 구성을 똑같이 하였으므로 보너스에 대한 설명은 굳이 하지 않겠다.
주의해야할 점 !!!
1. 보너스 과제에 대해서는 보너스 헤더를 꼭 get_next_line_bonus.h로 선언하고, 보너스 과제에서 호출하는 헤더를 꼭 모두 이것으로 해주는 것을 주의해야한다. (보너스 과제인데 실수로 get_next_line.h 를 호출할 경우 망하는 거다...)
2. 문장은 개행을 중심으로 구분하여, 개행이 포함되지 않은, 개행의 앞부분을 뜻한다. 따라서 "\n" 1문장을 읽었을 때, line에는 "\0"이 들어가도록 해야한다.
3. 혼자서, 내 앞선 게시글에 나온 main함수로 디버깅 하는 경우에, printf("%d - %s\n", ret, line)을 실행하여, get_next_line()의 리턴값까지 같이 출력해주는 것이 정확히 디버깅 하는 꿀팁이니 참고하자!
참고로, 이번 과제에서는 메모리 누수에 대한 문제가 굉장히 중요하게 다루어진다. 따라서 메모리를 해제하고 널포인터로 만들어주는 과정을 꼼꼼히 구현하였다.
먼저 get_next_line_utils 의 함수들을 리뷰해보겠다.
get_next_line_utils.c 에서는 get_next_line.c에서 쓰이게될 함수들을 구현해놓았다.
(1) get_next_line_utils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
#include "get_next_line.h"
size_t ft_strlen(char *str)
{
size_t len;
len = 0;
while (str[len])
len++;
return (len);
}
int find_newline(char *str)
{
int i;
i = 0;
while (str[i])
{
if (str[i] == '\n')
return (i);
i++;
}
return (-1);
}
char *ft_strdup(char *s1)
{
char *ptr;
size_t len;
size_t i;
i = 0;
len = ft_strlen(s1);
if (!s1)
return (0);
if (!(ptr = malloc(sizeof(char) * (len + 1))))
return (0);
while (i < len)
{
ptr[i] = s1[i];
i++;
}
ptr[i] = '\0';
return (ptr);
}
char *ft_strjoin(char *s1, char *s2)
{
int i;
int j;
int index;
char *str;
i = 0;
j = 0;
index = 0;
str = malloc(sizeof(char) * (ft_strlen(s1) + ft_strlen(s2) + 1));
if (!(str))
return (0);
while (s1[i])
str[index++] = s1[i++];
while (s2[j])
str[index++] = s2[j++];
str[index] = '\0';
free(s1);
s1 = 0;
return (str);
}
|
cs |
libft에서 구현한 함수들을 그대로, 혹은 수정하여 사용하였다.
- ft_strlen() : 문자열의 길이를 구하는 함수
- find_newline() : 주어진 문자열 내의 개행의 위치를 리턴하는 함수 (개행이 없다면 -1 리턴)
- ft_strdup() : 주어진 문자열을 복사하여 만든 새로운 문자열을 포인터를 리턴하는 함수
- ft_strjoin() : 주어진 두 문자열을 합한 새로운 문자열의 포인터를 리턴하는 함수
여기에서 눈여겨보아야 할 부분은 추가로 ft_strjoin()에서 free(s1); s1 = 0; 처리를 해준 부분이다.
이 부분은 나중에 get_next_line.c에서 이어서 설명하겠다.
(2) get_next_line
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
#include "get_next_line.h"
int error_return(char **backup)
{
if (*backup)
{
free(*backup);
*backup = 0;
}
return (-1);
}
int get_one_line(char **backup, char **line, int nl_idx)
{
char *tmp;
(*backup)[nl_idx] = '\0';
if (!(*line = ft_strdup(*backup)))
return (error_return(backup));
if (!(tmp = ft_strdup(*backup + nl_idx + 1)))
return (error_return(backup));
free(*backup);
*backup = tmp;
tmp = 0;
return (1);
}
int get_all(char **backup, char **line)
{
int nl_idx;
if (*backup && ((nl_idx = find_newline(*backup)) >= 0))
return (get_one_line(backup, line, nl_idx));
else if (*backup)
{
if (!(*line = ft_strdup(*backup)))
return (error_return(backup));
free(*backup);
*backup = 0;
}
return (0);
}
int get_next_line(int fd, char **line)
{
static char *backup[OPEN_MAX];
char buf[BUFFER_SIZE + 1];
int read_size;
int nl_idx;
if (fd < 0 || fd > OPEN_MAX || !line || BUFFER_SIZE <= 0)
return (-1);
if (!backup[fd])
{
if (!(backup[fd] = malloc(sizeof(char))))
return (-1);
backup[fd][0] = '\0';
}
while ((read_size = read(fd, buf, BUFFER_SIZE)) > 0)
{
buf[read_size] = '\0';
if (!(backup[fd] = ft_strjoin(backup[fd], buf)))
return (-1);
if ((nl_idx = find_newline(backup[fd])) >= 0)
return (get_one_line(&backup[fd], line, nl_idx));
}
if (read_size < 0)
return (error_return(&backup[fd]));
return (get_all(&backup[fd], line));
}
|
cs |
- static 변수는 여러개의 fd 를 처리할 수 있도록 2차원으로 선언하였으며, fd의 범위인 0 ~ OPEN_MAX 만큼 배열으로 만들어주었다.
read()에서 값을 받아올 버퍼 또한 buf라는 배열로 BUFFER_SIZE + 1 크기만큼 만들어주었다.
- get_next_line 은 3 개의 리턴값을 갖는다.
- 오류 발생 시 : -1
- EOF 도달 시 : 0
- 1문장 읽음 : 1
1. (- 1) 리턴하는 경우
- 먼저 프로그램이 시작될 때, fd가 범위에서 벗어나거나 line 의 주소가 존재하지 않거나, 버퍼사이즈가 0 이하인 경우에 대해 에러를 리턴하게끔 처리해주었다. (-1) 리턴
이 부분은 프로그램이 처음 시작될 때부터 검사되는 부분이다. 따라서 저 조건식에 걸린다면, get_next_line()을 처음 호출한 경우부터 해당 될 것이다. 그리고 그때는 backup에 메모리가 할당되지 않은 초기 상태일 것이다. 따라서 error_return()의 호출 대신, 단순히 (-1)만 리턴하는 것으로 처리한 것이다.
- malloc()이 사용되는 함수에서는 error_return ()을 호출하여 널 가드로 (-1) 을 리턴하게끔 처리하였다.
할당할 메모리가 부족하면 함수가 정상적으로 실행될 수 없으므로 오류가 발생한 상황이다. 따라서 -1 을 리턴하여 함수를 종료할 필요가 있다. 그러나 backup이라는 정적 변수에 할당된 메모리가 남아있으면 메모리 누수가 발생할 가능성이 있다. 따라서 (-1)을 리턴하기 전에 error_return()을 호출하였다. 이를 통해 backup에 남은 메모리가 있다면 이를 해제해준 후 널포인터로 만들어주는 작업을 하여, 메모리릭을 예방하였다.
- read()를 통해 읽어온 바이트가 음수일 경우는 오류이므로 (-1)을 리턴하게끔 처리하였다.
2. 0 또는 1 리턴하는 경우
- while()문의 조건식을 통해 read를 수행하고, 현재 읽어온 부분을 저번에 읽었던 부분에 합친다. (ft_strjoin() 사용)
read()를 통해 버퍼에 읽어온 문자열과 기존의 backup[fd]의 값을 합쳐 새로운 메모리를 할당한다. 이때 기존의 메모리 주소를 새롭게 합친 문자열의 메모리 주소로 업데이트하는 방식이기 때문에, 기존의 메모리를 free() 해주어야 메모리 누수를 예방할 수 있다. 이때문에 ft_strjoin() 내 에서, 첫번째 인자로 받은 문자열의 주소는 맨마지막에 free() 하도록 수정해 준 것이다.
그렇지만 맨 처음에 backup[fd] 에 메모리가 할당되어 있지 않다면, ft_strjoin()을 실행했을 때 backup[fd]를 free()할 수 없어 오류가 난다. 따라서, while()문 이전에 backup[fd]에 맨 먼저 임의로 1만큼 메모리를 할당하고 그 안에 값으로 '\0'을 넣어주었다.
(참고로, static 배열은 모든 요소가 처음에 0으로 초기화되어있다. 따라서 backup[fd]에 메모리 할당이 되지 않은 경우, backup[fd]의 초기값은 0이 된다는 것을 알아야한다)
- while() 실행 중에, 읽은 문자열 내에 개행이 존재한다면, get_one_line()을 호출하여 1문장만 추출하고 1을 리턴하도록 처리하였다.
- get_one_line 은 개행을 기준으로 개행 앞의 문장은 line에 붙여주고, 그 뒷부분은 static 변수에 복사하여 저장하는 역할을 한다.
이때 null을 만날 때까지 문자열을 복사하는 ft_strdup()를 사용하기 위해, 개행이 있던 부분의 인덱스 값에 먼저 '\0'을 할당해주었다.
개행이 있던 부분의 뒷부분을 static 변수에 새로 복사하고 저장하는 과정에서, 기존의 메모리를 free 해주는 작업을 잊으면 안된다.
- while() 실행 중에, 읽은 문자열 내에 개행이 존재하지 않는다면 계속 반복해서 read()를 수행한다.
이 작업은 read_size 가 0이 될때까지 (EOF)에 도달할 때 혹은 중간에 개행이 나올 때까지 계속된다.
- 더이상 read()를 통해 읽을 값이 없다면 get_all()을 통해 남은 문자열을 처리해준다.
만약에 backup에 남은 문자열이 있다면, 그 안에 개행이 있는지 검사한다.
(버퍼사이즈가 클 경우, 개행이 있는 문장들을 이전에 한번에 여러개 다 읽어왔을 수가 있다)
만약 개행이 존재한다면 get_one_line()을 호출하여 그쪽으로 backup을 보내서 처리를 해준 후 1이 리턴되도록 한다.
(ex : 버퍼 사이즈가 커서 이전에 남은 문장들을 이미 다 읽어왔던 경우, backup에 남아있는 개행이 붙은 문장들이 1개 이상 존재할 수 있다. 예시를 들자면, 개행 8개로만 이루어진 텍스트파일이 있다고 가정하자)
그 안에 개행이 없다면, 아무리 길든 짧든, EOF로 끝나는 마지막 1문장이라는 뜻이다. 따라서 backup에 남은 문자열을 복사하여 line에 붙여준 후, backup의 메모리를 해제하고 이를 널포인터로 만들어준다. 함수가 -1 혹은 0으로 종료되기 전에 backup에 남은 메모리가 없어야 메모리 누수를 예방할 수 있다.
get_all 이 완료되었다면 EOF까지 도달했다는 뜻이므로 0을 리턴한다.
참고) 함수의 매개변수로 backup[fd]의 주소를 넘겨주는 이유는?
코드를 보면, get_one_line(&backup[fd], line, nl_idx) 이런 식으로 backup[fd]의 주소를 넘겨준다.
이렇게 해주는 이유가 무엇일까?
만약 backup[fd]를 그대로 넘겨주게 되면 backup[fd]의 주소가 가르키는, "해당 주소 내부"의 문자열 값에 접근할 수 있게 된다.
그러나 backup[fd]의 주솟값을 바꿀 수는 없다.
그렇지만 get_one_line() 함수는 backup[fd]의 기존 메모리를 free()하고 새로운 문자열을 담은 새로운 메모리를 backup[fd]에 할당해줘야한다. 즉 포인터가 가르키는 메모리의 주솟값이 바뀐다는 이야기다.
따라서 "해당 주소 내부의 값에 접근" 할 수 있게 하는 것과 동시에, "해당 주솟값을 변경"할 수 있게하기 위해서 backup[fd]의 주소를 넘겨준다고 보면된다.
참고) 버퍼사이즈로 큰 값(10000000)를 넣어줄 경우는 어떻게 될까?
segfault가 발생한다. buf[BUFFER_SIZE + 1]은 스택 메모리에 할당된 것이기 때문에 스택 사이즈를 초과하는 크기로 선언할 수 없다.
되도록이면 buf를 동적할당하고 싶었으나, 이럴 경우 리턴 전에 free하는 부분을 추가하느라 함수 구조가 더욱 복잡해지고, norminette 규정으로 정해진 함수의 줄 제한을 넘어가게 되었다. 따라서 어쩔 수 없이 정적인 배열로 선언하게 되었다.
(3) get_next_line 헤더
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#ifndef GET_NEXT_LINE_H
# define GET_NEXT_LINE_H
# include <unistd.h>
# include <stdlib.h>
# define OPEN_MAX 1024
int get_next_line(int fd, char **line);
size_t ft_strlen(char *str);
int find_newline(char *str);
char *ft_strjoin(char *s1, char *s2);
char *ft_strdup(char *s1);
#endif
|
cs |
fd 의 개수 범위인 OPEN_MAX를 헤더파일을 통해 매크로로 정의해주었다.
OPEN_MAX를 통해 내가 최대한 열고자하는 파일의 개수를 정의해준 것이기 때문에 임의의 상수로 설정해주어도 상관이 없다.
이 외의 방법으로는 limits.h 헤더를 통해 open_max를 받아오는 방법도 있다. 그러나 이 경우는 허용되지 않은 외부 헤더를 호출했다며 디펜스에서 논란이 될 여지가 있다. 따라서 나는 상수로 정의해주는 것을 선택했다.
get_next_line_utils에서 size_t 자료형과 malloc()이 쓰이기 때문에 그에 필요한 헤더 2개 또한 include 해주었다.
그 외에 함수의 프로토타입 선언도 추가해주었다.
버퍼사이즈는 과제에 명시된 대로, 컴파일 단계에서 -D 옵션을 통해 정의해주는 것이다. 어차피 테스터나 뮬리넷에서는 컴파일 옵션으로 정의될 것이기 때문에 굳이 ifndef 으로 기본값을 정의해주지 않아도 될 것으로 생각했다. 따라서 BUFFER_SIZE는 매크로로 정의하지 않았다.
구현 과정에서 어려웠던 부분
1) 메모리 누수 부분을 잡는 것이 힘들었다.
특히 ft_strjoin()을 통해 첫번째 인자로 들어온 기존의 backup 메모리를 해제해주는 데, ft_strjoin()의 내부에서 backup의 주소가 null일 경우를 처리하는 것에서 자꾸 테스터기에서 오답이 났다. ft_strdup()을 사용하거나 여러 방법을 써봤는데도 테스트기를 통과하기 힘들었다.
2) 기존의 남아있는 개행을 가진 문장들이 한번에 backup에 전부 읽혔을 경우(버퍼사이즈가 큰 경우)를 생각하지 못했었다.
왜냐면 개행이 있으면 get_all() 실행이 되지 않고 매번 get_one_line()이 바로 호출되도록 이어진다고 오해했기 때문이다.
그러나 버퍼사이즈가 크면 read() 실행 시 EOF까지 모두 읽어올 수도 있으며, 처리하고 남은 문장들은 backup[fd]에 남는다. 따라서 다음번 호출부터는 새로 read()를 실행하지 않기 때문에, backup 변수에 남아있는 개행이 있는 문장까지 get_all()에서 처리해줘야한다.
따라서 get_all() 함수에서 이를 처리하는 조건문을 추가하는 데 시간이 걸렸다.
어떻게 해결했나?
1) 그냥 ft_strjoin()에 들어가기 전에 backup[fd]에 할당된 메모리가 없는지 체크했다. 메모리 주소가 null 이라면 시작부터 backup[fd]에 메모리를 1칸 할당해주었다. 이 경우에도 오류를 피하지 못했으나, 할당된 메모리의 값으로 '\0' 을 넣어주고나니 그제서야 테스터를 통과할 수 있었다.
2) backup에 개행을 가진 문장들이 남아있을 경우를 고려하여, get_all()에서도 backup의 개행여부를 체크하여 get_one_line()을 호출하는 코드를 추가하였다.
동료평가 하면서 새로 알게 된 지식
1) 정적인 배열은 스택 메모리 영역을 사용하고, 동적인 메모리 할당은 힙 메모리 영역을 사용한다. 그리고 static 자료형은 데이터 영역을 사용한다.
static 자료형은 스택이나 힙이 아닌, 데이터 영역을 사용하여 메모리를 저장하는 것이기 때문에 메모리의 값을 계속 유지할 수가 있는 것이다.
이때, 주의해야할 점을 하나 알 수 있다.
일반 포인터에, static 메모리의 주소를 붙여주면 오류가 날 수 있다.
데이터 영역의 메모리에 static이 아닌 포인터가 접근하는 것이기 때문이다.
따라서 strdup을 통해 힙메모리로 static 메모리의 내용을 복사해준 후 포인터에 해당 힙메모리의 주소를 붙여주는 것이 바람직하다.
2) 주로 데이터를 읽어오는 단위를 2의 n 승으로 선언하게 된 관례 :
주로 메모리의 크기가 2의 배수로 선언되어 있기 때문에, 메모리를 한번에 가져오는 단위를 2의 n승으로 하는 것이 더 효율적으로 작용한다.
3) read()가 음수를 반환하는 경우? : 에러 발생
- EAGAIN : O_NONBLOCK으로 열렸지만 즉시 읽을 수 있는 데이터가 없다
- EIO : I/O 에러 발생
- EFAULT : buf가 접근할 수 없는 주소 공간을 가리킨다
추가) malloc() 실패 시 널가드 처리를 통해 get_next_line() 함수에서 (-1)이 반환될 수 있도록 하는 처리를 해주었다.
그런데 이때 malloc()이 쓰이는 함수인 ft_strdup()에 대해 get_all() 내에서 이 처리를 딱 하나 빼먹은 것을 이후에 발견하였다.
따라서 게시글의 코드를 다시 수정하였다. 이 부분에서 실수를 하지않도록 꼼꼼히 점검해야한다. 내 친구도 이 부분을 딱 하나 빠뜨려서 디팬스에 실패하였다고 한다.
참고 자료 링크 :
'IT > 42Seoul' 카테고리의 다른 글
[born2beroot] monitoring 파트 정리1 (0) | 2021.11.17 |
---|---|
[born2beroot] 개념 정리 - 프로젝트 개요 (0) | 2021.11.15 |
[GET_NEXT_LINE] 구현 준비 (0) | 2021.06.04 |
Libft 과제 시 유의점 (테스터기, protected, Makefile) (0) | 2021.05.31 |
malloc/calloc 관련 정리 (0) | 2021.05.30 |
[GET_NEXT_LINE] 구현 준비
혹시나 문제가 된다면 바로 비공개 처리하겠습니다. 지적이나 댓글 환영합니다!
과제 중에 동료평가용 설명 정리 차원으로 블로그 글을 작성하기 때문에, 통과 완료 전까지는 계속 글을 수정할 수도 있습니다. 양해바랍니다. 감사합니다.
참고로, 이 포스팅의 중요한 본론은 (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 |
Libft 과제 시 유의점 (테스터기, protected, Makefile)
libft나 다른 하위 서클 함수들은 주로 어떤 정해진 것을 구현하는 문제인 경우가 많다. 따라서 깃허브에 libft 과제를 뮬리넷(42채점 서버)처럼 테스트 할 수 있는 테스터기가 있다. 이 테스터기들을 통해 미처 하지 못한 예외처리나 필수사항의 구현을 체크하여 코드를 보완할 수 있다. 아주 중요하다.
* 테스터기
- 내가 주로 쓴 테스터기는 4가지가 있다.
1) Libftest
2) libft-unit-test
3) libft-war-machine
4) libftester
깃허브에 쳐보면 이 외의 더 많은 테스터기들도 다 나온다.
기본적인 사용법은 이렇다. 일단 해당 깃 레포지터리를 깃클론 받는다. 그다음 libft-unit-test와 libftester에서는 그 안의 Makefile에 있는 평가할 libft 과제 디렉토리의 상대경로를 입력한다.
Libftest와 libft-war-machine에서는 ./grademe.sh 를 실행한 다음, 생성된 my_config.sh에서 libft 과제 디렉토리의 상대경로를 변경한다.
이런 식으로 채점될 파일의 경로를 다 설정해준 후에는, 해당 테스터들의 README.md 파일을 보면 알 수 있듯이, Libftest와 libft-war-machine에서는 ./grademe.sh를 실행하고, libft-unit-test에서는 make f, libftester에서는 make a 를 실행한다.
이러면 makefile의 구성요소가 잘 있는 지도 점검해주고, 보너스 함수까지 전체 파일을 채점해볼 수 있다. 보통은 3개의 테스트기만 돌려도 된다 하는 경우도 있는데, 4개까지 돌리기를 권장한다. (그렇지만 보통 libftester나 libft-unit-test가 제일 깐깐한 것 같다)
의외로 libftester 테스트기에서, 다른 테스트기가 빠뜨린 예외처리들이나 segfault를 몇 개 잡아주는 경우가 있다.
일단 테스트기에서 문제가 틀렸다고 나오면, (vs코드로 테스트기 폴더를 열어) 해당 테스트기의 main 함수를 확인하면서 디버깅하면 큰 도움이 된다.
아무래도 과제에서 함수를 40개 넘게 구현하는 것도 있고, 인터넷의 정보들도 완벽하지 않기 때문에 혼자서만 머리를 짜내서는 예외처리를 찾기 힘들 수 있다. 이때, 해당 테스트기의 main 함수나 그에 따른 정답으로 나오는 정상출력 결과값, 그리고 내 출력 결과값을 확인하며 디버깅하는 것이 과제 진행 속도를 올려준다.
* protected / unprotected
- 매개변수로 NULL이 들어왔을 때의 예외처리 여부
주로 libft-unit-test 테스트기를 돌려볼 때, 방패모양과 터지는 모양의 그림이 나오면서 이게 뭔지에 대한 궁금증이 생긴다.
방패 모양은 함수가 protected 되었다는 뜻이고, 터지는 그림은 함수가 unprotected 되었다는 뜻이다.
매개변수로 주어진 변수에 NULL 값이 들어올 경우의 예외처리를 하면 protected 된 함수, 안하면 unprotected 된 함수다.
어떤 방식을 택하든 뮬리넷의 채점 결과에서는 별 상관없지만, 동료평가에서는 그렇게 구현한 이유에 대해서 적당한 근거를 들어 설명하여 디팬스 해야한다.
나는 우선 unprotected 된 함수로 구현하였고, 내 초반의 논리는 이러했다.
일단 기존의 C 라이브러리 함수가 그렇게 구현되있기 때문이다. 특히 실제 개발자로 일하게 된다면 내 구현 함수보다는 최적화된 시중의 C 라이브러리를 사용할 텐데, 실제 함수를 사용할 때와 다른 결과값을 낼 수 있게 만들기보다는, 실제 함수의 동작에 익숙해지기 위해 unprotected로 구현하는 것을 택했다. 또한 protected 처리를 할 경우, 0을 리턴할 때 그 원인이 함수의 정상적인 결과값인지, malloc 실패로 인한 NULL 반환인지, 매개변수로 인한 protected 처리인지 원인을 파악하기 애매해지는 경우가 생길 수 있으며, 애초에 null을 매개변수에 넣을 수 있게 하는 것이 좋지 않은 습관이라고 생각한 점도 한 이유가 되었다.
그러나, 동료평가를 하면서 함수에 protected 처리를 해주는 게 더 바람직하다는 것을 알게되었다.
무엇보다 현직 개발직에서는 함수를 protected 로 만드는 것을 굉장히 중요시 여긴다고 한다. 어떤 상황에서도 프로그램이 segfault 등의 예기치 못한 오류로 종료되지 않고, 일단 어떻게든 동작을 이어가게끔 만들어야 하기 때문이다. 또한 고객이 함수의 매개변수에 null 값을 넣게 되는 경우도 빠짐 없이 처리해주어야하기 때문에, protected 된 함수를 만들 수 밖에 없다.
그래서, 만일 내가 다시 함수를 구현하게 되거나 후에 다시 libft.a 라이브러리를 사용하게 된다면, protected 처리를 더해 함수를 수정/보완할 계획이 있다.
* Makefile
- relink 방지가 중요 !
- relink : 변경된 object 파일 없이 make를 실행하였을 때, 굳이 필요없는데도 다시 링킹이 되는 현상
-> 변경된 object 파일이 없어도 다시 라이브러리 (libft.a)를 생성하게 된다.
- 원래 make는 파일의 수정 시간을 확인하여, 수정된 파일들만 다시 컴파일 하기 때문에, 불필요한 relink를 없애 효율적인 컴파일-링킹이 이뤄질 수 있게한다.
보통 흔하게 짜는 방식(깃허브 참고)으로 Makefile을 짜면, make 를 2번 실행 시, "libft.a가 최신이다(up-to-date)" 라는 안내문과 함께 리링크가 이루어지지 않는다. 그러나 보너스 함수까지 라이브러리에 같이 아카이브하는 make bonus를 2번 실행할 경우, libft.a로 보너스파일까지 링킹하여 다시 아카이브 되는 과정이 반복되서 이루어지는 것을 볼 수 있다. 즉, 리링크가 일어나는 것이다.
원래는 동료평가 시에, 보너스 파일의 리링크까지는 어쩔 수 없다고 보고 잡지 않는 사람들이 많았다.
하지만, 최근에 리링크 방지를 위해 조건문 + 매크로를 정의하여 bonus 리링크까지 방지하는 코드가 슬랙을 통해 퍼졌다.
그 후로, 깐깐한 카뎃들은 동료평가에서 이 부분까지 고려하여 fail을 주기도 한다. 따라서 나 또한 친구가 fail을 받은 것을 본 입장에서, 만일을 위해 리트라이 때 makefile을 저런 방식으로 수정하였다. 저 Makefile을 사용하는 경우에, github의 테스터기에서는 보너스파일이 존재하지 않는다며 채점되지 않는 경우도 생기지만, 뮬리넷 채점서버에서는 잘 동작하니 걱정하지 않아도 된다.
이 코드를 사용하면 make bonus를 2번 실행 시,
make WITH_BONUS=1 all
make[1]: Nothing to be done for `all'.
라는 문구와 함께 리링크가 되지 않는다.
다음으로는 코드 예시를 소개하겠다.
- Makefile 예시 코드 (relink 방지):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
SRCS = ft_atoi.c ft_bzero.c ft_calloc.c ft_isalnum.c ft_isascii.c\
ft_isalpha.c ft_isdigit.c ft_isprint.c ft_itoa.c ft_memccpy.c\
ft_memchr.c ft_memcmp.c ft_memcpy.c ft_memmove.c ft_memset.c\
ft_putchar_fd.c ft_putendl_fd.c ft_putnbr_fd.c ft_putstr_fd.c\
ft_split.c ft_strchr.c ft_strdup.c ft_strjoin.c ft_strlcat.c\
ft_strlcpy.c ft_strlen.c ft_strmapi.c ft_strncmp.c ft_strnstr.c\
ft_strrchr.c ft_strtrim.c ft_substr.c ft_tolower.c ft_toupper.c
SRCS_BONUS = ft_lstadd_back.c ft_lstadd_front.c\
ft_lstclear.c ft_lstdelone.c ft_lstiter.c\
ft_lstlast.c ft_lstmap.c ft_lstnew.c\
ft_lstsize.c
OBJS = $(SRCS:.c=.o)
OBJS_BONUS = $(SRCS_BONUS:.c=.o)
NAME = libft.a
ifdef WITH_BONUS
OBJ_FILES = $(OBJS) $(OBJS_BONUS)
else
OBJ_FILES = $(OBJS)
endif
CC = cc
RM = rm -f
FLAGS = -Wall -Werror -Wextra
%.o : %.c
$(CC) $(FLAGS) -c -o $@ $<
$(NAME): $(OBJ_FILES)
ar cr $@ $^
bonus:
make WITH_BONUS=1 all
all: $(NAME)
clean:
$(RM) $(OBJS) $(OBJS_BONUS)
fclean: clean
$(RM) $(NAME)
re: fclean all
.PHONY: bonus all clean fclean re
|
cs |
- ifdef ~ else ~ endif 문을 사용하여, 매크로가 정의되었는지 되지 않았는지에 따라 컴파일 될 object 파일들의 목록을 다르게 구성하였다.
예)
all :
ifdef CC
@echo "CC 매크로는 정의되어 있습니다."
else
@echo "CC 매크로는 정의되지 않았습니다."
endif
- ar rcs libft.a -> 정적 라이브러리를 만드는 명령어
- 참고) 정적 라이브러리 vs 동적 라이브러리
정적링크라이브러리 (Static Link Libarary - .lib
컴파일 시에 함수가 실행파일에 연결된다. 실행 파일에 함수의 코드가 복사되기 때문에, 실행파일의 크기가 커지는 단점이 있지만
실행파일은 완전한 단독 실행파일이 된다. 실행 파일에 함수의 코드가 포함되어 있기 때문에, 컴파일이 끝나면 lib 파일이 없어도 프로그램을 실행할 수 있다.
동적링크라이브러리 (Dynamic Link Library - .dll
정적라이브러리처럼 컴파일 시에 함수가 연결되는 방식이 아닌 런타임 시에 함수가 실행파일에 연결된다. 실행파일에는 호출할 함수의 정보만 포함되고, 실제 함수 코드는 복사되지 않으므로 실행파일의 크기가 작아진다. 하지만 실행 파일은 함수에 대한 정보만 가지고 있을 뿐 실제 코드를 가지고 있지 않기 때문에 프로그램 실행 시에는 dll 파일이 항상 존재해야 한다.
- 참고) 라이브러리 사용 이유
라이브러리는 다른 프로그램들과 링크되기 위하여 존재하는, 하나 이상의 서브루틴이나 함수들의 집합파일을 말하는데, 함께 링크될 수 있도록 보통 컴파일된 형태인 목적코드 형태로 존재한다.
라이브러리는 코드 재사용을 위해 조직화된 오래된 기법 중의 하나이며, 많은 프로그램 들에서 사용할 수 있도록 운영체계나 소프트웨어 개발 환경 제공자들에 의해 제공되는 경우가 많다.
참고자료 출처 :
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=muri1004&logNo=220027346833
Makefile - 조건부, 함수
▣ 조건부 make에서 조건문은 ifeq ~ else ~endif 이다. 예) all : ifeq ($(CC), gcc) @echo "C 컴파일...
blog.naver.com
https://yunslee-42seoul.tistory.com/3
Makefile 정복하기
1. Makefile 구성요소 Target(만들려는 녀석), Dependency(만들기위한 재료), Command(명령어) ,Macro(작성의 편리성) 해당설명은 이 블로그를 참고하자!![1] Makefile에서 반복되는 구조인 Rule block의 구조는..
yunslee-42seoul.tistory.com
'IT > 42Seoul' 카테고리의 다른 글
[GET_NEXT_LINE] 구현 예시 (0) | 2021.06.11 |
---|---|
[GET_NEXT_LINE] 구현 준비 (0) | 2021.06.04 |
malloc/calloc 관련 정리 (0) | 2021.05.30 |
[Libft] C 언어 라이브러리 구현_BONUS_보너스 함수 구현2 (0) | 2021.05.30 |
[Libft] C 언어 라이브러리 구현_BONUS_보너스 함수 구현1 (0) | 2021.05.30 |
malloc/calloc 관련 정리
42seoul에서 libft 과제의 동료평가를 진행하다보니, 고수분들께 평가받으며 내가 몰랐던 개념의 허점에 대해 많이 알게 되었다.
그에 대하여 정리하여 스스로 개념을 보완해보려 한다.
* malloc : 메모리를 할당
1) 프로토타입
#include <stdlib.h>
void *malloc(size_t size);
2) 인자들
size : 메모리 블록의 크기(바이트 단위)
3) 리턴값
메모리 할당에 성공했을 경우 : 할당한 메모리 블록을 가르키는 포인터를 리턴
(해당 포인터의 타입은 언제나 (void *)형이므로, 사용자가 원하는 타입으로 캐스팅 해줘야 함)
매모리 할당에 실패했을 경우 : 널 포인터를 리턴
주소값을 반환받기 떄문에, 할당된 힙 메모리 영역에 접근하려면 포인터를 사용해야한다.
사용 후 반드시 free() 해줘야한다 (메모리 해제)
주의할 점 : unix 기반이랑 리눅스 기반에서 다르게 작동
주의할 점 : malloc()이 실패할 경우, 해당 시점의 포인터에 아무 메모리도 할당되지 않고 null이 리턴된다.
- 사이즈만큼의 여유 메모리 공간이 남아있는지 먼저 확인하고, 여유 공간이 없을 경우에 아무런 할당 없이 null 을 리턴한다.
(처음에 나도 오해한 부분인데, malloc은 조금씩 메모리를 할당 시도를 거듭하다 갑자기 메모리가 부족하면 null을 리턴하는 것이 아니다! 애초부터 메모리 할당이 가능한지, 요청된 사이즈를 보고 확인부터 한다.)
* calloc() 의 특징
malloc과 마찬가지로 힙 메모리를 할당하는 함수이다.
1) 프로토타입
#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
2) 인자들
nmemb : 다음 파라미터인 size의 갯수. 즉, 할당할 배열의 개수
size : 메모리 블록의 크기(바이트 단위)
3) 리턴값
메모리 할당에 성공했을 경우 : 할당한 메모리 블록을 가르키는 포인터를 리턴 (nmemb * size byte 만큼의 힙 메모리 할당)
(해당 포인터의 타입은 언제나 (void *)형이므로, 사용자가 원하는 타입으로 캐스팅 해줘야 함)
매모리 할당에 실패했을 경우 : 널 포인터를 리턴
calloc()은 size byte 크기의 데이터 type을 nmemb개 저장할 수 있을 크기의 메모리를 할당
즉, size * nmemb 바이트의 메모리를 할당
calloc()은 heap 메모리를 할당하며, malloc()과는 달리 할당된 메모리를 0x00으로 초기화
주소값을 반환받기 떄문에, 할당된 힙 메모리 영역에 접근하려면 포인터를 사용해야한다.
사용 후 반드시 free() 해줘야한다 (메모리 해제)
참고)
만약, nmemb 또는 size가 0이면, NULL 또는 free() 시에 오류가 나지 않도록 적절한 포인터를 리턴한다.
-> unix의 경우에는 1 만큼의 동적할당이 이루어짐
만약, long long (64bit)를 넘어서는 숫자에 대해서는 null을 리턴한다.
-> 초기화할 메모리 주소에 오버플로우가 일어날 경우, 반환된 포인터의 주소가 0x00으로 NULL이 되는 것을 유의해야 한다.
만약 메모리 할당 후에 굳이 메모리에 대한 초기화가 필요 없다면, malloc()을 사용하고, 그렇지 않다면 calloc()을 사용하는 것이 좋다.
* 내 libft 과제 calloc() 구현의 보완점
1) size * count 가 0 일 때 예외처리
2) size * count 가 오버플로우 됬을 때 포인터가 null 주소를 갖도록 처리
참고 자료 출처 :
'IT > 42Seoul' 카테고리의 다른 글
[GET_NEXT_LINE] 구현 준비 (0) | 2021.06.04 |
---|---|
Libft 과제 시 유의점 (테스터기, protected, Makefile) (0) | 2021.05.31 |
[Libft] C 언어 라이브러리 구현_BONUS_보너스 함수 구현2 (0) | 2021.05.30 |
[Libft] C 언어 라이브러리 구현_BONUS_보너스 함수 구현1 (0) | 2021.05.30 |
Makefile 정리2 (0) | 2021.05.25 |
[Libft] C 언어 라이브러리 구현_BONUS_보너스 함수 구현2
혹시나 문제가 된다면 바로 비공개 처리하겠습니다. 지적이나 댓글 환영합니다!
이번 포스팅에서는 보너스 파트의 함수를 이어서 구현해보았다. 보너스 파트는 연결리스트(랑크드리스트)를 이용한 기본적인 기능을 구현한 함수들을 구현하였다. 이후에도 많이 사용하게 될 것 같다.
참고로, 내가 정의한 libft.h 헤더에는 <unistd.h>와 <stdlib.h>가 include 되어있다. 따라서 libft.h를 호출하면, 따로 정의하지 않고도 <unistd.h> 에 정의된 size_t 타입과 <stdlib.h>의 malloc/free를 사용할 수 있다. 또한 <unistd.h>에 있는 write 함수 또한 사용할 수 있다. 또한 t_list 구조체도 정의하여, 보너스 함수 구현에 이를 사용하였다.
(1) ft_lstdelone : 주어진 노드를 삭제
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
|
#include "libft.h"
void ft_lstdelone(t_list *lst, void (*del)(void *))
{
(del)(lst->content);
free(lst);
}
|
cs |
- del() 함수를 통해 주어진 노드의 content를 삭제한다.
- free()를 통해 주어진 노드의 메모리를 해제한다.
(2) ft_lstclear : 주어진 연결리스트 전체를 삭제
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include "libft.h"
void ft_lstclear(t_list **lst, void (*del)(void *))
{
t_list *tmp;
t_list *lst_ptr;
lst_ptr = (*lst);
while (lst_ptr)
{
tmp = lst_ptr;
lst_ptr = lst_ptr->next;
(del)(tmp->content);
free(tmp);
}
*lst = 0;
}
|
cs |
- 연결리스트의 끝까지 반복문을 도는데, 현재 노드의 주소를 tmp에 저장한다. 그다음 연결리스트 포인터를 다음 노드로 이동시킨다.
- 그 후, tmp 포인터와 del() 함수로 현재 노드의 내용물을 삭제하고 현재 노드의 메모리 또한 해제(free)시킨다.
- 주의사항 : 리스트의 삭제가 끝나면 연결리스트의 원래 시작주소에 0을 넣어주고 종료한다. (이걸 안하면 unit-test에서 틀린다)
(3) ft_lstiter : 연결리스트의 모든 노드에 (매개변수로 받은) 함수를 적용
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
|
#include "libft.h"
void ft_lstiter(t_list *lst, void (*f)(void *))
{
while (lst)
{
(f)(lst->content);
lst = lst->next;
}
}
|
cs |
- while문을 이용하여, 연결리스트 하나씩 포인터를 이동시켜가며 함수를 적용한다.
(4) ft_lstmap : 기존의 연결리스트에 (매개변수로 받은)함수를 적용한 새로운 연결리스트를 구함
- 구현 코드 예시 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include "libft.h"
t_list *ft_lstmap(t_list *lst, void *(*f)(void *), void (*del)(void *))
{
t_list *new_lst;
t_list *tmp;
new_lst = 0;
tmp = 0;
while (lst)
{
tmp = ft_lstnew((f)(lst->content));
if (!(tmp))
{
ft_lstclear(&new_lst, (del));
return (0);
}
ft_lstadd_back(&new_lst, tmp);
lst = lst->next;
}
lst = 0;
return (new_lst);
}
|
cs |
- 새로운 노드의 할당이 실패하면, ft_lstclear()을 통해 그동안 만들어진 새 연결리스트의 노드들을 모두 해제해준 뒤 0을 리턴한다.
- 성공적으로 새 연결리스트를 생성하는 것을 완료하면, 새 연결리스트의 포인터를 리턴해준다.
- 주의사항 : while문에 들어가기 앞서 new_lst = 0 을 해줘야, ft_lstadd_back()을 통해 첫번째 노드를 생성할 수 있다.
(그 이유는 ft_lstadd_back() 함수 구현 코드를 보면 알 수 있다)
참고) lst 함수 관련 메인함수 예제
: 동료 평가 디팬스에서는 bonus 함수를 활용한 메인함수를 짜라고 요구하는 경우도 간혹 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include “libft.h”
int main(void)
{
t_list *node;
node = ft_lstnew(ft_strdup("1"));
ft_lstadd_back(&node, ft_lstnew(ft_strdup("2")));
ft_lstadd_back(&node, ft_lstnew(ft_strdup("3")));
while (node)
{
printf("%s\n", (char *)(node->content));
node = node->next;
}
return (0);
}
|
cs |
출력 예시 :
1
2
3
'IT > 42Seoul' 카테고리의 다른 글
Libft 과제 시 유의점 (테스터기, protected, Makefile) (0) | 2021.05.31 |
---|---|
malloc/calloc 관련 정리 (0) | 2021.05.30 |
[Libft] C 언어 라이브러리 구현_BONUS_보너스 함수 구현1 (0) | 2021.05.30 |
Makefile 정리2 (0) | 2021.05.25 |
Makefile 정리1 (0) | 2021.05.25 |