Linux v6.6 · 개념과 코드 읽기

태스크 객체를 만들기 위한 저장소와 한도를 준비합니다

이 코드는 어떤 문제를 푸나요?

프로세스를 실제 생성하기 전에 task_struct를 효율적으로 할당할 캐시와 생성 제한을 준비합니다. 따라서 fork_init의 실행 자체가 새 프로세스 하나를 만드는 것은 아닙니다. 이 초기화 결과를 이후 fork 계열 경로가 사용합니다.

읽을 범위: v6.6 · kernel/fork.c · fork_init 1041–1086행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.

먼저 알아둘 개념

슬랩 캐시

같은 종류와 크기의 커널 객체를 반복 할당할 저장소입니다.

생성 한도

자원 사용량을 제한하여 태스크 생성이 시스템 메모리를 무한히 차지하지 않게 합니다.

처음 읽을 때

코드를 읽을 때 task_struct의 크기·정렬·usercopy 허용 범위를 구합니다. 그다음 어느 상태가 바뀌는지 각 줄에서 확인하세요.

더 깊이 살펴볼 때

이 경로의 호출 문맥, 잠금·인터럽트 상태, 오류 시 남는 자원을 함께 추적해 보세요. 호출된 함수가 수행하는 작업과 현재 함수가 직접 보장하는 범위를 구분하는 것이 중요합니다.

그림으로 보는 변화

태스크 객체를 만들기 위한 저장소와 한도를 준비합니다의 단계별 개념 그림
각 단계에 화살표 의미와 생략 범위를 표시했습니다. 주소·숫자 예제는 실제 장치 값을 뜻하지 않습니다.
1단계 설명

1단계 고정

GIF 원본 열기

1. 객체 형식 준비

task_struct의 크기·정렬·usercopy 허용 범위를 구합니다.

화살표는 초기화 순서입니다.

2. 캐시와 한도 설정

태스크 캐시를 만들고 스레드·사용자별 제한을 설정합니다.

한도 설정과 실제 태스크 생성은 다릅니다.

3. 부가 기능 준비

스택 보호·lockdep·uprobes 관련 초기화를 마칩니다.

설정에 따라 실행되지 않는 단계도 있습니다.

fork_init를 한 줄씩 읽기

줄 번호는 v6.6 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.

void __init fork_init(void)
{
	int i;
#ifndef CONFIG_ARCH_TASK_STRUCT_ALLOCATOR
#ifndef ARCH_MIN_TASKALIGN
#define ARCH_MIN_TASKALIGN	0
#endif
	int align = max_t(int, L1_CACHE_BYTES, ARCH_MIN_TASKALIGN);
	unsigned long useroffset, usersize;

	/* create a slab on which task_structs can be allocated */
	task_struct_whitelist(&useroffset, &usersize);
	task_struct_cachep = kmem_cache_create_usercopy("task_struct",
			arch_task_struct_size, align,
			SLAB_PANIC|SLAB_ACCOUNT,
			useroffset, usersize, NULL);
#endif

	/* do the arch specific task caches init */
	arch_task_cache_init();

	set_max_threads(MAX_THREADS);

	init_task.signal->rlim[RLIMIT_NPROC].rlim_cur = max_threads/2;
	init_task.signal->rlim[RLIMIT_NPROC].rlim_max = max_threads/2;
	init_task.signal->rlim[RLIMIT_SIGPENDING] =
		init_task.signal->rlim[RLIMIT_NPROC];

	for (i = 0; i < UCOUNT_COUNTS; i++)
		init_user_ns.ucount_max[i] = max_threads/2;

	set_userns_rlimit_max(&init_user_ns, UCOUNT_RLIMIT_NPROC,      RLIM_INFINITY);
	set_userns_rlimit_max(&init_user_ns, UCOUNT_RLIMIT_MSGQUEUE,   RLIM_INFINITY);
	set_userns_rlimit_max(&init_user_ns, UCOUNT_RLIMIT_SIGPENDING, RLIM_INFINITY);
	set_userns_rlimit_max(&init_user_ns, UCOUNT_RLIMIT_MEMLOCK,    RLIM_INFINITY);

#ifdef CONFIG_VMAP_STACK
	cpuhp_setup_state(CPUHP_BP_PREPARE_DYN, "fork:vm_stack_cache",
			  NULL, free_vm_stack_cache);
#endif

	scs_init();

	lockdep_init_task(&init_task);
	uprobes_init();
}
void __init fork_init(void)

나중에 task_struct를 할당할 캐시와 초기 태스크 생성 한도를 준비합니다. 인자가 없는 부팅 초기화 함수이며 이 호출 자체가 자식 프로세스를 만들지는 않습니다.

	int i;

초기 user namespace의 자원 종류별 한도를 순회할 인덱스입니다. 아래 UCOUNT_COUNTS 반복에서 사용합니다.

#ifndef CONFIG_ARCH_TASK_STRUCT_ALLOCATOR

아키텍처가 task_struct 전용 할당 함수를 제공하는지 나타냅니다. #ifndef이므로 전용 구현이 없을 때만 아래의 공통 slab 캐시를 만들며, 전용 구현을 쓰는 빌드는 그 생성을 건너뜁니다. 이 선택은 전처리 단계에서 이루어지며, CPU가 실행 중 이 줄의 조건을 검사하지 않습니다.

#ifndef ARCH_MIN_TASKALIGN

아키텍처가 task_struct의 별도 최소 정렬을 정의하지 않았다면 공통 기본값을 제공하는 구간입니다. 정의가 있는 아키텍처의 요구값을 덮어쓰지 않으려는 조건입니다. 이 선택은 전처리 단계에서 이루어지며, CPU가 실행 중 이 줄의 조건을 검사하지 않습니다.

#define ARCH_MIN_TASKALIGN	0

아키텍처별 추가 정렬 요구가 없음을 0으로 표시합니다. 실제 캐시 정렬은 아래에서 L1_CACHE_BYTES와의 최댓값을 사용하므로 객체가 무정렬로 배치되는 것은 아닙니다. 이 이름은 전처리 단계에서 정의한 값으로 치환되며, 실행 중 값을 담을 변수를 만드는 것은 아닙니다.

#endif

아키텍처가 정렬 매크로를 제공하지 않은 경우의 기본값 정의를 마칩니다. 이제 공통 식에서 캐시라인 크기와 비교할 수 있습니다.

	int align = max_t(int, L1_CACHE_BYTES, ARCH_MIN_TASKALIGN);

캐시라인 크기와 아키텍처의 task_struct 최소 정렬 요구 중 더 큰 값을 선택합니다. max_t의 int는 비교·결과에 사용할 타입이며 이후 객체 캐시가 이 정렬을 지킵니다.

	unsigned long useroffset, usersize;

task_struct에서 사용자와 복사해도 되는 영역의 시작 오프셋과 길이를 담습니다. 전체 객체 크기와 구분되는 허용 범위입니다.

	task_struct_whitelist(&useroffset, &usersize);

task_struct 안에서 usercopy를 허용할 시작 위치와 크기를 얻습니다.

	task_struct_cachep = kmem_cache_create_usercopy("task_struct",

정해진 크기의 객체 캐시를 만들면서 사용자와 복사할 수 있는 바이트 범위를 제한합니다.

			arch_task_struct_size, align,

캐시에 넣을 task_struct의 실제 크기와 앞서 계산한 정렬을 넘깁니다. 아키텍처별 추가 상태 때문에 고정 숫자를 쓰지 않습니다. 앞줄에서 시작한 호출이나 식에 이어지는 부분이므로, 이 줄만으로 별도의 함수 호출이 생기지는 않습니다.

			SLAB_PANIC|SLAB_ACCOUNT,

필수 캐시 생성이 실패하면 부팅을 중단하고(SLAB_PANIC), 이 캐시에서 할당되는 객체를 메모리 cgroup 회계에 포함할 수 있게(SLAB_ACCOUNT) 지정합니다. 앞줄에서 시작한 호출이나 식에 이어지는 부분이므로, 이 줄만으로 별도의 함수 호출이 생기지는 않습니다.

			useroffset, usersize, NULL);

사용자 복사가 허용되는 바이트 범위를 전달하고 객체 생성자 콜백은 등록하지 않습니다. useroffset과 usersize는 앞의 whitelist 호출에서 얻었습니다.

#endif

아키텍처 전용 할당기가 없는 경우의 공통 task_struct 캐시 생성을 마칩니다. 뒤의 아키텍처 보조 캐시·태스크 수 한도 준비는 이어서 수행합니다.

	arch_task_cache_init();

아키텍처별 태스크 관련 캐시를 준비합니다.

	set_max_threads(MAX_THREADS);

메모리 크기와 정책에 맞춰 최대 태스크 수를 제한합니다.

	init_task.signal->rlim[RLIMIT_NPROC].rlim_cur = max_threads/2;

초기 태스크의 프로세스·스레드 수 soft limit을 시스템 최대 스레드 수의 절반으로 설정합니다. 이후 태스크가 물려받는 자원 제한의 출발점입니다.

	init_task.signal->rlim[RLIMIT_NPROC].rlim_max = max_threads/2;

같은 자원의 hard limit도 절반으로 설정합니다. soft limit의 현재 적용값과 일반 사용자가 올릴 수 있는 상한을 별도 필드에 둡니다.

	init_task.signal->rlim[RLIMIT_SIGPENDING] =

대기 중인 시그널 수 제한의 초기값을 프로세스 수 제한과 같게 설정하기 시작합니다. 다음 줄에서 soft·hard limit을 함께 가진 구조체를 복사합니다.

		init_task.signal->rlim[RLIMIT_NPROC];

앞줄의 대기 시그널 한도에 RLIMIT_NPROC의 soft·hard limit 쌍을 넘깁니다. 태스크나 주소 공간 자체를 복제하는 대입이 아닙니다.

	for (i = 0; i < UCOUNT_COUNTS; i++)

user namespace가 추적하는 각 자원 계수 종류를 순회합니다. 같은 기본 상한을 빠짐없이 정하기 위한 반복입니다.

		init_user_ns.ucount_max[i] = max_threads/2;

초기 user namespace에서 i번째 자원 계수의 최대값을 설정합니다. 아래 rlimit 계열별 상한 설정과는 별도로 관리되는 배열입니다.

	set_userns_rlimit_max(&init_user_ns, UCOUNT_RLIMIT_NPROC,      RLIM_INFINITY);

초기 user namespace의 사용자별 프로세스·스레드 수에 대한 집계 상한을 설정합니다. RLIM_INFINITY를 넘겨도 저장 값은 표현 가능한 LONG_MAX로 제한됩니다. 이 namespace 상한과 각 태스크의 rlimit은 별개이므로 앞에서 설정한 태스크 한도까지 없애는 코드는 아닙니다.

	set_userns_rlimit_max(&init_user_ns, UCOUNT_RLIMIT_MSGQUEUE,   RLIM_INFINITY);

초기 user namespace의 POSIX 메시지 큐가 차지하는 바이트 수에 대한 집계 상한을 설정합니다. RLIM_INFINITY를 넘겨도 저장 값은 표현 가능한 LONG_MAX로 제한됩니다. 이 namespace 상한과 각 태스크의 rlimit은 별개이므로 앞에서 설정한 태스크 한도까지 없애는 코드는 아닙니다.

	set_userns_rlimit_max(&init_user_ns, UCOUNT_RLIMIT_SIGPENDING, RLIM_INFINITY);

초기 user namespace의 대기 중인 시그널 수에 대한 집계 상한을 설정합니다. RLIM_INFINITY를 넘겨도 저장 값은 표현 가능한 LONG_MAX로 제한됩니다. 이 namespace 상한과 각 태스크의 rlimit은 별개이므로 앞에서 설정한 태스크 한도까지 없애는 코드는 아닙니다.

	set_userns_rlimit_max(&init_user_ns, UCOUNT_RLIMIT_MEMLOCK,    RLIM_INFINITY);

초기 user namespace의 메모리에 고정해 두는 페이지의 바이트 수에 대한 집계 상한을 설정합니다. RLIM_INFINITY를 넘겨도 저장 값은 표현 가능한 LONG_MAX로 제한됩니다. 이 namespace 상한과 각 태스크의 rlimit은 별개이므로 앞에서 설정한 태스크 한도까지 없애는 코드는 아닙니다.

#ifdef CONFIG_VMAP_STACK

커널 스택을 가상 주소에 매핑하고 guard page로 스택 넘침을 잡는 구성입니다. 이 구성에서만 CPU가 내려갈 때 캐시에 남은 가상 스택을 정리할 콜백을 등록합니다. 이 선택은 전처리 단계에서 이루어지며, CPU가 실행 중 이 줄의 조건을 검사하지 않습니다.

	cpuhp_setup_state(CPUHP_BP_PREPARE_DYN, "fork:vm_stack_cache",

CPU 상태 전환 시 호출할 준비·정리 함수를 등록합니다.

			  NULL, free_vm_stack_cache);

CPU 준비 시 실행할 별도 콜백은 없고, CPU가 내려갈 때 캐시된 가상 커널 스택을 정리할 free_vm_stack_cache를 등록합니다. 앞줄에서 시작한 호출이나 식에 이어지는 부분이므로, 이 줄만으로 별도의 함수 호출이 생기지는 않습니다.

#endif

가상 커널 스택 캐시의 CPU 종료 콜백 등록 부분을 마칩니다. 이 조건은 스택 캐시 정리에 대한 것이며 뒤의 공통 태스크 추적 초기화까지 제외하지 않습니다.

	scs_init();

반환 주소를 별도로 보호하는 shadow call stack 체계의 초기화를 수행합니다.

	lockdep_init_task(&init_task);

초기 태스크가 사용할 잠금 의존성 추적 상태를 준비합니다.

	uprobes_init();

사용자 프로그램의 probe 추적 기능을 준비합니다.

함께 생각해 볼 질문

kmem_cache_create가 task_struct 한 개만 만드나요?

이후 객체를 할당할 캐시를 만드는 호출입니다.

usercopy 범위를 왜 따로 정하나요?

객체 안의 모든 바이트를 사용자와 교환하도록 허용하지 않기 위해서입니다.

CONFIG_VMAP_STACK이 없으면 해당 콜백도 필요한가요?

해당 코드 구간은 구성에 따라 빌드에서 빠집니다.

출처와 읽은 범위

Linux stable v6.6 · kernel/fork.c

해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고

맨 위로 ↑