Android 커널의 벤더 훅(Vendor Hook) 840개 — GKI가 out-of-tree 코드를 받아들이는 방식과 그 대가

Android 기기는 처음부터 메인라인과 상당히 다른 커널을 써 왔습니다. GKI(Generic Kernel Image)가 도입되면서 그 차이는 많이 줄었습니다. 그래도 지금 출시되는 기기의 커널에는 여전히 out-of-tree 변경이 대량으로 들어 있습니다. Android가 이런 변경을 받아들이는 공식 경로가 벤더 훅(vendor hook)입니다. 10월 프라하에서 열린 LPC 2026(Linux Plumbers Conference) Android 트랙에서 Google의 토드 코스(Todd Kjos)가 벤더 훅의 현황을 발표했고, LWN이 이를 정리했습니다. 이 글에서는 발표 내용과 함께 Android 공통 커널(ACK) 소스를 직접 열어 벤더 훅이 어떻게 구현돼 있는지, 발표의 숫자가 실제 소스와 맞는지 확인합니다.

Android 6.6 커널의 out-of-tree 패치 3,100개

현재 가장 많은 Android 기기가 쓰는 커널은 6.6입니다. 6.18은 주로 플래그십 기기에 들어갑니다. 코스에 따르면 Android 6.6 커널에는 메인라인에 없는 패치가 3,100개 조금 넘게 들어 있습니다.

분류패치 수내용
백포트1,531새 커널 릴리스의 기능을 가져온 것
from list287메일링 리스트에서 논의 중이지만 아직 메인라인에 없는 것
순수 Android1,306 (약 42%)업스트림에 없고 갈 예정도 없는 것

순수 Android 패치 중 3분의 1 이상이 벤더 훅입니다. 나머지는 대부분 빌드 시스템과 설정 변경입니다. 또 순수 Android 패치의 15%는 벤더 훅이 쓸 심볼을 export하는 패치입니다. Android는 이 훅들의 ABI를 안정적으로 유지하려 합니다. 코스는 그 때문에 커널 변경이 자주 제약을 받는다고 말했습니다.

벤더 훅은 특수 목적 트레이스포인트

가장 낮은 수준에서 보면 벤더 훅은 Android 커널에 추가한 트레이스포인트입니다. 이벤트를 기록하는 trace event가 아니라 모듈이 함수를 붙일 수 있는 tracepoint입니다. 벤더는 자기 기기에서 특수 커널 모듈을 로드해 이 지점에 핸들러를 붙입니다. 붙은 핸들러는 자료구조를 바꾸고, 새 코드를 끼워 넣고, 메인라인 코드를 건너뛸 수도 있습니다. 사실상 제약이 거의 없는 커널 코드입니다.

훅은 두 종류입니다. ACK의 include/trace/hooks/vendor_hooks.h를 보면 일반 훅은 업스트림 트레이스포인트 매크로를 그대로 씁니다.

#if defined(CONFIG_TRACEPOINTS) && defined(CONFIG_ANDROID_VENDOR_HOOKS)

#define DECLARE_HOOK DECLARE_TRACE

int android_rvh_probe_register(struct tracepoint *tp, void *probe, void *data);
...
	static inline int						\
	register_trace_##name(void (*probe)(data_proto), void *data) 	\
	{								\
		return android_rvh_probe_register(&__tracepoint_##name,	\
						  (void *)probe, data);	\
	}								\
	/* vendor hooks cannot be unregistered */			\

제한 훅(restricted hook)은 별도 매크로 DECLARE_RESTRICTED_HOOK로 선언하고 전용 등록 함수를 씁니다. 등록 함수의 구현(kernel/tracepoint.c)에서 두 가지 제약이 드러납니다.

#define ANDROID_RVH_NR_PROBES_MAX	2
static int rvh_func_add(struct tracepoint *tp, struct tracepoint_func *func)
{
	int i;

	if (!static_key_enabled(&tp->key)) {
		/* '+ 1' for the last NULL element */
		tp->funcs = rvh_zalloc_funcs(ANDROID_RVH_NR_PROBES_MAX + 1);
		if (!tp->funcs)
			return ENOMEM;
	}

	for (i = 0; i < ANDROID_RVH_NR_PROBES_MAX; i++) {
		if (!tp->funcs[i].func) {
			if (!static_key_enabled(&tp->key))
				tp->funcs[i].data = func->data;
			WRITE_ONCE(tp->funcs[i].func, func->func);

			return 0;
		}
	}

	return -EBUSY;
}
구분일반 훅제한 훅
이름android_vh_*android_rvh_*
선언 매크로DECLARE_HOOK (= DECLARE_TRACE)DECLARE_RESTRICTED_HOOK
붙일 수 있는 핸들러일반 트레이스포인트와 같음최대 2개, 넘으면 -EBUSY
해제가능불가. 붙인 모듈은 언로드할 수 없음
용도대부분의 훅CPU가 오프라인이거나 원자적이지 않은 컨텍스트에서도 반드시 불려야 하는 스케줄러 훅 등

용도 설명은 Android 공식 문서 “GKI용 커널 코드 개발”에 나온 내용입니다. 실제 사용 예는 CFS 스케줄러의 CPU 선택 함수에서 볼 수 있습니다. 훅이 target_cpu를 채우면 메인라인 로직을 통째로 건너뜁니다.

static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int wake_flags)
{
	...
	int target_cpu = -1;
	...
	trace_android_rvh_select_task_rq_fair(p, prev_cpu, sd_flag,
			wake_flags, &target_cpu);
	if (target_cpu >= 0)
		return target_cpu;

벤더 모듈 쪽은 초기화 시점에 핸들러를 등록하기만 하면 됩니다. 아래는 Android 문서의 예시입니다.

/* include/trace/hooks/exit.h */
DECLARE_HOOK(android_vh_sched_exit,
             TP_PROTO(struct task_struct *p),
             TP_ARGS(p));

/* drivers/android/vendor_hooks.c */
EXPORT_TRACEPOINT_SYMBOL_GPL(android_vh_sched_exit);

/* 코어 커널, do_exit() 안 */
trace_android_vh_sched_exit(tsk);

/* 벤더 모듈 */
static void foo_sched_exit_handler(void *data, struct task_struct *p) { ... }
rc = register_trace_android_vh_sched_exit(foo_sched_exit_handler, NULL);

ACK 소스에서 세어 본 훅 840개

코스는 커널에 벤더 훅이 약 800개 있고, 절반 가까이가 메모리 관리와 CPU 스케줄러에 있다고 했습니다. ACK android15-6.6 브랜치(10월 23일 커밋 745990bb615d)의 include/trace/hooks/ 헤더 73개를 내려받아 직접 세어 봤습니다.

#!/usr/bin/env python3
"""ACK include/trace/hooks/*.h에서 벤더 훅 선언 수를 헤더별로 센다."""
import re, sys, pathlib

d = pathlib.Path(sys.argv[1])
rows = []
for f in sorted(d.glob("*.h")):
    t = f.read_text(errors="ignore")
    vh = len(re.findall(r"^\s*DECLARE_HOOK\(\s*android_vh_", t, re.M))
    rvh = len(re.findall(r"^\s*DECLARE_RESTRICTED_HOOK\(\s*android_rvh_", t, re.M))
    if vh + rvh:
        rows.append((f.name, vh, rvh))
tv = sum(r[1] for r in rows); tr = sum(r[2] for r in rows)
print(f"headers={len(rows)} vh={tv} rvh={tr} total={tv+tr}")
for name, vh, rvh in sorted(rows, key=lambda r: -(r[1] + r[2]))[:12]:
    print(f"{name:20s} vh={vh:4d} rvh={rvh:4d} total={vh+rvh:4d}")
$ python3 count_hooks.py hooks/
headers=72 vh=637 rvh=203 total=840
mm.h                 vh= 230 rvh=  23 total= 253
sched.h              vh=  33 rvh=  85 total= 118
vmscan.h             vh=  45 rvh=   7 total=  52
dtask.h              vh=  50 rvh=   1 total=  51
net.h                vh=  30 rvh=  11 total=  41
ogki_honor.h         vh=  23 rvh=  13 total=  36
binder.h             vh=  32 rvh=   0 total=  32
ufshcd.h             vh=  16 rvh=   2 total=  18
fs.h                 vh=  12 rvh=   2 total=  14
rwsem.h              vh=  13 rvh=   0 total=  13
blk.h                vh=   9 rvh=   1 total=  10
cgroup.h             vh=   4 rvh=   5 total=   9

선언은 840개로 발표의 “약 800개”와 맞습니다. 메모리 관리(mm.h와 vmscan.h, 305개)와 스케줄러(sched.h, 118개)를 합치면 423개로 정확히 절반입니다. 눈에 띄는 차이도 있습니다. 메모리 관리 훅은 대부분 일반 훅인데, 스케줄러 훅은 118개 중 85개가 해제할 수 없는 제한 훅입니다. 스케줄러는 원자적 컨텍스트 제약이 가장 까다로운 곳이기 때문입니다. 참고로 drivers/android/vendor_hooks.c의 EXPORT_TRACEPOINT_SYMBOL_GPL은 722개입니다.

왜 벤더 훅을 쓰나

코스가 꼽은 가장 큰 이유는 시급성입니다. 지금 출시할 제품에 필요한 변경인데 업스트림 절차가 너무 느립니다. 벤더들은 “좋은 의도”를 갖고 있지만 메인라인까지 가져갈 인내심은 부족합니다. 나중에 실제로 업스트림에 보내는 경우도 있지만 대개는 그러지 않습니다. 업스트림 드라이버가 없는 하드웨어를 지원하려고 쓰는 경우도 있습니다.

GKI 정책상 커널에 기능을 넣는 길은 업스트림 기여를 빼면 벤더 훅뿐입니다. 그리고 Android 메인테이너들은 세상의 모든 기기를 이해하지 못합니다. 어떤 변경이 정말 중요한지 판단할 수 없으니 훅 추가 정책은 관대할 수밖에 없다는 것이 코스의 설명입니다.

대표 용도내용
Qualcomm WALT현재 가장 복잡한 벤더 훅. 메인라인 부하 추적기 PELT(per-entity load tracking)를 대체하는 Window Assisted Load Tracking. 모바일 워크로드에 더 낫다고 알려져 있으며, 10년 전 업스트림에 제출된 적이 있고 일부 아이디어는 PELT에 흡수됨
우선순위 역전 회피락 프리미티브를 손보거나, 중요한 프로세스용 별도 런큐를 스케줄러에 둠
목표사용자 입력에 빠르게 반응하고 프레임 드롭 없이 화면을 그리는 것

sched_ext도 해법이 되지 못했다

스케줄러 쪽 훅이 이렇게 많다면 sched_ext로 벤더를 유도하면 되지 않을까요? LWN의 조너선 코벳(Jonathan Corbet)이 세션에서 직접 이 질문을 던졌습니다. 코스의 답은 씁쓸했습니다. sched_ext는 이미 Android 커널에서 지원되는데, 그 결과 sched_ext 인프라에도 벤더 훅이 붙었다는 것입니다.

코스는 솔직히 말해 완전히 트리 밖에 있는 스케줄러 구현으로 옮겨 가느니 벤더 훅으로 남는 편이 낫다고 생각한다고 덧붙였습니다. 벤더 훅은 적어도 메인라인 스케줄러 위에 얹힌 변경입니다. 반면 BPF로 짠 독자 스케줄러는 스케줄러 전체가 트리 밖으로 나가는 셈입니다. 같은 Android 트랙 후반에는 아이췬 위(Aiqun Yu)가 실시간 태스크 스케줄링을 sched_ext로 옮겨 일부 벤더 훅을 없애는 작업을 발표했습니다. 방향은 아직 정리되지 않은 셈입니다.

훅 추가를 더 강하게 거절하면 되지 않느냐는 청중 질문도 나왔습니다. 코스는 Android 팀에 모든 훅을 판단할 전문성이 없어 특정 요청을 타당하지 않다고 말하기 어렵다는 답을 반복했습니다. 세션은 거기서 시간이 끝났습니다. 한 LWN 독자는 댓글에서 이 세션의 다른 이름은 “클리프행어”였다고 꼬집었습니다.

심볼 export 정책과의 대비

LWN 댓글에서는 정책의 비대칭을 지적하는 질문이 나왔습니다. 메인라인은 out-of-tree 코드를 어렵게 하려고 심볼 export를 엄격하게 막는 것처럼 보이는데, Android는 비슷한 효과를 내는 벤더 훅에 관대하다는 것입니다. 답변들은 다음과 같이 정리됩니다.

  • 두 정책은 서로 다른 집단이 정합니다. 심볼 export는 코어 커널 개발자가, 벤더 훅은 Google의 Android 커널 팀이 관리합니다.
  • 메인라인 커널에는 벤더 훅 메커니즘 자체가 없습니다. Android 문서도 벤더 훅은 ACK 전용이니 업스트림에 보내지 말라고 명시합니다.
  • Android의 심볼 export 정책은 오히려 메인라인보다 엄격합니다. EXPORT_SYMBOL()에 더해 심볼이 허용 목록(allowlist)에 있어야 합니다. 트레이스포인트도 인자 0~8개용 함수만 허용돼, 인자 9개짜리 트레이스포인트는 지금 쓸 수 없다는 증언도 있었습니다.

정리

벤더 훅은 GKI가 “커널은 하나, 차별화는 모듈로”라는 목표를 지키기 위해 치르는 비용입니다. 덕분에 벤더는 공통 커널을 건드리지 않고 자기 코드를 넣습니다. 대신 Android는 훅 840개의 ABI를 계속 유지해야 하고, 그 훅들이 업스트림에 올라갈 일은 대부분 없습니다. Android 커널에서 성능 문제를 분석한다면 메모리 관리와 스케줄러의 동작이 메인라인과 다를 수 있다는 전제를 깔고 시작해야 합니다. 특히 스케줄러 쪽은 해제할 수 없는 제한 훅이 메인라인 로직을 통째로 건너뛰고 있을 수 있습니다.

sched_ext가 스케줄러 훅을 대체할지, 아니면 훅이 하나 더 붙는 자리가 될지는 아직 열려 있습니다. 같은 고민은 메인라인 쪽에서도 반복되고 있습니다. 트리 밖 코드를 위해 기능을 얼마나 열어 줄 것인가 하는 문제입니다.

참고

답글 남기기