이 글에서 다루는 것
"Apache httpd 소스 파헤치기" 연재의 여섯 번째 글입니다. 5편에서 main() 이 설정을 읽고 pre_config·post_config 같은 단계를 차례로 부르는 것을 봤습니다. 그때 "단계를 부른다"고만 하고 넘어간 것이 있습니다. ap_run_post_config() 를 부르면 누구의 함수가, 어떤 순서로 불리는가입니다. 이번 글이 그 답입니다.
답하려는 질문은 네 가지입니다.
- 모듈 하나는 C 코드로 보면 무엇인가.
httpd.conf의LoadModule alias_module modules/mod_alias.so한 줄은 소스의 어디로 이어지는가 - 모듈이 "이 단계에서 내 함수를 불러 달라"고 등록하는 훅(hook)은 어떻게 만들어져 있는가 —
AP_DECLARE_HOOK같은 매크로는 무엇으로 펼쳐지는가 - 같은 훅에 여러 모듈이 걸려 있을 때 누가 먼저 불리고, 누구의 반환값이 뒤를 멈추는가
.so파일은 어떻게 실행 중인 서버에 실리는가
| 항목 | 내용 |
|---|---|
| 대상 | Apache httpd |
| 버전 | 2.5.1-dev (trunk) |
| 커밋 | e4af30fe6d (2026-10-24) |
| 함께 읽는 라이브러리 | APR-util 1.6.3 (태그 1.6.3, 커밋 28327eeca2) — 훅 매크로와 정렬 |
| 주로 읽는 파일 | include/http_config.h, server/config.c, include/ap_hooks.h, modules/core/mod_so.c, APR-util include/apr_hooks.h·hooks/apr_hooks.c |
글에 나오는 줄 번호와 세부 동작은 모두 이 커밋(APR-util 은 위 태그) 기준입니다. 같은 커밋의 소스를 받고 예제용 도커 이미지를 만드는 방법은 1편에 있습니다.
이 글은 처음 소스를 여는 분이 따라올 수 있는 큰 줄기만 골랐습니다. 빼놓은 것은 글 끝의 "더 깊이 들어가면"에 이름과 위치만 적어 두었습니다.
먼저 알아 둘 말
모듈(module)은 httpd 에 기능 하나를 보태는 C 코드 묶음입니다. mod_alias(Alias·Redirect), mod_ssl(HTTPS), mod_proxy(리버스 프록시)처럼 우리가 설정 파일에서 이름을 보는 것들이 모두 모듈이고, 3편에서 봤듯이 코어(core.c)와 HTTP 처리(http_core.c)도 모듈입니다.
훅(hook, 갈고리)은 서버가 일하는 흐름 중간중간에 뚫어 둔 "여기서 다른 함수를 불러 줄 수 있음" 자리입니다. 예를 들어 handler 훅은 "응답 본문을 만들 차례", fixups 훅은 "핸들러를 부르기 직전 마지막 손질" 자리입니다. 모듈은 훅마다 자기 함수를 걸어 두고(등록), 서버는 그 자리에 오면 걸린 함수들을 차례로 부릅니다(실행).
함수 포인터는 "함수가 있는 주소"를 담는 변수입니다. 변수에 담아 두었다가 나중에 그 변수로 함수를 부를 수 있습니다. 훅에 함수를 "건다"는 것은 결국 함수 포인터를 배열에 넣어 둔다는 뜻입니다. 매크로는 컴파일 전에 글자 그대로 다른 코드로 바뀌는 이름입니다(#define). 둘 다 2편에서 자세히 풀었습니다.
DSO(Dynamic Shared Object)는 실행 중인 프로그램이 나중에 불러 올릴 수 있는 라이브러리 파일입니다. 리눅스에서는 .so 파일이고, httpd 에서는 LoadModule 디렉티브가 이것을 싣습니다. 서버 실행 파일 안에 처음부터 묶여 들어간 모듈은 정적(static) 모듈, .so 로 실리는 모듈은 공유(shared) 모듈이라고 부릅니다.
OK·DECLINED·DONE 은 훅에 건 함수가 돌려주는 약속된 값입니다. include/httpd.h 에 이렇게 정의되어 있습니다.
#define OK 0 /**< Module has handled this stage. */
#define DECLINED -1 /**< Module declines to handle */
#define DONE -2 /**< Module has served the response completely
* - it's safe to die() with no more output
*/
#define SUSPENDED -3 /**< Module will handle the remainder of the request.
* The core will never invoke the request again */
OK 는 "내가 이 단계를 처리했다", DECLINED 는 "나는 할 일이 없다, 다음 사람에게 넘겨라", DONE 은 "응답을 완전히 끝냈다"입니다. 이 밖에 404·500 같은 HTTP 상태 번호를 돌려주면 오류로 끝내라는 뜻이 됩니다. 이 값들이 흐름을 어떻게 바꾸는지가 4절의 주제입니다.
1. 모듈은 구조체 하나입니다
C 에는 클래스가 없습니다. httpd 는 "모듈이란 이런 것"을 구조체 하나로 정했습니다. include/http_config.h 의 module 입니다. (필드마다 붙은 설명 주석은 모두 지웠고, 코드 줄은 소스 그대로입니다.)
typedef struct module_struct module;
struct module_struct {
int version;
int minor_version;
int module_index;
const char *name;
void *dynamic_load_handle;
struct module_struct *next;
unsigned long magic;
void (*rewrite_args) (process_rec *process);
void *(*create_dir_config) (apr_pool_t *p, char *dir);
void *(*merge_dir_config) (apr_pool_t *p, void *base_conf, void *new_conf);
void *(*create_server_config) (apr_pool_t *p, server_rec *s);
void *(*merge_server_config) (apr_pool_t *p, void *base_conf,
void *new_conf);
const command_rec *cmds;
void (*register_hooks) (apr_pool_t *p);
int flags;
};
include/http_config.h 347–424행
void *(*create_dir_config) (apr_pool_t *p, char *dir); 처럼 이름 앞에 (* 가 붙은 줄이 함수 포인터 필드입니다. 이 구조체는 크게 네 부분입니다.
- 신분증 —
version·minor_version(이 모듈이 맞춰 컴파일된 서버 API 판),module_index(설정 벡터의 칸 번호, 아래에서 봅니다),name(소스 파일 이름),magic(진짜 모듈 구조체인지 확인하는 표식),next(모듈끼리 잇는 연결 리스트). - 설정 만들기·합치기 —
create_dir_config등 함수 넷.<Directory>마다, 가상 호스트마다 이 모듈의 설정 구조체를 만들고 합칩니다. 7편에서 봅니다. cmds— 이 모듈이 알아듣는 디렉티브의 표.mod_alias라면Alias,Redirect같은 줄을 어떤 함수가 읽는지 적혀 있습니다. 역시 7편에서 봅니다.register_hooks— 이 글의 주인공. 서버가 이 함수를 한 번 불러 주면, 모듈은 그 안에서 "나는 이 훅, 저 훅에 이 함수를 걸겠다"고 등록합니다.
실제 모듈은 이 구조체를 변수 하나로 만들어 둡니다. mod_alias 의 끝부분입니다.
static void register_hooks(apr_pool_t *p)
{
static const char * const aszSucc[]={ "mod_userdir.c",
"mod_vhost_alias.c",NULL };
ap_hook_translate_name(translate_alias_redir,NULL,aszSucc,APR_HOOK_MIDDLE);
ap_hook_fixups(fixup_redir,NULL,NULL,APR_HOOK_MIDDLE);
}
AP_DECLARE_MODULE(alias) =
{
STANDARD20_MODULE_STUFF,
create_alias_dir_config, /* dir config creater */
merge_alias_dir_config, /* dir merger --- default is to override */
create_alias_config, /* server config */
merge_alias_config, /* merge server configs */
alias_cmds, /* command apr_table_t */
register_hooks /* register hooks */
};
modules/mappers/mod_alias.c 783–801행
register_hooks() 는 두 줄입니다. translate_name 훅(URL 을 파일 경로로 바꾸는 단계, 10편)에 translate_alias_redir 를, fixups 훅에 fixup_redir 를 겁니다. 두 번째·세 번째 인자(NULL,aszSucc)와 마지막 인자(APR_HOOK_MIDDLE)는 순서를 정하는 값으로, 5절에서 봅니다.
AP_DECLARE_MODULE(alias) 와 STANDARD20_MODULE_STUFF 는 매크로입니다.
#define AP_DECLARE_MODULE(foo) \
APLOG_USE_MODULE(foo); \
module AP_MODULE_DECLARE_DATA foo##_module
include/http_config.h 467–469행
#define STANDARD20_MODULE_STUFF MODULE_MAGIC_NUMBER_MAJOR, \
MODULE_MAGIC_NUMBER_MINOR, \
-1, \
__FILE__, \
NULL, \
NULL, \
MODULE_MAGIC_COOKIE, \
NULL /* rewrite args spot */
include/http_config.h 489–496행
## 는 매크로 안에서 글자를 이어 붙이는 연산자입니다. 그래서 AP_DECLARE_MODULE(alias) = { ... } 는 module alias_module = { ... } 이라는 전역 변수 정의가 됩니다(앞의 APLOG_USE_MODULE 은 모듈별 LogLevel 을 위한 것으로 16편에서 봅니다). STANDARD20_MODULE_STUFF 는 구조체의 앞 여덟 칸 — version, minor_version, module_index, name, dynamic_load_handle, next, magic, rewrite_args — 을 순서대로 채웁니다. 모듈 작성자는 이 매크로 한 단어 뒤에 나머지 여섯 칸 — 함수 다섯과 디렉티브 표 하나 — 만 적으면 됩니다(마지막 flags 는 적지 않으면 0 입니다).
노란 상자가 모듈마다 한 칸씩 쓰는 설정 벡터, 파란 상자가 서버의 흐름에 끼어드는 두 통로(디렉티브와 훅)입니다.
module_index — 모듈마다 받는 칸 번호
module_index 는 처음에 -1 이고, 서버에 등록될 때 0, 1, 2… 순서대로 번호를 받습니다(2절). 이 번호는 설정 벡터의 칸 번호입니다. 설정 벡터는 모듈 수만큼 칸이 있는 void * 배열이고, 칸마다 그 모듈의 설정 구조체 주소가 들어 있습니다. 모듈이 자기 설정을 꺼낼 때 쓰는 매크로가 이것을 그대로 보여 줍니다.
#define ap_get_module_config(v,m) \
(((void **)(v))[(m)->module_index])
include/http_config.h 553–554행
배열의 module_index 번째 칸을 꺼낼 뿐입니다. 서버는 모듈들의 설정 구조체가 어떻게 생겼는지 전혀 모르고, 각 모듈이 자기 칸을 자기 타입으로 해석합니다. 이 방식의 장단점은 2편에서 다뤘습니다.
헷갈리기 쉬운 곳: 모듈 이름이 세 가지입니다
같은 모듈이 자리에 따라 다른 이름으로 불립니다.
| 어디서 | 이름 | 어디서 오나 |
|---|---|---|
LoadModule 첫 인자, <IfModule>, httpd -M |
alias_module |
AP_DECLARE_MODULE(alias) 가 만든 변수 이름 |
httpd -l, 훅 순서의 앞뒤 지정, SHOW_HOOKS 출력 |
mod_alias.c |
구조체의 name 칸, 곧 __FILE__(컴파일한 소스 파일 이름) |
LoadModule 둘째 인자 |
modules/mod_alias.so |
빌드가 만든 파일 이름 |
__FILE__ 에 경로가 붙어 들어오는 컴파일러가 있어서, ap_add_module() 이 마지막 /(윈도 경로라면 \) 뒤만 남기도록 잘라 냅니다(server/config.c 620–629행). 둘째 줄의 이름이 5절에서 다시 중요해집니다.
2. 모듈이 서버에 등록되는 길
모듈 구조체가 있다고 서버가 저절로 아는 것은 아닙니다. 등록되는 길은 두 갈래입니다. 실행 파일에 묶인 정적 모듈은 기동하자마자, .so 로 된 공유 모듈은 httpd.conf 를 읽다가 LoadModule 줄을 만났을 때입니다.
두 갈래가 ap_add_module() 에서 만납니다. 노란 상자 — 훅의 함수 배열 — 는 등록 순서대로 쌓이고, 정렬은 ③에서 따로 합니다.
정적 모듈: modules.c 와 ap_setup_prelinked_modules()
어떤 모듈을 실행 파일에 묶을지는 ./configure 때 정해지고, 빌드가 소스 맨 위 폴더에 modules.c 라는 파일을 만들어 냅니다. 저장소에는 없는 파일입니다(저장소의 os/win32/modules.c 같은 것은 다른 운영체제용으로 손으로 적어 둔 판입니다). 예제 컨테이너의 빌드 폴더에서 꺼내 보면 이렇습니다(이 연재의 이미지는 --enable-mods-shared=most 로 빌드해서 정적 모듈이 셋뿐입니다).
### 0. /usr/src/httpd/modules.c (generated at build time), lines 21-26
AP_DECLARE_DATA module *ap_prelinked_modules[] = {
&core_module,
&so_module,
&http_module,
NULL
};
모듈 구조체의 주소를 늘어놓은 배열입니다. 같은 파일에 ap_preloaded_modules[](실을 수 있는 모듈)도 있는데 이 빌드에서는 내용이 같습니다. main() 은 기동 초반에 ap_setup_prelinked_modules() 를 부르고(server/main.c 574행), 이 함수가 배열을 돌며 번호를 주고 하나씩 등록합니다.
AP_DECLARE(const char *) ap_setup_prelinked_modules(process_rec *process)
{
/* ... */
apr_hook_global_pool=process->pconf;
/* ... */
total_modules = 0;
for (m = ap_preloaded_modules; *m != NULL; m++)
(*m)->module_index = total_modules++;
max_modules = total_modules + DYNAMIC_MODULE_LIMIT + 1;
conf_vector_length = max_modules;
/* ... */
for (m = ap_prelinked_modules; *m != NULL; m++) {
error = ap_add_module(*m, process->pconf, NULL);
if (error) {
return error;
}
}
apr_hook_sort_all();
return NULL;
}
설정 벡터의 길이(conf_vector_length)를 "정적 모듈 수 + DYNAMIC_MODULE_LIMIT + 1" 로 미리 잡아 두는 것이 보입니다. 나중에 실릴 공유 모듈의 칸까지 남겨 두는 것입니다. DYNAMIC_MODULE_LIMIT 의 기본값은 256 입니다(include/httpd.h 119–120행).
모든 모듈이 지나는 문: ap_add_module()
정적이든 공유든 등록은 결국 ap_add_module() 입니다.
AP_DECLARE(const char *) ap_add_module(module *m, apr_pool_t *p,
const char *sym_name)
{
/* ... */
if (m->version != MODULE_MAGIC_NUMBER_MAJOR) {
return apr_psprintf(p, "Module \"%s\" is not compatible with this "
"version of Apache (found %d, need %d). Please "
"contact the vendor for the correct version.",
m->name, m->version, MODULE_MAGIC_NUMBER_MAJOR);
}
if (m->module_index == -1) {
/* ... */
m->module_index = total_modules++;
dynamic_modules++;
}
/* ... */
if (m->next == NULL) {
m->next = ap_top_module;
ap_top_module = m;
}
/* ... */
ap_add_module_commands(m, p);
/* ... */
ap_register_hooks(m, p);
return NULL;
}
- 판을 확인합니다. 모듈이 컴파일될 때 박힌
version(= 그때의MODULE_MAGIC_NUMBER_MAJOR)이 지금 서버의 값과 다르면 거절합니다. - 번호를 줍니다. 아직
-1이면(공유 모듈) 다음 빈 번호를 줍니다. 정적 모듈은 앞 함수에서 이미 받았습니다. - 목록 맨 앞에 붙입니다.
ap_top_module이 가리키는 연결 리스트의 머리에 끼웁니다. - 디렉티브를 등록하고(
ap_add_module_commands()) 훅을 등록합니다(ap_register_hooks()).
ap_register_hooks() 는 짧습니다.
AP_DECLARE(void) ap_register_hooks(module *m, apr_pool_t *p)
{
if (m->register_hooks) {
if (getenv("SHOW_HOOKS")) {
printf("Registering hooks for %s\n", m->name);
apr_hook_debug_enabled = 1;
}
apr_hook_debug_current = m->name;
m->register_hooks(p);
}
}
모듈의 register_hooks 함수 포인터를 부르기 직전에 전역 변수 apr_hook_debug_current 에 모듈 이름(mod_alias.c)을 적어 둡니다. 모듈 안의 ap_hook_*() 는 인자로 모듈 이름을 받지 않는데, 등록되는 함수에 "누구의 것"이라는 이름표가 붙는 비결이 이것입니다(3절의 펼친 코드에서 확인합니다). 환경 변수 SHOW_HOOKS 를 켜면 등록과 정렬 과정을 화면에 찍어 주는데, 6절의 예제에서 씁니다.
공유 모듈: mod_so 와 LoadModule
LoadModule 디렉티브는 코어가 아니라 정적 모듈 mod_so 의 것입니다.
static const command_rec so_cmds[] = {
AP_INIT_TAKE2("LoadModule", load_module, NULL, RSRC_CONF | EXEC_ON_READ,
"a module name and the name of a shared object file to load it from"),
modules/core/mod_so.c 448–450행
TAKE2 는 인자 두 개짜리 디렉티브, EXEC_ON_READ 는 "설정 파일을 읽는 그 자리에서 바로 실행"이라는 뜻입니다. 모듈을 실어야 그 모듈의 디렉티브를 알아들을 수 있으니, 다른 디렉티브처럼 나중에 몰아서 처리할 수 없습니다(디렉티브 처리 순서는 7편). 실제 일은 load_module() 이 합니다. 이미 실렸는지 확인하는 앞부분과 오류 메시지를 만드는 부분을 줄였습니다.
error = dso_load(cmd, &modhandle, filename, &module_file);
if (error)
return error;
/* ... */
if (apr_dso_sym(&modsym, modhandle, modname) != APR_SUCCESS) {
/* ... */
}
modp = (module*) modsym;
modp->dynamic_load_handle = (apr_dso_handle_t *)modhandle;
modi->modp = modp;
/* ... */
if (modp->magic != MODULE_MAGIC_COOKIE) {
return apr_psprintf(cmd->pool, "API module structure '%s' in file %s "
"is garbled - expected signature %08lx but saw "
/* ... */);
}
/* ... */
error = ap_add_loaded_module(modp, cmd->pool, modname);
if (error) {
return error;
}
/* ... */
apr_pool_cleanup_register(cmd->pool, modi, unload_module, apr_pool_cleanup_null);
/* ... */
ap_single_module_configure(cmd->pool, cmd->server, modp);
return NULL;
}
modules/core/mod_so.c 265–322행
- 파일을 엽니다.
dso_load()안의apr_dso_load()가.so파일을 프로세스 안에 올립니다(리눅스라면 그 아래는dlopen()입니다). - 이름으로 구조체를 찾습니다.
apr_dso_sym()이LoadModule첫 인자 —alias_module— 와 같은 이름의 심벌, 곧AP_DECLARE_MODULE(alias)가 만든 전역 변수를 찾아 그 주소를 줍니다. 1절의 "이름이 세 가지" 중 첫째 이름이 여기서 쓰입니다. - 진짜인지 확인합니다.
magic칸이MODULE_MAGIC_COOKIE와 같은지 봅니다. 이 커밋의 값은0x41503235UL /* "AP25" */입니다(include/ap_mmn.h 749행). - 등록합니다.
ap_add_loaded_module()이 위의ap_add_module()을 부릅니다(server/config.c 703–731행). 여기서register_hooks()가 불립니다. - 내릴 준비를 합니다. 설정 풀(
pconf)에 cleanup 을 걸어 두어, 재시작이나 종료로 이 풀이 지워질 때 모듈을 목록에서 빼고.so를 내립니다. 풀과 cleanup 은 4편에서 봤습니다. - 기본 설정을 만듭니다.
ap_single_module_configure()가 서버 설정과 기본 디렉터리 설정 칸을 채웁니다.
예제로 확인: httpd -l, -M, -L
이 예제는 기준 커밋(e4af30fe6d)을 그대로 빌드한 httpd 2.5.1-dev 로 도커 컨테이너(Ubuntu 24.04)에서 실행했습니다. 6절에서 쓸 모듈 셋(mod_alpha, mod_beta, mod_gamma)을 apxs -c 로 컴파일해 LoadModule 로 싣는 최소 설정입니다. 예제 전체는 ex/u06-hooks/run.sh 에 있습니다.
### 1. httpd -l
Compiled in modules:
core.c
mod_so.c
http_core.c
### 2. httpd -M
Loaded Modules:
core_module (static)
so_module (static)
http_module (static)
mpm_event_module (shared)
unixd_module (shared)
authz_core_module (shared)
alpha_module (shared)
beta_module (shared)
gamma_module (shared)
### 3. httpd -L | grep -A1 ^LoadModule
LoadModule (mod_so.c)
a module name and the name of a shared object file to load it from
-l은 정적 모듈만, 그것도name칸(core.c)으로 보여 줍니다.ap_show_modules()가ap_loaded_modules[n]->name을 찍기 때문입니다(server/config.c 2507–2514행). 설정 파일을 읽기 전에 끝나므로 공유 모듈은 나오지 않습니다(server/main.c 647–649행).-M은 설정을 다 읽은 뒤에 변수 이름(alpha_module)으로 보여 줍니다. 이 목록을 찍는 것도mod_so가test_config훅에 건 함수입니다(modules/core/mod_so.c 378–419행, 등록은 444행).-L은 디렉티브마다 그것을 가진 모듈과 설명을 찍습니다.LoadModule의 주인이mod_so.c라는 것과, 설명 문장이 위so_cmds표의 문자열 그대로라는 것이 보입니다.
헷갈리기 쉬운 곳
- 2.4 용으로 빌드한 모듈은 이 커밋의 서버에 실리지 않습니다. 2.4.x 브랜치의
MODULE_MAGIC_COOKIE는"AP24"(0x41503234UL),MODULE_MAGIC_NUMBER_MAJOR는20120211이고, 이 커밋은"AP25"와20211221입니다(include/ap_mmn.h 749–752행). 그런 모듈은ap_add_module()의 판 확인까지 가기 전에,load_module()의magic확인에서 걸립니다. 모듈은 실행할 서버의 헤더로 다시 컴파일해야 합니다. register_hooks()는 한 번만 불리는 함수가 아닙니다. 설정 풀이 지워질 때 pre-cleanup 으로 모든 훅 등록을 지우고(server/main.c 321–327행, 걸어 두는 곳은 339행), 설정을 다시 읽는 루프의 첫머리에서 정적 모듈의register_hooks()를 다시 부릅니다(server/main.c 816–818행). 공유 모듈은 설정을 다시 읽으며LoadModule을 다시 만날 때 다시 등록됩니다. 5편에서 본 "설정을 두 번 읽는" 기동과 재시작마다 이 일이 되풀이되므로,register_hooks()에서는 훅 등록만 하고 다른 상태를 만들지 않는 것이 안전합니다.
3. 훅 하나는 매크로 세 개로 만들어집니다
이제 ap_hook_translate_name()·ap_hook_fixups() 같은 함수가 어디서 왔는지 봅니다. 소스를 grep 해 보면 이 이름의 함수 정의가 어디에도 없습니다. 매크로가 만들어 내기 때문입니다. handler 훅 하나를 예로 따라가겠습니다.
① 선언: AP_DECLARE_HOOK
헤더 파일에 이 한 줄이 있습니다.
AP_DECLARE_HOOK(int,handler,(request_rec *r))
"handler 라는 훅이 있고, 여기 걸 함수는 request_rec *r 을 받아 int 를 돌려준다"는 뜻입니다. 이 매크로는 httpd 의 것이지만 하는 일은 APR-util 의 매크로에 이름 접두사를 붙여 넘기는 것뿐입니다.
#define AP_DECLARE_HOOK(ret,name,args) \
APR_DECLARE_EXTERNAL_HOOK(ap,AP,ret,name,args)
ap 는 만들어질 함수 이름의 접두사, AP 는 AP_DECLARE 처럼 내보내기 표시 매크로의 접두사입니다. 넘겨받는 쪽이 APR-util 의 본체입니다.
#define APR_DECLARE_EXTERNAL_HOOK(ns,link,ret,name,args) \
typedef ret ns##_HOOK_##name##_t args; \
link##_DECLARE(void) ns##_hook_##name(ns##_HOOK_##name##_t *pf, \
const char * const *aszPre, \
const char * const *aszSucc, int nOrder); \
link##_DECLARE(ret) ns##_run_##name args; \
APR_IMPLEMENT_HOOK_GET_PROTO(ns,link,name); \
typedef struct ns##_LINK_##name##_t \
{ \
ns##_HOOK_##name##_t *pFunc; \
const char *szName; \
const char * const *aszPredecessors; \
const char * const *aszSuccessors; \
int nOrder; \
} ns##_LINK_##name##_t;
APR-util include/apr_hooks.h 118–132행
ns 에 ap, name 에 handler 를 넣고 ## 로 이어 붙이면 다섯 가지가 생깁니다.
| 펼쳐진 이름 | 무엇 |
|---|---|
ap_HOOK_handler_t |
훅에 걸 함수의 모양(타입). int (request_rec *r) |
ap_hook_handler() |
모듈이 부르는 등록 함수의 선언. 함수 포인터, 앞에 와야 할 모듈들, 뒤에 와야 할 모듈들, 순서 값을 받는다 |
ap_run_handler() |
코어가 부르는 실행 함수의 선언. 인자는 훅과 같은 (request_rec *r) |
ap_hook_get_handler() |
등록된 배열을 꺼내 보는 함수의 선언 |
ap_LINK_handler_t |
등록 한 건을 담는 구조체. 함수 포인터 + 이름표 + 앞뒤 목록 + 순서 값 |
헤더에서 생기는 것은 선언뿐입니다. 몸통은 아직 없습니다. 훅에 걸 함수의 모양이 typedef 로 정해져 있어서, 모듈이 모양이 다른 함수를 ap_hook_handler() 에 넘기면 컴파일러가 알아챕니다. 인자가 둘이고 반환값이 없는 함수를 일부러 걸어 예제 컨테이너(gcc 13.3)에서 컴파일해 봤습니다(ex/u06-hooks/mod_badtype.c).
### 10. a function of the wrong shape passed to ap_hook_handler()
mod_badtype.c:10:21: warning: passing argument 1 of 'ap_hook_handler' from incompatible pointer type [-Wincompatible-pointer-types]
오류가 아니라 경고라서 .so 는 그대로 만들어지니 경고를 흘려보내면 안 되지만, C 로 짠 플러그인 구조치고 타입이 꽤 단단히 묶인 편입니다.
② 보관소: APR_HOOK_STRUCT
몸통은 훅을 "가진" 소스 파일 하나에 있습니다. handler 훅의 주인은 server/config.c 입니다. 파일 첫머리에 그 파일이 가진 훅들의 보관소를 만듭니다.
APR_HOOK_STRUCT(
APR_HOOK_LINK(header_parser)
/* ... */
APR_HOOK_LINK(handler)
/* ... */
)
#define APR_HOOK_STRUCT(members) \
static struct { members } _hooks;
/** macro to link the hook structure */
#define APR_HOOK_LINK(name) \
apr_array_header_t *link_##name;
APR-util include/apr_hooks.h 135–140행
펼치면 static struct { apr_array_header_t *link_handler; ... } _hooks; — 훅마다 배열 하나를 가리키는 포인터를 칸으로 가진 구조체 변수입니다. static 이 붙은 전역 변수는 그 .c 파일 안에서만 보입니다. 그래서 파일마다 _hooks 라는 같은 이름을 써도 부딪히지 않습니다. server/request.c 도 자기 _hooks 를 가지고 fixups·translate_name 같은 요청 단계 훅을 보관합니다(server/request.c 61–77행).
③ 구현: AP_IMPLEMENT_HOOK_RUN_FIRST
같은 파일에 이 줄이 몸통을 만듭니다.
AP_IMPLEMENT_HOOK_RUN_FIRST(int, handler, (request_rec *r),
(r), DECLINED)
인자 넷째 (r) 은 "받은 인자를 다음 함수에 넘길 때 이렇게 적어라", 다섯째 DECLINED 는 "이 값이면 다음으로 넘어가라"입니다. 이 매크로도 ap_hooks.h 에서 APR-util 의 APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST 로 넘어가고(include/ap_hooks.h 137–139행), 그 매크로는 등록 함수를 만드는 APR_IMPLEMENT_EXTERNAL_HOOK_BASE(APR-util include/apr_hooks.h 143–165행)와 실행 함수의 몸통을 이어 붙입니다.
매크로를 매크로로 읽는 것보다 컴파일러가 펼친 결과를 보는 편이 빠릅니다. gcc -E 는 컴파일하지 않고 전처리(매크로 펼치기)까지만 한 결과를 내놓습니다. 위 두 매크로만 적은 작은 파일(ex/u06-hooks/expand.c)을 예제 컨테이너에서 펼쳤습니다. 전처리 결과는 한 줄로 나오므로 run.sh 안의 sed·awk 로 줄 바꿈과 들여쓰기만 넣었고, 글자는 바꾸지 않았습니다(빈 문장 ; 몇 개는 지웠습니다 — 꺼져 있는 디버깅용 probe 매크로가 남긴 것입니다).
### 9. gcc -E: what AP_IMPLEMENT_HOOK_RUN_FIRST(handler) becomes
static struct {
apr_array_header_t *link_handler;
}
_hooks;
void ap_hook_handler(ap_HOOK_handler_t *pf,const char * const *aszPre, const char * const *aszSucc,int nOrder) {
ap_LINK_handler_t *pHook;
if(!_hooks.link_handler) {
_hooks.link_handler=apr_array_make(apr_hook_global_pool,1,sizeof(ap_LINK_handler_t));
apr_hook_sort_register("handler",&_hooks.link_handler);
}
pHook=apr_array_push(_hooks.link_handler);
pHook->pFunc=pf;
pHook->aszPredecessors=aszPre;
pHook->aszSuccessors=aszSucc;
pHook->nOrder=nOrder;
pHook->szName=apr_hook_debug_current;
if(apr_hook_debug_enabled) apr_hook_debug_show("handler",aszPre,aszSucc);
}
apr_array_header_t * ap_hook_get_handler(void) {
return _hooks.link_handler;
}
int ap_run_handler (request_rec *r) {
ap_LINK_handler_t *pHook;
int n;
int rv = -1;
if(_hooks.link_handler) {
pHook=(ap_LINK_handler_t *)_hooks.link_handler->elts;
for(n=0 ; n < _hooks.link_handler->nelts ; ++n) {
rv=pHook[n].pFunc (r);
if(rv != -1) break;
}
}
return rv;
}
(AP_DECLARE(void) 가 그냥 void 로 펼쳐진 것은 윈도가 아닌 곳에서 AP_DECLARE(type) 이 type 그대로로 정의되어 있기 때문입니다(include/ap_config.h 58–67행). -1 은 DECLINED 가 펼쳐진 값입니다.)
이것이 훅의 전부입니다.
ap_hook_handler()— 모듈이register_hooks()안에서 부르는 그 함수입니다. 처음 불리면 배열을 만들고apr_hook_sort_register()로 "정렬할 배열 목록"에 이 배열을 올립니다. 그다음은 배열 끝에 한 칸(ap_LINK_handler_t)을 붙이고 함수 포인터·앞뒤 목록·순서 값을 적을 뿐입니다. 이름표szName은 2절에서 본 전역 변수apr_hook_debug_current— 지금register_hooks()를 부르고 있는 모듈의 이름 — 에서 가져옵니다.ap_run_handler()— 코어가 부르는 함수입니다. 배열을 앞에서부터 돌며 함수 포인터로 하나씩 부르고,DECLINED가 아닌 값이 나오면 멈춥니다.
노란 상자, 곧 static 배열 하나를 사이에 두고 등록하는 쪽(모듈)과 실행하는 쪽(코어)이 만납니다. 둘은 서로를 모릅니다.
헷갈리기 쉬운 곳
ap_hook_handler를 grep 해도 정의가 안 나옵니다. 위처럼AP_IMPLEMENT_HOOK_*한 줄이 만들어 내기 때문입니다. 훅의 몸통을 찾을 때는AP_IMPLEMENT_HOOK_[A-Z_]*(.*, *훅이름으로 찾으면 됩니다.handler는server/config.c,fixups는server/request.c에 있습니다.- 손으로 펼쳐 둔 옛 코드가 주석 안에 있습니다.
server/config.c의post_config구현(102–105행) 바로 아래에post_config훅을 손으로 펼쳐 둔 코드가 있는데, 통째로 주석(/* ... */) 안에 들어 있는 옛 기록입니다(server/config.c 107–158행). 지금의 매크로와 다른 곳도 있으니(예:int를 돌려줘야 할 함수가return;으로 끝납니다) 실제 펼친 결과는 위의gcc -E출력을 믿는 편이 낫습니다.
4. RUN_FIRST 와 RUN_ALL — 돌려준 값이 흐름을 바꿉니다
훅을 구현하는 매크로는 세 종류이고, 차이는 언제 멈추느냐입니다.
| 매크로 | 실행 함수의 동작 | 이 커밋의 예 |
|---|---|---|
AP_IMPLEMENT_HOOK_VOID |
반환값 없음. 걸린 함수를 전부 부른다 | child_init, test_config, insert_filter |
AP_IMPLEMENT_HOOK_RUN_ALL |
OK·DECLINED 이면 계속, 그 밖의 값이면 멈추고 그 값을 돌려준다. 끝까지 가면 OK |
pre_config, post_config, fixups, access_checker |
AP_IMPLEMENT_HOOK_RUN_FIRST |
DECLINED 이면 계속, 그 밖의 값이면 멈추고 그 값을 돌려준다. 끝까지 가면 DECLINED |
handler, translate_name, map_to_storage, type_checker |
예는 server/config.c 85–175행과 server/request.c 79–107행에서 골랐습니다. RUN_ALL 의 몸통에서 RUN_FIRST 와 다른 곳은 반복문 안쪽입니다.
#define APR_IMPLEMENT_EXTERNAL_HOOK_RUN_ALL(ns,link,ret,name,args_decl,args_use,ok,decline) \
/* ... */
ret rv = ok; \
/* ... */
for(n=0 ; n < _hooks.link_##name->nelts ; ++n) \
{ \
/* ... */
rv=pHook[n].pFunc args_use; \
/* ... */
if(rv != ok && rv != decline) \
break; \
rv = ok; \
} \
/* ... */
return rv; \
}
APR-util include/apr_hooks.h 222–250행
if(rv != ok && rv != decline) break; 다음 줄의 rv = ok; 를 보세요. DECLINED 를 돌려준 함수가 있어도 결과는 OK 로 덮입니다. RUN_ALL 훅에서 OK 와 DECLINED 는 흐름상 차이가 없습니다 — 둘 다 "다음 함수로". 멈추게 하는 것은 DONE 이나 HTTP_FORBIDDEN(403) 같은 그 밖의 값뿐입니다. RUN_FIRST 의 같은 자리는 if(rv != decline) break; 한 줄입니다(APR-util include/apr_hooks.h 267–295행). OK 도 멈춤 사유입니다.
빨간 상자가 뒤의 함수들이 불리지 않게 되는 곳입니다.
두 방식은 훅이 묻는 질문에 맞춰 골라져 있습니다.
handler는 "누가 이 요청의 응답을 만들 것인가"입니다. 답하는 사람은 한 명이면 됩니다. 그래서 RUN_FIRST 입니다. 모듈마다 자기 일이 아니면(r->handler가 자기 이름이 아니면)DECLINED로 넘기고, 처음으로OK를 돌려준 모듈이 응답을 맡습니다.fixups는 "핸들러 직전에 각자 손볼 것이 있으면 손봐라"입니다. 모두에게 기회가 가야 합니다. 그래서 RUN_ALL 입니다. 대신 누군가 "이 요청은 여기서 끊어야 한다"(403 등)고 하면 거기서 멈춥니다.
코어는 결과를 어떻게 쓰나
handler 훅을 실제로 부르는 곳은 ap_invoke_handler() 입니다.
result = ap_run_handler(r);
/* ... */
return result == DECLINED ? HTTP_INTERNAL_SERVER_ERROR : result;
}
끝까지 아무도 맡지 않으면(DECLINED) 500 입니다. 하지만 실제로는 거의 그럴 일이 없습니다. 코어가 handler 훅의 맨 끝(APR_HOOK_REALLY_LAST)에 파일을 내보내는 default_handler 를 걸어 두었기 때문입니다(server/core.c 6171행). 그 함수는 12편에서 봅니다. fixups 의 결과는 ap_process_request_internal() 의 마지막에서 OK 가 아니면 그대로 돌려줍니다(server/request.c 435–439행). 요청 처리 단계의 전체 순서는 10편에서 봅니다.
헷갈리기 쉬운 곳
- RUN_ALL 은 아무도 걸려 있지 않아도
OK입니다.rv가ok로 시작하기 때문입니다. APR-util 소스에도 "아무것도 실행되지 않았을 때OK를 돌려주는데, 아마DECLINED가 맞을 것이다. 하지만 그렇게 바꾸면 Apache 가 깨진다"는 FIXME 주석이 남아 있습니다(APR-util include/apr_hooks.h 204–206행). - RUN_ALL 훅에서
DECLINED를 돌려준다고 "빠지는" 것이 아닙니다. 위에서 봤듯이OK와 똑같이 다음으로 넘어갑니다. 반대로 RUN_FIRST 훅에서 "할 일은 했지만 다른 모듈도 하게 두고 싶다"면OK대신DECLINED를 돌려줘야 합니다.OK를 돌려주는 순간 뒤의 모듈은 불리지 않습니다. - 훅 종류는 헤더만 봐서는 알 수 없습니다.
AP_DECLARE_HOOK선언에는 RUN_FIRST 인지 RUN_ALL 인지가 없고,.c파일의AP_IMPLEMENT_HOOK_*에만 있습니다. 새 훅에 함수를 걸 때는 구현 줄을 먼저 찾아보는 것이 좋습니다.
5. 순서 정하기 — 순서 값과 앞뒤 모듈 이름
훅에 함수를 걸 때 넘기는 인자 넷 가운데 셋이 순서에 관한 것이었습니다. ap_hook_handler(fn, aszPre, aszSucc, nOrder).
순서 값(nOrder)은 대략적인 자리입니다. 숫자가 작을수록 앞입니다.
/* Hook orderings */
/** run this hook first, before ANYTHING */
#define APR_HOOK_REALLY_FIRST (-10)
/** run this hook first */
#define APR_HOOK_FIRST 0
/** run this hook somewhere */
#define APR_HOOK_MIDDLE 10
/** run this hook after every other hook which is defined*/
#define APR_HOOK_LAST 20
/** run this hook last, after EVERYTHING */
#define APR_HOOK_REALLY_LAST 30
APR-util include/apr_hooks.h 297–307행
그냥 정수이므로 APR_HOOK_MIDDLE - 1 처럼 사이 값을 줄 수도 있습니다. handler 훅 선언 위의 주석은 "특정 핸들러 이름에만 반응하는 핸들러는 MIDDLE, 무엇이든 받는 핸들러는 LAST" 를 권합니다(include/http_config.h 1396행).
앞뒤 모듈 이름(aszPre, aszSucc)은 정확한 관계입니다. "mod_include.c 의 함수가 나보다 먼저", "나는 mod_userdir.c 보다 먼저"처럼 모듈 이름의 목록을 NULL 로 끝나는 배열로 넘깁니다. 1절의 mod_alias 가 translate_name 훅에 "mod_userdir.c, mod_vhost_alias.c 보다 앞"이라고 적었고, mod_userdir 쪽도 짝을 맞춰 적어 두었습니다.
static void register_hooks(apr_pool_t *p)
{
static const char * const aszPre[]={ "mod_alias.c",NULL };
static const char * const aszSucc[]={ "mod_vhost_alias.c",NULL };
ap_hook_translate_name(translate_userdir,aszPre,aszSucc,APR_HOOK_MIDDLE);
modules/mappers/mod_userdir.c 385–390행
설정 파일에서 Alias /~foo /somewhere 처럼 mod_alias 와 mod_userdir 의 규칙이 겹칠 때 Alias 가 이기는 이유가 이 두 줄입니다. 셋 다 같은 MIDDLE 이지만 이름으로 mod_alias 가 앞에 서고, translate_name 은 RUN_FIRST 라서 mod_alias 가 OK 를 돌려주면 mod_userdir 은 불리지 않습니다.
apr_hook_sort_all() — 언제, 어떻게 정렬하나
등록할 때는 배열 끝에 붙이기만 했습니다. 정렬은 따로 apr_hook_sort_all() 이 합니다. 이 함수는 ap_hook_*() 가 처음 불릴 때 apr_hook_sort_register() 로 올려 둔 배열 목록을 돌며 하나씩 sort_hook() 에 넘깁니다(APR-util hooks/apr_hooks.c 244–259행). 부르는 자리는 ap_setup_prelinked_modules() 의 끝(2절)과, main() 에서 설정을 읽은 직후·설정 트리를 처리한 직후입니다(server/main.c 740행, 758행; 재시작 루프 안에서 설정을 다시 읽을 때도 같은 두 자리, 836행·855행). 설정을 읽는 동안 LoadModule 로 새 모듈이 끼어들었을 수 있으니 그때마다 다시 정렬하는 것입니다.
정렬은 두 단계입니다.
- 순서 값으로 대충 줄 세우기.
prepare()가 먼저 C 표준 라이브러리의qsort()로nOrder순으로 정렬합니다(88행). 비교 함수 이름도crude_order(대충 순서)이고, 하는 일은a->nOrder-b->nOrder한 줄입니다(75–81행). 그다음aszPre·aszSucc의 이름을 다른 항목의szName과strcmp로 맞춰 보며 "누가 누구 앞" 관계를 만듭니다.aszSucc은 거꾸로 뒤집어 상대 쪽의 "앞에 와야 할 것" 목록에 넣으므로, 결국 관계는 전부 "앞에 와야 할 것" 하나로 통일됩니다(96–125행). - 관계를 지키도록 다시 세우기.
tsort()(위상 정렬)가 결과를 하나씩 뽑습니다. 앞에서부터 아직 안 뽑힌 항목을 보다가, 그 항목에 아직 안 뽑힌 "앞에 와야 할 것"이 있으면 그쪽으로 건너가 그것부터 뽑습니다(139–186행). 소스 주석은 이것을 "순서가 어긋난 항목을 앞으로 끌어온다(dragging out-of-order items to the front)"고 설명합니다(130–138행).
2단계 때문에 생기는 결과가 하나 있습니다. 앞뒤 관계가 순서 값보다 셉니다. 순서 값이 LAST 인 함수라도 FIRST 인 함수의 "앞에 와야 할 것"으로 지목되면 그 앞으로 끌려 나옵니다. 6절의 예제가 바로 이 경우입니다.
6절 예제의 fixups 훅. 순서 값으로는 맨 뒤였던 mod_alpha.c 가 mod_gamma.c 에게 끌려 둘째 자리로 옵니다.
6. 예제: 세 모듈이 같은 훅에 걸리면
직접 쓴 모듈 셋이 fixups(RUN_ALL)와 handler(RUN_FIRST)에 함수를 하나씩 겁니다. 각 함수는 불릴 때마다 응답 헤더 X-Trail 에 "나 불렸고 이 값을 돌려준다"를 한 줄씩 남깁니다. 오류 응답에도 남도록 r->err_headers_out 에 적습니다.
| 모듈 | fixups 순서 |
fixups 반환 |
handler 순서 |
handler 반환 |
|---|---|---|---|---|
mod_alpha.c |
LAST |
DECLINED |
FIRST |
언제나 DECLINED |
mod_beta.c |
MIDDLE |
OK |
MIDDLE |
OK, 단 ?pass 면 DECLINED |
mod_gamma.c |
FIRST + 앞에 mod_alpha.c |
OK, 단 ?stop 이면 403 |
LAST |
OK |
세 모듈 가운데 가장 많은 것을 보여 주는 mod_gamma.c 입니다. 나머지 둘도 모양이 같습니다(예제 폴더 ex/u06-hooks/).
/* 이 글을 위해 쓴 예제입니다 (httpd 의 소스가 아닙니다) */
#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_request.h"
static int gamma_fixups(request_rec *r)
{
if (r->args && !strcmp(r->args, "stop")) { /* ?stop 으로 오면 403 으로 끊는다 */
apr_table_add(r->err_headers_out, "X-Trail", "fixups: gamma -> 403");
return HTTP_FORBIDDEN;
}
apr_table_add(r->err_headers_out, "X-Trail", "fixups: gamma -> OK");
return OK;
}
static int gamma_handler(request_rec *r)
{
if (strcmp(r->handler, "hookdemo")) return DECLINED;
apr_table_add(r->err_headers_out, "X-Trail", "handler: gamma -> OK");
ap_set_content_type(r, "text/plain");
ap_rputs("response written by mod_gamma\n", r);
return OK;
}
static void register_hooks(apr_pool_t *p)
{
/* 순서 값은 FIRST 지만 "mod_alpha.c 보다 뒤에" 라고 못 박는다.
* 이름은 모듈 구조체의 name, 곧 소스 파일 이름이다. */
static const char * const pre[] = { "mod_alpha.c", NULL };
ap_hook_fixups(gamma_fixups, pre, NULL, APR_HOOK_FIRST);
ap_hook_handler(gamma_handler, NULL, NULL, APR_HOOK_LAST);
}
AP_DECLARE_MODULE(gamma) = {
STANDARD20_MODULE_STUFF,
NULL, NULL, NULL, NULL, NULL,
register_hooks
};
handler 함수의 첫 줄 if (strcmp(r->handler, "hookdemo")) return DECLINED; 가 RUN_FIRST 훅의 예절입니다. 설정의 SetHandler hookdemo 로 정해진 이름이 자기 것이 아니면 곧바로 넘깁니다. 설정은 2절의 LoadModule 셋에 <Location /demo> SetHandler hookdemo </Location> 을 더한 것입니다.
정렬 결과 보기: SHOW_HOOKS
2절에서 본 SHOW_HOOKS 환경 변수를 켜고 설정 검사(-t)만 돌렸습니다. 출력이 길어서 fixups·handler 와 모듈 등록 줄만 grep -n 으로 골랐습니다(앞의 숫자는 원래 출력의 줄 번호입니다).
### 4. SHOW_HOOKS=1 httpd -t | grep (fixups, handler)
1:Registering hooks for core.c
20: Hooked handler
21: Hooked handler
23: Hooked fixups
36:Registering hooks for mod_so.c
38:Registering hooks for http_core.c
61:Sorting handler: core.c core.c
63:Sorting fixups: core.c
81:Registering hooks for event.c
97:Registering hooks for mod_unixd.c
101:Registering hooks for mod_authz_core.c
106:Registering hooks for mod_alpha.c
107: Hooked fixups
108: Hooked handler
109:Registering hooks for mod_beta.c
110: Hooked fixups
111: Hooked handler
112:Registering hooks for mod_gamma.c
113: Hooked fixups pre(mod_alpha.c)
114: Hooked handler
128:Sorting handler: core.c mod_alpha.c mod_beta.c mod_gamma.c core.c
130:Sorting fixups: core.c mod_alpha.c mod_gamma.c mod_beta.c
173:Sorting handler: core.c mod_alpha.c mod_beta.c mod_gamma.c core.c
175:Sorting fixups: core.c mod_alpha.c mod_gamma.c mod_beta.c
읽을 거리가 여럿입니다.
- 정렬이 세 번 일어납니다(61·63행, 128·130행, 173·175행). 5절에서 본 세 자리 — 정적 모듈 등록 직후, 설정을 읽은 직후, 설정 트리를 처리한 직후 — 입니다. 첫 정렬 때는 정적 모듈 셋만 있어서
core.c뿐입니다. handler의 양 끝은 코어입니다. 앞의core.c는REALLY_FIRST로 걸린core_upgrade_handler, 뒤의core.c는REALLY_LAST의default_handler입니다(server/core.c 6170–6171행). 우리 셋은 순서 값대로FIRST(alpha) →MIDDLE(beta) →LAST(gamma) 로 그 사이에 섭니다.fixups는 순서 값대로가 아닙니다. 순서 값만 보면 gamma(FIRST) → beta(MIDDLE) → alpha(LAST) 여야 하지만, 결과는 alpha → gamma → beta 입니다. 5절 그림 그대로, gamma 의 "앞에 와야 할 것"인 alpha 가 앞으로 끌려 나왔습니다. alpha 는 beta 와는 아무 관계도 없는데 beta 보다도 앞에 섰다는 점에 주목하세요.
요청을 보내 보기
서버를 띄우고 curl -si 로 세 가지 요청을 보낸 결과입니다(응답 줄, X-Trail 헤더, 본문만 골랐습니다).
### 5. curl /demo
HTTP/1.1 200 OK
X-Trail: fixups: alpha -> DECLINED
X-Trail: fixups: gamma -> OK
X-Trail: fixups: beta -> OK
X-Trail: handler: alpha -> DECLINED
X-Trail: handler: beta -> OK
response written by mod_beta
### 6. curl /demo?pass
HTTP/1.1 200 OK
X-Trail: fixups: alpha -> DECLINED
X-Trail: fixups: gamma -> OK
X-Trail: fixups: beta -> OK
X-Trail: handler: alpha -> DECLINED
X-Trail: handler: beta -> DECLINED
X-Trail: handler: gamma -> OK
response written by mod_gamma
### 7. curl /demo?stop
HTTP/1.1 403 Forbidden
X-Trail: fixups: alpha -> DECLINED
X-Trail: fixups: gamma -> 403
/demo—fixups(RUN_ALL)는 alpha 가DECLINED를 돌려줬는데도 gamma, beta 까지 모두 불렸습니다.handler(RUN_FIRST)는 alpha 가 넘기고 beta 가OK를 돌려주자 거기서 멈췄습니다. gamma 의 핸들러는 불리지 않았습니다./demo?pass— beta 도DECLINED로 넘기자 다음 차례인 gamma 가 응답을 만들었습니다. 같은 요청 주소라도 앞 모듈의 대답에 따라 응답을 만드는 모듈이 바뀝니다./demo?stop—fixups에서 gamma 가 403 을 돌려주자 RUN_ALL 이 멈췄습니다. 뒤의 beta 는 불리지 않았고, 핸들러 단계까지 가지도 못하고 403 응답이 나갔습니다.
위 curl 출력의 X-Trail 줄을 왼쪽부터 순서대로 옮겼습니다. 회색은 DECLINED, 초록은 OK, 빨강은 오류, 점선 칸은 불리지 않은 함수입니다.
헷갈리기 쉬운 곳: 앞뒤 이름을 틀리면 조용히 무시됩니다
예제를 만들며 확인한 함정입니다. gamma 의 "앞에 와야 할 것"을 "mod_alpha.c" 대신 LoadModule 에 쓰는 이름인 "alpha_module" 로 적어 컴파일하고 같은 검사를 돌렸습니다.
### 8. predecessor written as "alpha_module" instead of "mod_alpha.c"
Sorting fixups: core.c mod_gamma.c mod_beta.c mod_alpha.c
오류도 경고도 없이, 순서 값만으로 정렬된 결과(gamma → beta → alpha)가 나왔습니다. prepare() 는 이름이 맞는 항목을 찾으면 관계를 만들고, 못 찾으면 그냥 넘어갑니다(APR-util hooks/apr_hooks.c 99–111행). 이 성질은 일부러 쓸모 있게 쓰이기도 합니다. mod_alias 가 mod_userdir.c 를 적어 두어도 mod_userdir 을 싣지 않은 서버에서 아무 문제가 없는 것이 그 덕분입니다. 하지만 오타도 똑같이 조용히 무시되므로, 앞뒤 이름은 httpd -l 이나 SHOW_HOOKS 에 나오는 .c 이름으로 적고 정렬 결과를 한 번 눈으로 확인하는 것이 좋습니다.
그 밖에 두 가지를 덧붙입니다.
- 순서 값이 같은 함수끼리의 순서에는 기대지 않는 것이 좋습니다. 1단계는
qsort()인데, C 표준은qsort()가 같은 값끼리의 원래 순서를 지켜 준다고 약속하지 않습니다. 이 예제는 그래서handler의 순서 값을 셋 다 다르게 주었습니다. 꼭 지켜야 할 순서가 있으면 앞뒤 이름으로 못 박으세요. - 앞뒤 관계가 고리를 이루면 정렬할 수 없습니다.
tsort()는 고리를 만나면assert(0); /* we have a loop... */에 걸리도록 되어 있습니다(APR-util hooks/apr_hooks.c 149–150행). 고리를 만든 예제는 돌려 보지 않아서, 실제 서버가 그때 어떻게 끝나는지는 확인하지 않았습니다.
7. 맛보기: optional function 과 provider
훅은 "여러 모듈이 한 자리에 함수를 거는" 방식입니다. 반대로 한 모듈이 가진 함수를 다른 모듈이 이름으로 찾아 부르고 싶을 때도 있습니다. 그 모듈이 실려 있지 않을 수도 있어서, 보통의 함수 호출로 적으면 링크가 깨집니다. httpd 는 이것을 두 가지로 풉니다.
Optional function 은 함수 하나를 이름으로 등록하고 찾는 장치입니다. mod_so 는 register_hooks() 안에서 APR_REGISTER_OPTIONAL_FN(ap_find_loaded_module_symbol); 한 줄로 자기 함수 하나를 내놓습니다(modules/core/mod_so.c 440–446행). APR_REGISTER_OPTIONAL_FN 은 함수 이름 문자열을 열쇠로 해시 테이블에 함수 포인터를 넣고, 쓰는 쪽은 APR_RETRIEVE_OPTIONAL_FN(이름) 으로 꺼냅니다(APR-util include/apr_optional.h 70–85행). 그 모듈이 없으면 NULL 이 나오므로 쓰는 쪽은 확인만 하면 됩니다. main() 도 ap_signal_server 를 이렇게 찾습니다(server/main.c 785행). 등록이 register_hooks() 안에서 일어난다는 점, 그래서 훅처럼 재시작 때 지워졌다 다시 등록된다는 점(apr_hook_deregister_all() 이 이 해시도 비웁니다, APR-util hooks/apr_hooks.c 266–284행)이 훅과 닮았습니다.
Provider 는 같은 일을 하는 구현 여럿을 "그룹 + 이름 + 판"으로 등록해 두고 고르는 장치입니다. ap_register_provider(pool, 그룹, 이름, 판, 구조체 주소) 로 등록하고 ap_lookup_provider(그룹, 이름, 판) 으로 꺼냅니다(server/provider.c 35–39행, 99–101행). 설정의 AuthBasicProvider file 에서 file 이 바로 이 이름입니다. 인증 모듈들이 이것으로 어떻게 엮이는지는 13편, mod_proxy 의 로드 밸런서가 고르는 방식은 14편에서 봅니다.
해시 이름과 세 겹 구조는 APR-util hooks/apr_hooks.c 378–392행과 server/provider.c 51–71행에서 확인했습니다.
더 깊이 들어가면
이 글은 큰 줄기를 위해 많은 것을 뺐습니다. 이름과 위치만 적어 둡니다.
- Optional hook — 훅을 가진 모듈이 실려 있지 않아도 걸 수 있는 훅.
AP_OPTIONAL_HOOK(include/ap_hooks.h 169–170행), 코어가mod_proxy의 훅에 거는 예(server/core.c 6176–6177행), 저장소apr_optional_hook_add()(APR-util hooks/apr_hooks.c 339–369행). - 훅 probe —
APR_HOOK_PROBES_ENABLED로 켜는, 훅 실행 앞뒤에 끼우는 추적 매크로(APR-util include/apr_hooks.h 52–109행). 인자 없는 훅에서 끄는 자리(server/config.c 181–195행). - 모듈을 빼는 쪽 —
ap_remove_module()·ap_remove_loaded_module()(server/config.c 662–761행),mod_so의unload_module()(modules/core/mod_so.c 129–144행). - 설정 벡터 길이 조정 — 설정을 읽은 뒤 벡터를 실제 모듈 수로 줄이는
conf_vector_length_pre_config()와 칸을 미리 잡는ap_reserve_module_slots()(server/config.c 2269–2292행, 2556–2559행). -D DUMP_MODULE_DATA—-M의 정보를 TOML 형식으로 찍는 이 커밋의 기능(modules/core/mod_so.c 364–376행). 2.4.x 브랜치의mod_so.c에는 없습니다.AP_DEBUG판의ap_get_module_config— 매크로 대신 함수로 바뀌는 경우(include/http_config.h 551–558행).
정리
| 개념 | 한 줄 요약 |
|---|---|
module 구조체 |
모듈의 전부. 신분증(판·번호·이름·magic) + 설정 함수 넷 + 디렉티브 표 + register_hooks |
AP_DECLARE_MODULE(foo) |
module foo_module 전역 변수를 만든다. LoadModule 첫 인자가 이 변수 이름 |
module_index |
설정 벡터(void * 배열)의 칸 번호. 등록될 때 0 부터 차례로 받는다 |
ap_add_module() |
정적·공유 모듈이 모두 지나는 등록 문. 판 확인 → 번호 → 목록 → 디렉티브 → register_hooks() |
mod_so |
LoadModule 의 주인. .so 를 열고, 이름으로 구조체를 찾고, magic 을 확인하고, ap_add_loaded_module() |
AP_DECLARE_HOOK |
헤더에서 함수 모양·등록 한 건 구조체·ap_hook_X/ap_run_X 선언을 만든다 |
AP_IMPLEMENT_HOOK_* |
.c 에서 static 배열에 넣는 ap_hook_X() 와 배열을 도는 ap_run_X() 의 몸통을 만든다 |
| RUN_FIRST | DECLINED 가 아니면 멈춘다. "누가 맡을까" 형 훅(handler, translate_name) |
| RUN_ALL | OK·DECLINED 는 계속, 그 밖의 값이면 멈춘다. "모두 한 번씩" 형 훅(fixups, post_config) |
| 순서 값 | REALLY_FIRST(-10) ~ REALLY_LAST(30). qsort() 로 대충 줄 세운다 |
| 앞뒤 이름 | .c 이름 목록. tsort() 가 지키며, 순서 값보다 세다. 틀린 이름은 조용히 무시된다 |
| optional function / provider | 훅의 반대 방향 — 다른 모듈의 함수·구현을 이름으로 찾아 쓴다 |
다음에 볼 것
모듈이 서버에 끼어드는 두 통로 가운데 훅을 봤습니다. 다른 하나는 1절에서 이름만 보고 넘긴 cmds, 곧 디렉티브입니다. httpd.conf 의 Alias /icons/ "/usr/share/icons/" 한 줄이 어떻게 읽혀 mod_alias 의 함수에 전달되고, <Directory> 와 <VirtualHost> 마다 만들어진 설정 구조체가 어떻게 합쳐지는지 — 이 글의 module_index 와 설정 벡터가 실제로 쓰이는 모습을 7편: 설정 파일과 디렉티브 — httpd.conf 한 줄이 C 구조체가 되기까지에서 봅니다.
Spring 은 같은 문제를 이렇게 풀었습니다. "만드는 흐름에 바깥 코드를 끼워 넣는 자리"라는 생각은 Spring 6편의 BeanPostProcessor 와 같습니다. 등록된 것을 모두 차례로 부르는 방식은 RUN_ALL 과, Ordered 의 숫자가 작을수록 먼저인 규칙은 nOrder 와 닮았습니다. 다른 점은 둘입니다. Spring 은 인터페이스를 구현한 객체를 목록에 넣고, httpd 는 typedef 로 모양을 정한 함수 포인터를 훅마다 따로 둔 배열에 넣습니다. 그리고 httpd 는 숫자 말고도 "mod_alias.c 뒤에"처럼 상대의 이름으로 순서를 적을 수 있습니다. RUN_FIRST 처럼 "처음으로 답하는 하나"를 고르는 방식은 Spring 에서는 21편의 DispatcherServlet 이 HandlerMapping 목록에서 null 이 아닌 첫 답을 쓰는 모습과 닮았습니다.