namalzi

← 글 목록

nginx 소스 파헤치기 4: 기동과 설정 읽기 — main()에서 ngx_init_cycle까지

nginx · 게시

nginx 를 켜면 main() 이 명령줄을 읽고, ngx_init_cycle() 이 nginx.conf 를 읽어 cycle 이라는 설정 한 벌을 만든 뒤에야 마스터 프로세스가 돌기 시작합니다. 설정 파서가 파일·블록·명령줄(-g) 세 경우를 한 함수로 처리하는 법, 한 글자씩 읽어 낱말로 나누는 ngx_conf_read_token(), 오류 메시지가 찍히는 자리, reload 가 같은 ngx_init_cycle() 을 다시 부르며 옛 cycle 에서 듣기 소켓과 공유 메모리를 물려받는 과정을 nginx 1.31.9 소스와 gdb·strace 예제로 따라갑니다.

이 글에서 다루는 것

"nginx 소스 파헤치기" 연재의 네 번째 글입니다. 2편에서 마스터·워커 프로세스, 모듈, cycle, 설정 문맥으로 이루어진 전체 지도를 그렸고, 3편에서 nginx 가 메모리를 다루는 풀(ngx_pool_t)과 기본 자료구조를 봤습니다. 이번에는 nginx 명령을 친 순간부터 마스터 프로세스가 돌기 시작하기 직전까지, 그리고 그 사이에 nginx.conf 가 읽히는 과정을 봅니다.

답하려는 질문은 네 가지입니다.

항목 내용
대상 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" 상자를 확대합니다.

연재 지도에서 이 글의 위치. A 준비 구역에 기동·cycle(4편, 이 글), 모듈·디렉티브(5편), 메모리 풀·자료구조(3편)가 있고, 그 아래 B 프로세스와 이벤트 구역(마스터·워커 6편, 이벤트 루프 7편), 그 아래 C HTTP 요청 하나 구역(요청 읽기 8편, phase·location 9편, 필터·정적 파일 10편, upstream·proxy 11편, 부하 분산·keepalive·stream 12편)이 이어진다. 기동·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() 호출 한 줄이 있고, 그 앞은 "설정을 읽을 준비", 그 뒤는 "읽은 설정으로 무엇을 할지 고르기" 입니다.

main() 의 흐름. ngx_get_options()가 명령줄 옵션을 전역 변수에 담고, -v나 -V만 있으면 버전을 찍고 끝난다. 그렇지 않으면 스택에 init_cycle이라는 빈 cycle을 만들고, ngx_os_init, ngx_add_inherited_sockets, ngx_preinit_modules를 거쳐 ngx_init_cycle(&init_cycle)을 부른다. 실패하면 return 1. 성공하면 세 갈래로 나뉜다: -t나 -T는 test is successful을 찍고 return 0, -s는 ngx_signal_process로 pid 파일의 pid에 kill, 그 밖에는 ngx_init_signals, ngx_daemon, ngx_create_pidfile을 거쳐 ngx_master_process_cycle 또는 ngx_single_process_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;
    }

src/core/nginx.c 253–260행

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() 전에 불리는 것이 셋 더 있습니다.

설정을 읽은 뒤: 세 갈래

    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);
    }

src/core/nginx.c 300–338행

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

ngx_cycle_t 가 들고 있는 것. conf_ctx 는 모듈 index 마다 한 칸으로 모든 디렉티브의 값을, modules 는 정적 모듈과 load_module 로 실은 모듈의 배열을, listening 은 listen 에서 온 ngx_listening_t 배열을, open_files 는 access_log·error_log 같은 열 파일 목록을, shared_memory 는 limit_req_zone·keys_zone=·zone 같은 공유 메모리 zone 목록을, conf_file·conf_param 은 -c 와 -g 로 준 설정 파일 경로와 문자열을, old_cycle 은 이 cycle 을 만들 때 받은 옛 cycle 을 가리킨다. pool 은 cycle 풀로, 위의 것을 전부 여기서 할당하므로 cycle 을 버릴 때 이 풀 하나를 없앤다. 위 표를 구조로 그린 것. 파란 칸 둘째 줄은 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줄이 넘는 함수 하나입니다. 길지만 순서는 곧습니다. 아홉 단계로 나눠 봤습니다.

ngx_init_cycle의 아홉 단계. ① 새 풀, 새 cycle(old_cycle에서 경로·크기를 물려받는다) ② ngx_cycle_modules()와 코어 모듈 create_conf ③ ngx_conf_param() 다음 ngx_conf_parse() — -g 먼저, 그다음 nginx.conf ④ 코어 모듈 init_conf로 기본값. 여기서 -s로 띄운 프로세스는 return한다. ⑤ pid·lock 파일, 경로, 로그 파일 열기 ⑥ 공유 메모리 zone, 옛 zone이 맞으면 다시 쓴다 ⑦ 듣기 소켓 — 옛 fd를 물려받고 ngx_open_listening_sockets() ⑧ ngx_init_modules()로 모듈마다 init_module ⑨ 옛 cycle 정리. ③, ⑤, ⑥, ⑦에서 오류가 나면 새로 만든 것만 되돌리고 NULL을 돌려주며 옛 cycle은 그대로다. 노란 상자는 자원(메모리·파일·소켓)을 마련하는 단계, 파란 상자는 모듈 콜백을 부르는 단계입니다.

① 새 풀, 새 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;

src/core/ngx_cycle.c 67–83행

새 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;
        }
    }

src/core/ngx_cycle.c 233–248행

바로 앞에서 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;

src/core/nginx.c 1162–1168행

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 ... */
    }

src/core/ngx_cycle.c 269–290행

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;
    }

src/core/ngx_cycle.c 297–318행

파일을 다 읽은 뒤에야 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 파일 경로만 알면 되니까요.

⑤ 파일, ⑥ 공유 메모리, ⑦ 듣기 소켓

나머지는 설정이 요구하는 운영체제 자원을 실제로 마련하는 단계입니다.

⑥·⑦에는 공통점이 있습니다. 옛 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);
    }

src/core/ngx_cycle.c 632–652행

/* 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;
    }

src/core/ngx_cycle.c 783–789행

기동 때(옛 cycle 이 init_cycle)와 마스터 프로세스에서는 옛 cycle 의 풀을 바로 없앱니다. 마스터는 클라이언트 연결을 들고 있지 않으므로 옛 설정을 붙들 이유가 없습니다. 옛 설정으로 연결을 처리하던 쪽은 옛 워커이고, 워커는 fork 될 때 옛 cycle 을 통째로 복사해 가졌으므로 마스터가 자기 사본을 없애도 상관없습니다. 그 밖의 경우(master_process off 로 프로세스 하나만 돌릴 때)는 옛 cycle 을 ngx_old_cycles 배열에 넣어 두고 30초 타이머(ngx_clean_old_cycles())가 그 cycle 의 연결이 다 끝났는지 보며 치웁니다(792–830행).

ngx_init_cycle 뒷부분의 갈림길. ⑤ 파일, ⑥ 공유 메모리, ⑦ 듣기 소켓 단계에서 실패하면 goto failed 로 새 cycle 이 새로 연 것만 닫고 새 풀을 없애며, 옛 cycle 은 그대로 살아 있다. 성공하면 /* commit the new cycle configuration */ 주석을 지나 새 cycle 로 확정된다. ⑧ ngx_init_modules() 는 실패하면 되돌리지 않고 exit(1) 한다. ⑨ 옛 cycle 에만 있는 zone 과 듣기 소켓을 닫고 옛 cycle 의 파일은 전부 닫는다. 그다음 마스터 프로세스이거나 옛 cycle 이 기동 때의 init_cycle 이면 옛 cycle 의 풀을 바로 ngx_destroy_pool() 하는데, 워커는 fork 때 받은 자기 사본을 쓰기 때문이다. 아니면(master_process off) 옛 cycle 을 ngx_old_cycles 배열에 넣고 30초 타이머 ngx_clean_old_cycles() 가 연결이 다 끝났는지 보며 치운다. 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 을 보낸 때입니다.

  1. 첫 호출은 main() 의 300행에서, old_cycle->conf_ctx=(nil) — 옛 cycle 은 스택 위의 init_cycle 입니다(주소 0x7fff… 가 스택 영역입니다). 이때 ngx_process 는 아직 0(NGX_PROCESS_SINGLE)입니다. 마스터로 바뀌는 것은 설정을 읽은 뒤인 main() 346–348행이기 때문입니다.
  2. 두 번째 호출은 ngx_master_process_cycle() 의 247행에서 — reload 입니다. 이번 옛 cycle 은 힙 주소의 진짜 cycle 이고 conf_ctx 가 채워져 있습니다. 같은 함수가 "옛 것을 받아 새 것을 만든다" 는 모양 하나로 기동과 reload 를 다 맡습니다.
  3. 세 번째 호출의 옛 cycle(0x555555788130)은 두 번째 호출이 만든 cycle 입니다. 두 번째 호출의 옛 cycle(0x555555784120)과 주소가 다릅니다. reload 할 때마다 cycle 이 한 세대씩 바뀝니다.
  4. 시그널러(pid 76)도 ngx_init_cycle() 을 부릅니다. ngx_process=2(NGX_PROCESS_SIGNALLER)라서 ④의 출구에서 돌아갑니다.

예제 1의 출력을 시간 순서로 그린 그림. 마스터(pid 49)의 호출 1은 main() 300행에서 ngx_process=0 으로 일어나고, 옛 cycle 은 스택 위의 init_cycle(0x7fffffffe930, conf_ctx nil)이며 새 cycle 0x555555784120 을 만든다. 시그널러 nginx -s reload(pid 76)도 main() 300행에서 ngx_process=2 로 ngx_init_cycle 을 부르는데, 옛 cycle 은 자기 스택 위의 init_cycle(0x7fffffffe940, conf_ctx nil)이고 ④의 출구에서 돌아간 뒤 마스터에게 SIGHUP 을 보낸다. 그러면 마스터의 호출 2가 ngx_master_process_cycle() 247행에서 ngx_process=1 로 일어나 옛 cycle 0x555555784120 에서 새 cycle 0x555555788130 을 만든다. 8절에서 틀린 설정으로 kill -HUP 을 보내자 호출 3이 247행에서 옛 cycle 0x555555788130 을 받아 unknown directive 로 실패하고, 옛 cycle 이 계속 돈다. 위 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절). 끝 표시마다 할 일이 정해져 있습니다.

블록 안으로 들어가는 법 — 재귀

그럼 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행

  1. 지금의 파서 상태 *cf 를 지역 변수 pcf 에 통째로 복사해 둡니다.
  2. 문맥을 바꿉니다 — 설정을 채울 곳(ctx)은 이벤트 모듈들의 설정 배열로, 모듈 종류는 NGX_EVENT_MODULE, 문맥 비트는 NGX_EVENT_CONF 로.
  3. ngx_conf_parse(cf, NULL) — 파일 이름이 없고 지금 읽던 파일이 열려 있으므로 parse_block 입니다. 같은 파일의 같은 버퍼를 이어서 읽다가 짝이 맞는 } 에서 돌아옵니다.
  4. *cf = pcf 로 원래 문맥을 되돌립니다. 다음 디렉티브는 다시 main 문맥에서 읽힙니다.

블록을 읽는 재귀. ngx_init_cycle이 파일 이름과 cmd_type=MAIN, module_type=CORE로 ngx_conf_parse를 부른다. ngx_conf_read_token이 worker_processes 1과 NGX_OK를 돌려주면 ngx_conf_handler가 디렉티브를 찾아 set을 부르고 돌아온다. 다음에 events와 NGX_CONF_BLOCK_START가 오면 ngx_conf_handler가 ngx_events_block을 부르고, 이 함수는 pcf에 cf를 저장하고 ctx와 종류를 EVENT로 바꾼 뒤 ngx_conf_parse(NULL)를 부른다. 이 안쪽 파서는 같은 파일을 이어 읽다가 worker_connections 64를 처리하고 }에서 NGX_CONF_BLOCK_DONE을 받아 돌아온다. ngx_events_block은 cf를 되돌리고 init_conf를 부른 뒤 돌아가고, 바깥 파서는 다시 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, { 로 끝남).

  1. 첫 줄은 -g 로 준 daemon off, 줄 번호 0 — 파일보다 먼저 읽힙니다(3절 ③). ngx_conf_param() 이 가짜 파일의 line 을 0 으로 둡니다(91행).
  2. 블록 디렉티브(events, http, server, location)는 last=1 이고, 그 다음 줄부터 문맥이 바뀝니다 — CORE/01000000 → EVNT/02000000, HTTP/02000000 → 04000000 → 08000000. 4절에서 본 재귀가 문맥을 바꾸는 모습입니다. location / 의 블록이 끝난 뒤 location /v 는 다시 04000000(server 문맥)으로 읽혔습니다.
  3. module_type 이 글자로 읽힙니다. 모듈 종류 상수가 네 글자를 정수로 만든 값이라서입니다 — NGX_CORE_MODULE 은 0x45524F43 /* "CORE" */(ngx_conf_file.h 70행). 리틀 엔디언 기계에서 메모리에 놓인 바이트를 순서대로 읽으면 "CORE" 가 됩니다.
  4. 따옴표 안의 공백과 ; 는 낱말을 끊지 않고, 따옴표 자신은 빠지며, \" 는 " 가 됩니다 — <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}

ngx_conf_read_token의 상태. 처음은 낱말 사이(last_space=1) 상태다. 공백과 줄바꿈은 그대로 낱말 사이에 머문다. #이면 주석(sharp_comment=1)으로 가서 줄바꿈에서 돌아온다. 보통 글자나 $, \이면 낱말 안으로 간다. 큰따옴표나 작은따옴표면 따옴표 안으로 간다. 낱말 안에서 공백을 만나면 낱말 하나가 끝나고 낱말 사이로 돌아간다. 따옴표 안에서 같은 따옴표를 만나면 낱말 하나가 끝나고 닫는 따옴표 뒤(need_space=1) 상태가 되며, 여기서 공백을 만나면 낱말 사이로 간다. 낱말 안이나 닫는 따옴표 뒤에서 ; 또는 {를 만나면 반환하고, 낱말 사이에서 ;, {, }를 만나도 반환한다. 주요 흐름만 그렸습니다. \ 뒤의 한 글자, ${ 의 예외는 본문을 보세요.

새 낱말이 시작되는 자리에서의 판단이 함수의 한가운데입니다.

            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행

낱말이 끝나면(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 가 디렉티브 하나를 처리하는 순서와 오류가 갈리는 곳. 낱말 배열의 첫 낱말을 디렉티브 이름으로 삼아 모든 모듈의 commands 표를 처음부터 훑는다. 이름이 같은 ngx_command_t 가 끝까지 없으면(found = 0) unknown directive 오류다(예제 3의 unknown). 있으면 found = 1 로 두고 모듈 종류와 문맥 비트(cf->module_type, cf->cmd_type)가 맞는지 보며, 맞지 않으면 다음 후보로 간다. 끝까지 맞는 것이 없고 found 가 1 이면 directive is not allowed here 오류다(예제 3의 not-allowed). 맞으면 끝 표시를 본다. 블록은 {, 나머지는 ; 이어야 하고, 틀리면 is not terminated by ";" 또는 has no opening "{" 오류다(예제 3의 no-semicolon). 다음으로 인자 개수가 type 비트와 맞지 않으면 invalid number of arguments 오류다(예제 3의 arg-count). 모두 맞으면 설정 구조체를 찾아 cmd->set(cf, cmd, conf) 를 부르고, set 이 문자열을 돌려주면 "X" directive 뒤에 그 문자열을 붙인 오류가 된다(예제 3의 duplicate). NGX_CONF_OK 이면 다음 디렉티브로 간다. 검사 순서대로 그린 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 {
  1. not-allowed — worker_processes 라는 이름은 ngx_core_module 에 있으니 found = 1 이 됩니다. 하지만 http {} 안에서는 cf->module_type 이 NGX_HTTP_MODULE 이라 코어 모듈의 디렉티브는 건너뛰고, 끝까지 맞는 것이 없어 "not allowed here" 입니다.
  2. no-semicolon — 오류가 4행을 가리킵니다. ; 가 없으니 토큰 읽기는 줄을 넘어 events 까지 같은 디렉티브의 낱말로 모으고, { 에서 NGX_CONF_BLOCK_START 를 돌려줍니다. 낱말은 worker_processes, 2, events 셋이 되었고, 블록이 아닌 디렉티브가 { 로 끝났으니 "not terminated by ";"" 입니다. 인자 개수 검사(421행~)보다 끝 표시 검사(405행)가 먼저라서 "invalid number of arguments" 가 아니라 이 메시지가 나옵니다.
  3. duplicate·bad-flag 는 디렉티브 함수(set)가 돌려준 문자열입니다. "is duplicate" 는 ngx_set_worker_processes() 가 필드가 이미 NGX_CONF_UNSET 이 아니면 돌려주고(nginx.c 1631–1633행), daemon 의 메시지는 표준 setter ngx_conf_set_flag_slot() 이 직접 찍습니다(ngx_conf_file.c 1055–1058행).
  4. 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;
  1. -g 는 "파일 값을 덮어쓰는" 옵션이 아닙니다. 파일에도 worker_processes 1; 이 있으면 -g 'worker_processes 3;' 이 먼저 읽혀 값을 채우고, 그 뒤 파일의 줄이 "is duplicate" 로 실패합니다. 오류는 파일 쪽(sets-one.conf:3)을 가리킵니다. 3절 ③의 순서 그대로입니다.
  2. ; 를 빼면 문자열 끝에 닿았을 때 fd 가 NGX_INVALID_FILE 이므로 "unexpected end of parameter" 입니다(ngx_conf_read_token() 545–549행). 파일이었다면 "unexpected end of file" 이 나왔을 자리입니다.
  3. 블록은 4절의 parse_param 검사(288–293행)에서 막힙니다. 그래서 -g 로는 main 문맥의 디렉티브만 줄 수 있습니다. gzip on; 은 HTTP 모듈의 디렉티브라 main 문맥에서는 "not allowed here" 입니다.
  4. 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 에서 같은 것을 찾아 물려받기 때문입니다.

옛 cycle과 새 cycle. 옛 cycle에는 listening 배열(127.0.0.1:8080, fd=6), shared_memory(zone one 10m), open_files(access.log, fd=4)가 있다. 새 cycle을 만들 때 listening 배열의 127.0.0.1:8080은 fd=-1로 시작하는데, 주소가 같으면 옛 fd를 넘겨받고 옛 쪽에 remain=1을 표시한다. shared_memory의 zone one 10m은 이름, tag, 크기가 같으면 shm.addr를 넘겨받는다. open_files의 access.log는 새로 open한다. remain이 아닌 옛 소켓, 안 쓰는 zone, 옛 파일은 닫는다. 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;
                    }

src/core/ngx_cycle.c 519–548행

바로 앞(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; 로 두었습니다.

  1. -t 도 socket()·bind()·listen() 을 진짜로 부릅니다. 프로세스가 끝나면서 소켓은 닫힙니다.
  2. 포트가 쓰이고 있으면 bind() 는 EADDRINUSE 로 실패하지만 -t 는 "successful" 입니다. 오류 줄도 없습니다. 돌고 있는 서버 옆에서 nginx -t 를 해도 되는 이유입니다. 거꾸로 말하면, -t 가 성공해도 그 포트로 기동할 수 있다는 보장은 없습니다.
  3. 진짜 기동은 다섯 번 시도하고 약 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() 을 부릅니다.

reload 의 순서. 사람이 nginx -s reload를 치면 새 프로세스가 ngx_init_cycle()로 nginx.conf 전체를 읽는다. 설정에 오류가 있으면 [emerg]를 찍고 exit 1로 끝나며 시그널을 보내지 않는다. 문법이 맞으면 pid 파일의 pid에 SIGHUP을 보낸다. 마스터는 ngx_init_cycle(cycle)로 새 cycle을 만든다. 새 cycle이 NULL이면 error.log에 emerg를 남기고 옛 cycle로 계속 돈다. 성공하면 ngx_start_worker_processes()로 새 워커를 띄우고 옛 워커에 SIGQUIT을 보내 하던 연결을 마치고 끝나게 한다. 설정 오류는 두 번 걸러집니다. 한 번은 시그널러가, 한 번은 마스터가.

마스터 쪽 코드는 시그널을 받은 뒤 깨어난 루프 안에 있습니다.

            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 와 시각은 실행마다 다릅니다.

  1. 정상 reload 의 SIGHUP 은 "received from 76" — 예제 1에서 본 시그널러의 pid 입니다. 워커가 55·56 에서 79·80 으로 바뀌었고 옛 워커는 "gracefully shutting down" 했습니다.
  2. 틀린 설정에서 nginx -s reload 는 exit 1 로 끝났고, 마스터의 로그에는 아무것도 남지 않았습니다. 1절의 "헷갈리기 쉬운 곳" 그대로, 시그널러가 설정을 읽다 실패해 시그널을 보내지 않았기 때문입니다. 마스터에게는 reload 를 하라는 말이 아예 가지 않았습니다.
  3. 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 파일의 위치가 설정에 있기 때문입니다.

이 글에서 뺀 것

정리

개념 한 줄 요약
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편에 있습니다.

다른 글