이 글에서 다루는 것
"nginx 소스 파헤치기" 연재의 네 번째 글입니다. 2편에서 마스터·워커 프로세스, 모듈, cycle, 설정 문맥으로 이루어진 전체 지도를 그렸고, 3편에서 nginx 가 메모리를 다루는 풀(ngx_pool_t)과 기본 자료구조를 봤습니다. 이번에는 nginx 명령을 친 순간부터 마스터 프로세스가 돌기 시작하기 직전까지, 그리고 그 사이에 nginx.conf 가 읽히는 과정을 봅니다.
답하려는 질문은 네 가지입니다.
main()은 무엇을 하고,nginx -t와nginx -s reload는 어디서 갈라지는가ngx_init_cycle()은 왜 "옛 cycle" 을 받아 "새 cycle" 을 만드는가- nginx.conf 의 한 줄은 어떻게 낱말로 나뉘고,
events {}같은 블록 안으로는 어떻게 들어가는가 unknown directive,is not allowed here같은 오류 메시지는 어디서 나오고, 줄 번호는 무엇을 가리키는가
| 항목 | 내용 |
|---|---|
| 대상 | nginx |
| 버전 | nginx 1.31.9 (master, 개발판) |
| 커밋 | 9a7439fd79 (2026-10-23) |
| 주로 읽는 파일 | src/core/nginx.c, src/core/ngx_cycle.c·ngx_cycle.h, src/core/ngx_conf_file.c·ngx_conf_file.h, src/core/ngx_connection.c |
글에 나오는 줄 번호는 모두 이 커밋 기준입니다. 같은 커밋의 소스를 받고 예제용 도커 이미지를 만드는 방법은 1편에 있습니다.
ngx_cycle.c 는 1,484줄, ngx_conf_file.c 는 1,505줄, nginx.c 는 1,747줄입니다. 이 글은 기동의 큰 줄기와 설정 파서의 뼈대만 골랐습니다. 디렉티브 하나가 모듈의 설정 구조체에 값을 넣는 과정(ngx_command_t 의 set·offset, http {} 블록의 create·merge)은 5편이, 마스터가 시그널을 받아 워커를 갈아 끼우는 과정은 6편이 맡습니다.
2편의 지도에서 이 글은 "A. 준비" 구역의 "기동·cycle" 상자를 확대합니다.
파란 상자가 이 글입니다. 나머지는 회색으로 두었습니다.
먼저 알아 둘 말
cycle(ngx_cycle_t)은 "설정 한 벌과, 그 설정으로 연 자원 전부"를 담은 구조체입니다. 모듈마다의 설정 구조체, 듣기 소켓 목록, 열어 둔 로그 파일, 공유 메모리, 연결 배열이 모두 여기에 매달립니다. 전역 변수 ngx_cycle 이 지금 쓰고 있는 cycle 을 가리킵니다. 2편에서 지도의 한가운데에 둔 그 구조체입니다.
옛 cycle 과 새 cycle: 설정을 읽는 함수 ngx_init_cycle(old_cycle) 은 지금의 cycle 을 받아 새 cycle 을 하나 더 만들어 돌려줍니다. 기동할 때는 "옛 cycle" 자리에 거의 비어 있는 가짜 cycle(init_cycle)을 넣고, reload 할 때는 돌고 있던 진짜 cycle 을 넣습니다.
코어 모듈(NGX_CORE_MODULE)은 nginx.conf 의 맨 바깥(main 문맥)에서 쓰는 디렉티브를 가진 모듈입니다. worker_processes·pid 를 가진 ngx_core_module, events {} 블록을 가진 ngx_events_module, http {} 블록을 가진 ngx_http_module 이 여기에 듭니다. 코어 모듈은 create_conf(빈 설정 구조체 만들기)와 init_conf(안 적은 값에 기본값 넣기) 두 콜백을 가질 수 있습니다. 모듈의 생김새는 2편에서 훑었고 5편에서 자세히 봅니다.
문맥 비트: 디렉티브마다 "어디에 써도 되는가"를 비트로 적어 둡니다. main 문맥은 NGX_MAIN_CONF, events {} 안은 NGX_EVENT_CONF, server {} 안은 NGX_HTTP_SRV_CONF 같은 식입니다. 설정 파서는 지금 읽고 있는 자리의 비트를 cf->cmd_type 에 들고 다니다가 디렉티브의 비트와 AND 해 봅니다.
토큰(token): 설정 파일을 공백·;·{·} 로 끊어 얻은 낱말입니다. worker_processes 2; 는 낱말 worker_processes, 2 와 끝 표시 ; 로 읽힙니다.
시그널러(signaller): nginx -s reload 처럼 -s 를 붙여 띄운 nginx 는 서버가 아니라 "돌고 있는 마스터에 시그널을 보내고 끝나는" 짧은 프로세스입니다. 소스에서는 ngx_process = NGX_PROCESS_SIGNALLER 로 표시합니다. 이 프로세스도 설정 파일을 읽는다는 것이 이 글의 놀라운 점 하나입니다.
1. main() — 설정을 읽기 전과 후
main() 은 src/core/nginx.c 200–407행, 200줄 남짓입니다. 한가운데에 ngx_init_cycle() 호출 한 줄이 있고, 그 앞은 "설정을 읽을 준비", 그 뒤는 "읽은 설정으로 무엇을 할지 고르기" 입니다.
노란 상자가 cycle 을 만드는 곳, 회색 상자는 서버를 띄우지 않고 끝나는 길입니다.
설정을 읽기 전: 옵션과 빈 cycle
맨 앞의 ngx_get_options()(832–992행)는 argv 를 한 글자씩 보는 switch 입니다. 여기서는 값을 전역 변수에 적어 두기만 합니다. -c 는 ngx_conf_file, -g 는 ngx_conf_params, -t 는 ngx_test_config = 1, -s 는 ngx_signal 과 ngx_process = NGX_PROCESS_SIGNALLER 입니다. -s 뒤에 올 수 있는 말은 stop·quit·reopen·reload 넷뿐이고 그 밖의 말이면 여기서 invalid option 으로 끝납니다(950–960행).
그다음 main() 은 설정 파일을 읽기 전에 필요한 최소한을 준비합니다. 로그(ngx_log_init() — 아직 nginx.conf 의 error_log 를 모르므로 컴파일할 때 정한 기본 경로나 -e 로 준 파일), 그리고 스택 위의 빈 cycle 입니다.
ngx_memzero(&init_cycle, sizeof(ngx_cycle_t));
init_cycle.log = log;
ngx_cycle = &init_cycle;
init_cycle.pool = ngx_create_pool(1024, log);
if (init_cycle.pool == NULL) {
return 1;
}
init_cycle 은 main() 의 지역 변수입니다. 로그와 풀, 그리고 바로 뒤의 ngx_process_options()(1038–1138행)가 채우는 경로들(prefix, conf_file, conf_param …)만 가집니다. 모듈 설정을 담을 conf_ctx 는 NULL 그대로입니다. 이 "conf_ctx 가 NULL 인 cycle" 이 기동 때의 옛 cycle 노릇을 하고, 소스는 그것을 매크로 ngx_is_init_cycle(cycle) — (cycle->conf_ctx == NULL) — 로 알아봅니다(ngx_cycle.h 125행).
ngx_init_cycle() 전에 불리는 것이 셋 더 있습니다.
ngx_os_init()— 페이지 크기, CPU 수(worker_processes auto가 쓰는ngx_ncpu), 파일 디스크립터 한도 같은 운영체제 값을 읽습니다.ngx_add_inherited_sockets()(482–538행) — 환경 변수NGINX에 소켓 번호가 적혀 있으면 그 소켓들을init_cycle.listening에 넣어 둡니다. 실행 중에 바이너리를 바꾸는(USR2) 경우에만 쓰이는 길이고, 보통은 488–492행에서 바로 돌아갑니다. 바이너리 교체는 6편에서 봅니다.ngx_preinit_modules()— configure 가 만든 정적 모듈 배열ngx_modules[]에 번호(index)와 이름을 매깁니다(src/core/ngx_module.c26–39행). 5편의 주제입니다.
설정을 읽은 뒤: 세 갈래
cycle = ngx_init_cycle(&init_cycle);
if (cycle == NULL) {
if (ngx_test_config) {
ngx_log_stderr(0, "configuration file %s test failed",
init_cycle.conf_file.data);
}
return 1;
}
if (ngx_test_config) {
if (!ngx_quiet_mode) {
ngx_log_stderr(0, "configuration file %s test is successful",
cycle->conf_file.data);
}
/* ... -T 이면 읽은 설정 파일들을 그대로 찍는다 ... */
return 0;
}
if (ngx_signal) {
return ngx_signal_process(cycle, ngx_signal);
}
nginx -t는ngx_init_cycle()이 성공하면 "test is successful" 을 찍고 끝납니다. 그러니-t가 하는 시험은 기동과 같은 함수를 끝까지 돌려 보는 것입니다. 그 직전의 "syntax is ok" 한 줄은ngx_init_cycle()안, 설정 파일을 다 읽은 직후에 찍힙니다(ngx_cycle.c292–295행). 두 줄 사이에 일어나는 일(로그 파일 열기, 공유 메모리, 듣기 소켓)은 3절과 7절에서 봅니다.nginx -s reload는ngx_signal_process()(ngx_cycle.c1097–1147행)로 갑니다. 설정에서 pid 파일의 경로를 알아내 그 파일의 숫자를 읽고,ngx_os_signal_process()가"reload"라는 이름을signals[]표에서 찾아kill(pid, SIGHUP)을 부릅니다(src/os/unix/ngx_process.c632–648행).- 나머지는 서버입니다. 시그널 처리기를 걸고(
ngx_init_signals()),daemon on(기본값)이면ngx_daemon()으로 fork 해 터미널에서 떨어지고, pid 파일을 쓰고(382행),master_process on(기본값)이면ngx_master_process_cycle()로 들어갑니다(399–404행). 마스터의 루프는 6편이 이어받습니다.
pid 파일을 ngx_init_cycle() 안이 아니라 main() 끝에서 쓰는 이유는 소스 주석에 있습니다. "첫 ngx_init_cycle() 호출에서는 pid 파일을 만들지 않는다. 데몬이 된 프로세스의 pid 를 적어야 하기 때문이다"(ngx_cycle.c 330–333행). ngx_daemon() 의 fork 뒤에는 pid 가 바뀌기 때문입니다.
헷갈리기 쉬운 곳: -s reload 도 nginx.conf 를 통째로 읽습니다
위 코드에서 if (ngx_signal) 은 ngx_init_cycle() 뒤에 있습니다. 시그널을 보낼 pid 파일의 경로가 nginx.conf 의 pid 디렉티브에 있으니 설정을 읽어야 하는데, 그 읽기가 ngx_init_cycle() 의 설정 파싱 전체입니다. http {} 블록 안까지 다 읽습니다. 그래서 설정에 문법 오류가 있으면 nginx -s reload 는 오류를 찍고 시그널을 보내지 않은 채 끝납니다. 8절의 예제에서 확인합니다. 다만 시그널러는 설정을 읽고 코어 모듈의 init_conf 까지만 한 뒤 돌아갑니다(3절의 ④). 로그 파일·공유 메모리·소켓은 건드리지 않습니다.
2. ngx_cycle_t — 설정 한 벌이 사는 곳
ngx_init_cycle() 이 채우는 구조체 struct ngx_cycle_s 는 ngx_cycle.h 39–86행에 있습니다. 필드가 서른 개 남짓인데, 이 글에 나오는 것만 표로 추렸습니다.
| 필드 | 담는 것 | nginx.conf 에서 보던 것 |
|---|---|---|
conf_ctx |
모듈 번호(index)마다 그 모듈의 설정을 가리키는 포인터 배열 |
모든 디렉티브의 값 |
pool |
이 cycle 의 모든 것을 할당한 풀. cycle 을 버릴 때 이 풀 하나를 없앤다 | — |
modules |
이 cycle 이 쓰는 모듈 배열(정적 모듈 + load_module 로 실은 동적 모듈) |
load_module |
listening |
듣기 소켓(ngx_listening_t) 배열 |
listen |
open_files |
열 파일 목록 | access_log, error_log |
shared_memory |
공유 메모리 zone 목록 | limit_req_zone, proxy_cache_path keys_zone=, upstream { zone } |
old_cycle |
이 cycle 을 만들 때 받은 옛 cycle | — |
conf_file, conf_param |
설정 파일 경로, -g 로 준 문자열 |
-c, -g |
위 표를 구조로 그린 것. 파란 칸 둘째 줄은 nginx.conf·명령줄에서 그것과 이어지는 것이고, 모두 노란 cycle 풀에서 할당됩니다.
conf_ctx 의 별 넷(void ****)이 처음에는 겁나 보입니다. 맨 바깥은 "모듈 수만큼의 칸을 가진 포인터 배열" 이고, 칸마다 무엇이 들어 있는지는 모듈이 정합니다. ngx_core_module 의 칸에는 ngx_core_conf_t 하나가 바로 들어 있고, events 의 칸에는 "이벤트 모듈들의 설정 배열" 로 이어지는 포인터가, http 의 칸에는 main/srv/loc 세 배열을 가진 ngx_http_conf_ctx_t 를 가리키는 포인터가 들어 있습니다. 가장 깊은 쪽에 맞춘 타입이 void **** 입니다. 이 모양은 2편의 그림에서 봤고, 디렉티브가 이 안의 어느 칸을 찾아가는지는 5편에서 봅니다. 이 글에서는 "칸 하나에 모듈 하나" 만 기억하면 됩니다. 꺼낼 때는 매크로 ngx_get_conf(conf_ctx, module) — conf_ctx[module.index] — 를 씁니다(ngx_conf_file.h 180행).
3. ngx_init_cycle() — 새 cycle 을 만드는 아홉 단계
ngx_init_cycle() 은 ngx_cycle.c 39–954행, 900줄이 넘는 함수 하나입니다. 길지만 순서는 곧습니다. 아홉 단계로 나눠 봤습니다.
노란 상자는 자원(메모리·파일·소켓)을 마련하는 단계, 파란 상자는 모듈 콜백을 부르는 단계입니다.
① 새 풀, 새 cycle
log = old_cycle->log;
pool = ngx_create_pool(NGX_CYCLE_POOL_SIZE, log);
/* ... */
cycle = ngx_pcalloc(pool, sizeof(ngx_cycle_t));
/* ... */
cycle->pool = pool;
cycle->log = log;
cycle->old_cycle = old_cycle;
새 cycle 은 자기만의 풀(NGX_CYCLE_POOL_SIZE, 기본 16KB 블록 — ngx_palloc.h 22행의 NGX_DEFAULT_POOL_SIZE)에서 태어납니다. 3편에서 본 대로 풀은 하나씩 free 하지 않고 통째로 없애는 메모리 묶음입니다. 그래서 이 cycle 에 딸린 설정 구조체·문자열·배열은 모두 이 풀에서 나오고, 나중에 cycle 을 버릴 때는 ngx_destroy_pool(cycle->pool) 한 번이면 됩니다.
85–194행은 옛 cycle 에서 경로(conf_file, prefix, error_log, conf_param)를 새 풀로 복사하고, 배열·리스트(paths, open_files, shared_memory, listening)의 처음 크기를 옛 cycle 의 개수에 맞춰 잡습니다. 옛 cycle 의 풀은 나중에 사라지므로 포인터를 그대로 빌려 오면 안 되기 때문입니다.
② 모듈 배열과 코어 모듈의 create_conf
for (i = 0; cycle->modules[i]; i++) {
if (cycle->modules[i]->type != NGX_CORE_MODULE) {
continue;
}
module = cycle->modules[i]->ctx;
if (module->create_conf) {
rv = module->create_conf(cycle);
if (rv == NULL) {
ngx_destroy_pool(pool);
return NULL;
}
cycle->conf_ctx[cycle->modules[i]->index] = rv;
}
}
바로 앞에서 conf_ctx 를 ngx_max_module 칸으로 만들고(200행), ngx_cycle_modules() 가 정적 모듈 배열을 이 cycle 의 modules 로 복사합니다(227행). 그리고 코어 모듈만 골라 create_conf 를 부릅니다. HTTP 모듈이나 이벤트 모듈의 설정은 여기서 만들지 않습니다. 그것들은 파서가 http {}·events {} 블록을 만났을 때 그 블록의 디렉티브 함수가 만듭니다(4절).
create_conf 가 하는 일은 "빈 설정 구조체를 만들고 모든 필드를 아직 안 정함으로 표시하기" 입니다. ngx_core_module 의 것을 보면 이렇습니다.
ccf->daemon = NGX_CONF_UNSET;
ccf->master = NGX_CONF_UNSET;
ccf->timer_resolution = NGX_CONF_UNSET_MSEC;
ccf->shutdown_timeout = NGX_CONF_UNSET_MSEC;
ccf->worker_processes = NGX_CONF_UNSET;
ccf->debug_points = NGX_CONF_UNSET;
NGX_CONF_UNSET 은 -1 입니다(ngx_conf_file.h 56행). 0 은 "끔" 이라는 정상 값일 수 있으므로, "설정 파일에 안 적었다" 는 따로 표시해야 기본값을 넣을 자리를 알 수 있습니다.
③ 설정 읽기: -g 먼저, 그다음 파일
conf.ctx = cycle->conf_ctx;
conf.cycle = cycle;
conf.pool = pool;
conf.log = log;
conf.module_type = NGX_CORE_MODULE;
conf.cmd_type = NGX_MAIN_CONF;
/* ... */
if (ngx_conf_param(&conf) != NGX_CONF_OK) {
/* ... 새 풀을 없애고 return NULL ... */
}
if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {
/* ... 새 풀을 없애고 return NULL ... */
}
ngx_conf_t conf 는 파서의 현재 상태입니다. 지금 어느 cycle 을 채우는지, 어느 풀에서 할당하는지, 그리고 지금 읽는 자리가 어느 문맥인지(module_type = NGX_CORE_MODULE, cmd_type = NGX_MAIN_CONF — 파일의 맨 바깥)를 들고 다닙니다. conf.ctx 가 방금 만든 conf_ctx 를 가리키므로 디렉티브들이 그 칸을 채우게 됩니다.
읽는 순서가 중요합니다. -g 로 준 문자열(ngx_conf_param())을 먼저 읽고, 그다음에 nginx.conf(ngx_conf_parse())를 읽습니다. 이 순서가 만드는 함정은 6절에서 봅니다. 파서가 어떻게 생겼는지는 4·5절의 주제입니다.
④ 코어 모듈의 init_conf, 그리고 시그널러의 출구
for (i = 0; cycle->modules[i]; i++) {
if (cycle->modules[i]->type != NGX_CORE_MODULE) {
continue;
}
module = cycle->modules[i]->ctx;
if (module->init_conf) {
/* ... module->init_conf() 를 부르고, NGX_CONF_ERROR 면 return NULL ... */
}
}
if (ngx_process == NGX_PROCESS_SIGNALLER) {
return cycle;
}
파일을 다 읽은 뒤에야 init_conf 를 부릅니다. 여기서 "아직 안 정함" 인 필드에 기본값이 들어갑니다. ngx_core_module_init_conf() 의 첫 줄들이 바로 우리가 아는 기본값입니다 — daemon 1, master_process 1, worker_processes 1(nginx.c 1191–1196행). pid 를 안 적었으면 컴파일할 때 정한 경로(NGX_PID_PATH)가 들어가는 것도 이 함수입니다.
-s 로 띄운 시그널러는 여기서 돌아갑니다. pid 파일 경로만 알면 되니까요.
⑤ 파일, ⑥ 공유 메모리, ⑦ 듣기 소켓
나머지는 설정이 요구하는 운영체제 자원을 실제로 마련하는 단계입니다.
- ⑤ pid 파일(
-t일 때와 reload 로 pid 경로가 바뀌었을 때만, 322–348행), lock 파일 시험,proxy_temp_path같은 경로 만들기, 그리고open_files에 쌓인 로그 파일을 하나씩open()합니다(365–409행). 그 뒤cycle->log = &cycle->new_log;(411행)로, 지금까지 쓰던 기동용 로그 대신 nginx.conf 의error_log로 바꿉니다. - ⑥
shared_memory목록의 zone 마다 공유 메모리를 잡습니다(415–508행). - ⑦
listening배열의 소켓을 엽니다(511–638행).
⑥·⑦에는 공통점이 있습니다. 옛 cycle 에 같은 것이 있으면 새로 만들지 않고 물려받습니다. reload 가 연결을 끊지 않는 비밀이 여기에 있으므로 7절에서 따로 봅니다.
이 자원들을 설정 파싱이 끝난 뒤에 한꺼번에 마련한다는 점도 볼 만합니다. listen 8080; 디렉티브는 읽힐 때 소켓을 열지 않습니다. http {} 블록이 끝날 때 ngx_http_optimize_servers()(ngx_http.c 336행)를 거쳐 ngx_create_listening() 이 cycle->listening 에 fd = -1 인 항목만 넣어 둡니다(ngx_connection.c 28행, 79행). access_log·limit_req_zone 도 목록에 이름만 올려 둡니다(공유 메모리는 ngx_shared_memory_add(), 1306–1376행). 설정이 끝까지 맞는지 확인된 뒤에 이런 운영체제 자원을 건드리므로, 문법 오류가 있는 설정은 로그 파일·공유 메모리·듣기 소켓을 하나도 마련하지 않은 채 실패합니다(설정 파일 자신과 include 한 파일은 읽는 동안 열립니다).
⑧ init_module, ⑨ 옛 cycle 정리
if (ngx_open_listening_sockets(cycle) != NGX_OK) {
goto failed;
}
if (!ngx_test_config) {
ngx_configure_listening_sockets(cycle);
}
/* commit the new cycle configuration */
if (!ngx_use_stderr) {
(void) ngx_log_redirect_stderr(cycle);
}
pool->log = cycle->log;
if (ngx_init_modules(cycle) != NGX_OK) {
/* fatal */
exit(1);
}
/* commit the new cycle configuration */ 주석이 경계선입니다. 여기까지는 실패하면 되돌릴 수 있고, 여기서부터는 새 cycle 로 확정입니다. ngx_init_modules() 는 모든 모듈의 init_module 콜백을 부릅니다(ngx_module.c 66–79행). 예를 들어 이벤트 코어 모듈은 여기서 accept_mutex 등이 쓰는 작은 공유 메모리를 잡습니다(ngx_event_module_init(), src/event/ngx_event.c 488행부터). 이 단계의 실패는 되돌리지 않고 exit(1) — 소스 주석 그대로 "fatal" 입니다.
그다음 655–779행이 옛 cycle 에만 있고 새 cycle 에는 없는 공유 메모리 zone 과 듣기 소켓을 닫고, 옛 cycle 의 파일은 전부 닫습니다(파일은 ⑤에서 새 cycle 이 이미 다시 열었습니다). 마지막으로 옛 cycle 의 풀을 언제 없앨지 정합니다.
if (ngx_process == NGX_PROCESS_MASTER || ngx_is_init_cycle(old_cycle)) {
ngx_destroy_pool(old_cycle->pool);
cycle->old_cycle = NULL;
return cycle;
}
기동 때(옛 cycle 이 init_cycle)와 마스터 프로세스에서는 옛 cycle 의 풀을 바로 없앱니다. 마스터는 클라이언트 연결을 들고 있지 않으므로 옛 설정을 붙들 이유가 없습니다. 옛 설정으로 연결을 처리하던 쪽은 옛 워커이고, 워커는 fork 될 때 옛 cycle 을 통째로 복사해 가졌으므로 마스터가 자기 사본을 없애도 상관없습니다. 그 밖의 경우(master_process off 로 프로세스 하나만 돌릴 때)는 옛 cycle 을 ngx_old_cycles 배열에 넣어 두고 30초 타이머(ngx_clean_old_cycles())가 그 cycle 의 연결이 다 끝났는지 보며 치웁니다(792–830행).
commit 주석을 경계로, 앞에서 실패하면 되돌리고 뒤에서 실패하면 끝냅니다. 소스는 ngx_cycle.c 632–830행, 실패 경로는 833–953행입니다.
실패하면: 새로 연 것만 닫는다
중간에 goto failed 하면 833–953행이 새 cycle 이 새로 연 것만 되돌립니다. 새로 연 파일을 닫고, 옛 cycle 과 같지 않은 새 공유 메모리를 풀고, 새로 연 소켓(ls[i].open)을 닫고, 새 풀을 없앱니다. 옛 cycle 은 아무것도 건드리지 않았으므로 그대로 살아 있습니다. reload 가 실패해도 서버가 옛 설정으로 계속 도는 것은 이 구조 덕분입니다(8절).
예제 1: ngx_init_cycle() 은 언제, 누가 부르나
ngx_init_cycle() 에 gdb 중단점을 걸고, 들어올 때마다 프로세스 번호와 ngx_process 값, 옛 cycle 의 conf_ctx 를 찍은 뒤 계속 가게 했습니다. 마스터를 gdb 아래에서 띄우고(-g 'daemon off;' — 데몬이 되면 gdb 가 따라가지 못하므로), 그동안 다른 셸에서 nginx -s reload 도 gdb 아래에서 돌렸습니다.
이 예제는 기준 커밋(9a7439fd79)을 그대로 빌드한 nginx 1.31.9 로 도커 컨테이너(Debian bookworm, --with-debug)에서 실행했습니다. 예제 폴더는 ex/u04-init-cycle-gdb 이고, gdb 명령 파일(init_cycle.gdb)은 break ngx_init_cycle 에 commands 로 printf 한 줄과 bt 3, continue 를 붙인 것입니다. fork 된 워커는 놓아 주고(set detach-on-fork on), SIGHUP·SIGCHLD 같은 시그널에서는 멈추지 않게 했습니다.
마스터 쪽 gdb 출력입니다(gdb 의 안내 줄은 뺐습니다).
--- ngx_init_cycle() in pid 49, ngx_process=0, old_cycle->conf_ctx=(nil)
#0 ngx_init_cycle (old_cycle=0x7fffffffe930) at src/core/ngx_cycle.c:57
#1 0x000055555557ee55 in main (argc=5, argv=0x7fffffffed08) at src/core/nginx.c:300
--- ngx_init_cycle() in pid 49, ngx_process=1, old_cycle->conf_ctx=0x555555785310
#0 ngx_init_cycle (old_cycle=0x555555784120) at src/core/ngx_cycle.c:57
#1 0x00005555555ca301 in ngx_master_process_cycle (cycle=0x555555784120) at src/os/unix/ngx_process_cycle.c:247
#2 0x000055555557f1c0 in main (argc=5, argv=0x7fffffffed08) at src/core/nginx.c:403
--- ngx_init_cycle() in pid 49, ngx_process=1, old_cycle->conf_ctx=0x5555557885e0
#0 ngx_init_cycle (old_cycle=0x555555788130) at src/core/ngx_cycle.c:57
#1 0x00005555555ca301 in ngx_master_process_cycle (cycle=0x555555788130) at src/os/unix/ngx_process_cycle.c:247
#2 0x000055555557f1c0 in main (argc=5, argv=0x7fffffffed08) at src/core/nginx.c:403
그리고 nginx -s reload 를 띄운 gdb 의 출력입니다.
--- ngx_init_cycle() in pid 76, ngx_process=2, old_cycle->conf_ctx=(nil)
#0 ngx_init_cycle (old_cycle=0x7fffffffe940) at src/core/ngx_cycle.c:57
#1 0x000055555557ee55 in main (argc=5, argv=0x7fffffffed18) at src/core/nginx.c:300
pid 와 주소는 실행마다 다릅니다. 마스터에서 세 번째 호출은 8절의 예제에서 틀린 설정으로 SIGHUP 을 보낸 때입니다.
- 첫 호출은
main()의 300행에서,old_cycle->conf_ctx=(nil)— 옛 cycle 은 스택 위의init_cycle입니다(주소0x7fff…가 스택 영역입니다). 이때ngx_process는 아직 0(NGX_PROCESS_SINGLE)입니다. 마스터로 바뀌는 것은 설정을 읽은 뒤인main()346–348행이기 때문입니다. - 두 번째 호출은
ngx_master_process_cycle()의 247행에서 — reload 입니다. 이번 옛 cycle 은 힙 주소의 진짜 cycle 이고conf_ctx가 채워져 있습니다. 같은 함수가 "옛 것을 받아 새 것을 만든다" 는 모양 하나로 기동과 reload 를 다 맡습니다. - 세 번째 호출의 옛 cycle(
0x555555788130)은 두 번째 호출이 만든 cycle 입니다. 두 번째 호출의 옛 cycle(0x555555784120)과 주소가 다릅니다. reload 할 때마다 cycle 이 한 세대씩 바뀝니다. - 시그널러(pid 76)도
ngx_init_cycle()을 부릅니다.ngx_process=2(NGX_PROCESS_SIGNALLER)라서 ④의 출구에서 돌아갑니다.
위 gdb 출력의 네 줄을 시간 순서로 놓은 것. pid·주소는 예제 1의 실제 출력이고, 호출 3의 결과는 8절 예제 5의 출력입니다.
ngx_process 의 값은 src/os/unix/ngx_process_cycle.h 23–27행의 #define 입니다(0 SINGLE, 1 MASTER, 2 SIGNALLER, 3 WORKER, 4 HELPER).
4. ngx_conf_parse() — 파일, 블록, 명령줄을 한 함수로
ngx_conf_parse(cf, filename) 은 설정을 읽는 유일한 입구입니다(ngx_conf_file.c 161–358행). 그런데 세 가지 다른 상황에서 불립니다.
| 경우 | 부르는 곳 | filename |
끝나는 조건 |
|---|---|---|---|
parse_file |
ngx_init_cycle(), include |
파일 경로 | 파일 끝(NGX_CONF_FILE_DONE) |
parse_block |
events {}·http {}·server {} 같은 블록 디렉티브의 함수 |
NULL (지금 읽던 파일을 이어서) |
짝이 맞는 }(NGX_CONF_BLOCK_DONE) |
parse_param |
ngx_conf_param() — -g |
NULL (버퍼에 문자열만) |
문자열 끝 |
함수 앞부분이 이 셋을 가립니다.
if (filename) {
/* open configuration file */
fd = ngx_open_file(filename->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
/* ... */
buf.start = ngx_alloc(NGX_CONF_BUFFER, cf->log);
/* ... */
type = parse_file;
/* ... */
} else if (cf->conf_file->file.fd != NGX_INVALID_FILE) {
type = parse_block;
} else {
type = parse_param;
}
src/core/ngx_conf_file.c 179–245행
파일 이름이 있으면 파일을 열고 4KB(NGX_CONF_BUFFER, 11행) 버퍼를 만듭니다. 이름이 없으면 "지금 읽고 있는 파일" 이 있느냐로 블록과 -g 를 가립니다. -g 는 ngx_conf_param() 이 fd 가 NGX_INVALID_FILE 인 가짜 파일을 만들고 버퍼에 문자열을 담아 넘깁니다(73–96행).
그다음은 무한 루프입니다. 줄인 모양은 이렇습니다.
for ( ;; ) {
rc = ngx_conf_read_token(cf);
/* ... */
if (rc == NGX_CONF_BLOCK_DONE) {
if (type != parse_block) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "unexpected \"}\"");
goto failed;
}
goto done;
}
/* ... 파일 끝(NGX_CONF_FILE_DONE), -g 안의 { 도 같은 식으로 검사 ... */
/* rc == NGX_OK || rc == NGX_CONF_BLOCK_START */
/* ... cf->handler 가 있으면(types {} 같은 블록) 그것에게 넘긴다 ... */
rc = ngx_conf_handler(cf, rc);
if (rc == NGX_ERROR) {
goto failed;
}
}
src/core/ngx_conf_file.c 248–330행
ngx_conf_read_token() 이 디렉티브 하나를 읽어 오면(5절), ngx_conf_handler() 가 그 이름의 디렉티브를 찾아 함수를 부릅니다(6절). 끝 표시마다 할 일이 정해져 있습니다.
}를 만났는데 블록 안이 아니다 →unexpected "}"- 파일이 끝났는데 블록 안이다 →
unexpected end of file, expecting "}" -g인데{가 나왔다 →block directives are not supported in -g option
블록 안으로 들어가는 법 — 재귀
그럼 events { 를 만나면 무엇이 블록 안을 읽을까요? ngx_conf_parse() 는 events 라는 디렉티브를 찾아 그 함수 ngx_events_block() 을 부를 뿐입니다. 블록 안을 읽는 것은 그 함수가 다시 부르는 ngx_conf_parse(cf, NULL) 입니다.
pcf = *cf;
cf->ctx = ctx;
cf->module_type = NGX_EVENT_MODULE;
cf->cmd_type = NGX_EVENT_CONF;
rv = ngx_conf_parse(cf, NULL);
*cf = pcf;
src/event/ngx_event.c 1001–1008행
- 지금의 파서 상태
*cf를 지역 변수pcf에 통째로 복사해 둡니다. - 문맥을 바꿉니다 — 설정을 채울 곳(
ctx)은 이벤트 모듈들의 설정 배열로, 모듈 종류는NGX_EVENT_MODULE, 문맥 비트는NGX_EVENT_CONF로. ngx_conf_parse(cf, NULL)— 파일 이름이 없고 지금 읽던 파일이 열려 있으므로parse_block입니다. 같은 파일의 같은 버퍼를 이어서 읽다가 짝이 맞는}에서 돌아옵니다.*cf = pcf로 원래 문맥을 되돌립니다. 다음 디렉티브는 다시 main 문맥에서 읽힙니다.
블록 하나가 C 함수 호출 한 층입니다. http {} → server {} → location {} 이면 main 문맥보다 세 층 깊어집니다.
http {} 도 같은 모양입니다 — ngx_http_block() 이 238–240행에서 module_type = NGX_HTTP_MODULE, cmd_type = NGX_HTTP_MAIN_CONF 로 바꾸고 ngx_conf_parse(cf, NULL) 을 부르고, server {} 는 NGX_HTTP_SRV_CONF(ngx_http_core_module.c 3100–3104행), location {} 은 NGX_HTTP_LOC_CONF(3358–3360행)로 한 층씩 들어갑니다. 그래서 nginx.conf 의 중괄호 중첩이 그대로 C 호출 스택의 깊이가 됩니다. 블록 함수가 그 안에서 모듈들의 설정 구조체를 만들고, 블록이 끝난 뒤 합치는(merge) 과정은 5편에서 봅니다.
httpd 와 비교하면 차이가 뚜렷합니다. httpd 는 설정 파일 전체를 먼저 트리(ap_directive_t)로 읽어 두고, 그 트리를 다시 걸으며 디렉티브를 실행했습니다(httpd 7편). nginx 에는 그런 트리가 없습니다. 낱말을 읽는 즉시 디렉티브 함수를 부르고, 블록은 함수 호출로 들어갑니다. 중간 자료구조가 없는 대신, 디렉티브는 파일에 적힌 순서대로 실행됩니다.
예제 2: 디렉티브마다 파서가 보는 것
ngx_conf_handler() 에 중단점을 걸고, 불릴 때마다 지금 문맥 비트(cf->cmd_type), 모듈 종류(cf->module_type 의 네 바이트를 글자로), 줄 번호, 낱말 배열(cf->args)을 찍었습니다. nginx -t -g 'daemon off;' 를 gdb 아래에서 돌렸습니다. 예제 폴더는 ex/u04-conf-tokens 이고 설정은 이렇습니다.
# 이 글을 위해 쓴 예제입니다 (nginx 의 소스가 아닙니다)
# ngx_conf_read_token() 이 한 줄을 어떻게 낱말로 나누는지 보려는 설정
worker_processes 1; # 주석은 줄 끝까지 버린다
error_log /tmp/u04t/error.log;
pid /tmp/u04t/nginx.pid;
events { worker_connections 64; }
http {
access_log off;
log_format main '$remote_addr "$request"';
server {
listen 127.0.0.1:8080;
location / {
return 200 "a b; c \"quoted\"";
}
location /v { return 200 ${uri}x; }
}
}
=== ngx_conf_handler() calls during nginx -t -g 'daemon off;' ===
cmd_type=01000000 module_type=CORE line= 0 | <daemon> <off> last=0
cmd_type=01000000 module_type=CORE line= 3 | <worker_processes> <1> last=0
cmd_type=01000000 module_type=CORE line= 4 | <error_log> </tmp/u04t/error.log> last=0
cmd_type=01000000 module_type=CORE line= 5 | <pid> </tmp/u04t/nginx.pid> last=0
cmd_type=01000000 module_type=CORE line= 7 | <events> last=1
cmd_type=02000000 module_type=EVNT line= 7 | <worker_connections> <64> last=0
cmd_type=01000000 module_type=CORE line= 9 | <http> last=1
cmd_type=02000000 module_type=HTTP line=10 | <access_log> <off> last=0
cmd_type=02000000 module_type=HTTP line=11 | <log_format> <main> <$remote_addr "$request"> last=0
cmd_type=02000000 module_type=HTTP line=12 | <server> last=1
cmd_type=04000000 module_type=HTTP line=13 | <listen> <127.0.0.1:8080> last=0
cmd_type=04000000 module_type=HTTP line=14 | <location> </> last=1
cmd_type=08000000 module_type=HTTP line=15 | <return> <200> <a b; c "quoted"> last=0
cmd_type=04000000 module_type=HTTP line=17 | <location> </v> last=1
cmd_type=08000000 module_type=HTTP line=17 | <return> <200> <${uri}x> last=0
nginx: the configuration file /work/nginx.conf syntax is ok
nginx: configuration file /work/nginx.conf test is successful
last 는 ngx_conf_read_token() 의 반환값입니다(0 = NGX_OK, ; 로 끝남 / 1 = NGX_CONF_BLOCK_START, { 로 끝남).
- 첫 줄은
-g로 준daemon off, 줄 번호 0 — 파일보다 먼저 읽힙니다(3절 ③).ngx_conf_param()이 가짜 파일의line을 0 으로 둡니다(91행). - 블록 디렉티브(
events,http,server,location)는last=1이고, 그 다음 줄부터 문맥이 바뀝니다 —CORE/01000000→EVNT/02000000,HTTP/02000000→04000000→08000000. 4절에서 본 재귀가 문맥을 바꾸는 모습입니다.location /의 블록이 끝난 뒤location /v는 다시04000000(server 문맥)으로 읽혔습니다. module_type이 글자로 읽힙니다. 모듈 종류 상수가 네 글자를 정수로 만든 값이라서입니다 —NGX_CORE_MODULE은0x45524F43 /* "CORE" */(ngx_conf_file.h70행). 리틀 엔디언 기계에서 메모리에 놓인 바이트를 순서대로 읽으면 "CORE" 가 됩니다.- 따옴표 안의 공백과
;는 낱말을 끊지 않고, 따옴표 자신은 빠지며,\"는"가 됩니다 —<a b; c "quoted">.${uri}x의{도 블록 시작으로 읽히지 않았습니다. 이것이 5절의 주제입니다.
헷갈리기 쉬운 곳: 0x02000000 은 셋입니다
위 출력에서 events {} 안과 http {} 바로 안이 같은 cmd_type=02000000 입니다. 실제로 값이 겹칩니다.
#define NGX_DIRECT_CONF 0x00010000
#define NGX_MAIN_CONF 0x01000000
#define NGX_ANY_CONF 0xFF000000
src/core/ngx_conf_file.h 49–52행
NGX_EVENT_CONF 는 0x02000000(ngx_event.h 397행), NGX_HTTP_MAIN_CONF 도 0x02000000(ngx_http_config.h 41행), NGX_STREAM_MAIN_CONF·NGX_MAIL_MAIN_CONF 도 0x02000000 입니다. 위쪽 8비트(TT, 헤더 주석 16–20행의 "command type")는 모듈 종류마다 따로 쓰는 공간이고, 둘을 가르는 것은 cf->module_type 입니다. ngx_conf_handler() 는 문맥 비트를 보기 전에 모듈 종류부터 맞춰 봅니다(6절). 그래서 worker_connections(이벤트 모듈) 를 http {} 바로 안에 써도 비트는 맞지만 모듈 종류가 달라 거절됩니다.
5. ngx_conf_read_token() — 한 글자씩 읽어 낱말로
ngx_conf_read_token()(509–823행)은 정규식이나 파서 생성기 없이 문자 하나씩 보는 손으로 짠 상태 기계입니다. 상태는 몇 개의 플래그 변수입니다.
| 변수 | 뜻 |
|---|---|
last_space |
직전 글자가 공백이었다 — 다음 글자는 새 낱말의 시작 |
need_space |
따옴표 낱말이 막 끝났다 — 다음은 공백·;·{ 여야 한다 |
d_quoted, s_quoted |
"…" / '…' 안 |
quoted |
직전 글자가 \ — 이번 글자는 특별한 뜻 없음 |
sharp_comment |
# 를 만났다 — 줄 끝까지 버린다 |
variable |
직전이 $ — 바로 뒤의 { 는 블록이 아니라 ${name} |
주요 흐름만 그렸습니다. \ 뒤의 한 글자, ${ 의 예외는 본문을 보세요.
새 낱말이 시작되는 자리에서의 판단이 함수의 한가운데입니다.
switch (ch) {
case ';':
case '{':
if (cf->args->nelts == 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"unexpected \"%c\"", ch);
return NGX_ERROR;
}
if (ch == '{') {
return NGX_CONF_BLOCK_START;
}
return NGX_OK;
case '}':
if (cf->args->nelts != 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"unexpected \"}\"");
return NGX_ERROR;
}
return NGX_CONF_BLOCK_DONE;
case '#':
sharp_comment = 1;
continue;
src/core/ngx_conf_file.c 673–700행
;나{앞에 낱말이 하나도 없으면(;;처럼)unexpected ";"입니다.}는 낱말이 하나도 없을 때만 블록의 끝입니다.worker_processes 1 }처럼;를 빠뜨리고}가 오면unexpected "}"입니다.#는 낱말이 시작될 자리에서만 주석입니다. 이switch는last_space일 때만 오므로,abc#def처럼 낱말 가운데의#는 그냥 글자입니다.
낱말이 끝나면(766–820행) 풀에서 메모리를 받아 낱말을 복사하면서 \", \', \\ 의 \ 를 지우고 \t·\r·\n 을 진짜 탭·CR·LF 로 바꿉니다(777–807행). 그리고 808행에서 끝에 '\0' 을 붙입니다. 3편에서 ngx_str_t 는 NUL 로 끝나지 않을 수 있는 문자열이라고 했는데, 설정 낱말만큼은 NUL 로 끝납니다. 그래서 디렉티브 함수들이 ngx_strcmp(value[1].data, "auto") 처럼 C 문자열 함수를 바로 쓸 수 있습니다(ngx_set_worker_processes(), nginx.c 1637행). 예제 2의 gdb 가 %s 로 낱말을 찍을 수 있었던 것도 이 덕분입니다.
4KB 버퍼와 "too long parameter"
파일은 한 번에 다 읽지 않고 4KB 버퍼로 조금씩 읽습니다. 버퍼를 다 쓰면 539–617행이 아직 끝나지 않은 낱말 조각을 버퍼 맨 앞으로 옮기고(ngx_memmove) 그 뒤를 파일에서 채웁니다. 그런데 낱말 하나가 버퍼 전체(4,096바이트)를 차지하면 옮겨도 자리가 나지 않습니다. 그때 오류가 납니다(563–583행). 따옴표 안이었다면 "따옴표를 안 닫은 것 같다" 고 알려 줍니다 — too long parameter, probably missing terminating """ character. 닫는 따옴표를 빠뜨리면 파일의 나머지가 통째로 한 낱말이 되기 쉬우므로 그럴듯한 추측입니다. 이 메시지는 소스로만 확인했고 예제로 돌려 보지는 않았습니다.
헷갈리기 쉬운 곳: 오류의 줄 번호는 "읽다가 멈춘 줄" 입니다
cf->conf_file->line 은 LF 를 만날 때마다 1씩 올라갑니다(621–622행). 오류를 찍는 ngx_conf_log_error() 는 메시지 뒤에 그 순간의 파일 이름과 줄 번호를 in %s:%ui 로 붙이고, 가짜 파일(fd 가 NGX_INVALID_FILE)이면 in command line 을 붙입니다(ngx_conf_file.c 1014–1027행).
그러니 ; 를 빠뜨리면 오류는 그 줄이 아니라 파서가 끝 표시를 찾아 읽어 내려간 줄을 가리킵니다. 6절의 예제에서 3행의 worker_processes 2 뒤에 ; 를 빠뜨렸더니 4행이 찍혔습니다. -g 로 준 문자열의 오류는 줄 번호 대신 "in command line" 으로 끝납니다.
6. ngx_conf_handler() 와 오류 메시지
ngx_conf_handler()(362–505행)는 낱말 배열의 첫 낱말로 디렉티브를 찾아 그 함수를 부릅니다. 찾는 법은 단순합니다.
for (i = 0; cf->cycle->modules[i]; i++) {
cmd = cf->cycle->modules[i]->commands;
if (cmd == NULL) {
continue;
}
for ( /* void */ ; cmd->name.len; cmd++) {
/* ... 길이와 글자가 같은 이름이 아니면 continue ... */
found = 1;
if (cf->cycle->modules[i]->type != NGX_CONF_MODULE
&& cf->cycle->modules[i]->type != cf->module_type)
{
continue;
}
/* is the directive's location right ? */
if (!(cmd->type & cf->cmd_type)) {
continue;
}
src/core/ngx_conf_file.c 374–403행
모든 모듈의 디렉티브 표를 처음부터 차례로 훑습니다. 해시도 정렬도 없습니다. httpd 가 디렉티브 이름을 해시 테이블에 넣어 두고 찾는 것(httpd 7편)과 다릅니다. 설정을 읽는 것은 기동·reload·-t·-s 때뿐이고 디렉티브 수도 수백 개이니, 이 단순함으로 충분하다고 본 것으로 읽힙니다(소스에 이유가 적혀 있지는 않습니다).
이름이 같은 디렉티브를 찾으면 found = 1 을 해 두고, 모듈 종류와 문맥 비트가 맞지 않으면 다음 후보를 계속 찾습니다. 같은 이름을 여러 모듈이 가질 수 있기 때문입니다(예: proxy_pass 는 HTTP 의 proxy 모듈과 stream 의 proxy 모듈에 다 있습니다). 끝까지 맞는 것이 없을 때 found 가 두 오류를 가릅니다(486–496행).
이어지는 405–449행은 끝 표시(블록 디렉티브는 {, 나머지는 ;)와 인자 개수(NGX_CONF_TAKE1, NGX_CONF_FLAG, NGX_CONF_1MORE …)를 검사하고, 451–467행이 설정 구조체의 위치를 찾아 469행의 cmd->set(cf, cmd, conf) 를 부릅니다. set 이 문자열을 돌려주면 그 문자열이 그대로 "이름" directive <문자열> 오류가 됩니다(479–480행). 이 판단 순서와 설정 구조체를 찾는 포인터 계산은 5편이 그림과 함께 자세히 다룹니다. 여기서는 오류 메시지가 어디서 나오는지만 정리합니다.
검사 순서대로 그린 ngx_conf_handler(). 빨간 칸마다 아래 예제 3에서 그 오류를 낸 설정의 이름을 적었습니다.
| 오류 메시지 | 찍는 곳 | 뜻 |
|---|---|---|
unknown directive "X" |
ngx_conf_handler() 493–494행 |
어느 모듈에도 그 이름이 없다(오타, 또는 그 모듈을 빌드하지 않음) |
"X" directive is not allowed here |
487–488행 | 이름은 있지만 지금 문맥·모듈 종류에 맞는 것이 없다 |
directive "X" is not terminated by ";" |
406–408행 | 블록이 아닌 디렉티브가 { 로 끝났다 |
directive "X" has no opening "{" |
413–415행 | 블록 디렉티브가 ; 로 끝났다 |
invalid number of arguments in "X" directive |
500–502행 | 인자 개수가 type 비트와 맞지 않다 |
"X" directive is duplicate 등 |
479–480행 | set 함수가 돌려준 문자열 |
unexpected "}", unexpected end of file, expecting "}" |
ngx_conf_parse() 268·278–279행, ngx_conf_read_token() 691–692행 |
중괄호 짝이 안 맞다 |
예제 3: 틀린 설정을 nginx -t 로
멀쩡한 설정 하나를 두고, 한 곳씩만 틀린 설정 여러 벌을 만들어 nginx -t 를 돌렸습니다. 예제 폴더는 ex/u04-config-errors 입니다. 바탕 설정은 error_log·pid 두 줄, events {}, server { listen 127.0.0.1:8080; return 200 "ok\n"; } 하나를 가진 http {} 로 15행이고, 3행(main 문맥)·9행(http {} 안)·끝에 틀린 줄을 끼웁니다(run.sh 의 make_conf()).
| 이름 | 끼운 줄 |
|---|---|
unknown |
3행 worker_processess 2; (s 하나 더) |
not-allowed |
9행(http {} 안) worker_processes 2; |
no-semicolon |
3행 worker_processes 2 (; 없음) |
arg-count |
3행 worker_processes 1 2; |
duplicate |
3행 worker_processes 1; worker_processes 2; |
bad-flag |
3행 daemon maybe; |
extra-brace |
끝에 } 하나 더 |
missing-brace |
마지막 } 를 지움 |
=== nginx -t: good ===
nginx: the configuration file /tmp/u04/good.conf syntax is ok
nginx: configuration file /tmp/u04/good.conf test is successful
=== nginx -t: unknown ===
nginx: [emerg] unknown directive "worker_processess" in /tmp/u04/unknown.conf:3
nginx: configuration file /tmp/u04/unknown.conf test failed
=== nginx -t: not-allowed ===
nginx: [emerg] "worker_processes" directive is not allowed here in /tmp/u04/not-allowed.conf:9
nginx: configuration file /tmp/u04/not-allowed.conf test failed
=== nginx -t: no-semicolon ===
nginx: [emerg] directive "worker_processes" is not terminated by ";" in /tmp/u04/no-semicolon.conf:4
nginx: configuration file /tmp/u04/no-semicolon.conf test failed
=== nginx -t: arg-count ===
nginx: [emerg] invalid number of arguments in "worker_processes" directive in /tmp/u04/arg-count.conf:3
nginx: configuration file /tmp/u04/arg-count.conf test failed
=== nginx -t: duplicate ===
nginx: [emerg] "worker_processes" directive is duplicate in /tmp/u04/duplicate.conf:3
nginx: configuration file /tmp/u04/duplicate.conf test failed
=== nginx -t: bad-flag ===
nginx: [emerg] invalid value "maybe" in "daemon" directive, it must be "on" or "off" in /tmp/u04/bad-flag.conf:3
nginx: configuration file /tmp/u04/bad-flag.conf test failed
=== nginx -t: extra-brace ===
nginx: [emerg] unexpected "}" in /tmp/u04/extra-brace.conf:15
nginx: configuration file /tmp/u04/extra-brace.conf test failed
=== nginx -t: missing-brace ===
nginx: [emerg] unexpected end of file, expecting "}" in /tmp/u04/missing-brace.conf:14
nginx: configuration file /tmp/u04/missing-brace.conf test failed
=== where the errors point: no-semicolon.conf lines 3-4 ===
worker_processes 2
events {
not-allowed—worker_processes라는 이름은ngx_core_module에 있으니found = 1이 됩니다. 하지만http {}안에서는cf->module_type이NGX_HTTP_MODULE이라 코어 모듈의 디렉티브는 건너뛰고, 끝까지 맞는 것이 없어 "not allowed here" 입니다.no-semicolon— 오류가 4행을 가리킵니다.;가 없으니 토큰 읽기는 줄을 넘어events까지 같은 디렉티브의 낱말로 모으고,{에서NGX_CONF_BLOCK_START를 돌려줍니다. 낱말은worker_processes,2,events셋이 되었고, 블록이 아닌 디렉티브가{로 끝났으니 "not terminated by ";"" 입니다. 인자 개수 검사(421행~)보다 끝 표시 검사(405행)가 먼저라서 "invalid number of arguments" 가 아니라 이 메시지가 나옵니다.duplicate·bad-flag는 디렉티브 함수(set)가 돌려준 문자열입니다. "is duplicate" 는ngx_set_worker_processes()가 필드가 이미NGX_CONF_UNSET이 아니면 돌려주고(nginx.c1631–1633행),daemon의 메시지는 표준 setterngx_conf_set_flag_slot()이 직접 찍습니다(ngx_conf_file.c1055–1058행).extra-brace는 파일의 맨 바깥(parse_file)에서}를 만나ngx_conf_parse()의 268행이,missing-brace는http {}블록을 읽던 안쪽 파서(parse_block)가 파일 끝을 만나 278–279행이 찍었습니다.
-g 는 파일보다 먼저 읽힙니다
같은 예제의 뒷부분은 -g 입니다.
=== -g 'worker_processes 3;' (file has none) ===
nginx: the configuration file /tmp/u04/good.conf syntax is ok
nginx: configuration file /tmp/u04/good.conf test is successful
=== -g 'worker_processes 3;' when the file also sets it ===
nginx: [emerg] "worker_processes" directive is duplicate in /tmp/u04/sets-one.conf:3
nginx: configuration file /tmp/u04/sets-one.conf test failed
=== -g without the semicolon ===
nginx: [emerg] unexpected end of parameter, expecting ";" in command line
nginx: configuration file /tmp/u04/good.conf test failed
=== -g with a block ===
nginx: [emerg] block directives are not supported in -g option in command line
nginx: configuration file /tmp/u04/good.conf test failed
=== -g with an http-level directive ===
nginx: [emerg] "gzip" directive is not allowed here in command line
nginx: configuration file /tmp/u04/good.conf test failed
=== -T dump shows the file, not -g ===
# configuration file /tmp/u04/good.conf:
error_log /tmp/u04/error.log;
pid /tmp/u04/nginx.pid;
-g는 "파일 값을 덮어쓰는" 옵션이 아닙니다. 파일에도worker_processes 1;이 있으면-g 'worker_processes 3;'이 먼저 읽혀 값을 채우고, 그 뒤 파일의 줄이 "is duplicate" 로 실패합니다. 오류는 파일 쪽(sets-one.conf:3)을 가리킵니다. 3절 ③의 순서 그대로입니다.;를 빼면 문자열 끝에 닿았을 때 fd 가NGX_INVALID_FILE이므로 "unexpected end of parameter" 입니다(ngx_conf_read_token()545–549행). 파일이었다면 "unexpected end of file" 이 나왔을 자리입니다.- 블록은 4절의
parse_param검사(288–293행)에서 막힙니다. 그래서-g로는 main 문맥의 디렉티브만 줄 수 있습니다.gzip on;은 HTTP 모듈의 디렉티브라 main 문맥에서는 "not allowed here" 입니다. nginx -T의 덤프에는-g가 나오지 않습니다. 덤프는ngx_conf_parse()가 파일을 열 때만 그 내용을 모아 두기 때문입니다(222–237행의ngx_conf_add_dump()).
7. 옛 cycle 에서 물려받기 — 듣기 소켓과 공유 메모리
reload 를 하면 nginx 는 설정 파일을 처음부터 다시 읽어 cycle 을 새로 만듭니다. 그런데도 80번 포트가 잠깐이라도 닫히지 않고, limit_req_zone 의 카운터나 proxy_cache 의 키 목록도 날아가지 않습니다. ngx_init_cycle() 의 ⑥·⑦ 단계가 옛 cycle 에서 같은 것을 찾아 물려받기 때문입니다.
fd 번호와 zone 이름은 설명을 위한 예입니다. 파일은 물려받지 않고 새로 엽니다.
듣기 소켓: 주소가 같으면 fd 를 넘긴다
nls = cycle->listening.elts;
for (n = 0; n < cycle->listening.nelts; n++) {
for (i = 0; i < old_cycle->listening.nelts; i++) {
/* ... ignore · 이미 remain · 소켓 종류가 다르면 건너뛴다 ... */
if (ngx_cmp_sockaddr(nls[n].sockaddr, nls[n].socklen,
ls[i].sockaddr, ls[i].socklen, 1)
== NGX_OK)
{
nls[n].fd = ls[i].fd;
nls[n].previous = &ls[i];
if (ls[i].protocol != nls[n].protocol) {
nls[n].change_protocol = 1;
} else {
nls[n].inherited = ls[i].inherited;
ls[i].remain = 1;
}
바로 앞(513–517행)에서 옛 소켓의 remain 을 모두 0 으로 지워 두고, 새 설정의 listen 마다 옛 소켓 가운데 주소와 포트, 소켓 종류가 같은 것을 찾아 그 fd 를 그대로 가져오고, 옛 쪽에는 remain = 1(남긴다)을 표시합니다. 그다음 ngx_open_listening_sockets() 는 fd 가 이미 있는 항목을 건너뛰고(ngx_connection.c 500–502행) fd == -1 인 것, 즉 새로 생긴 주소만 socket()·bind()·listen() 합니다. 마지막으로 정리 단계(719–751행)가 remain 이 아닌 옛 소켓, 즉 새 설정에서 사라진 주소만 닫습니다. 같은 포트의 소켓은 한 번도 닫히지 않으므로, 그 소켓의 커널 대기열에 쌓여 있던 접속도 사라지지 않습니다.
공유 메모리: 이름·주인·크기가 같으면 다시 쓴다
공유 메모리 zone 도 같은 생각입니다(440–491행). 새 zone 마다 옛 zone 을 훑어 이름이 같고, tag(보통 그 zone 을 쓰는 모듈 구조체의 주소)가 같고, 크기가 같으면 옛 주소(shm.addr)를 넘겨받고 zone 의 init 콜백에 옛 데이터를 건넵니다(473–487행). 크기가 다르면 새로 잡습니다(493–503행). 그래서 limit_req_zone ... zone=one:10m 을 20m 으로 바꿔 reload 하면 그 zone 은 새로 만들어져 쌓여 있던 상태가 사라집니다. 이 동작은 소스로만 확인했고 예제로 돌려 보지는 않았습니다. 공유 메모리 안의 할당자(ngx_slab.c)는 3편에서 맛만 봤습니다.
헷갈리기 쉬운 곳: nginx -t 도 포트를 열어 봅니다
-t 가 기동과 같은 ngx_init_cycle() 을 끝까지 돈다는 것은, -t 도 ⑦에서 ngx_open_listening_sockets() 를 부른다는 뜻입니다. 그런데 nginx 가 이미 돌고 있는 서버에서 nginx -t 를 하면 포트가 쓰이고 있을 텐데 실패하지 않습니다. 그 사정이 bind() 바로 뒤에 있습니다.
if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {
err = ngx_socket_errno;
if (err != NGX_EADDRINUSE || !ngx_test_config) {
ngx_log_error(NGX_LOG_EMERG, log, err,
"bind() to %V failed", &ls[i].addr_text);
}
/* ... 소켓을 닫는다 ... */
if (err != NGX_EADDRINUSE) {
return NGX_ERROR;
}
if (!ngx_test_config) {
failed = 1;
}
continue;
}
src/core/ngx_connection.c 628–651행
-t 일 때 EADDRINUSE("이미 쓰는 주소")는 로그도 남기지 않고 실패로 치지도 않습니다. 다른 오류(예: 권한이 없어 80번 포트를 못 여는 EACCES)는 -t 에서도 실패입니다. 기동일 때는 EADDRINUSE 를 failed = 1 로 표시하고, 바깥의 for (tries = 5; tries; tries--) 루프(444행)가 500ms 쉬고 다시 시도합니다(724–729행). 다섯 번 다 실패하면 "still could not bind()" 입니다.
예제 4: -t 의 bind(), 포트가 이미 쓰일 때
ex/u04-listen-test 는 listen 127.0.0.1:8080; 하나뿐인 설정으로 세 가지를 봅니다. ① 포트가 비어 있을 때 strace 로 nginx -t 의 시스템 콜, ② python3 의 http.server 가 8080 을 잡고 있을 때 nginx -t, ③ 같은 상황에서 진짜 기동.
=== 1. port 8080 is free: strace nginx -t ===
nginx: the configuration file /work/nginx.conf syntax is ok
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 5
bind(5, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
listen(5, 511) = 0
nginx: configuration file /work/nginx.conf test is successful
=== 2. another process holds 127.0.0.1:8080 ===
nginx: the configuration file /work/nginx.conf syntax is ok
bind(5, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EADDRINUSE (Address already in use)
nginx: configuration file /work/nginx.conf test is successful
=== 3. same situation, but really start nginx (stderr hidden, see error.log) ===
exit code: 1
took 2.5 s
=== error.log ===
2026/11/07 02:59:31 [emerg] 32#32: bind() to 127.0.0.1:8080 failed (98: Address already in use)
2026/11/07 02:59:31 [notice] 32#32: try again to bind() after 500ms
... (같은 두 줄이 세 번 더)
2026/11/07 02:59:31 [emerg] 32#32: bind() to 127.0.0.1:8080 failed (98: Address already in use)
2026/11/07 02:59:31 [notice] 32#32: try again to bind() after 500ms
2026/11/07 02:59:31 [emerg] 32#32: still could not bind()
시각과 pid 는 실행마다 다릅니다. error.log 는 같은 두 줄이 다섯 번 되풀이되어 가운데를 줄였습니다. ③의 설정은 try again 줄(NOTICE 등급)도 보이게 error_log … notice; 로 두었습니다.
-t도socket()·bind()·listen()을 진짜로 부릅니다. 프로세스가 끝나면서 소켓은 닫힙니다.- 포트가 쓰이고 있으면
bind()는EADDRINUSE로 실패하지만-t는 "successful" 입니다. 오류 줄도 없습니다. 돌고 있는 서버 옆에서nginx -t를 해도 되는 이유입니다. 거꾸로 말하면,-t가 성공해도 그 포트로 기동할 수 있다는 보장은 없습니다. - 진짜 기동은 다섯 번 시도하고 약 2.5초 뒤에 끝났습니다. 500ms × 5 번입니다(마지막 시도 뒤에도 한 번 쉬고 나서
failed를 봅니다).
막혔던 곳: ③의 error.log 는 2.5초 동안 찍힌 줄들인데 시각이 모두 02:59:31 로 같습니다. nginx 의 로그는 매번 시계를 읽지 않고, 캐시해 둔 시각 문자열(ngx_cached_err_log_time, src/core/ngx_log.c 117–118행)을 씁니다. 그 캐시는 ngx_time_update() 를 불러야 바뀌는데, 이 재시도 루프는 ngx_msleep()(= usleep(), src/os/unix/ngx_time.h 62행)으로 쉬기만 할 뿐 시각을 갱신하지 않습니다. 시각 캐시는 7편에서 이벤트 루프와 함께 봅니다.
8. reload — 같은 함수를 다시 부른다
이제 reload 를 처음부터 끝까지 이어 볼 수 있습니다. nginx -s reload 를 치면 두 프로세스가 각자 ngx_init_cycle() 을 부릅니다.
설정 오류는 두 번 걸러집니다. 한 번은 시그널러가, 한 번은 마스터가.
마스터 쪽 코드는 시그널을 받은 뒤 깨어난 루프 안에 있습니다.
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");
cycle = ngx_init_cycle(cycle);
if (cycle == NULL) {
cycle = (ngx_cycle_t *) ngx_cycle;
continue;
}
ngx_cycle = cycle;
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
ngx_core_module);
ngx_start_worker_processes(cycle, ccf->worker_processes,
NGX_PROCESS_JUST_RESPAWN);
ngx_start_cache_manager_processes(cycle, 1);
/* allow new processes to start */
ngx_msleep(100);
live = 1;
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
src/os/unix/ngx_process_cycle.c 245–265행
ngx_init_cycle() 이 NULL 이면 전역 ngx_cycle(옛 cycle)을 다시 집어 들고 continue 합니다. 3절에서 본 대로 실패한 ngx_init_cycle() 은 옛 cycle 을 건드리지 않았으므로, 서버는 아무 일 없었던 것처럼 옛 설정으로 계속 돕니다. 성공하면 새 cycle 로 워커를 띄우고, 옛 워커에는 NGX_SHUTDOWN_SIGNAL(SIGQUIT)을 보냅니다. NGX_PROCESS_JUST_RESPAWN 표시와 옛 워커가 연결을 마치고 끝나는 과정은 6편에서 봅니다.
예제 5: 틀린 설정으로 reload 하면
예제 1과 같은 폴더(ex/u04-init-cycle-gdb)의 뒷부분입니다. 정상 reload 로 워커를 한 번 바꾼 뒤, 설정의 events {} 안에 bogus_directive on; 한 줄을 넣고 ① nginx -s reload, ② 시그널러를 거치지 않고 kill -HUP 를 직접 보냈습니다.
=== 1. start the master under gdb (daemon off) ===
master pid: 49 workers: 55 56
=== 2. nginx -s reload, the signaller itself under gdb ===
...
workers after reload: 79 80
=== 3. break the config, then nginx -s reload ===
nginx: [emerg] unknown directive "bogus_directive" in /tmp/u04g/nginx.conf:9
exit code: 1
=== 4. same broken config, but send SIGHUP directly ===
workers: 79 80
curl: hello
2번 단계의 ... 는 예제 1에 실은 gdb 출력입니다. 마스터의 error.log 에서 해당 부분만 옮깁니다.
2026/11/07 02:54:26 [notice] 49#49: signal 1 (SIGHUP) received from 76, reconfiguring
2026/11/07 02:54:26 [notice] 49#49: reconfiguring
2026/11/07 02:54:26 [notice] 49#49: using the "epoll" event method
2026/11/07 02:54:26 [notice] 49#49: start worker processes
2026/11/07 02:54:26 [notice] 49#49: start worker process 79
2026/11/07 02:54:26 [notice] 49#49: start worker process 80
2026/11/07 02:54:26 [notice] 55#55: gracefully shutting down
2026/11/07 02:54:26 [notice] 56#56: gracefully shutting down
...
2026/11/07 02:54:28 [notice] 49#49: signal 1 (SIGHUP) received from 1, reconfiguring
2026/11/07 02:54:28 [notice] 49#49: reconfiguring
2026/11/07 02:54:28 [emerg] 49#49: unknown directive "bogus_directive" in /tmp/u04g/nginx.conf:9
pid 와 시각은 실행마다 다릅니다.
- 정상 reload 의 SIGHUP 은 "received from 76" — 예제 1에서 본 시그널러의 pid 입니다. 워커가 55·56 에서 79·80 으로 바뀌었고 옛 워커는 "gracefully shutting down" 했습니다.
- 틀린 설정에서
nginx -s reload는 exit 1 로 끝났고, 마스터의 로그에는 아무것도 남지 않았습니다. 1절의 "헷갈리기 쉬운 곳" 그대로, 시그널러가 설정을 읽다 실패해 시그널을 보내지 않았기 때문입니다. 마스터에게는 reload 를 하라는 말이 아예 가지 않았습니다. kill -HUP로 직접 보내면 마스터가ngx_init_cycle()을 불러 실패합니다("received from 1" 은 컨테이너에서 pid 1 인run.sh의 셸이 보냈다는 뜻입니다). 로그에 "reconfiguring" 다음 emerg 한 줄이 남고, 워커는 79·80 그대로, curl 은 여전히hello입니다. 245–250행의continue가 옛 cycle 로 계속 돈 것입니다. 이 경우가 예제 1의 세 번째ngx_init_cycle()호출입니다.
정리하면, nginx -s reload 의 종료 코드와 메시지가 첫 번째 안전장치이고, 마스터의 롤백이 두 번째입니다. 시그널러를 거치지 않고 kill -HUP 으로 reload 하면 첫 번째가 없으므로, 실패는 error.log 에만 남습니다.
httpd 와 비교: 설정을 몇 번 읽나
httpd 5편에서 httpd 는 기동할 때 설정을 두 번 읽었습니다(미리 읽어 검사하고, 루프 안에서 다시 읽기). nginx 의 main() 은 ngx_init_cycle() 을 한 번만 부릅니다. 예제 1의 마스터 출력에서 기동 때의 호출이 하나뿐인 것이 그 증거입니다. 재시작을 "설정을 처음부터 다시 읽어 새 세대를 만든다" 로 처리한다는 생각은 둘이 같습니다 — httpd 는 pconf 풀을 비우고 main() 의 루프를 한 바퀴 더 돌고, nginx 는 새 풀에 새 cycle 을 만듭니다. 다른 점은 nginx 가 옛 세대를 지우기 전에 새 세대를 다 만들어 본다는 것입니다. 그래서 새 설정이 틀리면 옛 세대로 계속 돌 수 있습니다. httpd 5편의 "새 설정에 오류가 있으면 graceful 은 서버를 내린다" 와 대조됩니다. 또 httpd 의 -k 처럼 nginx 의 -s 도 설정을 읽은 뒤에 시그널을 보냅니다. 둘 다 pid 파일의 위치가 설정에 있기 때문입니다.
이 글에서 뺀 것
ngx_command_t의set·conf·offset으로 설정 구조체의 칸을 찾는 포인터 계산(NGX_DIRECT_CONF·NGX_MAIN_CONF·cf->ctx + cmd->conf):ngx_conf_file.c451–467행 — 5편http {}블록의 create·merge·postconfiguration:src/http/ngx_http.c의ngx_http_block()(123행부터) — 5편include와 glob(*.conf):ngx_conf_include(),ngx_conf_file.c827–889행types {}처럼 블록 안을 디렉티브가 아닌 자기 방식으로 읽는cf->handler:ngx_conf_parse()298–322행nginx -T덤프를 모으는ngx_conf_add_dump()(105–157행).--with-debug로 빌드하면-T가 아니어도 덤프를 모읍니다(226–228행의#if (NGX_DEBUG) || 1)- 환경 변수 처리(
env디렉티브,ngx_set_environment()):nginx.c542행부터 - 바이너리 교체(USR2)와
NGINX환경 변수로 소켓 물려받기:ngx_exec_new_binary()(nginx.c720행),ngx_set_inherited_sockets()(ngx_connection.c135행) — 6편 - 이 판(1.31.x)에 새로 들어온 Control API(
--with-control-api,-l옵션,src/os/unix/ngx_control.c에서도ngx_init_cycle()을 부릅니다). 1.30.6 의auto/options에는 이 옵션이 없고, 이 글의 빌드에서는 켜지 않았습니다 master_process off일 때 옛 cycle 을 30초마다 치우는ngx_clean_old_cycles():ngx_cycle.c1380행부터
정리
| 개념 | 한 줄 요약 |
|---|---|
main() |
옵션 → 스택 위의 빈 init_cycle → ngx_init_cycle() 한 번 → -t 면 끝, -s 면 시그널, 아니면 데몬·pid 파일·마스터 루프 |
ngx_cycle_t |
설정 한 벌과 그 설정으로 연 자원(소켓·파일·공유 메모리)을 한 풀에 담은 것. conf_ctx[모듈 index] 에 모듈 설정 |
ngx_init_cycle(old) |
옛 cycle 을 받아 새 cycle 을 만든다. 기동(init_cycle)과 reload(돌던 cycle)가 같은 함수 |
| 아홉 단계 | 풀 → create_conf → -g·파일 파싱 → init_conf → 파일 → 공유 메모리 → 소켓 → init_module → 옛 cycle 정리 |
| 실패 | 새로 만든 것만 되돌리고 NULL. 옛 cycle 은 그대로라 reload 실패해도 서버는 계속 돈다 |
ngx_conf_parse() |
파일·블록·-g 를 한 함수로. 블록은 블록 디렉티브 함수가 cf 를 바꾸고 다시 부르는 재귀 |
| 문맥 | cf->module_type + cf->cmd_type. 위쪽 비트는 모듈 종류마다 겹치므로(0x02000000) 둘을 함께 본다 |
ngx_conf_read_token() |
손으로 짠 문자 단위 상태 기계. 4KB 버퍼, 낱말은 NUL 로 끝남, 따옴표·\·#·${ 처리 |
ngx_conf_handler() |
모든 모듈의 디렉티브 표를 차례로 훑는다. found 로 unknown 과 not allowed here 를 가른다 |
-g |
파일보다 먼저, main 문맥에서만, 블록 불가. 파일에 같은 디렉티브가 있으면 duplicate |
| 물려받기 | 같은 주소의 듣기 소켓 fd, 같은 이름·tag·크기의 공유 메모리는 옛 cycle 에서 가져온다 |
nginx -t |
기동과 같은 함수를 끝까지 돈다. 포트도 열어 보되 EADDRINUSE 는 봐준다 |
nginx -s reload |
이것도 설정 전체를 읽는다. 틀리면 시그널을 보내지 않는다 |
다음에 볼 것
이 글에서 "디렉티브의 함수를 부른다" 고만 하고 넘어간 곳이 많았습니다. worker_processes 2; 의 2 가 ngx_core_conf_t.worker_processes 에 들어가기까지 ngx_command_t 의 conf·offset 이 어떻게 쓰이는지, http {} 블록이 모듈마다 main·srv·loc 설정을 만들고 합치는 순서, 그리고 직접 만든 동적 모듈을 load_module 로 싣는 법이 5편: 모듈과 디렉티브 — nginx.conf 한 줄이 모듈의 설정 구조체가 되기까지에서 이어집니다. reload 의 나머지 절반, 즉 마스터가 새 워커를 띄우고 옛 워커가 연결을 마치고 물러나는 과정은 6편: 마스터와 워커 — 프로세스, 시그널, 무중단 reload에서 봅니다.
httpd 와의 비교는 4절과 8절에 적었습니다(httpd 5편, httpd 7편). Tomcat 이 server.xml 을 Digester 로 읽어 객체를 만들고 Lifecycle 의 init()·start() 로 기동하는 과정은 Tomcat 3편에 있습니다.