- Apr 25, 2021
kubernetes bootstrap on Linux
Kubernetes Cluster Setup on Linux
전반적인 흐름은 먼저 컨테이너 런타임을 설치한다.
컨테이너 런타임은 일반적으로 도커를 많이 사용한다. 다른 선택지로는 containerd가 있으며, 그 외에도 lightVM을 컨테이너 런타임으로 사용하는 방법도 있다.
그 뒤, kubeadm과 kubelet, kubectl을 설치한다. kubeadm은 클러스터를 쉽게 초기화 해주는 툴이며, kubectl은 컨트롤 노드에서 각종 클러스터 세팅을 도와주는 클라이언트다. kubectl은kube-system
의 팟들과 kube-api-server를 통해 컨트롤 한다.
kubelet은 각 노드를 컨트롤 하는 에어전트이며, kubelet이 팟을 띄우는 역할을 한다.그 외로 etcd도 있으며, k8s가 쓰는 storage라고 보면 된다.
kubeadm을 이용하여 클러스터를 셋업한 뒤, 네트워크를 설정해 준다.
네트워크는 아래에서는 Flannel을 사용하지만 일반적으로 Calico를 많이 사용하는 것 같다.1Install Docker
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository \ "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) \ stable" sudo apt-get update sudo apt-get install -y docker-ce=18.06.1~ce~3-0~ubuntu sudo apt-mark hold docker-ce
Install kubeadm, kubelet, and kubectl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - cat << EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list deb https://apt.kubernetes.io/ kubernetes-xenial main EOF sudo apt-get update sudo apt-get install -y kubelet=1.14.5-00 kubeadm=1.14.5-00 kubectl=1.14.5-00 sudo apt-mark hold kubelet kubeadm kubectl
Init kube master node
kubeadm은 kubernetes control plane을 매니징 하는 툴이다.
sudo kubeadm init --pod-network-cidr=string # ref https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-init/ # in kubeadm init log # kubectl refer this config mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config # in master, server/client info will be shown # in slave, only client info will be shown kubectl version
Setup Cluster
# in kubeadm init log # do this on each slave nodes sudo kubeadm join $some_ip:6443 --token $some_token --discovery-token-ca-cert-hash $some_hash # on master node # check there are master and slave nodes kubectl get nodes
Configure Network
echo "net.bridge.bridge-nf-call-iptables=1" | sudo tee -a /etc/sysctl.conf sudo sysctl -p kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/bc79dd1505b0c8681ece4de4c0d86c5cd2643275/Documentation/kube-flannel.yml kubectl get nodes
-
https://medium.com/@jain.sm/flannel-vs-calico-a-battle-of-l2-vs-l3-based-networking-5a30cd0a3ebd ↩
-
- Apr 18, 2021
About anonymous page
Reference: https://www.kernel.org/doc/html/latest/admin-guide/mm/concepts.html
Page의 종류
페이지는 크게 두 분류로 나뉜다.
- 익명 페이지(
Anonymous page
) - 파일-기반 페이지(
File-backed page
)1
파일-기반 페이지는 파일으로부터 매핑된 페이지를 뜻한다.
익명 페이지는 파일으로부터 매핑되지 않은, 커널로부터 할당된 페이지를 뜻한다.
본 글에서는file-backed page
는 다루지 않는다.Anonymous Page
익명 페이지는 커널로부터 프로세스에게 할당된 일반적인 메모리 페이지이다.
즉, 익명 페이지는 힙을 거치지 않고 할당받은 메모리 공간이다.
(힙도 익명 페이지이다. malloc, new 같은 메모리 할당자는 익명 페이지에서 일부 메모리를 잘라 할당 받는것이다.)
먼저 ‘익명’ 이라는 뜻은 파일에 기반하고 있지 않은(파일로부터 매핑되지 않은) 페이지라는 뜻이다.
페이지가 파일에 매핑되어 있다면, 그 메모리는 파일 내용을 담고 있을 것이다.
하지만 익명 페이지는 파일에 매핑되어 있지 않았기 때문에 0으로 초기화된 값을 담고 있다.프로세스가
mmap()
으로 커널에게 익명 페이지를 할당 요청하게 되면, 커널은 프로세스에게 가상 메모리 주소 공간을 부여하게 된다.
부여된 가상 메모리 공간은 아직까지는 실제 물리 메모리 페이지로 할당되지 않은 공간이다.
부여된 가상 메모리는 메모리 읽기 쓰기시, 다음과 같은 커널 도움을 받아 zero 페이지로 에뮬레이션 되거나, 실제 물리 페이지로 매핑된다.- 프로세스가 그 메모리 공간에 읽기 작업 시, 커널은 zero로 초기화된 메모리 페이지 (file-backed page with
/dev/zero
)을 제공한다. - 프로세스가 그 메모리 공간에 쓰기 작업 시, 커널은 실제 물리 페이지를 할당하고 write된 데이터를 보관한다.
익명 페이지는
private
또는shared
로 할당받을 수 있다.
프로세스의 힙과 스택이private
로 할당된anonymous page
이다.
shared
는 프로세스간 통신을 위해 사용되는anonymous page
이다.익명 페이지를 할당 받으려면 다음과 같은 코드가 이용된다.
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0); // anonymous page는 fd가 -1이여야 하며, offset은 0이여야 한다. // 이후 익명 페이지를 읽으면 0으로 초기화된 값이 읽히며, // 익명 페이지를 쓰면 실제 물리 메모리 페이지가 할당된다.
Reverse Mapping of Anonymous page
To be moved to reverse mapping section
시스템에서 address는 virtual address에서 physical address로 변환된다.
변환되는 과정은 page table을 통해 이루어진다.
그러면 physical address에서 virtual address로 변환해야 하는 경우 어떨까?
그런 일이 필요할까?
우리는 physical address에서 virtual address로 주소를 변환하는 과정을 reverse mapping이라 부른다.
(정확히는 page table의 PTE(virtual address)를 알아내는 작업이다.)왜 reverse mapping이 필요한가?
대표적으로 page reclaiming이 있다.
어떤 물리 페이지를 swapout하고 싶다면, 관련된 모든 virtual address(PTE)를 찾아 invalidate시켜 주어야 한다. 이 때 reverse mapping이 필요하다.초기의 reverse mapping
단순히
struct page
에 각각의 PTE를 linked list 형태로 가지고 있어, 바로 pte에 접근할 수 있었다.
하지만 process 개수가 증가할수록struct page
가 가진 PTE의 수가 선형적으로 증가한다는 문제가 있었다.
file-backed page에서 shared library의 경우 전체 프로세스의 개수만큼 PTE 정보를 유지해야 했다.
실제 kernel 2.6에서는 이러한 방식으로 관리가 되었고, 메모리 낭비가 상당히 심했다.Reverse mapping key idea
결국
struct page
가 linked list의 형태로 PTE를 가지고 있기 때문에 메모리 낭비가 심했다.
메모리 낭비를 줄이기 위해 object-based reverse mapping이 등장하였다.
Revserse mapping의 목적은 physical address로부터 PTE를 얻어내는 것이다.
만약 physical address로부터 virtual address를 얻어낸다면, page table을 통해 PTE를 쉽게 얻어낼 수 있다.
이 때문에,struct page
가 매핑된vm_area_struct
2가 있다면, virtual address를 다음과 같이 계산할 수 있지 않을까?unsigned long vaddr = vm_area_struct->vm_start + page_offset
page_offset
은 virtual address에 매핑할 때,page->index
에 미리 저장해 두었다고 가정하자.
결국,vm_area_struct
만 얻어내면 된다.struct page
가vm_area_struct
만 들고 있으면 해결되는 문제이다.
문제는vm_area_struct
는 프로세스의 개수, 그리고 프로세스 내 매핑에 따라 여러개가 존재한다.
여러vm_area_struct
가 존재하므로 이를 묶어서 관리해야 한다.
묶어서 관리하는 구조체를anon_vma
라 하자.
3다음과 같이
anon_vma
가 여러vm_area_struct
를 들고 있고, 이를 이용해 virtual address를 구한다.
virtual address를mm_struct
에 있는 page table을 이용하여 PTE를 구하게 된다.여기서 중요한 점은
page->index
가 한개(위 그림에서page descr
)임을 상기하자.
즉,page_offset
은 다른 프로세스의 가상주소 공간 내에서도 바뀌지 않음을 의미한다.
심지어mremap()
을 통해 다른 virtual address로 매핑시킨다 하더라도,page_offset
은 변하지 않는다.mremap - remap a virtual memory address
…
(offsets relative to the starting address of the mapping
should be employed).이런식으로 physical address로부터 PTE를 구할 수 있고,
문제가 되었던 메모리 낭비 문제는 프로세스마다 가지고 있는vm_area_struct
를 재활용함으로써 해결할 수 있었다.
만약 프로세스가 하나 더 생기면 그 프로세스의vm_area_struct
를anon_vma
안에 넣어주면 끝이다.
다만, 이 방식은struct page
에서PTE
를 얻어내는 방식에 비해 약간 느릴 수 있다.- 기존 방식: struct page -> PTE
- 새로운 방식: struct page -> anon_vma -> vm_area_struct -> mm_struct -> page table -> PTE
추가적인 오버헤드가 생겼으나, 그 오버헤드는 negligible하다.
Reference
- Yizhou Shan’s Home Page
- Columbia W4118 by Jungfeng Yang
- McCracken, Dave. “Object-based reverse mapping.” Linux Symposium. 2004.
- The object-based reverse-mapping VM by corbet
- 익명 페이지(
- Apr 11, 2021
About kobject
Reference: https://lwn.net/Articles/54651/
Kernel Object
kobject
는 kernel 내부에서 구조체를 관리하기 위한 도구이다.
이 도구는 독립적으로는 잘 쓰이지 않으며, 주로(99.9%) 구조체에 임베딩 되어 쓰인다.
자신을 임베딩한 구조체를 관리하기 위한 기능을 여러가지 내장하고 있는데 다음과 같은 기능을 내장하고 있다.// kobject를 임베딩한 struct foo struct foo { struct kobject kobj; ... };
- 계층화
- 부모를 표현하기 위한
kobject->parent
- 내가 소속된
kset
(optional)
- 부모를 표현하기 위한
- 참조 카운트 관리
kobject_get()
- ref count 1 증가kobject_put()
- ref conut 1 감소
kobject
의 해제, 그리고kobject
디바이스 파일 RW를 담당하는ktype
- 디바이스 파일은
/sys
하위항목에 생성된다.
- 디바이스 파일은
kobject
를 그룹으로 관리하기 위한kset
1
즉,
kobject
는 구조체를/sys
하위에 노출 시키면서, 구조체를 계층적으로 관리할 수 있게 도와주는 도구라고 보면 된다.kobject의 생성
kobject
는kobject_init()
으로 생성할 수 있다.void kobject_init(struct kobject* kobj, struct kobj_type* ktype)
위에서 볼 수 있듯이, 임의의
kobj
를 인자로 받고,ktype
으로kobj
의 관리 정보들을 결정한다.
초기화된kobject
를 sysfs에 등록시키기 위해서는kobject_add()
를 이용한다.void kobject_add(struct kobject* kobj, struct kobject* parent, const char* fmt, ...)
이 때, sysfs root에 등록시키려면
parent == NULL
을 변수로 하여 호출한다.ktype
ktype
은kobject
의 해제, 그리고kobject
와 관련된 디바이스 파일 RW를 담당한다.struct kobj_type { void (*release)(struct kobject* kobj); const struct sysfs_ops* sysfs_ops; struct attribute** default_attrs; ... }
release
는kobject
가 임베딩 된 struct의 해제를 담당한다.struct foo { struct kobject kobj; ... }; void releasea_foo(struct kobject* kboj) { struct foo* p = container_of(kobj, struct foo, kobj); kfree(p) } static struct kobj_type foo_ktype { .release = release_foo, .default_attrs = attrs_foo, .sysfs_ops = sysfs_ops_foo, };
위에서 알 수 있듯이,
struct foo
에kobject
가 임베딩 되어 있고,release_foo()
를 콜하여struct foo
를 메모리 해제할 수 있다.
release_foo()
는release
에 펑션 포인터로 등록되어 있는데,kref
의 ref count가 0이 되면 자동으로 호출된다.default_attrs
는struct attribute*
를 배열로 가지고 있다.
kobject
가kobject_add()
에 의해 등록되면 내부에서struct attribute*
배열 크기 만큼sysfs_create_file()
를 호출한다.
struct attribute
내부에 name과 access mode를 멤버변수가 있다. 이 멤버변수들을 이용해sysfs_create_file()
으로 디바이스 파일을/sys/
하위에 생성한다.sysfs_ops
는struct attribute
에 의해 생성된 디바이스 파일을 접근(read/write)할 때, 동작할 핸들러를 지정한다.struct sysfs_ops { ssize_t (*show)(...); ssize_t (*store)(...); }
show()
는struct attribute
에 의해 생성된 디바이스 파일들을 읽을 때, 호출되며store()
는 디바이스 파일을 쓸 때 호출된다.서로 다른 디바이스 파일을 읽고 쓸 때, 똑같은
struct sysfs_ops
가 호출된다.
하지만 상식적으로 당연히 다른 디바이스 파일을 읽으면 다른struct sysfs_ops
가 호출되어야 한다.
이 때문에 다음과 같이 트릭을 쓰게 된다.struct custom_attr { struct attribute attr; ssize_t (*show)(...); ssize_t (*store)(...); } static ssize_t sysfs_show(struct kobject* kobj, struct attribute* attr, char* buf) { struct custom_attr* cattr = container_of(attr, struct custom_attr, attr) if (likely(cattr->show)) return cattr->show(buf); return -EIO; }
위에서
sysfs_show()
는struct sysfs_ops
에 등록된 common operation이다.
attribute
에 해당하는 custom operation을 호출하기 위해,attribute
를 포함하는 구조체에 원하는 handler를 등록해놓고container_of()
를 통해 핸들러를 접근하면 된다.kref
참조 카운트를 관리하는 변수이다.
만약 구조체의 스마트한 해제기능만을 사용하기 위해서는kobject
를 쓰지 말고,kref
만을 포함하여 쓰는것이 맞다.
kobject_init()
는kobject
를 만들 시, ref count를 1로 셋팅한다.
kobject_get()
은 ref count를 1 올리며,kobject_put()
은 ref count를 1 내린다.
만약kobject
의 ref count가 0이 되면ktype->release()
를 호출한다.일반적으로 자식이 생기면 부모의
kobject
의 ref count가 증가되며, 자식이 해제되면 ref count가 감소된다.kset
kobject
를 그룹으로 관리하기 위한 구조체이며, 자체적으로kobject
를 포함하고 있다.
계층 구조를 만드려면kobject
내부에 있는kset
을 이용한다.
kobject->kset
을 설정하고kobject_add()
를 호출하면kset
리스트에kobject
가 등록되며,kobject->parent
는kset->kobj
가 된다.kobj->kset = kset_create(...); kobject_add(kobj, ...); // kobject_add에 parent가 NULL이면, kobj->parent == kset->kobj가 되며, kset 내부 리스트에 kobj가 등록된다. // 단, kobject_add()에 parent가 있다면 kobj->parent == parent이다. (kset 내부 리스트에는 kobj가 정상적으로 등록된다.)
-
kobject->kset
를 설정하고kobject_add()
를 호출하면 자동으로 현 kobject ↩
- 계층화
- Apr 4, 2021
About devm
Device Resource Management
devm이란?
리눅스 커널 디바이스 드라이버는 시스템 stability에 영향을 크게 끼친다.
이 때문에, 리눅스 커널 모듈은 자신이 할당한 리소스에 대하여 세밀한 관리가 필요하다.
하지만 가끔 디바이스 드라이버를 작성할때, 리소스에 대한 관리가 잘 되지 않을 때가 있다.
(예를 들어, irq handler를 등록한 후, irq handler를 리소스 해제하지 않는 행위 등을 뜻한다.)
일반 응용 개발과 달리, 커널 리소스가 제대로 관리되지 않은 경우, 시스템 크래시를 유발할 수 있다.
이를 위해 리눅스 커널은devm_*
이라는 함수를 가지고 있다.
이 함수는struct device
가 할당 리소스의 생명 주기를 모두 관리할 수 있게 해주며,
struct device
가 해제될 시, 관리되고 있는 모든 리소스가 해제된다.
(임의의 리소스를 해제할 수 있는 함수 또한 제공한다.)
devm_*
함수로 할당된 모든 리소스는devres
라는 용어로 통칭된다.devm_* 함수 리스트
devm_* 함수들
devm_request_irq()
devm_kmalloc()
devm_get_free_pages()
__devm_alloc_percpu()
devm_***
등1
위 함수들은 앞 인자에
struct device* dev
가 있다는것이 다를 뿐, 그 이외에는 같다.devm_* 함수들의 동작 원리
할당
할당은 다음과 같은 컨셉으로 이루어진다.
{ struct custom_devres* customdevres = devres_alloc(devm_xx_release, sizeof(custom_devres), GFP_KERNEL); // custom setting to custom_devres customdevres.order = a // devres_add(dev, customdevres); }
위에서
devres_alloc()
을 부르게 되면, 다음과 같이 메모리를 할당하게 된다.sizeof(devres) + sizeof(custom_devres)
만큼을 커널 메모리(kmem_cache)에서 할당한다.- 만약 할당된 주소를
X
라고 하면,customdevres
에는X + sizeof(devres)
를 리턴하게 된다.
메모리 레이아웃은 다음과 같다.
struct devres { struct devres_node node; u8 __aligned(ARCH_KMALLOC_MINALIGN) data[]; --> struct custom_devres; };
union을 활용하면 다음과 같은 의미를 같는다.
struct devres { struct devres_node node; // linked list head를 갖고 있는 부분 union { u8 data[]; struct custom_devres customdevres; } };
다만 리눅스 커널은
union
형태처럼 정적 형태로 자료구조를 선언할 수 없기 때문에u8 data[];
와 같이 변수를 선언한다. 2
devres_add()
함수를 통해customdevres
를dev.devres_head
에 링크드 리스트로 연결한다.
customdevres
는list_head
를 갖고 있지 않으나,devres.node.entry
가list_head
형태이므로 링크드 리스트로 연결할 수 있다.3해제
devres_release_all()
은 모든 리소스와struct devres
를 해제한다.
devres_release_all()
은dev->devres_head
에 링크되어 있는 모든struct devres
를 커스텀 리소스 해제 함수를 부른 뒤,sturct devres
를 해제한다.
devres_remove()
는 이와 다르게 리소스를 해제하지 않고,struct devres
만을 해제한다.devres group
devres
를 grouping해서 관리할 수 있는 기능이다.
devres_open_group()
을 통해 이후 할당되는devres
(bydevm
)은 opened group에 묶이게 된다.
그리고devres_close_group()
을 통해 group을 닫을 수 있다.
그룹 id에 어떠한 변수라도 넣을 수 있으며,NULL
인경우 자동적으로 최근 open된 group이 선택된다.
재미있는것은devres_open_group()
을 콜 하고 이후devres
관련된 함수 콜을 진행한 뒤,devres_close_group()
으로 그룹을 닫으면, 다음과 같은 링크드 리스트가dev.devres_head
에 생성된다.<: open group (devres_group) kmalloc: devres iomap_resource: devres >: closed group (devres_group)
이후
dev_release_group()
을 호출하게 되면, group id에 해당하는<, >
을 찾고, 그 안에 있는devres
들을 해제하게 된다.
만약 어떠한 에러가 발생하여>
가 발견되지 않으면,<
로 시작하는 모든devres
를 할당 해제하게 된다.if (!devres_open_group(dev, NULL, GFP_KERNEL)) return -ENOMEM; acquire A; if (failed) goto err; acquire B; if (failed) goto err; ... devres_close_group(dev, NULL); return 0; err: devres_release_group(dev, NULL); return err_code;
-
풀 리스트는 Kernel Document, devres.txt에서 확인할 수 있다. ↩
-
Zero sized array라고 불리는 기법이며, GCC manual: Array of Length Zero에서 확인할 수 있다. void*도 좋은 방법이나, 포인터 사이즈를 낭비하게 된다. ↩
-
container_of()
를 통해 변수data
로부터struct devres
주소를 구한다. 그 뒤,node->entry
를dev.devres_head
에 링크드 리스트로 엮는다. ↩
- Mar 29, 2021
task_struct - 2
task_struct
exit_state
exit_state
필드는 태스크가 종료 상태(TASK_DEAD
)일 때 가질 수 있는 하위 상태를 나타낸다. 하위 상태는 총 2가지가 있다.exit_state의 종료 하위 상태
종료 하위 상태는 다음과 같이 두 가지가 있다.
EXIT_ZOMBIE
- 태스크가 좀비 상태로 최소한의 리소스(
task_struct
리소스 중 일부)을 들고 있는 상태이다. - 최소한의 리소스를 남기는 이유는 부모에게 종료 사실을 알리고 부모가 부모 자신과 관련되어 있는 리소스를 정리할 수 있게 하기 위함이다.
- 태스크가 좀비 상태로 최소한의 리소스(
EXIT_DEAD
- 실제로 task 관련 리소스가 부모에 의해 수거된 상태이다.
- 부모와 관련되어 있는 task 리소스는 cgroup, /proc 하위 정보, 그리고 pending signal이다.
태스크의 종료 흐름
어떤 태스크가 kill signal(9)에 의해 종료되었다고 가정하자.
do_signal() -> do_exit()
형태로 수행된다.do_exit()
중,exit_notify()
가 현재 태스크의 상태를EXIT_ZOMBIE
상태로 변경한다.- 이 때, 부모가
SIGCHLD
시그널을 받을 수 없는 상태(ZOMBIE를 정리할 수 없는 상태)라면EXIT_DEAD
상태로 변경하고, 부모가 아닌 자체적으로 정리하도록 한다. 1
- 이 때, 부모가
- 이후
do_exit()
은 마지막__schedule(false)
를 부르며 자신의 업무를 종료한다. - 부모 프로세스에서는
sys_wait4()
시스템콜에 의하여 자식 프로세스의 리소스 정리를 시작한다.sys_wait4() -> wait_consider_task() -> wait_task_zombie()
로 들어간다.
- 부모 프로세스는
wait_task_zombie()
에서 자식 태스크의 상태를EXIT_DEAD
상태로 변경한다.- 이후,
release_task()
를 콜하여 부모와 관련되어 있는 task 리소스들을 정리한다.
- 이후,
task_struct
메모리 자체는 SoftIRQ 핸들러에 의해 정리된다.task_struct
는 부모가 task 관련 정보를 정리한 뒤,SoftIRQ
핸들러로써delayed_put_task_struct()
가 실행되어 reference count가 0인 경우task_struct
를 해제한다.- 3번 흐름 이후,
context_switch() -> finish_task_switch()
에서도task_struct
해제가 가능하나, 일반적으로 레퍼런스 카운트만 줄이는것으로 보인다.
결론
즉,
EXIT_ZOMBIE
는 종료는 되었지만 부모가 리소스를 정리하기 전, 태스크 기능을 할 수 없는 시체(?)같은 태스크 종료 상태라고 보면 될 것 같다.
EXIT_DEAD
는 태스크가 완전히 정리되어__put_task_struct()
에 의해 정리되기 바로 직전 상태라고 보면 될 것 같다.
-
커널 내부에서는 autoreap 이라고 부르는 과정인 것 같다. 하지만 이 부분은 예외 케이스이기 때문에 넘어가도록 한다. ↩
Posts