Posts

  • 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를 많이 사용하는 것 같다.1

    Install 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
    

    1. 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의 종류

    페이지는 크게 두 분류로 나뉜다.

    1. 익명 페이지(Anonymous page)
    2. 파일-기반 페이지(File-backed page)1

    파일-기반 페이지는 파일으로부터 매핑된 페이지를 뜻한다.
    익명 페이지는 파일으로부터 매핑되지 않은, 커널로부터 할당된 페이지를 뜻한다.
    본 글에서는 file-backed page는 다루지 않는다.

    Anonymous Page

    익명 페이지는 커널로부터 프로세스에게 할당된 일반적인 메모리 페이지이다.
    즉, 익명 페이지는 힙을 거치지 않고 할당받은 메모리 공간이다.
    (힙도 익명 페이지이다. malloc, new 같은 메모리 할당자는 익명 페이지에서 일부 메모리를 잘라 할당 받는것이다.)
    먼저 ‘익명’ 이라는 뜻은 파일에 기반하고 있지 않은(파일로부터 매핑되지 않은) 페이지라는 뜻이다.
    페이지가 파일에 매핑되어 있다면, 그 메모리는 파일 내용을 담고 있을 것이다.
    하지만 익명 페이지는 파일에 매핑되어 있지 않았기 때문에 0으로 초기화된 값을 담고 있다.

    프로세스가 mmap()으로 커널에게 익명 페이지를 할당 요청하게 되면, 커널은 프로세스에게 가상 메모리 주소 공간을 부여하게 된다.
    부여된 가상 메모리 공간은 아직까지는 실제 물리 메모리 페이지로 할당되지 않은 공간이다.
    부여된 가상 메모리는 메모리 읽기 쓰기시, 다음과 같은 커널 도움을 받아 zero 페이지로 에뮬레이션 되거나, 실제 물리 페이지로 매핑된다.

    1. 프로세스가 그 메모리 공간에 읽기 작업 시, 커널은 zero로 초기화된 메모리 페이지 (file-backed page with /dev/zero)을 제공한다.
    2. 프로세스가 그 메모리 공간에 쓰기 작업 시, 커널은 실제 물리 페이지를 할당하고 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_struct2가 있다면, virtual address를 다음과 같이 계산할 수 있지 않을까?

    unsigned long vaddr = vm_area_struct->vm_start + page_offset
    

    page_offset은 virtual address에 매핑할 때, page->index에 미리 저장해 두었다고 가정하자.
    결국, vm_area_struct만 얻어내면 된다. struct pagevm_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_structanon_vma 안에 넣어주면 끝이다.
    다만, 이 방식은 struct page에서 PTE를 얻어내는 방식에 비해 약간 느릴 수 있다.

    1. 기존 방식: struct page -> PTE
    2. 새로운 방식: struct page -> anon_vma -> vm_area_struct -> mm_struct -> page table -> PTE
      추가적인 오버헤드가 생겼으나, 그 오버헤드는 negligible하다.

    Reference

    1. Yizhou Shan’s Home Page
    2. Columbia W4118 by Jungfeng Yang
    3. McCracken, Dave. “Object-based reverse mapping.” Linux Symposium. 2004.
    4. The object-based reverse-mapping VM by corbet

    1. 파일 기반 페이지라는 한글 번역은 구글 번역기에서 내놓은 번역이다. 그 외의 번역이 있는지 모르겠다. 

    2. vm_area_struct는 프로세스 가상 메모리 공간에서 연속된 가상 메모리 공간을 관리하는 구조체이다. 

    3. Yizhou Shan’s Home Page 

  • 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;
        ...
    };
    
    1. 계층화
      • 부모를 표현하기 위한 kobject->parent
      • 내가 소속된 kset (optional)
    2. 참조 카운트 관리
      • kobject_get() - ref count 1 증가
      • kobject_put() - ref conut 1 감소
    3. kobject의 해제, 그리고 kobject 디바이스 파일 RW를 담당하는 ktype
      • 디바이스 파일은 /sys 하위항목에 생성된다.
    4. kobject를 그룹으로 관리하기 위한 kset 1

    즉, kobject는 구조체를 /sys 하위에 노출 시키면서, 구조체를 계층적으로 관리할 수 있게 도와주는 도구라고 보면 된다.

    kobject의 생성

    kobjectkobject_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

    ktypekobject의 해제, 그리고 kobject와 관련된 디바이스 파일 RW를 담당한다.

    struct kobj_type {
        void (*release)(struct kobject* kobj);
        const struct sysfs_ops* sysfs_ops;
        struct attribute** default_attrs;
        ...
    }
    

    releasekobject가 임베딩 된 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 fookobject가 임베딩 되어 있고, release_foo()를 콜하여 struct foo를 메모리 해제할 수 있다.
    release_foo()release에 펑션 포인터로 등록되어 있는데, kref의 ref count가 0이 되면 자동으로 호출된다.

    default_attrsstruct attribute*를 배열로 가지고 있다.
    kobjectkobject_add()에 의해 등록되면 내부에서 struct attribute* 배열 크기 만큼 sysfs_create_file()를 호출한다.
    struct attribute 내부에 name과 access mode를 멤버변수가 있다. 이 멤버변수들을 이용해 sysfs_create_file()으로 디바이스 파일을 /sys/ 하위에 생성한다.

    sysfs_opsstruct 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->parentkset->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가 정상적으로 등록된다.)
    

    1. 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_* 함수들

    1. devm_request_irq()
    2. devm_kmalloc()
    3. devm_get_free_pages()
    4. __devm_alloc_percpu()
    5. 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()을 부르게 되면, 다음과 같이 메모리를 할당하게 된다.

    1. sizeof(devres) + sizeof(custom_devres) 만큼을 커널 메모리(kmem_cache)에서 할당한다.
    2. 만약 할당된 주소를 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()함수를 통해 customdevresdev.devres_head에 링크드 리스트로 연결한다.
    customdevreslist_head를 갖고 있지 않으나, devres.node.entrylist_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(by devm)은 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;
    

    4


    1. 풀 리스트는 Kernel Document, devres.txt에서 확인할 수 있다. 

    2. Zero sized array라고 불리는 기법이며, GCC manual: Array of Length Zero에서 확인할 수 있다. void*도 좋은 방법이나, 포인터 사이즈를 낭비하게 된다. 

    3. container_of()를 통해 변수 data로부터 struct devres 주소를 구한다. 그 뒤, node->entrydev.devres_head에 링크드 리스트로 엮는다. 

    4. 예문 코드 

  • Mar 29, 2021

    task_struct - 2

    task_struct


    exit_state

    exit_state 필드는 태스크가 종료 상태(TASK_DEAD)일 때 가질 수 있는 하위 상태를 나타낸다. 하위 상태는 총 2가지가 있다.

    exit_state의 종료 하위 상태

    종료 하위 상태는 다음과 같이 두 가지가 있다.

    1. EXIT_ZOMBIE
      • 태스크가 좀비 상태로 최소한의 리소스(task_struct 리소스 중 일부)을 들고 있는 상태이다.
      • 최소한의 리소스를 남기는 이유는 부모에게 종료 사실을 알리고 부모가 부모 자신과 관련되어 있는 리소스를 정리할 수 있게 하기 위함이다.
    2. EXIT_DEAD
      • 실제로 task 관련 리소스가 부모에 의해 수거된 상태이다.
      • 부모와 관련되어 있는 task 리소스는 cgroup, /proc 하위 정보, 그리고 pending signal이다.

    태스크의 종료 흐름

    어떤 태스크가 kill signal(9)에 의해 종료되었다고 가정하자.

    1. do_signal() -> do_exit() 형태로 수행된다.
    2. do_exit() 중, exit_notify()가 현재 태스크의 상태를 EXIT_ZOMBIE 상태로 변경한다.
      • 이 때, 부모가 SIGCHLD 시그널을 받을 수 없는 상태(ZOMBIE를 정리할 수 없는 상태)라면 EXIT_DEAD 상태로 변경하고, 부모가 아닌 자체적으로 정리하도록 한다. 1
    3. 이후 do_exit()은 마지막 __schedule(false)를 부르며 자신의 업무를 종료한다.
    4. 부모 프로세스에서는 sys_wait4() 시스템콜에 의하여 자식 프로세스의 리소스 정리를 시작한다.
      • sys_wait4() -> wait_consider_task() -> wait_task_zombie()로 들어간다.
    5. 부모 프로세스는 wait_task_zombie()에서 자식 태스크의 상태를 EXIT_DEAD 상태로 변경한다.
      • 이후, release_task()를 콜하여 부모와 관련되어 있는 task 리소스들을 정리한다.
    6. 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()에 의해 정리되기 바로 직전 상태라고 보면 될 것 같다.


    1. 커널 내부에서는 autoreap 이라고 부르는 과정인 것 같다. 하지만 이 부분은 예외 케이스이기 때문에 넘어가도록 한다.