전체 흐름
copy_process()는 새 task를 준비하고 부모·PID 관계에 연결해 아직 실행되지 않은 task_struct *를 반환한다. 일반적인 kernel_clone() 호출자는 그 반환값을 받은 뒤 별도로 wake_up_new_task()를 호출한다. 두 함수의 경계를 합치면 공개 시점과 실행 시작을 혼동한다 (kernel/fork.c:2735, kernel/fork.c:2940).
실선은 정상 경로, 주황색은 공개 전 실패 정리다. 그림 크게 열기.
create_io_thread()처럼 copy_process()를 직접 호출하는 경로도 있다. 이 함수는 비활성 task를 반환하며 호출자가 깨워야 한다 (kernel/fork.c:2838). 따라서 wakeup은 copy_process()의 일반적 내부 후속 단계가 아니다.
소스 코드 위치
아래 행 번호는 이 글의 Linux v6.6 고정 소스에 맞춘 것이다. 다른 버전의 유사 코드와 섞지 않는다.
| 관찰 지점 | 고정 소스 | 질문 |
|---|---|---|
| 생성·자원·오류 | kernel/fork.c:2327 | 어느 자원을 확보한 뒤 어떤 레이블로 빠지는가? |
| 부모·PID 공개 | kernel/fork.c:2626 | 잠금 아래에서 목록과 PID가 언제 연결되는가? |
| 호출자 wakeup | kernel/fork.c:2909 | 비활성 task를 받은 뒤 누가 깨우는가? |
| runqueue 반영 | kernel/sched/core.c:4848 | TASK_RUNNING, enqueue, 선점 판단은 어디서 일어나는가? |
원본 트리: kernel/fork.c, kernel/sched/core.c.
대표 코드
모든 발췌는 해당 고정 버전의 실제 연속 행이다. 이전의 “wake_up_new_task”라는 제목 아래 create_io_thread()와 kernel_clone() 시작 부분을 묶은 발췌는 실제 호출 지점과 스케줄러 구현으로 교체했다.
1. task와 stack 확보 — clone_flags 인자 없음
kernel/fork.c:1103–1120 · 고정 소스 그대로 발췌
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
struct task_struct *tsk;
int err;
if (node == NUMA_NO_NODE)
node = tsk_fork_get_node(orig);
tsk = alloc_task_struct_node(node);
if (!tsk)
return NULL;
err = arch_dup_task_struct(tsk, orig);
if (err)
goto free_tsk;
err = alloc_thread_stack_node(tsk, node);
if (err)
goto free_tsk;2. copy_* 호출과 단계별 실패 이동
kernel/fork.c:2489–2512 · 고정 소스 그대로 발췌
retval = copy_files(clone_flags, p, args->no_files);
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(p, args);
if (retval)
goto bad_fork_cleanup_io;3. 부모 목록·PID 공개
kernel/fork.c:2695–2713 · 고정 소스 그대로 발췌
list_add_tail(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks);
attach_pid(p, PIDTYPE_TGID);
attach_pid(p, PIDTYPE_PGID);
attach_pid(p, PIDTYPE_SID);
__this_cpu_inc(process_counts);
} else {
current->signal->nr_threads++;
current->signal->quick_threads++;
atomic_inc(¤t->signal->live);
refcount_inc(¤t->signal->sigcnt);
task_join_group_stop(p);
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++;4. kernel_clone의 별도 wakeup
kernel/fork.c:2909–2940 · 고정 소스 그대로 발췌
p = copy_process(NULL, trace, NUMA_NO_NODE, args);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
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, args->parent_tid);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
if (IS_ENABLED(CONFIG_LRU_GEN) && !(clone_flags & CLONE_VM)) {
/* lock the task to synchronize with memcg migration */
task_lock(p);
lru_gen_add_mm(p->mm);
task_unlock(p);
}
wake_up_new_task(p);5. scheduler의 새 task 활성화
kernel/sched/core.c:4848–4887 · 고정 소스 그대로 발췌
void wake_up_new_task(struct task_struct *p)
{
struct rq_flags rf;
struct rq *rq;
raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
WRITE_ONCE(p->__state, TASK_RUNNING);
#ifdef CONFIG_SMP
/*
* Fork balancing, do it here and not earlier because:
* - cpus_ptr can change in the fork path
* - any previously selected CPU might disappear through hotplug
*
* Use __set_task_cpu() to avoid calling sched_class::migrate_task_rq,
* as we're not fully set-up yet.
*/
p->recent_used_cpu = task_cpu(p);
rseq_migrate(p);
__set_task_cpu(p, select_task_rq(p, task_cpu(p), WF_FORK));
#endif
rq = __task_rq_lock(p, &rf);
update_rq_clock(rq);
post_init_entity_util_avg(p);
activate_task(rq, p, ENQUEUE_NOCLOCK);
trace_sched_wakeup_new(p);
check_preempt_curr(rq, p, WF_FORK);
#ifdef CONFIG_SMP
if (p->sched_class->task_woken) {
/*
* Nothing relies on rq->lock after this, so it's fine to
* drop it.
*/
rq_unpin_lock(rq, &rf);
p->sched_class->task_woken(rq, p);
rq_repin_lock(rq, &rf);
}
#endif
task_rq_unlock(rq, p, &rf);
}함수별 분석
dup_task_struct(orig,node)에는 clone_flags 매개변수가 없다. flags 정책은 copy_process()의 검증과 각 copy_*() helper에서 처리한다 (kernel/fork.c:2327, kernel/fork.c:2489).
CLONE_VM/CLONE_FILES는 독립적인 예시다. clone 조합 자체의 허용 여부는 앞선 검증 조건을 따른다.
| 호출 | 조건·동작 | 이후 또는 실패 |
|---|---|---|
| dup_task_struct | current와 node를 받아 task·stack 확보 (kernel/fork.c:1103) | 이 단계는 CLONE_* 공유 정책을 직접 선택하지 않음 |
| copy_files | CLONE_FILES면 count 증가·공유, 아니면 dup_fd (kernel/fork.c:1766) | dup_fd(oldf, NR_OPEN_MAX, &error) 실패를 error 값으로 반환한다. |
| copy_mm | CLONE_VM면 mmget·공유, 아니면 dup_mm (kernel/fork.c:1708) | current→mm이 없는 kernel thread는 별도 처리 |
| copy_* 연속 | files → fs → sighand → signal → mm → namespaces → io → thread (kernel/fork.c:2489) | 각 실패 지점이 이미 얻은 자원에 맞는 label로 이동 |
객체와 연결 관계
alloc_pid()만으로 사용자에게 실행 가능한 task가 되는 것은 아니다. tasklist_lock 아래 부모 관계·task 목록·PID 연결 뒤 copy_process()가 비활성 task를 반환한다.복제한 task 본체와 공유 가능한 하위 객체의 소유권을 따로 추적한다.
| 객체 / 연결 | 이 버전의 관찰 지점 |
|---|---|
task_struct p | dup_task_struct()가 새 task와 stack을 준비 (kernel/fork.c:2327) |
mm / files | flags에 따라 공유 참조 또는 복제. dup_fd(oldf, NR_OPEN_MAX, &error) 실패를 error 값으로 반환한다. |
struct pid | alloc_pid() 후 attach_pid(); 할당과 공개를 구분 (kernel/fork.c:2517, kernel/fork.c:2695) |
real_parent / task 목록 | CLONE_PARENT|CLONE_THREAD면 current→real_parent, 아니면 current (kernel/fork.c:2626). 스레드 분기에는 thread_group과 thread_node를 연결한다. |
상태 전이와 오류 되감기
단계 번호는 소스 순서다. ④는 ①–③과 달리 kernel_clone() 호출자 쪽이다.
| 경계 / 실패 | 소스상 의미 | 검증 포인트 |
|---|---|---|
| 자원 준비 | copy_* 중 실패하면 해당 정리 label로 이동 (kernel/fork.c:2489) | 아직 얻지 않은 자원까지 해제하지 않음 |
| cgroup·추가 준비 | sched_cgroup_fork()은 이 버전에서 void; 실패 분기 없음 (kernel/fork.c:2596). 여기에는 futex hash 추가 할당 실패 분기가 없다. | 버전별 추가 실패 지점을 구별 |
| 마지막 실패 검사 | “No more failure paths” 주석 이후 목록·PID 연결 (kernel/fork.c:2664) | 공개 후 bad_fork unwind로 가는 설명을 피함 |
| 오류 레이블 | bad_fork_cancel_cgroup → PID/pidfd 정리 → 확보한 자원 역순 정리. sched_cancel_fork() 레이블은 없다. (kernel/fork.c:2737) | 진입 레이블에 따라 통과하는 정리 구간이 다름 |
| 별도 wakeup | select_task_rq() → activate_task(..., ENQUEUE_NOCLOCK) → check_preempt_curr(..., WF_FORK) (kernel/sched/core.c:4848) | copy_process 내부 단계로 그리지 않음 |
검토할 불변 조건
- flags 검증:
CLONE_THREAD,CLONE_SIGHAND,CLONE_VM의 조합 제약을 자원 helper 진입 전에 확인한다 (kernel/fork.c:2240). - 공개 전 준비: cgroup 및 scheduler 준비가 끝난 뒤
tasklist_lock아래 연결한다 (kernel/fork.c:2596, kernel/fork.c:2626). - 오류 정리: 각
goto bad_fork_*는 그 시점까지 성공한 작업에 맞는 레이블로 들어간다. 하나의 공통 “rollback” 함수로 단순화하지 않는다 (kernel/fork.c:2737). - 호출자 경계: 성공한
copy_process()는 task를 깨우지 않는다. 일반kernel_clone()과create_io_thread()의 호출자 책임을 구별한다 (kernel/fork.c:2909).
계측과 검증
tracepoint는 소스에서 호출된 위치를 기준으로 해석한다. event가 보였다고 앞선 모든 사용자 관찰이나 자식의 실제 실행이 끝났다는 뜻은 아니다.
| 관찰 이벤트 | 이 버전의 호출 위치 | 해석 한계 |
|---|---|---|
task_newtask | copy_process() 반환 전 (kernel/fork.c:2729) | 새 task 생성 기록; 자식 실행 시작 아님 |
sched_process_fork | kernel_clone()에서 성공 반환 후, wake 전 (kernel/fork.c:2919) | copy_process의 중간 실패 위치를 이 이벤트만으로 구별 못함 |
sched_wakeup_new | wake_up_new_task()의 enqueue 뒤 (kernel/sched/core.c:4873) | 실제 CPU에서 자식 명령 실행 완료를 보장하지 않음 |
grep -E 'task_newtask|sched_process_fork|sched_wakeup_new' /sys/kernel/tracing/available_events위 확인은 읽기 전용이다. 이벤트가 없는 커널 구성에서는 그 이벤트로 단계가 관찰된다고 가정하지 않는다.
추가 확인 사항
CLONE_FILES나CLONE_VM의 공유 참조 증가가 실패 unwind의 어느 해제 함수와 짝을 이루는가?CLONE_PARENT|CLONE_THREAD여부에 따라real_parent와 thread 목록 연결이 어떻게 달라지는가?- 실패가 kernel/fork.c:2596 전후 어느 label에 도달하는가? 성공 시 kernel/fork.c:2940 호출 전에 무엇이 관찰 가능한가?
- sched_cgroup_fork()은 이 버전에서 void; 실패 분기 없음. 이 버전의 scheduler 상태를 kernel/sched/core.c:4848의 enqueue와 구분할 수 있는가?