do_fork()

 

프로세스 생성

리눅스 유저 application에서 fork() 함수를 사용할 때 공유 라이브러리인 glibc를 통해 더 이상 fork syscall을 호출하지 않고 clone syscall을 호출한다. 때문에 clone과 fork는 동일하게 사용한다. 리눅스 커널은 backward 호환을 이유로 fork에 대한 syscall을 열어둔 상태이긴 하지만 거의 사용되지 않는다고 보면된다. 그 동안 fork와 vfork를 많이 비교하는 글들이 있지만 리눅스 커널 버전이 향상되면서 논쟁이 되었던 글들이 조금씩 핀트를 벗어나고 있다. 커널이 v4.x에 이르렀으므로 이제 조금 정리를 해보고자 한다.

 

최초 리눅스 구현 시 vfork가 fork에 비해 매우 light한 구동을 보여줬다. vfork로 생성되는 자식 프로세스는 부모 프로세스와 파이프 등을 사용하여 교류를 할 수 없고 단지 부모 프로세스가 자식 프로세스에게 argument만 전달하고 반대로 종료(exit) 결과만 부모 프로세스에 알려줄 수 있다. 이제는 과거지만 그러한 점 때문에 심플하게 프로세스를 구동하고자 할 때에는 vfork를 선호하였었다. 그러나 추후 리눅스가 fork에 COW(Copy On Write) 기능을 사용하면서 fork 역시 light한 동작을 갖게되었다. 이로인해 vfork를 사용해야 할 커다란 이유가 없어졌고, vfork의 사용은 추천되지 않게되었다. 최종적으로 vfork가 fork(clone)와 매우 유사하게 되었는데, 정확히 vfork와 fork(clone)의 차이 점을 구별해 낼 수 없다면 그냥 fork(clone)를 사용하여야 한다.

 

fork vs vfork

  • fork와 달리 vfork는부모 프로세스와 파이프를 통한 교류를 할 수 없다.
  • vfork는 자식 태스크를 생성시 부모 프로세스를 잠시 블럭한다.
  • vfork는 스레드와 같은 동작을 하도록 CLONE_VM을 사용하여 메모리의 사용량을 대폭 줄였다.
    • 부모 프로세스가 사용하는 mm 디스크립터 생성하지 않고 공유한다.
    • 부모 프로세스가 사용하는 페이지 테이블(pgd)을 공유한다.
      • fork는 새 페이지 테이블(pgd)을 사용하여 COW(Copy On Write) 동작으로 필요 시 마다 메모리를 할당하여 사용한다.
    • 부모 프로세스가 사용하는 vm 영역을 복사하지 않고 공유한다.
    • 예) 부모 프로세스가 20M의 메모리를 사용할 때 자식 프로세스가 사용하는 메모리 비교
      • fork: 약 12 ~ 21M
        • 처음 fork를 하였을 때에는 COW 동작에 메모리를 복사하지 않지만 write 동작이 가해지면 메모리를 할당하여 사용한다.
        • 부모 태스크가 사용했었던 vm을 그대로 상속하기 위해 vma 및 페이지 테이블을 새롭게 복사하여 사용한다.
      • vfork: 약 1.5M
        • application에 대해서는 동일한 가상 주소 메모리를 공유하여 사용하므로 자식 프로세스 관리에 필요한 메모리만 조금 추가된다.
  • 참고

 

 

스레드 생성

프로세스보다 가벼운 스레드가 필요한 경우에는 커널 v2.6부터 구현되어 현재까지도 사용되는 NPTL 라이브러리(-lpthread)를 사용한 POSIX thread를 사용한다. pthread_create() 함수를 사용 시 CLONE_VM을 추가한 clone syscall을 사용한다. 이를 통해 light한 메모리 사용과 빠른 스레드 생성을 할 수있다.

 

리눅스 태스크 생성

do_fork()

 

다음 그림은 유저 모드에서 syscall을 통해 프로세스나 스레드를 생성할 때 대응하는 커널 함수와 CLONE 플래그들을 보여준다.

 

다음 그림은 태스크를 생성 시 CLONE 플래그에 따라 각 루틴이 하는 일들을 보여준다.

  • CLONE 플래그에 해당하는 항목이 설정된 경우 함수에서의 동작이 노란 색 항목과 같이 수행된다.

 

kernel/fork.c -1/2-

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
long do_fork(unsigned long clone_flags,
              unsigned long stack_start,
              unsigned long stack_size,
              int __user *parent_tidptr,
              int __user *child_tidptr)
{
        struct task_struct *p;
        int trace = 0;
        long nr;

        /*
         * Determine whether and which event to report to ptracer.  When
         * called from kernel_thread or CLONE_UNTRACED is explicitly
         * requested, no event is reported; otherwise, report if the event
         * for the type of forking is enabled.
         */
        if (!(clone_flags & CLONE_UNTRACED)) {
                if (clone_flags & CLONE_VFORK)
                        trace = PTRACE_EVENT_VFORK; 
                else if ((clone_flags & CSIGNAL) != SIGCHLD)
                        trace = PTRACE_EVENT_CLONE;
                else
                        trace = PTRACE_EVENT_FORK;

                if (likely(!ptrace_event_enabled(current, trace)))
                        trace = 0;
        }

        p = copy_process(clone_flags, stack_start, stack_size,
                         child_tidptr, NULL, trace);

부모 태스크를 복사(fork)한 새 태스크를 스케줄러에서 깨운다. CLONE 플래그 요청에 따라 각각의 리소스에 대해 부모 리소스를 같이 공유하여 사용하거나 별도로 새로 생성된 태스크에 독립적으로 리소스를 사용하도록 배정한다.

  • 코드 라인 23~33에서 CLONE_UNTRACED 커널 옵션을 지정하여 요청한 경우 ptreacer로 이벤트를 리포팅하지 않도록 제한시킨다. 또한
    • kernel_thread() 함수를 통해 만든 커널 스레드는 항상 CLONE_UNTRACED 커널 옵션을 사용한다.
  • 코드 라인 35~36 copy_process() 함수를 호출하여 각 플래그 요청을 기반으로 자원의 공유 여부를 결정하고 부모 태스크를 기반으로 새 태스크를 상속받아 만든다. (clone)

 

kernel/fork.c -2/2-

        /*
         * Do this prior waking up the new thread - the thread pointer
         * might get invalid after that point, if the thread exits quickly.
         */
        if (!IS_ERR(p)) {
                struct completion vfork;
                struct pid *pid;

                trace_sched_process_fork(current, p);

                pid = get_task_pid(p, PIDTYPE_PID);
                nr = pid_vnr(pid);

                if (clone_flags & CLONE_PARENT_SETTID)
                        put_user(nr, parent_tidptr);

                if (clone_flags & CLONE_VFORK) {
                        p->vfork_done = &vfork;
                        init_completion(&vfork);
                        get_task_struct(p);
                }

                wake_up_new_task(p);

                /* forking complete and child started to run, tell ptracer */
                if (unlikely(trace))
                        ptrace_event_pid(trace, pid);

                if (clone_flags & CLONE_VFORK) {
                        if (!wait_for_vfork_done(p, &vfork))
                                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
                }

                put_pid(pid);
        } else {
                nr = PTR_ERR(p);
        }
        return nr;
}
  • 코드 라인 11~12에서 글로벌 pid 값을 가져온다. 참조 카운터를 1 증가시킨다. 태스크가 소속된 namespace에서 upid 번호를 알아온다.
  • 코드 라인 14~15에서 유저 모드에서 clone syscall을 통해 즉, sys_clone() 함수를 통해 호출된 경우 부모 태스크의 tid에 child 태스크의 upid 번호를 설정한다.
  • 코드 라인 17~21에서 CLONE_VFORK 플래그를 사용하여 요청한 경우 fork 완료 대기를 위해 준비한다.
  • 코드 라인 23에서 생성된 새 태스크를 깨워 동작시킨다.
  • 코드 라인 29~32에서 CLONE_VFORK 플래그를 사용하여 요청한 경우 태스크가 종료될 때 까지 호출한 부모 태스크는 대기한다.
    • 리눅스에서 fork 대신 vfork를 사용하면 부모와 자식간의 race가 발생하지 않도록 부모 태스크는 여기서 잠시 block되어 있다가 자식 task가 fork된 후에야 wakeup한다.
  • 코드 라인 34에서 pid의 참조 카운터를 1 감소시킨다.

 

kernel/fork.c -1/8-

/*
 * This creates a new process as a copy of the old one,
 * but does not actually start it yet.
 *
 * It copies the registers, and all the appropriate
 * parts of the process environment (as per the clone
 * flags). The actual kick-off is left to the caller.
 */
static struct task_struct *copy_process(unsigned long clone_flags,
                                        unsigned long stack_start,
                                        unsigned long stack_size,
                                        int __user *child_tidptr,
                                        struct pid *pid,
                                        int trace)
{
        int retval;
        struct task_struct *p;

        if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
                return ERR_PTR(-EINVAL);

        if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
                return ERR_PTR(-EINVAL);

        /*
         * Thread groups must share signals as well, and detached threads
         * can only be started up within the thread group.
         */
        if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
                return ERR_PTR(-EINVAL);

        /*
         * Shared signal handlers imply shared VM. By way of the above,
         * thread groups also imply shared VM. Blocking this case allows
         * for various simplifications in other code.
         */
        if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
                return ERR_PTR(-EINVAL);

        /*
         * Siblings of global init remain as zombies on exit since they are
         * not reaped by their parent (swapper). To solve this and to avoid
         * multi-rooted process trees, prevent global and container-inits
         * from creating siblings.
         */
        if ((clone_flags & CLONE_PARENT) &&
                                current->signal->flags & SIGNAL_UNKILLABLE)
                return ERR_PTR(-EINVAL);

        /*
         * If the new process will be in a different pid or user namespace
         * do not allow it to share a thread group or signal handlers or
         * parent with the forking task.
         */
        if (clone_flags & CLONE_SIGHAND) {
                if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
                    (task_active_pid_ns(current) !=
                                current->nsproxy->pid_ns_for_children))
                        return ERR_PTR(-EINVAL);
        }

기존 프로세스의 정보들을 사용하여 새로운 프로세스를 생성한다.

  • 코드 라인 19~20에서 새로운 mount namespace로 태스크 생성을 요청했지만 파일 시스템 정보를 공유할 수 없으므로 실패로 함수를 빠져나간다.
  • 코드 라인 22~23에서 새로운 user namespace로 태스크 생성을 요청했지만 파일 시스템 정보를 공유할 수 없으므로 실패로 함수를 빠져나간다.
  • 코드 라인 29~30에서 thread 생성을 요청했지만 시그널 핸들러의 공유가 필요하다. 그렇지 않은 경우 실패로 함수를 빠져나간다.
    • 스레드 그룹끼리는 시그널을 공유해야 한다.
  • 코드 라인 37~38에서 시그널 핸들러를 공유할 때 같은 가상 주소(VM)를 사용해야한다. 그렇지 않은 경우 실패로 함수를 빠져나간다.
  • 코드 라인 46~48에서 부모 태스크에 SIGNAL_UNKILLABLE 시그널 플래그가 있는 경우 부모 태스크 클론을 요청한 경우 실패로 함수를 빠져나간다.
  • 코드 라인 55~60에서 새로운 user namespace 또는 pid namespace로 태스크 생성을 요청한 경우 시그널 핸들러를 공유할 수 없다. 이러한 경우 실패로 함수를 빠져나간다.

 

kernel/fork.c -2/8-

        retval = security_task_create(clone_flags);
        if (retval)
                goto fork_out;

        retval = -ENOMEM;
        p = dup_task_struct(current);
        if (!p)
                goto fork_out;

        ftrace_graph_init_task(p);

        rt_mutex_init_task(p);

#ifdef CONFIG_PROVE_LOCKING
        DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
        DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
        retval = -EAGAIN;
        if (atomic_read(&p->real_cred->user->processes) >=
                        task_rlimit(p, RLIMIT_NPROC)) {
                if (p->real_cred->user != INIT_USER &&
                    !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
                        goto bad_fork_free;
        }
        current->flags &= ~PF_NPROC_EXCEEDED;

        retval = copy_creds(p, clone_flags);
        if (retval < 0)
                goto bad_fork_free;
  • 코드 라인 1~3에서 태스크 생성 이전에 태스크 생성 관련 시큐리티 후크 함수를 호출한다. 만일 실패(0이 아닐 때)시 fork_out 레이블로 이동한다.
  • 코드 라인 5~8에서 태스크를 생성한다.
    • current task 디스크립터(task_struct 및 thread_info 포함)를 복제한다. 이 때 커널 스택도 생성된다.
  • 코드 라인 10에서 ftrace 추적을 위한 초기화를 수행한다.
  • 코드 라인 12에서 priority inheritency 처리 관련 초기화를 수행한다.
  • 코드 라인 18~24에서 최대 프로세스 생성 제한치를 초과하고, 태스크 생성을 루트 유저가 하지 않았으면 bad_fork_free 레이블로 이동한다.
    • 단 시스템 리소스와 시스템 관리자에 대한 capability가 설정된 경우에는 제한을 무시한다.
  • 코드 라인 25~29에서 부모 프로세스의 플래그에서 최대 프로세스 생성 제한 플래그를 클리어하고 인증(credentials)을 복사한다.

 

kernel/fork.c -3/8-

        /*
         * If multiple threads are within copy_process(), then this check
         * triggers too late. This doesn't hurt, the check is only there
         * to stop root fork bombs.
         */
        retval = -EAGAIN;
        if (nr_threads >= max_threads)
                goto bad_fork_cleanup_count;

        if (!try_module_get(task_thread_info(p)->exec_domain->module))
                goto bad_fork_cleanup_count;

        delayacct_tsk_init(p);  /* Must remain after dup_task_struct() */
        p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
        p->flags |= PF_FORKNOEXEC;
        INIT_LIST_HEAD(&p->children);
        INIT_LIST_HEAD(&p->sibling);
        rcu_copy_process(p);
        p->vfork_done = NULL;
        spin_lock_init(&p->alloc_lock);

        init_sigpending(&p->pending);

        p->utime = p->stime = p->gtime = 0;
        p->utimescaled = p->stimescaled = 0;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
        p->prev_cputime.utime = p->prev_cputime.stime = 0;
#endif
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
        seqlock_init(&p->vtime_seqlock);
        p->vtime_snap = 0;
        p->vtime_snap_whence = VTIME_SLEEPING;
#endif

#if defined(SPLIT_RSS_COUNTING)
        memset(&p->rss_stat, 0, sizeof(p->rss_stat));
#endif

        p->default_timer_slack_ns = current->timer_slack_ns;

        task_io_accounting_init(&p->ioac);
        acct_clear_integrals(p);

        posix_cpu_timers_init(p);

        p->start_time = ktime_get_ns();
        p->real_start_time = ktime_get_boot_ns();
        p->io_context = NULL;
        p->audit_context = NULL;
        if (clone_flags & CLONE_THREAD)
                threadgroup_change_begin(current);
        cgroup_fork(p);
  • 코드 라인 6~8에서 스레드 수가 fork_init()함수에서 초기화한 최대 스레드 수(max_threads) 이상인 경우 실패로 bad_fork_cleanup_count 레이블로 이동한다.
    • max_threads 디폴트: mempages / (8 * 커널 스택 사이즈 / 페이지 사이즈)
  • 코드 라인 10~11에서 현재 태스크의 실행 도메인이 디폴트 실행 도메인이 아니면서 모듈 정보가 없는 경우 bad_fork_cleanup_count 레이블로 이동한다.
  • 코드 라인 13에서 delay accounting이 동작 시 초기화를 수행한다.
  • 코드 라인 14~15에서 생성된 태스크의 수퍼 유저 권한 및 워커 스레드 플래그를 제거하고 태스크 생성 후 실행되지 않도록 PF_FORKNOEXEC 플래그를 제거한다.
  • 코드 라인 16~17에서 생성된 태스크의 자식 및 형제 관련한 리스트를 초기화한다.
  • 코드 라인 18에서 생성된 태스크의 rcu 관련 멤버의 초기화를 수행한다.
  • 코드 라인 22에서 시그널 펜딩 리스트를 초기화한다.
  • 코드 라인 24~25 생성된 프로세스의 각종 실행 타임을 0으로 초기화한다.
    • utime: 유저 코드 실행 시간(jiffies)
    • stime: 시스템 코드 실행 시간(jiffies)
  • 코드 라인 26~33 virtual cpu에 대한 time accounting을 초기화한다.
  • 코드 라인 35~37에서 rss 통계 카운터를 초기화한다.
    • MM_FILEPAGES 카운터, MM_ANONPAGES 카운터, MM_SWAPENTS 카운터
  • 코드 라인 39에서 절전을 위해 사용하는 타이머용 slack 나노초를 부모 값을 상속하여 사용한다.
  • 코드 라인 41에서 태스크에 대한 io 통계 카운터를 초기화한다.
    • CONFIG_TASK_XACCT 커널 옵션 사용시
      • 읽은 바이트 수
      • 기록한 바이트 수
      • 읽은 syscall 수
      • 기록한 syscall 수
    • CONFIG_TASK_IO_ACCOUNTING 커널 옵션 사용 시
      • 디스크로 부터 읽은 바이트 수
      • 디스크에 기록한 비이트 수
      • 디스크에 기록 취소한 바이트 수
  • 코드 라인 42에서 태스크에 대한 mm 관련 accounting 카운터 정보를 초기화한다.
  • 코드 라인 44에서 posix cpu 타이머들을 초기화한다.
  • 코드 라인 46~47에서 monotonic 시간(nsec)과 boot based 시간(nsec)을 구해와 대입한다.
  • 코드 라인 48~49에서 io 및 audit 컨택스트를 초기화한다.
  • 코드 라인 50~51에서 스레드 생성 요청 시 스레드 그룹 변경에 대한 락을 건다.
  • 코드 라인 52에서 cgroup 관련 초기화를 수행한다.

 

kernel/fork.c -4/8-

#ifdef CONFIG_NUMA
        p->mempolicy = mpol_dup(p->mempolicy);
        if (IS_ERR(p->mempolicy)) {
                retval = PTR_ERR(p->mempolicy);
                p->mempolicy = NULL;
                goto bad_fork_cleanup_threadgroup_lock;
        }
#endif
#ifdef CONFIG_CPUSETS
        p->cpuset_mem_spread_rotor = NUMA_NO_NODE;
        p->cpuset_slab_spread_rotor = NUMA_NO_NODE;
        seqcount_init(&p->mems_allowed_seq);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
        p->irq_events = 0;
        p->hardirqs_enabled = 0;
        p->hardirq_enable_ip = 0;
        p->hardirq_enable_event = 0;
        p->hardirq_disable_ip = _THIS_IP_;
        p->hardirq_disable_event = 0;
        p->softirqs_enabled = 1;
        p->softirq_enable_ip = _THIS_IP_;
        p->softirq_enable_event = 0;
        p->softirq_disable_ip = 0;
        p->softirq_disable_event = 0;
        p->hardirq_context = 0;
        p->softirq_context = 0;
#endif
#ifdef CONFIG_LOCKDEP
        p->lockdep_depth = 0; /* no locks held yet */
        p->curr_chain_key = 0;
        p->lockdep_recursion = 0;
#endif

#ifdef CONFIG_DEBUG_MUTEXES
        p->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHE
        p->sequential_io        = 0;
        p->sequential_io_avg    = 0;
#endif
  • 코드 라인 1~8에서 누마 시스템에서 메모리 정책을 복제한다.
  • 코드 라인 9~13에서 cpuset 관련 멤버들을 초기화한다.
  • 코드 라인 14~28에서 irq 시작과 끝에 대한 trace 정보 및 조작 관련 멤버를 초기화한다.
  • 코드 라인 29~33에서 lockdep 디버깅 관련 멤버를 초기화한다.
  • 코드 라인 35~37에서 뮤텍스 블럭 디버깅 멤버를 초기화한다.
  • 코드 라인 38~41에서 블럭 디바이스를 다른 디바이스의 캐시 대용으로 사용할 때 관련된 멤버들을 초기화한다.

 

kernel/fork.c -5/8-

        /* Perform scheduler related setup. Assign this task to a CPU. */
        retval = sched_fork(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_policy;

        retval = perf_event_init_task(p);
        if (retval)
                goto bad_fork_cleanup_policy;
        retval = audit_alloc(p);
        if (retval)
                goto bad_fork_cleanup_perf;
        /* copy all the process information */
        shm_init_task(p);
        retval = copy_semundo(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_audit;
        retval = copy_files(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_semundo;
        retval = copy_fs(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_files;
        retval = copy_sighand(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_fs;
        retval = copy_signal(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_sighand;
        retval = copy_mm(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_signal;
        retval = copy_namespaces(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_mm;
        retval = copy_io(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_namespaces;
        retval = copy_thread(clone_flags, stack_start, stack_size, p);
        if (retval)
                goto bad_fork_cleanup_io;

        if (pid != &init_struct_pid) {
                retval = -ENOMEM;
                pid = alloc_pid(p->nsproxy->pid_ns_for_children);
                if (!pid)
                        goto bad_fork_cleanup_io;
        }

        p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
        /*
         * Clear TID on mm_release()?
         */
        p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr : NULL;
#ifdef CONFIG_BLOCK
        p->plug = NULL;
#endif
#ifdef CONFIG_FUTEX
        p->robust_list = NULL;
#ifdef CONFIG_COMPAT
        p->compat_robust_list = NULL;
#endif
        INIT_LIST_HEAD(&p->pi_state_list);
        p->pi_state_cache = NULL;
#endif
  • 코드 라인 1~4에서 태스크의 스케줄러 관련 멤버들을 초기화한다.
    • p->state: TAKS_RUNNING 상태로 바꾼다.
    • p->prio: normal_prio로 설정한다. (pi boost에 의해 priority가 변경되어 있을 수도 있다)
    • p->sched_class: prio를 보고 rt 또는 cfs 스케줄러로 설정한다. (deadline인 경우 에러)
    • thread_info->cpu: cpu 설정
    • p->on_cpu: 0으로 초기화
    • p->pushable_tasks: rt 오버로드용 리스트 초기화
    • p->pusjable_dl_tasks: dl 오버로드용 RB 트리 초기화
    • thread_info->preempt_count: PREEMPT_ENABLE로 초기화
  • 코드 라인 6~8에서 perf 이벤트 관련한 context 들을 모두 초기화한다.
  • 코드 라인 9~11에서 audit 기능이 동작하는 경우 audit context 블럭을 할당한다.
  • 코드 라인 13에서 시스템V IPC용 공유 메모리(shm)에 관련 리스트의 초기화를 수행한다.
  • 코드 라인 14~16에서 부모 태스크에서 사용중인 시스템V IPC용 세마포어들에 대해 공유(CLONE_SYSVSEM)하거나 새로 초기화하여 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 초기화하여 사용한다.
  • 코드 라인 17~19에서 부모 태스크에서 열고 사용하고 있는 파일 정보를 공유(CLONE_FILES)하거나 복사하여 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 해당 파일들의 참조 카운터를 1부터 시작한다.
  • 코드 라인 20~22에서 부모 태스크에서 사용하는 루트 파일 시스템을 공유(CLONE_FS)하거나 복사하여 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 참조 카운터를 1부터 시작한다.
  • 코드 라인 23~25에서 부모 태스크에서 사용하는 시그널 핸들러 정보를 공유(CLONE_SIGHAND)하거나 복사하여 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 참조 카운터를 1부터 시작한다.
  • 코드 라인 26~28에서 부모 태스크에서 사용하는 시그널 rlimit 정보를 알아와서 시그널 디스크립터를 생성하여 초기화한다. 단 생성되는 태스크가 스레드인 경우에는 아무일도 하지 않고 성공(0)으로 함수를 빠져나간다.
  • 코드 라인 29~31에서 커널 스레드인경우 거의 아무일도 수행하지 않고, pthread, vfork를 통해 진입한 경우 별도의 mm을 만들지 않고 부모의 mm을 공유한다. 유저 프로세스(fork & clone)를 생성한 경우 자신의 vm을 만들되 부모의 mm 정보를 상속받아 사용한다. 부모가 사용하는 vma 정보와 페이지 테이블 정보를 복사하여 사용한다. (COW)
    • COW 방식을 사용하여 실제 사용메모리는 할당하지 않고, 부모 태스크가 사용하던 vma 및 페이지 테이블만 복사하여 자신의 vm을 구성하고, 유저 태스크가 추후 실제 페이지에 수정을 위해 접근하려 할 때 fault 되어 기존 페이지를 새로 할당받은 곳에 복사하여 사용하는 방식을 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 참조 카운터를 1부터 시작한다.
  • 코드 라인 32~34에서 부모 태스크에서 사용하는 namespace를 사용하거나 공유(CLONE_NEWNS, CLONE_NEWUTS | CLONE_NEWIPC, CLONE_NEWPID, CLONE_NEWNET) 요청이 있는 경우 부모 태스크의 namespace 정보를 사용하여 새로운 namespace를 생성한다.
    • 유저가  CAP_SYS_ADMIN 권한이 없는 경우 -EPERM 에러로 함수를 빠져나간다.
  • 코드 라인 35~37에서 부모 태스크가 사용하는 io context 정보를 공유(CLONE_IO)하거나 새로 생성하여 사용한다.
  • 코드 라인 38~40에서 부모 태스크가 사용하는 스레드 레지스터 정보를 사용하여 초기화한다. 부모 태스크가 사용하는 TLS 정보를 공유(CLONE_SETTLS)할 수도 있다.
  • 코드 라인 42~47에서 인자로 받은 pid 디스크립터가 &init_struct_pid가 아닌 경우 pid 디스크립터를 새로 할당받는다.
    • fork_idle() 함수를 통해 copy_process() 함수를 호출할 때 인자로 &init_struct_pid 디스크립터가 주어진다.

 

kernel/fork.c -6/8-

        /*
         * sigaltstack should be cleared when sharing the same VM
         */
        if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
                p->sas_ss_sp = p->sas_ss_size = 0;

        /*
         * Syscall tracing and stepping should be turned off in the
         * child regardless of CLONE_PTRACE.
         */
        user_disable_single_step(p);
        clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
        clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
        clear_all_latency_tracing(p);

        /* ok, now we should be set up.. */
        p->pid = pid_nr(pid);
        if (clone_flags & CLONE_THREAD) {
                p->exit_signal = -1;
                p->group_leader = current->group_leader;
                p->tgid = current->tgid;
        } else {
                if (clone_flags & CLONE_PARENT)
                        p->exit_signal = current->group_leader->exit_signal;
                else
                        p->exit_signal = (clone_flags & CSIGNAL);
                p->group_leader = p;
                p->tgid = p->pid;
        }

        p->nr_dirtied = 0;
        p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
        p->dirty_paused_when = 0;

        p->pdeath_signal = 0;
        INIT_LIST_HEAD(&p->thread_group);
        p->task_works = NULL;

        /*
         * Make it visible to the rest of the system, but dont wake it up yet.
         * Need tasklist lock for parent etc handling!
         */
        write_lock_irq(&tasklist_lock);

        /* CLONE_PARENT re-uses the old parent */
        if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
                p->real_parent = current->real_parent;
                p->parent_exec_id = current->parent_exec_id;
        } else {
                p->real_parent = current;
                p->parent_exec_id = current->self_exec_id;
        }
  • 코드 라인 4~5에서 vfork가 아닌 fork 또는 clone을 통해 VM을 공유하는 경우 시그널 보조 스택을 초기화한다.
  • 코드 라인 11에서 gdb 등이 사용하는 ptrace 디버그용 유저 모드 싱글 스텝을 disable한다.
    • arm 및 arm64 아키텍처는 하드웨어 디버거를 지원받아 사용한다.
    • 참고: ptrace
  • 코드 라인 12에서 trace용 syscall 플래그를 제거한다.
  • 코드 라인 13~15에서 emulator용 syscall 플래그를 제거한다.
  • 코드 라인 16에서 latency tracing을 위한 정보를 초기화한다.
  • 코드 라인 20~23에서 스레드를 생성한 경우 이 스레드의 그룹 리더와 스레드 그룹 리더(tgid)는 부모 태스크가 가리키는 그룹리더와 스레드 그룹 리더를 그대로 사용한다.
  • 코드 라인 24~31에서 스레드가 아닌 프로세스를 생성한 경우 그룹 리더와 스레드 그룹 리더는 자신이된다.

 

kernel/fork.c -7/8-

        spin_lock(&current->sighand->siglock);

        /*
         * Copy seccomp details explicitly here, in case they were changed
         * before holding sighand lock.
         */
        copy_seccomp(p);

        /*
         * Process group and session signals need to be delivered to just the
         * parent before the fork or both the parent and the child after the
         * fork. Restart if a signal comes in before we add the new process to
         * it's process group.
         * A fatal signal pending means that current will exit, so the new
         * thread can't slip out of an OOM kill (or normal SIGKILL).
        */
        recalc_sigpending();
        if (signal_pending(current)) {
                spin_unlock(&current->sighand->siglock);
                write_unlock_irq(&tasklist_lock);
                retval = -ERESTARTNOINTR;
                goto bad_fork_free_pid;
        }

        if (likely(p->pid)) {
                ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

                init_task_pid(p, PIDTYPE_PID, pid);
                if (thread_group_leader(p)) {
                        init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
                        init_task_pid(p, PIDTYPE_SID, task_session(current));

                        if (is_child_reaper(pid)) {
                                ns_of_pid(pid)->child_reaper = p;
                                p->signal->flags |= SIGNAL_UNKILLABLE;
                        }

                        p->signal->leader_pid = pid;
                        p->signal->tty = tty_kref_get(current->signal->tty);
                        list_add_tail(&p->sibling, &p->real_parent->children);
                        list_add_tail_rcu(&p->tasks, &init_task.tasks);
                        attach_pid(p, PIDTYPE_PGID);
                        attach_pid(p, PIDTYPE_SID);
                        __this_cpu_inc(process_counts);
                } else {
                        current->signal->nr_threads++;
                        atomic_inc(&current->signal->live);
                        atomic_inc(&current->signal->sigcnt);
                        list_add_tail_rcu(&p->thread_group,
                                          &p->group_leader->thread_group);
                        list_add_tail_rcu(&p->thread_node,
                                          &p->signal->thread_head);
                }
                attach_pid(p, PIDTYPE_PID);
                nr_threads++;
        }
  • 코드 라인 7에서 부모 태스크에서 사용하는 secure computing mode를 가져와서 새 태스크에 복사하여 사용한다.
    • 프로세스에 이미 열린 파일 디스크립터에 대해 exit(), sigreturn(), read() 그리고 write()를 제외한 모든 시스템 호출을 할 수 없게 보호한다. 만일 시도시 SIGKILL이 발생한다.
    • 참고: seccomp(2) – Linux manual page – man7.org
  • 코드 라인 17~23에서 부모 태스크의 시그널 상태를 재평가하여 펜딩된 경우 에러로 함수를 빠져나간다. 그렇지 않은 경우 플래그에서 시그널 펜딩 플래그를 제거한다.
  • 코드 라인  25~28에서 높은 확률로 생성된 태스크에 pid 디스크립터가 지정된 경우 이 태스크의 PIDTYPE_PID에 pid 디스크립터를 지정한다.
  • 코드 라인  29~31에서 생성된 태스크가 스레드 그룹의 리더인 경우 이 태스크의 PIDTYPE_PGID에 부모 태스크의 프로세스 그룹 리더의 pid 디스크립터를 지정한다. 또한 PIDTYPE_SID에 부모 태스크의 세션 id의 pid 디스크립터를 지정한다.
  • 코드 라인 33~36에서 이 태스크가 child reaper인 경우 이 pid namespace의 child_reaper에 생성 태스크를 지정한다. 또한 시그널 플래그에 SIGNAL_UNKILLABLE 플래그를 추가한다.
  • 코드 라인 38~39에서 시그널 리더 pid에 이 pid를 지정하고, 시그널에 지정된 tty도 부모 tty를 지정한다.
  • 코드 라인 40~41에서 init_tasks.tasks 리스트에 생성된 스레드 그룹 리더 태스크를 추가하고, 부모 태스크의 자식으로 이 태스크를 추가한다.
  • 코드 라인 42~43에서 생성된 태스크에 지정된 PIDTYPE_PGID 타입과 PIDTYPE_SID에 이 pid를 연결한다.
  • 코드 라인 44에서 전역 per-cpu 변수인 process_counts 카운터를 1 증가시킨다.
  • 코드 라인 45~48에서 생성된 태스크가 스레드인 경우 부모 태스크의 시그널 카운터들을 1씩 증가시킨다.
  • 코드 라인 49~53에서 프로세스 그룹리더의 스레드 그룹 리스트와 시그널에 생성된 스레드를 추가한다.
  • 코드 라인 54~55에서 생성된 태스크에 지정된 PIDTYPE_PID 타입에 이 pid를 연결한다. 그런 후 전역 변수인 스레드 수(nr_threads)를 증가시킨다.

 

kernel/fork.c -8/8-

        total_forks++;
        spin_unlock(&current->sighand->siglock);
        syscall_tracepoint_update(p);
        write_unlock_irq(&tasklist_lock);

        proc_fork_connector(p);
        cgroup_post_fork(p);
        if (clone_flags & CLONE_THREAD)
                threadgroup_change_end(current);
        perf_event_fork(p);

        trace_task_newtask(p, clone_flags);
        uprobe_copy_process(p, clone_flags);

        return p;

bad_fork_free_pid:
        if (pid != &init_struct_pid)
                free_pid(pid);
bad_fork_cleanup_io:
        if (p->io_context)
                exit_io_context(p);
bad_fork_cleanup_namespaces:
        exit_task_namespaces(p);
bad_fork_cleanup_mm:
        if (p->mm)
                mmput(p->mm);
bad_fork_cleanup_signal:
        if (!(clone_flags & CLONE_THREAD))
                free_signal_struct(p->signal);
bad_fork_cleanup_sighand:
        __cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:
        exit_fs(p); /* blocking */
bad_fork_cleanup_files:
        exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
        exit_sem(p);
bad_fork_cleanup_audit:
        audit_free(p);
bad_fork_cleanup_perf:
        perf_event_free_task(p);
bad_fork_cleanup_policy:
#ifdef CONFIG_NUMA
        mpol_put(p->mempolicy);
bad_fork_cleanup_threadgroup_lock:
#endif
        if (clone_flags & CLONE_THREAD)
                threadgroup_change_end(current);
        delayacct_tsk_free(p);
        module_put(task_thread_info(p)->exec_domain->module);
bad_fork_cleanup_count:
        atomic_dec(&p->cred->user->processes);
        exit_creds(p);
bad_fork_free:
        free_task(p);
fork_out:
        return ERR_PTR(retval);
}
  • 코드 라인 1에서 fork된 태스크의 수를 증가시킨다. (전역 변수 total_forks)
  • 코드 라인 3에서 syscall_tracepoint 관련하여 부모 태스크에 설정된 TIF_SYSCALL_TRACEPOINT 플래그 유무를 현재 태스크에 복사하여 설정한다.
  • 코드 라인 6에서 fork가 발생하였으므로 1개 이상의 대기중인 프로세스 이벤트 리스너들에게 넷링크를 통해 전송한다.
  • 코드 라인 7에서 새 태스크 생성 후 cgroup이 처리할 일을 수행한다.
    • cgroup 태스크 리스트에 추가하고 에서 처리할 일을 수행한다.
    • cgroup 서브시스템에 등록된 fork 후크 함수를 호출한다.
  • 코드 라인 8~9에서 스레드 생성 요청(CLONE_THREAD)인 경우 스레드 그룹 변경에 대한 락을 닫는다.
  • 코드 라인 10에서 fork에 대한 perf 이벤트 처리를 수행한다.
  • 코드 라인 13에서 부모 태스크에서 사용하던 Uprobe 기반 이벤트 트레이스에서 사용하는 context들을 vfork로 만들어진 새 유저 태스크에만 복사한다.

 

새 유저 태스크를 위한 mm 공유 또는 복제

copy_mm()

kernel/fork.c

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
        struct mm_struct *mm, *oldmm;
        int retval;

        tsk->min_flt = tsk->maj_flt = 0;
        tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
        tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif

        tsk->mm = NULL;
        tsk->active_mm = NULL;

        /*
         * Are we cloning a kernel thread?
         *
         * We need to steal a active VM for that..
         */
        oldmm = current->mm;
        if (!oldmm)
                return 0;

        /* initialize the new vmacache entries */
        vmacache_flush(tsk);

        if (clone_flags & CLONE_VM) {
                atomic_inc(&oldmm->mm_users);
                mm = oldmm;
                goto good_mm;
        }

        retval = -ENOMEM;
        mm = dup_mm(tsk);
        if (!mm)
                goto fail_nomem;

good_mm:
        tsk->mm = mm;
        tsk->active_mm = mm;
        return 0;

fail_nomem:
        return retval;
} 

새 태스크를 위해 부모 mm 디스크립터 및 페이지 테이블을 공유 또는 복사해온다. 다음 조건에 따라 동작이 구분된다.

  • kernel_thread() 함수를 통해 커널 스레드의 생성이 요청된 경우
    • 커널 스레드는 vm 정보 구성을 하지 않으므로 아무런 처리를 하지 않는다.
  • pthread_create() 또는 vfork() 함수를 통해 유저 스레드 또는 유저 태스크의 생성이 요청된 경우
    • 부모가 사용하는 mm 디스크립터 정보를 그대로 공유한다.
  • fork() 또는 clone() 함수를 통해 유저 태스크의 생성이 요청된 경우
    • 생성된 태스크용으로 mm 디스크립터와 페이지 테이블을 새롭게 할당한 후 부모가 사용하는 mm 디스크립터와 페이지 테이블을 복사하여 구성한다. (COW)

 

참고:

  • tsk->mm
    • 커널 스레드인 경우 null
    • 유저 태스크(유저 프로세스 및 유저 스레드)인 경우 mm 디스크립터를 가리킨다.
  • tsk->active_mm
    • 커널 스레드인 경우 마지막에 사용했었던 mm을 가리킨다.
      • 최초 init_mm을 제외하곤 항상 마지막 유저 프로세스의 mm 디스크립터를 가리킨다.
    • 유저 프로세스가 사용하는 mm 디스크립터를 가리킨다.
      • 유저 스레드들은 자신이 소속된 유저 프로세스의 mm 디스크립터를 가리킨다.

 

dup_mm()

kernel/fork.c

/*
 * Allocate a new mm structure and copy contents from the
 * mm structure of the passed in task structure.
 */
static struct mm_struct *dup_mm(struct task_struct *tsk)
{
        struct mm_struct *mm, *oldmm = current->mm;
        int err;

        mm = allocate_mm();
        if (!mm)
                goto fail_nomem;

        memcpy(mm, oldmm, sizeof(*mm));

        if (!mm_init(mm, tsk))
                goto fail_nomem;

        dup_mm_exe_file(oldmm, mm);

        err = dup_mmap(mm, oldmm);
        if (err)
                goto free_pt;

        mm->hiwater_rss = get_mm_rss(mm);
        mm->hiwater_vm = mm->total_vm;

        if (mm->binfmt && !try_module_get(mm->binfmt->module))
                goto free_pt;

        return mm;

free_pt:
        /* don't put binfmt in mmput, we haven't got module yet */
        mm->binfmt = NULL;
        mmput(mm);

fail_nomem:
        return NULL;
} 

새 유저 태스크를 위해 mm 디스크립터와 페이지 테이블을 할당한 후, 부모 태스크가 사용하던 mm 정보와 페이지 테이블을 복사한다.

  • 코드 라인 10~12에서 mm 디스크립터용 kmem 캐시를 통해 새 mm 디스크립터를 할당받아온다.
  • 코드 라인 14에서 부모 mm(oldmm) 디스크립터를 할당받은 mm 디스크립터에 모두 복사한다.
    • 부모 태스크가 사용했었던 모든 vm 정보들이 복사된다. (물론 여기에서는 페이지 테이블은 제외)
  • 코드 라인 16~17에서 부모 태스크의 mm 정보를 사용할 필요가 없는 새 태스크용 mm 디스크립터의 멤버를 일부 초기화시킨다. 이 때 함수내에서 유저 태스크용 페이지 테이블을 할당받아 mm->pgd에 대입한다.
  • 코드 라인 19에서 실행 파일 정보를 복제한다.
  • 코드 라인 21~23에서 부모 태스크가 사용하던 vma 정보들과 페이지 테이블에서 매핑된 유저 엔트리들을 복사한다.
    • COW(Copy On Write) 처리를 위해 실제 부모 태스크가 사용하던 페이지(코드, 데이터 등)을 복사하지 않고 vma 정보와 페이지 테이블만 복사한다. 추후 유저 태스크가 활성화되어 부모 태스크가 사용하던 해당 코드 또는 데이터를 공유하여 접근하는데 만일 쓰기 작업이 수행되는 일이 발생할 때에만 해당 페이지를 복사하는 방식으로 실제 메모리를 지연할당한다. -> 태스크 생성이 빨라지고, 실제 물리 메모리 소모가 줄어든다.

 

mm_init()

kernel/fork.c

static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p)
{
        mm->mmap = NULL;
        mm->mm_rb = RB_ROOT;
        mm->vmacache_seqnum = 0;
        atomic_set(&mm->mm_users, 1);
        atomic_set(&mm->mm_count, 1);
        init_rwsem(&mm->mmap_sem);
        INIT_LIST_HEAD(&mm->mmlist);
        mm->core_state = NULL;
        atomic_long_set(&mm->nr_ptes, 0);
        mm_nr_pmds_init(mm);
        mm->map_count = 0;
        mm->locked_vm = 0;
        mm->pinned_vm = 0;
        memset(&mm->rss_stat, 0, sizeof(mm->rss_stat));
        spin_lock_init(&mm->page_table_lock);
        mm_init_cpumask(mm);
        mm_init_aio(mm);
        mm_init_owner(mm, p);
        mmu_notifier_mm_init(mm);
        clear_tlb_flush_pending(mm);
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
        mm->pmd_huge_pte = NULL;
#endif

        if (current->mm) {
                mm->flags = current->mm->flags & MMF_INIT_MASK;
                mm->def_flags = current->mm->def_flags & VM_INIT_DEF_MASK;
        } else {
                mm->flags = default_dump_filter;
                mm->def_flags = 0;
        }

        if (mm_alloc_pgd(mm))
                goto fail_nopgd;

        if (init_new_context(p, mm))
                goto fail_nocontext;

        return mm;

fail_nocontext:
        mm_free_pgd(mm);
fail_nopgd:
        free_mm(mm);
        return NULL;
}

mm 디스크립터를 초기화하고 유저용 pgd 테이블도 할당한다.

  • 코드 라인 1~33에서 mm 디스크립터를 초기화한다.
  • 코드 라인 35~36에서 유저용 pgd 테이블도 할당하고 mm->pgd에 대입한다.
  • 코드 라인 38~39에서 context id를 0으로 초기화한다.

 

유저용 pgd 테이블 할당

arm 커널에서는 하나의 pgd 테이블을 커널 영역과 유저 영역을 같이 공유하여 사용되고, 각 영역을 분리(split)하는 사이즈는 커널 컴파일 옵션(rpi2: CONFIG_VMSPLIT_2G=y)에 따라 다르다. 유저용 pgdb 페이지 테이블을 만들때 arm 아키텍처는 다음과 같은 일을 수행한다.

  • init_mm->pgd 테이블에서 커널 영역에 해당하는 pgd 엔트리들을 내 pgd 테이블에 복사한다.
    • 커널 영역: CONFIG_VMSPLIT_2G + 16M(모듈 영역)
  • 유저 영역에 해당하는 pgd 엔트리들은 null(0)으로 초기화한다.
  • 만일 low exception 벡터를 사용하는 경우에는 low 벡터 주소가 유저 영역에 위치하므로 이를 access 할 수 있도록 low exception 벡터에 대한 pte 엔트리들도 할당하고 준비해야 한다.

 

32bit arm with LPAE 에서는 3개의 페이지 테이블을 구성하는 것이 달라지고, arm64에서는 커널용 페이지 테이블과 유저용 페이지 테이블이 아예 별도로 구성되어 있으므로 간단히 유저용 pgd 페이지 테이블만 할당 한다.

 

mm_alloc_pgd()

kernel/fork.c

static inline int mm_alloc_pgd(struct mm_struct *mm)
{
        mm->pgd = pgd_alloc(mm);
        if (unlikely(!mm->pgd))
                return -ENOMEM;
        return 0;
}

페이지 테이블을 할당한 후 mm->pgd에 지정한다.

  • 사용하는 아키텍처 및 커널 옵션 구성에 따라 페이지 테이블 할당 구현이 약간씩 다르다.

 

pgd_alloc() – for arm

arch/arm/mm/pgd.c

/*
 * need to get a 16k page for level 1
 */
pgd_t *pgd_alloc(struct mm_struct *mm)
{
        pgd_t *new_pgd, *init_pgd;
        pud_t *new_pud, *init_pud;
        pmd_t *new_pmd, *init_pmd;
        pte_t *new_pte, *init_pte;

        new_pgd = __pgd_alloc();
        if (!new_pgd)
                goto no_pgd;

        memset(new_pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));

        /*
         * Copy over the kernel and IO PGD entries
         */
        init_pgd = pgd_offset_k(0);
        memcpy(new_pgd + USER_PTRS_PER_PGD, init_pgd + USER_PTRS_PER_PGD,
                       (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));

        clean_dcache_area(new_pgd, PTRS_PER_PGD * sizeof(pgd_t));

#ifdef CONFIG_ARM_LPAE
        /*
         * Allocate PMD table for modules and pkmap mappings.
         */ 
        new_pud = pud_alloc(mm, new_pgd + pgd_index(MODULES_VADDR),
                            MODULES_VADDR);
        if (!new_pud)
                goto no_pud;

        new_pmd = pmd_alloc(mm, new_pud, 0);
        if (!new_pmd)
                goto no_pmd;
#endif

arm 아키텍처용 pgd 페이지 테이블을 할당한 후 반환한다.

  • 코드 라인 11~13에서 pgd 페이지 테이블을 할당해온다.
    • 3레벨을 사용하는 LPAE 시스템인 경우에는 4개 엔트리 * 8 바이트 = 32 바이트를 pgd 전용 kmem cache를 통해 할당한다.
    • 2레벨을 사용하는 경우에는 버디 시스템을 통해 4개 페이지 * 4K = 16K 페이지를 할당한다.
  • 코드 라인 15에서 pgd 테이블에서 유저 영역에 해당하는 부분만 0으로 초기화한다.
    • rpi2 예) 2048개의 리눅스 pgd 엔트리에서 하위 유저 영역에 해당하는 부분은 1024(하위 2G 해당)-8(16M 모듈)=1018개의 8바이트 엔트리를 0으로 초기화한다.
  • 코드 라인 20~22에서 커널용 pgd 테이블에서 커널 영역에 해당하는 부분만 새로 할당한 페이지의 커널 영역에 복사한다.
    • 주의: 커널 영역 사이즈는 커널 쪽 vm_split 영역 사이즈와 모듈 영역을 더해야 한다.
  • 코드 라인 24에서 할당받은 pgd 테이블 주소와 사이즈만큼에 해당하는 영역에 대해 data 캐시를 클린한다.
  • 코드 라인 26~38에서 3레벨 페이지 테이블을 구성해야 하는 LPAE 시스템에서는 pud는 pgd를 그대로 사용하여 패스시키고 pmd 테이블도 추가로 할당하여 구성한다.

 

        if (!vectors_high()) {
                /*
                 * On ARM, first page must always be allocated since it
                 * contains the machine vectors. The vectors are always high
                 * with LPAE.
                 */
                new_pud = pud_alloc(mm, new_pgd, 0);
                if (!new_pud)
                        goto no_pud;

                new_pmd = pmd_alloc(mm, new_pud, 0);
                if (!new_pmd)
                        goto no_pmd;

                new_pte = pte_alloc_map(mm, NULL, new_pmd, 0);
                if (!new_pte)
                        goto no_pte;

                init_pud = pud_offset(init_pgd, 0);
                init_pmd = pmd_offset(init_pud, 0);
                init_pte = pte_offset_map(init_pmd, 0);
                set_pte_ext(new_pte + 0, init_pte[0], 0);
                set_pte_ext(new_pte + 1, init_pte[1], 0);
                pte_unmap(init_pte);
                pte_unmap(new_pte);
        }

        return new_pgd;

no_pte:
        pmd_free(mm, new_pmd);
        mm_dec_nr_pmds(mm);
no_pmd:
        pud_free(mm, new_pud);
no_pud:
        __pgd_free(new_pgd);
no_pgd:
        return NULL;
}
  • 코드 라인 1~26에서 low exception 벡터를 사용하는 시스템에서는 벡터가 유저 영역에 위치하므로 이를 위한 별도의 매핑을 추가로 만들어줘야 한다.
  • 코드 라인 28에서 정상적으로 할당받은 pgd 테이블의 시작 주소를 반환한다.

 

pgd_alloc() – for arm64

arch/arm64/mm/pgd.c

pgd_t *pgd_alloc(struct mm_struct *mm)
{
        if (PGD_SIZE == PAGE_SIZE)
                return (pgd_t *)__get_free_page(PGALLOC_GFP);
        else
                return kmem_cache_alloc(pgd_cache, PGALLOC_GFP);
}

arm64에서는 커널용 페이지 테이블과 유저용 페이지 테이블이 아예 별도로 구성되어 있으므로 간단히 유저용 pgd 페이지 테이블만 할당 한다.

 

참고

 

방문자 통계 (2017년 1월 17~31일)

안녕하세요? 문c 블로그입니다.

 

방문자 수가 작년과 올 해 이렇다 할 큰 변화가 없습니다.

현재까지 리눅스 커널에 대한 분석글을 올린 글 수는 275개이고, 그림은 1300개 정도되었습니다.

 

해마다 더 많은 분들이 리눅스 커널 분석을 도전하시길 바랍니다.

 

방문자 통계 참고) 2017년 1월 17~31일

PID 관리

 

PID 관리

PID(Process ID)는 Posix에서 정의한 것으로는 정확히 프로세스를 의미한다. 그러나 리눅스 커널 v2.6에 도입된 Posix 스레드(NTPL을 통한 pthread) 생성이 지원되면서부터 커널 소스내에서 사용하는 pid 값이 process id이지만 개념은 task id이다. 따라서 pid가 process id를 의미하는지 task id를 의미하는지 소스 주변 상황을 보면서 확인해야한다.(조심!!!)

 

ps 툴 등에서 언급하는 pid와 관련된 항목들을 알아본다.

  • LWP
    • thread id를 의미한다.
  • TID
    • LWP와 동일하다.
  • PID
    • posix.1에서 정의한 process id이다.
  • TGID
    • PID와 동일하다.
  • PGID
    • process 그룹 id이다. (시그널 공유)
    • command line 파이프라인을 통해 shell에서 새로운 child 프로세스를 fork할 때 동일 process group id를 사용한다.
    • process group id는 1개의 세션에만 허락된다.
  • SID
    • 세션 id이다.
    • 1 개 이상의 process group을 가질 수 있다.

 

다음 그림과 같이 세션에서 쉘 sh가 구동되었고 그 아래에서 ppd가 동작된 상황이다.

예) 쉘에서 ps 명령어로 각종 pid 값을 확인해보았다.

# ps -T -p 2189 -o lwp -o tid -o pid -o tgid -o pgid -o sid -o tty -o cmd
  LWP   TID   PID  TGID  PGID   SID TTY   CMD
 2189  2189  2189  2189  2189  2188 pts/2 ppd -d
 2190  2190  2189  2189  2189  2188 pts/2 ppd -d
 2191  2191  2189  2189  2189  2188 pts/2 ppd -d
 2192  2192  2189  2189  2189  2188 pts/2 ppd -d
 2193  2193  2189  2189  2189  2188 pts/2 ppd -d

 

리눅스에서 실제 pid 구현과 관련된 항목들을 알아본다. (t=task 디스크립터, pid=pid  디스크립터)

  • t->pid
    • 글로벌하게 unique한 task(thread) id를 의미하고 리눅스 내부에서 태스크 생성 시 마다 발급한다.
  • t->tgid
    • 글로벌하게 unique한 프로세스 id를 의미하고 프로세스 생성시 마다 발급한다.
    • posix.1에서 정의한 PID와 동일하다.
  • t->pids[PIDTYPE_PID]
    • .node 
      • 아래가 가리키는 pid 디스크립터의 tasks[PIDTYPE_PID] 리스트에 연결될 때 사용한다.
    • .pid
      • task(thread)에 해당하는 pid 디스크립터를 가리킨다.
  • t->pids[PIDTYPE_PGID]
    • .node
      • 아래가 가리키는 pid 디스크립터의 tasks[PIDTYPE_PGID] 리스트에 연결될 때 사용한다.
    • .pid
      • 프로세스(스레드 제외)가 소속된 process group id의 pid 디스크립터를 가리킨다.
  • t->pids[PIDTYPE_SID]
    • .node 
      • 아래가 가리키는 pid 디스크립터의 tasks[PIDTYPE_SID] 리스트에 연결될 때 사용한다.
    • .pid 
      • 프로세스(스레드 제외)가 소속된 session id의 pid 디스크립터를 가리킨다.

 

  • pid->tasks[PIDTYPE_PID]
    • task(thread) 하나가 담기는 리스트이다.
  • pids->tasks[PIDTYPE_PGID]
    • 동일한 프로세스 그룹 id를 사용하는 프로세스(스레드 제외)들이 담기는 리스트이다.
  • pids->tasks[PIDTYPE_SID]
    • 동일한 세션 id를 사용하는 프로세스(스레드 제외)들이 담기는 리스트이다.

 

초기에는 PIDTYPE_TGID가 있었지만 지금은 제거되어 사용되지 않는다. (커널 v2.6.x) 현재 스레드를 가진 대표 태스크는 t->thread_group 리스트에 하위 스레드들을 담고있다.

 

각 태스크는 글로벌 값인 pid, tgid 및 namespace 별로 virtual하게 관리되는 pids[]를 관리한다.

include/linux/sched.h

struct task_struct {
         ...
        pid_t pid;
        pid_t tgid;

        /* PID/PID hash table linkage. */
        struct pid_link pids[PIDTYPE_MAX];
       ...

 

namespace에 따라 pid가 각각 관리되는 모습을 보여준다.

 

다음 그림은 task 디스크립터와 pid 디스크립터가 연결된 모습을 보여준다.

  • task 디스크립터는 1:1로 pid 방향으로 연결된다.
  • pid 디스크립터에는 1:n으로 1개 이상의 task 디스크립터를 가진다.

 

다음 그림은 새로운 namespace에서 태스크가 fork되었을 때 upid의 연결관계를 보여준다.

 

APIs

pid 참조 관련

get_pid()

include/linux/pid.h

static inline struct pid *get_pid(struct pid *pid)
{
        if (pid)
                atomic_inc(&pid->count);
        return pid;
}

사용할 pid 디스크립터의 참조 카운터를 1 증가시킨다.

 

put_pid()

kernel/pid.c

void put_pid(struct pid *pid)
{
        struct pid_namespace *ns;

        if (!pid)
                return;

        ns = pid->numbers[pid->level].ns;
        if ((atomic_read(&pid->count) == 1) ||
             atomic_dec_and_test(&pid->count)) {
                kmem_cache_free(ns->pid_cachep, pid);
                put_pid_ns(ns);
        }
}
EXPORT_SYMBOL_GPL(put_pid);

사용한 pid 디스크립터의 참조 카운터를 1 감소시키고 0이되면 할당 해제한다.  pid namespace 카운터도 1 감소시키고 0이되면 할당 해제한다.  단 초기 namespace의 할당 해제는 하지 않는다.

 

task로 pid 조회

get_task_pid()

kernel/pid.c

struct pid *get_task_pid(struct task_struct *task, enum pid_type type)
{
        struct pid *pid;
        rcu_read_lock();
        if (type != PIDTYPE_PID)
                task = task->group_leader;
        pid = get_pid(task->pids[type].pid);
        rcu_read_unlock();
        return pid;
}
EXPORT_SYMBOL_GPL(get_task_pid);

태스크가 소속된 프로세스의 pid 디스크립터를 반환하되 pid 참조 카운터를 1 증가시킨다.

  • 해당 태스크의 pid를 가져오는 것이 아니라 프로세스의 pid를 가져오는 것임에 주의해야 한다.

 

task_pid()

linux/sched.h

static inline struct pid *task_pid(struct task_struct *task)
{
        return task->pids[PIDTYPE_PID].pid;
}

태스크에 해당하는 pid 디스크립터를 반환한다.

 

task_tgid()

linux/sched.h

static inline struct pid *task_tgid(struct task_struct *task)
{
        return task->group_leader->pids[PIDTYPE_PID].pid;
}

태스크가 소속된 프로세스, 즉 스레드 그룹리더에 해당하는 pid 디스크립터를 반환한다.

 

task_pgrp()

linux/sched.h

/*
 * Without tasklist or rcu lock it is not safe to dereference
 * the result of task_pgrp/task_session even if task == current,
 * we can race with another thread doing sys_setsid/sys_setpgid.
 */
static inline struct pid *task_pgrp(struct task_struct *task)
{
        return task->group_leader->pids[PIDTYPE_PGID].pid;
}

태스크가 소속된 프로세스 그룹 리더의 pid 디스크립터를 반환한다.

 

task_session()

linux/sched.h

static inline struct pid *task_session(struct task_struct *task)
{
        return task->group_leader->pids[PIDTYPE_SID].pid;
}

태스크가 소속된 세션 pid 디스크립터를 반환한다.

 

pid로 task 조회

get_pid_task()

kernel/pid.c

struct task_struct *get_pid_task(struct pid *pid, enum pid_type type)
{
        struct task_struct *result;
        rcu_read_lock();
        result = pid_task(pid, type);
        if (result)
                get_task_struct(result);
        rcu_read_unlock();
        return result;
}
EXPORT_SYMBOL_GPL(get_pid_task);

pid 디스크립터가 관리하는 요청한 pid 타입의 첫 번째 태스크를 반환하되 태스크의 참조 카운터를 1 증가시킨다.

 

pid_task()

kernel/pid.c

struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{       
        struct task_struct *result = NULL;
        if (pid) {
                struct hlist_node *first;
                first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
                                              lockdep_tasklist_lock_is_held());
                if (first)
                        result = hlist_entry(first, struct task_struct, pids[(type)].node);
        }
        return result;
}
EXPORT_SYMBOL(pid_task);

pid 디스크립터가 관리하는 요청한 pid 타입의 첫 번째 태스크를 반환한다.

 

 

pid 디스크립터로 pid 번호 조회

pid_nr()

include/linux/pid.h

/*
 * the helpers to get the pid's id seen from different namespaces
 *
 * pid_nr()    : global id, i.e. the id seen from the init namespace;
 * pid_vnr()   : virtual id, i.e. the id seen from the pid namespace of
 *               current.
 * pid_nr_ns() : id seen from the ns specified.
 *
 * see also task_xid_nr() etc in include/linux/sched.h
 */

static inline pid_t pid_nr(struct pid *pid)
{
        pid_t nr = 0;
        if (pid)
                nr = pid->numbers[0].nr;
        return nr;
}

pid 디스크립터에서 global pid 번호를 알아온다.

 

pid_vnr()

kernel/pid.c

pid_t pid_vnr(struct pid *pid)
{
        return pid_nr_ns(pid, task_active_pid_ns(current));
}
EXPORT_SYMBOL_GPL(pid_vnr);

pid 디스크립터에서 virtual pid 번호를 알아온다.

 

pid_nr_ns()

kernel/pid.c

pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
{
        struct upid *upid;
        pid_t nr = 0;

        if (pid && ns->level <= pid->level) {
                upid = &pid->numbers[ns->level];
                if (upid->ns == ns)
                        nr = upid->nr;
        }
        return nr;
}
EXPORT_SYMBOL_GPL(pid_nr_ns);

pid 디스크립터 및 namespace로 virtual pid 번호를 알아온다.

 

 

pid 번호로 pid 디스크립터 조회

find_get_pid()

kernel/pid.c

struct pid *find_get_pid(pid_t nr)
{
        struct pid *pid;

        rcu_read_lock();
        pid = get_pid(find_vpid(nr));
        rcu_read_unlock();

        return pid;
}
EXPORT_SYMBOL_GPL(find_get_pid);

virtual pid 번호에 해당하는 pid 디스크립터를 반환하되 pid 디스크립터의 참조 카운터를 1 증가시킨다.

 

find_vpid()

kernel/pid.c

struct pid *find_vpid(int nr)
{
        return find_pid_ns(nr, task_active_pid_ns(current));
}
EXPORT_SYMBOL_GPL(find_vpid);

virtual pid 번호에 해당하는 pid 디스크립터를 반환한다.

 

find_pid_ns()

kernel/pid.c

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
        struct upid *pnr;

        hlist_for_each_entry_rcu(pnr,
                        &pid_hash[pid_hashfn(nr, ns)], pid_chain)
                if (pnr->nr == nr && pnr->ns == ns)
                        return container_of(pnr, struct pid,
                                        numbers[ns->level]);

        return NULL;
}
EXPORT_SYMBOL_GPL(find_pid_ns);

pid 해시 리스트에서 요청한 virtual pid 번호 및 namespace에 매치되는 pid 디스크립터를 찾아 반환한다.

 

namespace 조회

task_active_pid_ns()

kernel/pid.c

struct pid_namespace *task_active_pid_ns(struct task_struct *tsk)
{
        return ns_of_pid(task_pid(tsk));
}
EXPORT_SYMBOL_GPL(task_active_pid_ns);

task 디스크립터로 pid 디스크립터를 얻은 후 namespace를 찾아 반환한다.

 

ns_of_pid()

include/linux/pid.h

/*
 * ns_of_pid() returns the pid namespace in which the specified pid was
 * allocated.
 *
 * NOTE:
 *      ns_of_pid() is expected to be called for a process (task) that has
 *      an attached 'struct pid' (see attach_pid(), detach_pid()) i.e @pid
 *      is expected to be non-NULL. If @pid is NULL, caller should handle
 *      the resulting NULL pid-ns.
 */
static inline struct pid_namespace *ns_of_pid(struct pid *pid) 
{
        struct pid_namespace *ns = NULL;
        if (pid) 
                ns = pid->numbers[pid->level].ns;
        return ns;
}

pid 디스크립터로 namespace를 반환한다.

 

pid 할당과 해제

alloc_pid()

kernel/pid.c

struct pid *alloc_pid(struct pid_namespace *ns)
{
        struct pid *pid;
        enum pid_type type;
        int i, nr;
        struct pid_namespace *tmp;
        struct upid *upid;

        pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
        if (!pid)
                goto out;

        tmp = ns;
        pid->level = ns->level;
        for (i = ns->level; i >= 0; i--) {
                nr = alloc_pidmap(tmp);
                if (nr < 0)
                        goto out_free;

                pid->numbers[i].nr = nr;
                pid->numbers[i].ns = tmp;
                tmp = tmp->parent;
        }

        if (unlikely(is_child_reaper(pid))) {
                if (pid_ns_prepare_proc(ns))
                        goto out_free;
        }

        get_pid_ns(ns);
        atomic_set(&pid->count, 1);
        for (type = 0; type < PIDTYPE_MAX; ++type)
                INIT_HLIST_HEAD(&pid->tasks[type]);

        upid = pid->numbers + ns->level;
        spin_lock_irq(&pidmap_lock);
        if (!(ns->nr_hashed & PIDNS_HASH_ADDING))
                goto out_unlock;
        for ( ; upid >= pid->numbers; --upid) {
                hlist_add_head_rcu(&upid->pid_chain,
                                &pid_hash[pid_hashfn(upid->nr, upid->ns)]);
                upid->ns->nr_hashed++;
        }
        spin_unlock_irq(&pidmap_lock);

out:
        return pid;

out_unlock:
        spin_unlock_irq(&pidmap_lock);
        put_pid_ns(ns);

out_free:
        while (++i <= ns->level)
                free_pidmap(pid->numbers + i);

        kmem_cache_free(ns->pid_cachep, pid);
        pid = NULL;
        goto out;
}

 

free_pid()

kernel/pid.c

void free_pid(struct pid *pid)
{
        /* We can be called with write_lock_irq(&tasklist_lock) held */
        int i;
        unsigned long flags;

        spin_lock_irqsave(&pidmap_lock, flags);
        for (i = 0; i <= pid->level; i++) {
                struct upid *upid = pid->numbers + i;
                struct pid_namespace *ns = upid->ns;
                hlist_del_rcu(&upid->pid_chain);
                switch(--ns->nr_hashed) {
                case 2:
                case 1:
                        /* When all that is left in the pid namespace
                         * is the reaper wake up the reaper.  The reaper
                         * may be sleeping in zap_pid_ns_processes().
                         */
                        wake_up_process(ns->child_reaper);
                        break;
                case PIDNS_HASH_ADDING:
                        /* Handle a fork failure of the first process */
                        WARN_ON(ns->child_reaper);
                        ns->nr_hashed = 0;
                        /* fall through */
                case 0:
                        schedule_work(&ns->proc_work);
                        break;
                }
        }
        spin_unlock_irqrestore(&pidmap_lock, flags);

        for (i = 0; i <= pid->level; i++)
                free_pidmap(pid->numbers + i);

        call_rcu(&pid->rcu, delayed_put_pid);
}

 

 

 

구조체

pid 구조체

include/linux/pid.h

/*
 * What is struct pid?
 *
 * A struct pid is the kernel's internal notion of a process identifier.
 * It refers to individual tasks, process groups, and sessions.  While
 * there are processes attached to it the struct pid lives in a hash
 * table, so it and then the processes that it refers to can be found
 * quickly from the numeric pid value.  The attached processes may be
 * quickly accessed by following pointers from struct pid.
 *
 * Storing pid_t values in the kernel and referring to them later has a
 * problem.  The process originally with that pid may have exited and the
 * pid allocator wrapped, and another process could have come along
 * and been assigned that pid.
 *
 * Referring to user space processes by holding a reference to struct
 * task_struct has a problem.  When the user space process exits
 * the now useless task_struct is still kept.  A task_struct plus a
 * stack consumes around 10K of low kernel memory.  More precisely
 * this is THREAD_SIZE + sizeof(struct task_struct).  By comparison
 * a struct pid is about 64 bytes.
 *
 * Holding a reference to struct pid solves both of these problems.
 * It is small so holding a reference does not consume a lot of
 * resources, and since a new struct pid is allocated when the numeric pid
 * value is reused (when pids wrap around) we don't mistakenly refer to new
 * processes.
 */
struct pid
{
        atomic_t count;
        unsigned int level;
        /* lists of tasks that use this pid */
        struct hlist_head tasks[PIDTYPE_MAX];
        struct rcu_head rcu;
        struct upid numbers[1];
};
  • count
    • 참조카운터
  • level
    • namespace 레벨 (init=0)
  • tasks[]
    • 태스크들이 연결되는 리스트
  • rcu
    • pid 소멸 시 rcu 방법으로 사용하기 위한 rcu 노드
    • pid 해시 태에블에서 pid를 검색할 때 rcu_read_lock()을 사용하여 접근한다.
  • numbers[]
    • namespace 별로 관리하기 위해 upid 구조체가 사용된다.
    • namespace 레벨에 따라 배열이 조정된다.
    • numbers[0].nr에는 항상 init namespace의 global pid 값이 사용된다.

 

upid 구조체

include/linux/pid.h

/*
 * struct upid is used to get the id of the struct pid, as it is
 * seen in particular namespace. Later the struct pid is found with
 * find_pid_ns() using the int nr and struct pid_namespace *ns.
 */
struct upid {
        /* Try to keep pid_chain in the same cacheline as nr for find_vpid */
        int nr;
        struct pid_namespace *ns;
        struct hlist_node pid_chain;
};
  • nr
    • virtual pid 번호로 virtual 태스크 id와 동일하다.
  • *ns
    • namespcae를 가리키는 pid_namespace 구조체 포인터
  • pid_chain

 

pid_link 구조체

struct pid_link
{
        struct hlist_node node;
        struct pid *pid;
};
  • node
    • 태스크가 pid 해시 리스트에 연결할 때 사용
  • *pid
    • pid 구조체 포인터

 

참고

RCU(Read Copy Update) -4- (NOCB process)

 

RCU NO-CB (Offload RCU callback)

rcu의 cb를 처리하는 유형은 큰 흐름으로 다음과 같이 두 가지로 나뉜다.

  • cb 호출 – interrupt context에서 직접 처리
    • softirq에서 처리된다. 이러한 경우 보통 interrupt context에서 곧장 호출되어 처리되고 만일 softirq 처리 건 수가 많아져 지연되는 경우 softirqd 커널 스레드에서 호출되어 처리된다.
    • 디폴트 커널 설정이며 latency가 짧아 빠른 호출이 보장된다.
  • no-cb 호출 – 전용 rcu 커널 스레드에서 처리
    • 특정 rcu 커널 스레드에서 cb를 호출할 수 있도록 한다. 절전과 성능을 만족시키는 옵션이다.
    • CONFIG_RCU_NOCB_CPU 커널 옵션을 사용하고 다음 3가지 중 하나를 선택할 수 있다.
      • CONFIG_RCU_NOCB_CPU_NONE
        • 디폴트로 nocb 지정된 cpu는 없지만 “nocbs=” 커널 파라메터로 특정 cpu들을 nocb로 지정할 수 있다.
      • CONFIG_RCU_NOCB_CPU_ZERO
        • 디폴트로 cpu#0을 nocb 지정한다. 추가로 “nocbs=” 커널 파라메터를 사용하여 다른 cpu들을 nocb로 지정할 수 있다.
      • CONFIG_RCU_NOCB_CPU_ALL
        • 디폴트로 모든 cpu를 nocb로 지정한다.

 

Leader & Follower CPU

NO-CB용 콜백을 사용하는 경우 지정된 cpu에 nocb용 커널 스레드가 구성된다. grace period 관리 및 이에 수반되는 콜백 리스트의 cascade 관리를 위해 leader cpu를 지정하고 그 leader cpu가 처리해준 작업을 수행하는 follower cpu를 구성하여 처리한다.

  • Leader cpu가 하는 일
    • gp 관리
    • leader 및 follower에 속한 콜백 리스트의 이동
    • leader 콜백 호출
  • Follower cpu가 하는 일
    • follower 콜백 호출

 

NO-CB용 콜백 추가

__call_rcu_nocb()

kernel/rcu/tree_plugin.h

/*
 * This is a helper for __call_rcu(), which invokes this when the normal
 * callback queue is inoperable.  If this is not a no-CBs CPU, this
 * function returns failure back to __call_rcu(), which can complain
 * appropriately.
 *
 * Otherwise, this function queues the callback where the corresponding
 * "rcuo" kthread can find it.
 */
static bool __call_rcu_nocb(struct rcu_data *rdp, struct rcu_head *rhp,
                            bool lazy, unsigned long flags)
{

        if (!rcu_is_nocb_cpu(rdp->cpu))
                return false;
        __call_rcu_nocb_enqueue(rdp, rhp, &rhp->next, 1, lazy, flags);
        if (__is_kfree_rcu_offset((unsigned long)rhp->func))
                trace_rcu_kfree_callback(rdp->rsp->name, rhp,
                                         (unsigned long)rhp->func,
                                         -atomic_long_read(&rdp->nocb_q_count_lazy),
                                         -atomic_long_read(&rdp->nocb_q_count));
        else
                trace_rcu_callback(rdp->rsp->name, rhp,
                                   -atomic_long_read(&rdp->nocb_q_count_lazy),
                                   -atomic_long_read(&rdp->nocb_q_count));

        /*
         * If called from an extended quiescent state with interrupts
         * disabled, invoke the RCU core in order to allow the idle-entry
         * deferred-wakeup check to function.
         */
        if (irqs_disabled_flags(flags) &&
            !rcu_is_watching() &&
            cpu_online(smp_processor_id()))
                invoke_rcu_core();

        return true;
}

no-cb 지정된 cpu인 경우 rcu 커널 스레드(“rcuo”)에서 처리하도록 rcu cb를 큐잉한다.

  • 코드 라인 14~15에서 no-cb 지정된 cpu가 아니면 false를 반환한다.
  • 코드 라인 16에서 no-cb 큐에 rcu를 추가하고 rcu 커널 스레드를 호출하다.
  • 코드 라인 17~25에서 kfree용 rcu cb 및 함수 호출용 rcu cb 각각에 따른 trace 메시지 출력을 다르게 한다.
  • 코드 라인 32~35에서 irq disable이면서 확장 qs 상태에서 rcu 요청한 경우 softirq를 호출하여 rcu core를 호출한다.

 

다음 그림은 9개의 cpu가 nocb를 사용하는 모습을 보여준다.

 

RCU NO-CB 설정

rcu_is_nocb_cpu()

kernel/rcu/tree_plugin.h

/* Is the specified CPU a no-CBs CPU? */
bool rcu_is_nocb_cpu(int cpu)
{
        if (have_rcu_nocb_mask)
                return cpumask_test_cpu(cpu, rcu_nocb_mask);
        return false;
}

요청한 cpu가 no-cb(rcu 스레드 사용)으로 설정되었는지 여부를 반환한다.

  • have_rcu_nocb_mask 변수는 rcu_nocb_setup() 함수가 호출되어 rcu_nocb_mask라는 cpu 마스크가 할당된 경우 true로 설정된다.
  • CONFIG_RCU_NOCB_CPU_ALL 커널 옵션을 사용하는 경우 항상 rcu 스레드에서 동작시키기 위해 true를 반환한다.
  • CONFIG_RCU_NOCB_CPU_ALL 및 CONFIG_RCU_NOCB_CPU 커널 옵션 둘 다 사용하지 않는 경우 항상 callback 처리하기 위해 false를 반환한다.

 

rcu_nocb_setup()

kernel/rcu/tree_plugin.h

/* Parse the boot-time rcu_nocb_mask CPU list from the kernel parameters. */
static int __init rcu_nocb_setup(char *str)
{
        alloc_bootmem_cpumask_var(&rcu_nocb_mask);
        have_rcu_nocb_mask = true;
        cpulist_parse(str, rcu_nocb_mask);
        return 1;
}
__setup("rcu_nocbs=", rcu_nocb_setup);

커널 파라메터 “rcu_nocbs=”에 cpu 리스트를 설정한다. 이렇게 설정된 cpu들은 rcu callback 처리를 rcu 스레드에서 처리할 수 있다.

  • 예) rcu_nocbs=3-6,8-10

 

Leader cpu와 Follower cpu 구성

rcu_organize_nocb_kthreads()

kernel/rcu/tree_plugin.h

/*
 * Initialize leader-follower relationships for all no-CBs CPU.
 */
static void __init rcu_organize_nocb_kthreads(struct rcu_state *rsp)
{
        int cpu;
        int ls = rcu_nocb_leader_stride;
        int nl = 0;  /* Next leader. */
        struct rcu_data *rdp;
        struct rcu_data *rdp_leader = NULL;  /* Suppress misguided gcc warn. */
        struct rcu_data *rdp_prev = NULL;

        if (!have_rcu_nocb_mask)
                return;
        if (ls == -1) {
                ls = int_sqrt(nr_cpu_ids);
                rcu_nocb_leader_stride = ls;
        }

        /*
         * Each pass through this loop sets up one rcu_data structure and
         * spawns one rcu_nocb_kthread().
         */
        for_each_cpu(cpu, rcu_nocb_mask) {
                rdp = per_cpu_ptr(rsp->rda, cpu);
                if (rdp->cpu >= nl) {
                        /* New leader, set up for followers & next leader. */
                        nl = DIV_ROUND_UP(rdp->cpu + 1, ls) * ls;
                        rdp->nocb_leader = rdp;
                        rdp_leader = rdp;
                } else {
                        /* Another follower, link to previous leader. */
                        rdp->nocb_leader = rdp_leader;
                        rdp_prev->nocb_next_follower = rdp;
                }
                rdp_prev = rdp;
        }
}

nocb 지정된 cpu에서 동작하는 스레드들에 대해 leader와 follower cpu로 나누어 구성한다.

  • 코드 라인 13~14에서 nocb 설정된 cpu가 없으면 함수를 빠져나간다.
  • 코드 라인 15~18에서 nocb로 구성된 cpu에 대해 leader cpu를 포함하여 follower cpu를 몇 개로 구성할지 여부를 알아온다. 만일 -1로 설정된 경우 sqrt(online cpu수) 결과값으로 사용한다.
    • “rcu_nocb_leader_stride=4″로 설정하는 경우 4개의 cpu마다 그룹을 나누어 첫번째는 leader cpu로 구성하고 나머지는 3개는 follower cpu로 구성한다.
    • y = int_sqrt(x) 함수에서 x 값의 rough한 루트 근사치를 구해온다.
      • x=1~3    y=1
      • x=4~8   y=2
      • x=9~15   y=3
      • x=16~24   y=4
      • x=25~35   y=5
      • x=36~48   y=6
      • x=49~63   y=7
      • x=64~80   y=8
      • x=81~99   y=9
      • x=100~120   y=10
      • x=121~143   y=11
      • x=144~168   y=12
      • x=169~195   y=13
      • x=196~224   y=14
      • x=225~255   y=15
  • 코드 라인 24~37에서 leader 및 follower cpu를 구성한다.
    • 예) “rcu_nocb_leader_stride=4”, “rcu_nocbs=6-11,  16-19″인 경우
      • 6=leader,     7=follower
      • 8=leader,     9, 10, 11=follower
      • 16=leader,    17, 18, 19=follower

 

RCU NO-CB 처리용 커널 스레드

rcu_nocb_kthread()

kernel/rcu/tree_plugin.h

/*
 * Per-rcu_data kthread, but only for no-CBs CPUs.  Each kthread invokes
 * callbacks queued by the corresponding no-CBs CPU, however, there is
 * an optional leader-follower relationship so that the grace-period
 * kthreads don't have to do quite so many wakeups.
 */
static int rcu_nocb_kthread(void *arg)
{
        int c, cl;
        struct rcu_head *list;
        struct rcu_head *next;
        struct rcu_head **tail;
        struct rcu_data *rdp = arg;

        /* Each pass through this loop invokes one batch of callbacks */
        for (;;) {
                /* Wait for callbacks. */
                if (rdp->nocb_leader == rdp)
                        nocb_leader_wait(rdp);
                else
                        nocb_follower_wait(rdp);

                /* Pull the ready-to-invoke callbacks onto local list. */
                list = ACCESS_ONCE(rdp->nocb_follower_head);
                BUG_ON(!list);
                trace_rcu_nocb_wake(rdp->rsp->name, rdp->cpu, "WokeNonEmpty");
                ACCESS_ONCE(rdp->nocb_follower_head) = NULL;
                tail = xchg(&rdp->nocb_follower_tail, &rdp->nocb_follower_head);

                /* Each pass through the following loop invokes a callback. */
                trace_rcu_batch_start(rdp->rsp->name,
                                      atomic_long_read(&rdp->nocb_q_count_lazy),
                                      atomic_long_read(&rdp->nocb_q_count), -1);
                c = cl = 0;
                while (list) {
                        next = list->next;
                        /* Wait for enqueuing to complete, if needed. */
                        while (next == NULL && &list->next != tail) {
                                trace_rcu_nocb_wake(rdp->rsp->name, rdp->cpu,
                                                    TPS("WaitQueue"));
                                schedule_timeout_interruptible(1);
                                trace_rcu_nocb_wake(rdp->rsp->name, rdp->cpu,
                                                    TPS("WokeQueue"));
                                next = list->next;
                        }
                        debug_rcu_head_unqueue(list);
                        local_bh_disable();
                        if (__rcu_reclaim(rdp->rsp->name, list))
                                cl++;
                        c++;
                        local_bh_enable();
                        list = next;
                }
                trace_rcu_batch_end(rdp->rsp->name, c, !!list, 0, 0, 1);
                smp_mb__before_atomic();  /* _add after CB invocation. */
                atomic_long_add(-c, &rdp->nocb_q_count);
                atomic_long_add(-cl, &rdp->nocb_q_count_lazy);
                rdp->n_nocbs_invoked += c;
        }
        return 0;
}

no-cb용 커널 스레드로 무한 루프를 돌며 gp를 대기한다. gp가 완료되고 대기하던 콜백들이 최종적으로 follower 리스트로 이동하면 이를 모두 호출하여 처리한다.

  • 코드 라인 16~21에서 무한 루프를 돌며 현재 스레드가 nocb_leader로서 동작하는 경우와 nocb_follower로 동작하는 것을 구분하여 콜백을 처리할 준비를 하기 위해 대기한다.
  • 코드 라인 24~28에서 콜백들을 호출하기 위해 follower 리스트에 담긴 콜백들을 모두 비워 로컬에 있는 list로 옮긴다.
  • 코드 라인 34~53에서 로컬 리스트로 가져온 콜백들을 모두 호출하여 수행한다.
  • 코드 라인 55~57에서 처리한 콜백 수 만큼 nocb_q_count 및 nocb_q_count_lazy를 갱신한다.
  • 코드 라인 48에서 콜백 호출한 횟 수인 n_nocbs_invoked 카운터를 갱신한다.

 

nocb_leader_wait()

kernel/rcu/tree_plugin.h

/*
 * Leaders come here to wait for additional callbacks to show up.
 * This function does not return until callbacks appear.
 */
static void nocb_leader_wait(struct rcu_data *my_rdp)
{
        bool firsttime = true;
        bool gotcbs;
        struct rcu_data *rdp;
        struct rcu_head **tail;

wait_again:

        /* Wait for callbacks to appear. */
        if (!rcu_nocb_poll) {
                trace_rcu_nocb_wake(my_rdp->rsp->name, my_rdp->cpu, "Sleep");
                wait_event_interruptible(my_rdp->nocb_wq,
                                !ACCESS_ONCE(my_rdp->nocb_leader_sleep));
                /* Memory barrier handled by smp_mb() calls below and repoll. */
        } else if (firsttime) {
                firsttime = false; /* Don't drown trace log with "Poll"! */
                trace_rcu_nocb_wake(my_rdp->rsp->name, my_rdp->cpu, "Poll");
        }

        /*
         * Each pass through the following loop checks a follower for CBs.
         * We are our own first follower.  Any CBs found are moved to
         * nocb_gp_head, where they await a grace period.
         */
        gotcbs = false;
        for (rdp = my_rdp; rdp; rdp = rdp->nocb_next_follower) {
                rdp->nocb_gp_head = ACCESS_ONCE(rdp->nocb_head);
                if (!rdp->nocb_gp_head)
                        continue;  /* No CBs here, try next follower. */

                /* Move callbacks to wait-for-GP list, which is empty. */
                ACCESS_ONCE(rdp->nocb_head) = NULL;
                rdp->nocb_gp_tail = xchg(&rdp->nocb_tail, &rdp->nocb_head);
                gotcbs = true;
        }

        /*
         * If there were no callbacks, sleep a bit, rescan after a
         * memory barrier, and go retry.
         */
        if (unlikely(!gotcbs)) {
                if (!rcu_nocb_poll)
                        trace_rcu_nocb_wake(my_rdp->rsp->name, my_rdp->cpu,
                                            "WokeEmpty");
                WARN_ON(signal_pending(current));
                schedule_timeout_interruptible(1);

                /* Rescan in case we were a victim of memory ordering. */
                my_rdp->nocb_leader_sleep = true;
                smp_mb();  /* Ensure _sleep true before scan. */
                for (rdp = my_rdp; rdp; rdp = rdp->nocb_next_follower)
                        if (ACCESS_ONCE(rdp->nocb_head)) {
                                /* Found CB, so short-circuit next wait. */
                                my_rdp->nocb_leader_sleep = false;
                                break;
                        }
                goto wait_again;
        }

leader 및 follower에 등록된 신규 콜백들을 follower_gp 리스트로 이동시킨다. gp가 만료된 후 follower_gp 리스트에 있는 콜백들을 follower 리스트로 옮기고 콜백들을 호출하도록 해당 follower의 nocb용 커널 스레드를 깨운다.

  • 코드 라인 15~23에서 “rcu_nocb_poll” 커널 파라메터 설정이 없는 경우 이 라인에서 대기한다.
  • 코드 라인 30~40에서 follower cpu들에 대해 nocb용 리스트를 nocb_gp 리스트로 옮긴다. 만일 등록된 콜백이 없으면 skip 한다.
  • 코드 라인 46~63에서 leader에 대응하는 follower cpu들 모두에 등록된 콜백이 없으면 leader는 1 tick 만큼 슬립한 후 leader에 등록한 콜백을 검사해서 여전히 없으면 이 함수 처음 wait_agail 레이블로 다시 이동한다.

 

        /* Wait for one grace period. */
        rcu_nocb_wait_gp(my_rdp);

        /*
         * We left ->nocb_leader_sleep unset to reduce cache thrashing.
         * We set it now, but recheck for new callbacks while
         * traversing our follower list.
         */
        my_rdp->nocb_leader_sleep = true;
        smp_mb(); /* Ensure _sleep true before scan of ->nocb_head. */

        /* Each pass through the following loop wakes a follower, if needed. */
        for (rdp = my_rdp; rdp; rdp = rdp->nocb_next_follower) {
                if (ACCESS_ONCE(rdp->nocb_head))
                        my_rdp->nocb_leader_sleep = false;/* No need to sleep.*/
                if (!rdp->nocb_gp_head)
                        continue; /* No CBs, so no need to wake follower. */

                /* Append callbacks to follower's "done" list. */
                tail = xchg(&rdp->nocb_follower_tail, rdp->nocb_gp_tail);
                *tail = rdp->nocb_gp_head;
                smp_mb__after_atomic(); /* Store *tail before wakeup. */
                if (rdp != my_rdp && tail == &rdp->nocb_follower_head) {
                        /*
                         * List was empty, wake up the follower.
                         * Memory barriers supplied by atomic_long_add().
                         */
                        wake_up(&rdp->nocb_wq);
                }
        }

        /* If we (the leader) don't have CBs, go wait some more. */
        if (!my_rdp->nocb_follower_head)
                goto wait_again;
}
  • 코드 라인 2에서 gp의 완료까지 대기한다.
  • 코드 라인 13~30에서 leader에 속한 follower cpu들을 순회하며 nocb_gp 리스트에 있는 콜백들을 nocb_follower 리스트로 옮기고 등록된 콜백들을 호출하라고 해당 nocb 커널 스레드를 깨운다.
  • 코드 라인 33~34에서 leader에 등록된 콜백이 없으면 함수 처음으로 돌아간다.

 

rcu_nocb_wait_gp()

kernel/rcu/tree_plugin.h

/*
 * If necessary, kick off a new grace period, and either way wait
 * for a subsequent grace period to complete.
 */
static void rcu_nocb_wait_gp(struct rcu_data *rdp)
{
        unsigned long c;
        bool d;
        unsigned long flags;
        bool needwake;
        struct rcu_node *rnp = rdp->mynode;

        raw_spin_lock_irqsave(&rnp->lock, flags);
        smp_mb__after_unlock_lock();
        needwake = rcu_start_future_gp(rnp, rdp, &c);
        raw_spin_unlock_irqrestore(&rnp->lock, flags);
        if (needwake)
                rcu_gp_kthread_wake(rdp->rsp);

        /*
         * Wait for the grace period.  Do so interruptibly to avoid messing
         * up the load average.
         */
        trace_rcu_future_gp(rnp, rdp, c, TPS("StartWait"));
        for (;;) {
                wait_event_interruptible(
                        rnp->nocb_gp_wq[c & 0x1],
                        (d = ULONG_CMP_GE(ACCESS_ONCE(rnp->completed), c)));
                if (likely(d))
                        break;
                WARN_ON(signal_pending(current));
                trace_rcu_future_gp(rnp, rdp, c, TPS("ResumeWait"));
        }
        trace_rcu_future_gp(rnp, rdp, c, TPS("EndWait"));
        smp_mb(); /* Ensure that CB invocation happens after GP end. */
}

새 gp가 완료되기를 기다린다.

  • 코드 라인 13~18에서 노드락을 걸고 새 gp의 completed 번호를 발급받아온다. 결과 값에 다라 gp 커널 스레드의 wakeup을 요청한다.
  • 코드 라인 25~33에서 발급받은 completed 번호로 홀짝 구분한 대기큐에서 요청 노드의 completed가 발급 받은 번호 이상이될 때까지 대기한다.

 

nocb_follower_wait()

kernel/rcu/tree_plugin.h

/*
 * Followers come here to wait for additional callbacks to show up.
 * This function does not return until callbacks appear.
 */
static void nocb_follower_wait(struct rcu_data *rdp)
{
        bool firsttime = true;

        for (;;) {
                if (!rcu_nocb_poll) {
                        trace_rcu_nocb_wake(rdp->rsp->name, rdp->cpu,
                                            "FollowerSleep");
                        wait_event_interruptible(rdp->nocb_wq,
                                                 ACCESS_ONCE(rdp->nocb_follower_head));
                } else if (firsttime) {
                        /* Don't drown trace log with "Poll"! */
                        firsttime = false;
                        trace_rcu_nocb_wake(rdp->rsp->name, rdp->cpu, "Poll");
                }
                if (smp_load_acquire(&rdp->nocb_follower_head)) {
                        /* ^^^ Ensure CB invocation follows _head test. */
                        return;
                }
                if (!rcu_nocb_poll)
                        trace_rcu_nocb_wake(rdp->rsp->name, rdp->cpu,
                                            "WokeEmpty");
                WARN_ON(signal_pending(current));
                schedule_timeout_interruptible(1);
        }
}

follower cpu는 이 곳에서 대기한다. leader가 gp 만료되면 깨워준다.

  • “rcu_nocb_poll” 커널 파라메터가 설정된 경우 1 tick 마다 폴링한다.

 

참고

 

Namespace

 

Namespace 관리 리소스

리눅스에서 namespace는 lightweight 가상화 솔루션이다. XEN이나 KVM 같은 가상화 솔루션들은 커널 인스턴스들을 생성하여 동작시키는 것에 반하여 리눅스의 namespace는 커널 인스턴스를 만들지 않고 기존의 리소스들을 필요한 만큼의 namespace로 분리하여 묶어 관리하는 방법으로 사용한다. 리눅스의 cgroup과 namespace를 사용하여 container를 생성하여 사용하는 LXC, 그리고 LXC를 사용하는 docker 솔루션 등이 구현되었다.  커널이 부팅된 후 관리 자원은 각각의 초기 디폴트 namespace에서 관리한다. 그런 후 사용자의 필요에 따라 namespace를 추가하여 자원들을 별도로 분리하여 관리할 수 있다. 관리 가능한 namespace 리소스들은 다음과 같다.

 

Namespace 초기화

리눅스에서 user namespace를 제외한 다른 리소스들의 구현은 nsproxy를 통해 연결된다.

struct nsproxy init_nsproxy = {
        .count                  = ATOMIC_INIT(1),
        .uts_ns                 = &init_uts_ns,
#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
        .ipc_ns                 = &init_ipc_ns,
#endif
        .mnt_ns                 = NULL,
        .pid_ns_for_children    = &init_pid_ns,
#ifdef CONFIG_NET
        .net_ns                 = &init_net,
#endif
};

 

nsproxy_cache_init()

kernel/nsproxy.c

int __init nsproxy_cache_init(void)
{
        nsproxy_cachep = KMEM_CACHE(nsproxy, SLAB_PANIC);
        return 0;
}

nsproxy 구조체를 할당해줄 수 있는 kmem 슬랩 캐시를 준비한다.

 

새로운 Namespace 생성

kernel/nsproxy.c

SYSCALL_DEFINE2(setns, int, fd, int, nstype)
{
        struct task_struct *tsk = current;
        struct nsproxy *new_nsproxy;
        struct file *file;
        struct ns_common *ns;
        int err;

        file = proc_ns_fget(fd);
        if (IS_ERR(file))
                return PTR_ERR(file);

        err = -EINVAL;
        ns = get_proc_ns(file_inode(file));
        if (nstype && (ns->ops->type != nstype))
                goto out;

        new_nsproxy = create_new_namespaces(0, tsk, current_user_ns(), tsk->fs);
        if (IS_ERR(new_nsproxy)) {
                err = PTR_ERR(new_nsproxy);
                goto out;
        }

        err = ns->ops->install(new_nsproxy, ns);
        if (err) {
                free_nsproxy(new_nsproxy);
                goto out;
        }
        switch_task_namespaces(tsk, new_nsproxy);
out:
        fput(file);
        return err;
}

setns 라는 이름의 syscall을 호출하여 현재 태스크(스레드)를 새로 지정한 namespace에 연결시킨다.

  • 코드 라인 8~11에서 proc 인터페이스에 해당하는 파일 디스크립터로 file 구조체 포인터를 알아온다.
    • 예) “/proc/1/ns/mnt”
  • 코드 라인 13~16에서 file 디스크립터에 연결된 ns_common 구조체 포인터인 ns를 얻어온다. 만일 nstype이 지정된 경우 file 디스크립터의 namespace type과 다른 경우 에러를 반환한다.
  • 코드 라인 18~22에서 새로운 namespace에 현재 태스크를 지정한다.
  • 코드 라인 24~28에서 해당 리소스의 namespace의 install에 등록된 콜백 함수를 호출하여 설치한다.
    • pid: pidns_install()
    • ipc: ipcns_install()
    • net: netns_install()
    • user: userns_install()
    • mnt: mntns_install()
    • uts: utsns_install()
  • 코드 라인 29에서 태스크의 멤버 nsproxy 가 새로운 nsproxy로 연결하게 한다. 기존에 연결해두었던 nsproxy의 참조 카운터가 0이되면 해제(free)한다.

 

create_new_namespaces()

kernel/nsproxy.c

/*
 * Create new nsproxy and all of its the associated namespaces.
 * Return the newly created nsproxy.  Do not attach this to the task,
 * leave it to the caller to do proper locking and attach it to task.
 */
static struct nsproxy *create_new_namespaces(unsigned long flags,
        struct task_struct *tsk, struct user_namespace *user_ns,
        struct fs_struct *new_fs)
{
        struct nsproxy *new_nsp;
        int err;

        new_nsp = create_nsproxy();
        if (!new_nsp)
                return ERR_PTR(-ENOMEM);

        new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs);
        if (IS_ERR(new_nsp->mnt_ns)) {
                err = PTR_ERR(new_nsp->mnt_ns);
                goto out_ns;
        }

        new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns);
        if (IS_ERR(new_nsp->uts_ns)) {
                err = PTR_ERR(new_nsp->uts_ns);
                goto out_uts;
        }

        new_nsp->ipc_ns = copy_ipcs(flags, user_ns, tsk->nsproxy->ipc_ns);
        if (IS_ERR(new_nsp->ipc_ns)) {
                err = PTR_ERR(new_nsp->ipc_ns);
                goto out_ipc;
        }

        new_nsp->pid_ns_for_children =
                copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children);
        if (IS_ERR(new_nsp->pid_ns_for_children)) {
                err = PTR_ERR(new_nsp->pid_ns_for_children);
                goto out_pid;
        }

        new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns);
        if (IS_ERR(new_nsp->net_ns)) {
                err = PTR_ERR(new_nsp->net_ns);
                goto out_net;
        }

        return new_nsp;

out_net:
        if (new_nsp->pid_ns_for_children)
                put_pid_ns(new_nsp->pid_ns_for_children);
out_pid:
        if (new_nsp->ipc_ns)
                put_ipc_ns(new_nsp->ipc_ns);
out_ipc:
        if (new_nsp->uts_ns)
                put_uts_ns(new_nsp->uts_ns);
out_uts:
        if (new_nsp->mnt_ns)
                put_mnt_ns(new_nsp->mnt_ns);
out_ns:
        kmem_cache_free(nsproxy_cachep, new_nsp);
        return ERR_PTR(err);
}

nsproxy를 새로 할당한 후 namespace들을 연결하고 nsproxy를 반환한다. 플래그에는 새롭게 생성할 namespace를 지정한 플래그 값을 사용할 수 있다.

  • 코드 라인 13~15에서 nsproxy 구조체를 새로 할당해온다.
  • 코드 라인 17~21에서 CLONE_NEWNS 플래그가 지정된 경우 새롭게 mnt_namespace 구조체를 할당하고, 플래그가 지정되지 않은 경우 기존 mnt_namespace를 알아온다. 그리고 요청한 태스크를 mnt_namespcae에 지정한다.
    • namespace 생성 플래그:
      • CLONE_NEWNS
      • CLONE_NEWUTS
      • CLONE_NEWIPC
      • CLONE_NEWUSER
      • CLONE_NEWPID
      • CLONE_NEWNET
  • 코드 라인 23~46에서 mnt namespace와 비슷한 방식으로 uts, ipc, pid, net namespace도 처리한다.
  • 코드 라인 48에서 새롭게 만들어진 rxproxy 구조체 포인터를 반환한다.

 

create_nsproxy()

kernel/nsproxy.c

static inline struct nsproxy *create_nsproxy(void)
{
        struct nsproxy *nsproxy;

        nsproxy = kmem_cache_alloc(nsproxy_cachep, GFP_KERNEL);
        if (nsproxy)
                atomic_set(&nsproxy->count, 1);
        return nsproxy;
}

nsproxy 구조체를 할당해온다.

 

copy_pid_ns()

kernel/pid_namespace.c

struct pid_namespace *copy_pid_ns(unsigned long flags,
        struct user_namespace *user_ns, struct pid_namespace *old_ns)
{      
        if (!(flags & CLONE_NEWPID))
                return get_pid_ns(old_ns);
        if (task_active_pid_ns(current) != old_ns)
                return ERR_PTR(-EINVAL);
        return create_pid_namespace(user_ns, old_ns);
}

 

create_pid_namespace()

kernel/pid_namespace.c

static struct pid_namespace *create_pid_namespace(struct user_namespace *user_ns,
        struct pid_namespace *parent_pid_ns)
{
        struct pid_namespace *ns;
        unsigned int level = parent_pid_ns->level + 1;
        int i;
        int err;

        if (level > MAX_PID_NS_LEVEL) {
                err = -EINVAL;
                goto out;
        }

        err = -ENOMEM;
        ns = kmem_cache_zalloc(pid_ns_cachep, GFP_KERNEL);
        if (ns == NULL)
                goto out;

        ns->pidmap[0].page = kzalloc(PAGE_SIZE, GFP_KERNEL);
        if (!ns->pidmap[0].page)
                goto out_free;

        ns->pid_cachep = create_pid_cachep(level + 1);
        if (ns->pid_cachep == NULL)
                goto out_free_map;

        err = ns_alloc_inum(&ns->ns);
        if (err)
                goto out_free_map;
        ns->ns.ops = &pidns_operations;

        kref_init(&ns->kref);
        ns->level = level;
        ns->parent = get_pid_ns(parent_pid_ns);
        ns->user_ns = get_user_ns(user_ns);
        ns->nr_hashed = PIDNS_HASH_ADDING;
        INIT_WORK(&ns->proc_work, proc_cleanup_work);

        set_bit(0, ns->pidmap[0].page);
        atomic_set(&ns->pidmap[0].nr_free, BITS_PER_PAGE - 1);

        for (i = 1; i < PIDMAP_ENTRIES; i++)
                atomic_set(&ns->pidmap[i].nr_free, BITS_PER_PAGE);

        return ns;

out_free_map:
        kfree(ns->pidmap[0].page);
out_free:
        kmem_cache_free(pid_ns_cachep, ns);
out:
        return ERR_PTR(err);
}

새로 만들어지는 child pid namespace는 최대 32단계까지 생성가능하다.  pid_namespace를 생성하고 초기화하고 내부 pidmap의 구성은 다음을 참고한다.

 

Namespace 예)

아래 <pid> 값은 해당 태스크의 pid 숫자로 치환되어야 한다.

root@jake-VirtualBox:/proc/<pid>/ns# ls /proc/1/ns/ -la
합계 0
dr-x--x--x 2 root root 0  1월 11 11:16 .
dr-xr-xr-x 9 root root 0  1월  8 14:06 ..
lrwxrwxrwx 1 root root 0  1월 11 11:16 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  1월 11 11:16 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0  1월 11 11:16 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0  1월 11 11:16 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0  1월 11 11:16 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  1월 11 11:16 uts -> uts:[4026531838]

 

# unshare --help

Usage:
 unshare [options] <program> [<argument>...]

Run a program with some namespaces unshared from the parent.

Options:
 -m, --mount               unshare mounts namespace
 -u, --uts                 unshare UTS namespace (hostname etc)
 -i, --ipc                 unshare System V IPC namespace
 -n, --net                 unshare network namespace
 -p, --pid                 unshare pid namespace
 -U, --user                unshare user namespace
 -f, --fork                fork before launching <program>
     --mount-proc[=<dir>]  mount proc filesystem first (implies --mount)
 -r, --map-root-user       map current user to root (implies --user)
 -s, --setgroups allow|deny  control the setgroups syscall in user namespaces

 -h, --help     display this help and exit
 -V, --version  output version information and exit

 

참고