Linux

Linux kernel process

Roien 2018. 3. 18.
반응형

Process

프로세스는 실행 중인 프로그램

    프로세스는 사용 중인 
        파일, 대기 중인 시그널, 
        커널 내부 데이터, 프로세서 상태,

    하나 이상의 물리적 메모리 영역이 할당된 메모리 주소 공간,
    실행 중인 하나 이상의 스레드 정보,
    전역 데이터가 저장된 데이터 부분등 모든 자원을 포함하는 개념


    
    전통적인 유닉스 시스템에서 프로세스가 하나의 thread로 구성된다.    
    리눅스는 process와 thread를 구분하지 않는다.
    
    thread는 각자 고유한 가상 processor를 할당받지만 (context 관리, cpuinfo handle 의미)
        virtual memory는 공유
    
    
    프로세스는 작동 중인 program 및 그와 관련된 자원을 의미    
    리눅스에서 fork() system call을 통해 process가 만들어진다. 
    
    exec
        exec() 계열의 함수를 호출해 새로운 주소 공간을 만들고 새 프로그램을 불러들일 수 있다.
    
    fork
        fork() system call은 실제로는 clone() system call을 이용해 구현된다.
    
    exit() system call을 호출해 process를 종료
        부모 process는 특정 process가 종료될 때 까지 기다리는 wait4() system call을 이용해
        child process의 종료 상태를 확인할 수 있다.
    
    cf.
        process를 다른 말로 task라고 부른다.
        Linux kernel 내부에서는 process를 task라고 부르는 경우가 많다. 
        (이 책에서는 두 용어를 혼용)
        보통 kernel의 관점에서 process를 지칭하는 경우에 task라는 용어를 사용한다.



Process descirptor & task_struct

    커널은 process 목록을 task list라는 circular double linked list 형태로 저장한다.
    <linux/sched.h> 에 task list의 각 항목인 task_struct가 정의 되어 있다. 
    
    task_struct:
        1.7KB에 달하는 큰 구조체
        사용 중인 file, process address space, 대기 중인 signal, process status, 실행 중인 program 등    

    task_struct 구조체는 stab allocator를 사용해 할당



thread_info @ <asm/thread_info.h>

    thread_info 구조체가 stack의 밑바닥 혹은 꼭대기에 저장된다. (CPU architecture에 따라 다름)

    struct thread_info {
        struct task_struct      *task;
        ...
    };


    +--------------+ 최상위 memory address
    | stack 의 처음      |
    |     |             |
    |     |             |
    |     |             |
    +---|----------+ stack pointer
    |     |             |
    |     v             |
    |                      |
    +--------------+
    |                      |
    | struct              |
    | thread_struct      |
    +------^-------+ 최하위 memory address  <-- current_thread_info()
           |
           +-- process's struct task_struct

    
    thread_info 구조체에 process descriptor pointer가 들어 있다.
    process의 struct task_struct    

    stack의 끝에 thread_info가 위치한다.



process descriptor의 저장

    PID는 pid_t라는 숫자 값 (보통 int 형)
    기본은 short int로서 ~32,768이며, 이는 대용량 서버에서는 부족함.
    
    현재 실행 중인 task의 process descriptor를 빠르게 찾는 방법은 current macro 사용 
    
    curretn_thread_info() 함수가 thread_info 구조체의 위치를 계산한다.
        movl $-8192, %eax
        andl %esp, %eax
        
        위 코드는 stack의 크기가 8KB라고 가정한다.

    current macro는
        current_thread_info()->task;


process state

    다섯 가지 상태 

    TASK_RUNNING
    TASK_INTERRUPTIBLE
    TASK_UNINTERRUPTIBLE
    __TASK_TRACED
    __TASK_STOPPED
    
    
    기존 task가 fork로 
    process 생성 -------------> TASK_RUNNING ----------> TASK_RUNNING ------------> TASK 종료
                   task 생성      ^ ^        schedule()        | |       do_exit                            
                                  | |        context_switch()  | |
                                  | |                          | |
                                  | +--------------------------+ |
                    특정 조건으로 |         선점됨               | 대기열로 이동
                    깨어나 실행   |                              | (휴면 상태)
                    대기열로 이동 +---- TASK_INTERRUPTIBLE <-----+
                                        or
                                        TASK_UNINTERRUPTIBLE
                                         (대기중)


현재 process 상태 조작

    set_task_state(task, state);
    
        task->state = state;



process context

    시스템 호출과 예외 처리기는 잘 정의된 kernel 진입 interface이다.
     - trap, exception

    커널이 실행 중 == 커널이 process context 에 있다.    
    process context에 있을 시, current macro의 사용이 가능하다.



process hierachical tree

    프로세스 간 독특한 계층 구조
    모든 process는 PID가 1인 init process의 자손
    
    모든 process는 정확히 하나의 부모 process를 지님
    하나 이상의 자식 process를 가질 수 있음
    
    형제 process는 sibling이라고 함.

    task_struct 구조체에는 부모의 task_struct를 가리키는 parent pointer와 자식의 task_struct를 가리키는 children pointer가 있음.


    struct task_struct *my_parent = current->parent;
    
    or
    
    struct task_struct *task;
    struct list_head *list;
    
    list_for_each(list, &current->children) {
        task = list_entry(list, struct task_struct, sibling);   // task는 현재 process의 자식 process 중 하나
    }
    

    init task의 process descriptor는 init_task라는 이름으로 정적으로 할당된다.
    
    struct task_struct *task;
    
    for (task = current; task != &init_task; task = task->parent); /* task now points to init */
    
    
    system의 모든 process를 단순히 훑고 싶을 때,

        1) 다음 task 얻기
        list_entry(task->tasks.next, struct task_struct, tasks)
        
        2) 이전 task 얻기
        list_entry(task->tasks.prev, struct task_struct, tasks)


    전체 task list를 열거하는 for_each_process(task) 매크로    
    
        struct task_struct *task;
        
        for_each_process(task) {
            printk(“%s[%d]\n”, task->comm, task->pid);    // 각 task의 이름과 PID 출력
        }


Process creation

    fork(), exec()    
    라는 두 함수로 분리하는 특이한 방식을 사용
    
    exec()은 새로운 실행파일을 주소 공간에 불러오고 이를 실행한다.
    frok() 다음에 exec()을 실행하는 조합은 대부분의 OS에서는 하나의 함수로 제공한다.


copy-on-write

    fork()는 부모 process의 모든 자원을 복사 - clone
    단순하고 비효율적
    
    Linux에서는 copy-on-write page를 이용해 (기록사항 발생 시 복사) frok() 함수를 구현했다.
    프로세스 주소 공간을 복사하는 대신 부모와 자식 process가 같은 공간을 공유한다.
    
    기록 사항이 발생해 변경이 필요하면 그 순간 사본이 만들어지고 각 process가 별도의 내용을 가지게 된다.


process creation

    fork(), vfork(), __clone() library는 각자 적절한 flag를 사용해 clone()을 호출한다.
    
    clone() system call은 다시 do_fork() 함수를 호출
    
    do_fork()에서는 copy_process() 함수를 호출
    
    copy_process는
        1. dup_task_struct()를 호출하여 Kernel stack을 새로 만들고 thread_info, task_struct를 만든다.
        2. process 개수 제한 확인
        3. process descriptor의 값 초기화 
        4. TASK_UNINTERRUPTIBLE 생태로
        5. copy_flasg() 호출하여 task_struct의 flag를 정리
        6. alloc_pid로 PID 값 할당
        7. 열린 file, file system 정보, signal handlers, process address space, name space등을 복제 혹은 공유
        8. 생성된 child process의 pointer를 반환

    do_fork()의 결과는 즉, 새로운 자식 process의 pointer이다.
    이를 깨워서 실행한다.
    Kernel은 의도적으로 child process를 먼저 실행한다.

    자식은 보통 바로 exec()를 호출하기에 parent process가 먼저 실행되면 주소 공간에 쓰기 작업이 생겨 발생하는
    copy-on-write 작업을 막을 수 있다.


vfork()
    자식 process가 부모 공간 내에서 thread로 실행
    
    vfork() system call은 부모 process의 page table을 복사하지 않는다는 점만 fork()와 다름.
    부모 process는 child process가 exec()을 호출하거나 종료할 때까지 wait
    
    child process는 주소 공간의 내용을 바꿀 수 없다. 
    copy-on-write를 이용해 fork()를 구현할 수 없던 예전 3BSD 시절에는 유용했던 기법
    
    지금은 copy-on-write를 사용하기에 vfork()의 이점은 부모 process의 page table을 복사하지 않는 것 뿐
    
    vfork() system call은 clone() system call에 특별한 flag를 지정해 구현한다.
    
        1. copy_process
            task_struct의 vfork_done을 NULL로
        2. do_fork()에서 vfork_done pointer가 특정 주소를 가리키게 함.
        3. vfork_done pointer를 이용해 신호를 보낼 때까지 부모는 자식 반환을 기다림
        4. mm_release에서 vfork_done이 NULL이 아닌지를 확인 NULL이 아니면 부모 process에 신호
        5. do_fork() 함수로 돌아가서 부모 process를 깨우고 반환


Linux의 thread 구현

    리눅스는 기본적인 process로 모든 thread를 구현 (모두 task_struct)
    thread를 위한 별도의 자료구조나 특별한 scheduling 기법을 제공하지 않음
    Linux의 thread는 특정 자원을 다른 process와 공유하는 "특별한 process"일 뿐

    주소 공간과 같은 자원을 다른 process와 공유하고 있는 정상적인 process일 뿐
    (동일 page table을 지님)

    Windows의 process와 thread간의 관계
        4 개의 thread로 구성된 process를 생각해 보면,
        하나의 process descriptor에서 4개의 thread descriptor를 가리키는 정보가 들어 있음

    Linux
        단지 네개의 process가 있으며, 네 개의 정상적인 task_struct 구조체가 존재한다.
        이런 process는 일부 자원을 공유하게 설정되어 있을 것이다. --> 더 명쾌한 구조
        
        주소 공간을 가리키는 mm pointer를 공유 (부모의 것 사용)



thread creation

    thread는 정상적인 task와 마찬가지 방식으로 만들어진다.
    
    clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
        -> 이렇게 공유에 대한 flag들을 지정해서 생성하는 것이 thread이다.

    일반적인 fork은
        clone(SIGCHLD, 0);
    
    vfork는
        clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
    
    
    clone이 사용하는 flag들은 <linux/sched.h>에 정의
    
        CLONE_FILES Parent and child share open files.
        CLONE_FS Parent and child share filesystem information.
        CLONE_IDLETASK Set PID to zero (used only by the idle tasks).
        
        CLONE_NEWNS Create a new namespace for the child.
        CLONE_PARENT Child is to have same parent as its parent
        CLONE_PTRACE Continue tracing child.
        CLONE_SETTID Write the TID back to user-space.
        CLONE_SETTLS Create a new TLS for the child.
        CLONE_SIGHAND Parent and child share signal handlers and blocked signals.
        CLONE_SYSVSEM Parent and child share System V SEM_UNDO semantics.
        CLONE_THREAD Parent and child are in the same thread group.
        CLONE_VFORK vfork() was
        
        CLONE_UNTRACED Do not let the tracing process force CLONE_PTRACE on the
        child.
        CLONE_STOP Start process in the TASK_STOPPED state.
        CLONE_SETTLS Create a new TLS (thread-local storage) for the child.
        CLONE_CHILD_CLEARTID Clear the TID in the child.
        CLONE_CHILD_SETTID Set the TID in the child.
        CLONE_PARENT_SETTID Set the TID in the parent.
        CLONE_VM Parent and child share address space.



kernel thread

    kthread    
        커널 thread에는 주소 공간이 없다. (즉, process의 주소 공간을 가리키는 mm pointer가 NULL)    
        리눅스는 일부 작업을 커널 thread를 통해 처리, 대표적으로 flush 및 ksoftirqd 작업이 예
    
    ps -ef
        명령 시 linux kernel의 kernel thread들을 확인할 수 있음.    
    
    kthreadd kernel process
        리눅스는 kthreadd kernel process가 모든 kernel thread를 만드는 방식으로 kernel thread를 관리

    <linux/kthread.h>
    
    struct task_struct *kthread_create(int (*threadfn)(void *data),
            void *data,
            const char namefmt[],
            ...)

    kthread kernel process는 clone system call을 사용해 새로운 task를 만든다.
    
    kthread_run을 이용하면 바로 실행 가능한 process를 만들 수 있다.
    struct task_struct *kthread_run(int (*threadfn)(void *data),
            void *data,
            const char namefmt[],
            ...)

    #define kthread_run(threadfn, data, namefmt, ...)
    ({ \
        struct task_struct *k; \
        k = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__);  \
        ...
        

    kernel thread는 
        do_exit 함수 호출 
        혹은 kthread_create가 리턴한 task_struct로 kthread_stop 호출 시 까지 계속 실행됨



process 종료

    exit함수 호출 시, 
        main 함수 반환 시 묵시적으로 exit호출
    
    exit시 do_exit 수행
    
        1. task_struct의 flags에 PF_EXITING 설정
        2. 커널 timer 제거
        3. acct_update_integrals()로 BSD 관련 정보 기록. (BSD 방식 process 정보 기록 사용 시)
        4. exit_mm --> mm_struct를 반환
        5. exit_sem
        6. exit_files
        7. exit_code 설정
        8. exit_notify -> 부모에 signal
        9. schedule로 다른 process로 전환

    이 시점에서 task와 관련된 모든 객체가 반환
        EXIT_ZOMBIE 상태
    
    부모 process에 전달이 필요한 정보를 보관하기 위해서만 존재 
     . thread_info
     . task_struct
    
    커널이 정보가 더 이상 필요 없다고 알려주면 반환됨



process descriptor 제거

    do_exit 완료 후, 
        process descriptor는 여전히 남겨진 ZOMBIE 상태인데, 
    
    결국 process 종료와 process descriptor를 제거하는 작업은 분리된 별도의 작업
    
    wait() 계열 함수는 하나의 wait4() system call을 통해 구현된다.
    기본 동작은 함수를 호출한 process의 동작을 자식 process가 종료될 때까지 정지시키는 것이며  
    마침내 process descriptor에 할당된 memory를 제거해야 할 때가 되면 release_task() 함수를 호출하고
        1. __exit_signal(), __unhash_process, detach_pid
        2. __exit_signal()
        3. 부모에 좀비가 된 것을 알림
        4. put_task_struct 호출로 kernel stack 및 thread_info 구조체가 들어 있던 page를 반환
           task_struct 구조체가 들어 있던 slab cache를 반환



부모 없는 task의 딜레마

    부모 process가 자식 process보다 먼저 종료된 경우,
        다른 process를 자식 process의 부모로 지정하는 수단이 반드시 필요
        그렇지 않다면, 부모를 읽고 종료된 process는 영원히 좀비 process로 남아서 memory를 leak

    해결책
        해당 process가 속한 thread 군의 다른 process를 부모로 지정
        혹은 init을 부모로 지정
    
    do_exit는 exit_notify를 호출하고 여기서 forget_original_parent()를 호출
        find_new_reaper()를 호출 --> 여기서 부모 process 재지정 처리


    task가 추적(__TASK_TRACED) 상태에 있는 경우, 해당 process는 디버깅 process를 부모 process로 임시로 변경된다.
    
    자식 process list를 별도로 관리하는 간단한 방법.
    이를 통해 자식 process를 찾으려고 모든 process를 뒤지던 작업을 상대적으로 작은 두개의 프로세스 리스트를 탐색하는 
    작업으로 줄일 수 있다.


    init process는 주기적으로 wait 함수를 호출해 자신에게 할당된 좀비 process를 정리한다.

        - 자식 process의 종료 까지 정지
        - 종료된 자식 process의 PID를 반환 받음

반응형

댓글