foo = "bar"
bar = $(foo) foo # dynamic (renewing) assignment
foo := "boo" # one time assignment, $(bar) now is "boo foo"
foo ?= /usr/local # safe assignment, $(foo) and $(bar) still the same
bar += world # append, "boo foo world"
foo != echo fooo # exec shell command and assign to foo
# $(bar) now is "fooo foo world"
Magic variables
ut.o: src.c src.h
$@ # "out.o" (target)
$< # "src.c" (first prerequisite)
$^ # "src.c src.h" (all prerequisites)
%.o: %.c
$* # the 'stem' with which an implicit rule matches ("foo" in "foo.c")
also:
$+ # prerequisites (all, with duplication)
$? # prerequisites (new ones)
$| # prerequisites (order-only?)
$(@D) # target directory
Command prefixes
- | Ignore errors |
@ | Don’t print command |
+ | Run even if Make is in ‘don’t execute’ mode |
build:
@echo "compiling"
-gcc $< $@
-include .depend
Find files
js_files := $(wildcard test/*.js)
all_files := $(shell find images -name "*")
Substitutions
file = $(SOURCE:.cpp=.o) # foo.cpp => foo.o
outputs = $(files:src/%.coffee=lib/%.js)
outputs = $(patsubst %.c, %.o, $(wildcard *.c))
assets = $(patsubst images/%, assets/%, $(wildcard images/*))
More functions
$(strip $(string_var))
$(filter %.less, $(files))
$(filter-out %.less, $(files))
Building files
%.o: %.c
ffmpeg -i $< > $@ # Input and output
foo $^
Includes
-include foo.make
Options
make
-e, --environment-overrides
-B, --always-make
-s, --silent
-j, --jobs=N # parallel processing
Conditionals
foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif
Recursive
deploy:
$(MAKE) deploy2
'CS > Linux OS' 카테고리의 다른 글
malloc 처음부터 작성해보기 (0) | 2022.11.29 |
---|
해당 글은 malloc을 실제로 작성해보고 실제 할당이 이뤄지는 원리에 대해서 더욱 깊은 이해를 해보고자 하는 목적으로 작성되었습니다.
바로 본론으로 들어가봅시다.
malloc의 함수 프로토타입은 다음과 같습니다.
void *malloc(size_t size);
입력으로 바이트 수를 요구하고, 해당 크기의 메모리 블록에 대한 포인터를 반환합니다.
이를 구현할 수 있는 방법에는 여러가지가 있는데 해당 글에서는 sbrk를 사용하도록 하겠습니다. OS는 프로세스를 위한 스택 및 힙 공간을 예약하고, 이후 sbrk를 사용함으로써 유저는 힙을 조작할 수 있게 됩니다.
malloc을 가장 단순하게 구현한다면 다음과 같은 코드가 가능할 것입니다.
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
void *malloc(size_t size) {
void *p = sbrk(0);
void *request = sbrk(size);
if (request == (void*) -1) {
return NULL; // sbrk가 실패한 경우
} else {
assert(p == request); // thread-safe 하지 않은 경우
return p;
}
}
프로그램이 malloc에 공간을 요청할 때 malloc은 sbrk에게 힙 크기를 늘리도록 요청하고 힙으로부터 새 영역의 시작에 대한 포인터를 반환받게 됩니다.
이렇게 힙에 할당된 메모리는 메모리의 할당해제를 수행하는 free 함수를 통해 반환됩니다. free의 프로토타입은 다음과 같습니다.
void free(void *ptr);
free가 수행되면 앞서 malloc으로 반환된 포인터의 공간이 비워져야 합니다. 그러나 malloc에 의해 할당된 무언가에 대한 포인터가 주어지면 어떤 크기의 블록이 해당 할당과 연관되어 있는지는 알 수 없습니다. 해당 문제는 우리가 반환하는 포인터 바로 아래에서 멀리 떨어져 잇는 일부 공간에 메모리 영역에 대한 메타 정보를 저장함으로써 해결됩니다.
예를 들어 각 블록에 대해서 다음의 정보를 저장할 수 있을 것입니다.
struct block_meta {
size_t size; // 블록의 크기
struct block_meta *next; // 다음 블록의 위치
int free; // 할당 해제 여부
}
또한 연결된 블록에 대한 Head도 정의되어야 합니다.
void *global_base = NULL;
malloc 의 경우 가능하면 여유 공간을 재사용하고 기존 공간을 재사용할 수 없는 경우에만 공간을 할당합니다. 우리가 연결 리스트 구조를 가지고 있다는 점을 감안하면, 빈 블록이 있는지 여부를 확인하고 반환하는 것은 간단합니다. 어느 정도 크기의 요청을 받으면 연결된 목록을 반복하여 충분히 큰 여유 블록이 있는지를 확인합니다.
struct block_meta *find_free_block(struct block_meta **last, size_t size) {
// last: 마지막으로 참조된 블록 정보
// size: 할당하고자 하는 사이즈
struct block_meta *current = global_base;
while (current && !(current->free && current->size >= size)) {
*last = current;
current = current->next;
}
return current;
}
만약 사용가능한 블록을 찾지 못한다면 sbrk를 사용하여 OS에 공간을 요청하고 새 블록을 연결 목록 끝에 추가해야 합니다.
#define META_SIZE sizeof(struct block_meta)
struct block_meat *request_space(struct block_meta *last, size_t size) {
struct block_meta *block;
block = sbrk(0);
void *request = sbrk(size + META_SIZE);
assert((void *)block == request); // thread sagfe 하지 않은 경우
if (request == (void *) -1) {
return NULL; // sbrk를 통한 블록 할당 실패
}
if (last) {
last->next = block; // 만약 마지막 블록 정보가 매개변수로 들어왔다면, 이어 붙어줌
}
block->size = size; // 블록의 메타데이터에 size 정보를 저장
block->next = NULL; // 블록의 메타데이터에 마지막 블록임을 표시
block->free = 0; // 블록의 메타데이터에 할당되었음을 표시
return block;
}
앞서 작성한 함수들을 종합하게 되면 여유 공간이 있는지를 확인하고 공간을 요청할 수 있게 되어 malloc을 작성할 준비가 완료되었습니다. 만약 HEAD 포인터인 global_base가 NULL인 경우 공간을 요청하고 기본 포인터를 새 블록으로 설정해야 합니다.
반대로 HEAD 포인터가 실제 블록의 메타데이터를 가리키고 있다면 기존 공간을 재사용할 가능성이 있는지를 검사해야 합니다. 이떄 재사용이 가능하다면 재사용을 할 것이고, 그렇지 않다면 새로운 공간을 할당하게 될 것입니다.
void *malloc(size_t size)
{
struct block_meta *block; // 블록의 메타데이터를 저장할 구조체 포인터
if (size <= 0) // 할당하려는 블록의 크기가 0보다 작다면 NULL 반환
return NULL;
if (global_base == NULL) { // global_base가 NULL이라면, 즉 처음 malloc을 호출했다면
block = request_space(NULL, size); // request_space를 통해 블록 할당
if (!block) { // 할당에 문제가 있었다면
return NULL; // 종료
}
global_base = block; // global_base에 할당된 블록의 메타데이터를 저장
} else {
struct block_meta *last = global_base;
block = find_free_block(&last, size); // global_base부터 시작하여, size만큼의 빈 블록을 찾음
if (!block) { // 빈 블록을 찾지 못했다면
block = request_space(last, size); // request_space를 통해 블록 할당
if (!block) { // 할당에 문제가 있었다면
return NULL; // 종료
}
} else { // 빈 블록을 찾았다면
block->free = 0; // 블록의 메타데이터에 할당되었음을 표시
}
}
}
'CS > Linux OS' 카테고리의 다른 글
Makefile cheatsheet (0) | 2022.11.30 |
---|