namalzi

← All posts

Digging into Apache httpd's Source 3: The Big Picture — Core, APR, MPM, Modules and the Three Big Structs

Apache httpd · Published

A map for anyone opening the Apache httpd source for the first time. We look at the layers (operating system, APR, core, MPM, modules), why even the core is a module, how modules compiled into the binary differ from ones loaded with LoadModule, the three structs server_rec, conn_rec and request_rec, the pool hierarchy, and the path a single request takes — all checked against the real source and examples I actually ran.

What this post covers

This is the third post in "Digging into Apache httpd's Source". In Part 1 we fetched and built the source and unfolded a map of its folders, and in Part 2 we got comfortable with the structs, function pointers and macros you need to read a large C program like httpd. This post is the big-picture map you spread out before reading the code line by line.

Open the httpd repository and you find hundreds of C files in folders such as server/, modules/, include/ and os/. At first you can't tell where the center is or what calls what. This post answers these questions:

Item Details
Target Apache HTTP Server (httpd)
Version 2.5.1-dev (trunk)
Commit e4af30fe6d (2026-10-24)
APR APR 1.7.2 (commit e905526629)
Main files include/httpd.h, include/http_config.h, server/core.c, server/config.c, modules/http/http_core.c, modules/core/mod_so.c

All line numbers in this post refer to this commit. Part 1 shows how to get the same commit and build the Docker image used for the examples.

Since this post draws a map, it follows only the main lines. Each concept points ahead to the post that reads it in detail ("we'll see this in Part N").

Terms to know first

A module is a bundle of code that adds one feature to httpd. Names start with mod_: mod_mime (decides the Content-Type from the file extension), mod_dir (DirectoryIndex), mod_proxy (reverse proxy). In the source, a module is represented by a single C struct called module.

A DSO (Dynamic Shared Object) is a library file that can be loaded while a program is running. On Linux its extension is .so. One LoadModule line in httpd.conf loads one such .so file into the server process.

APR (Apache Portable Runtime) is a C library that wraps features that differ between operating systems (memory, files, sockets, threads, loading shared libraries, and so on) behind the same function names. It is developed and installed separately from httpd. Its functions start with apr_. Features such as buckets and hooks live in its companion library, APR-util.

An MPM (Multi-Processing Module) decides how many processes and threads to create and who handles each incoming connection. event, worker and prefork are the best known. As the name says, it is a module too.

A pool is APR's way of managing memory. Instead of calling free() on each piece of memory, you take memory from a pool and later destroy the whole pool at once. httpd ties lifetimes together this way: one pool per request, one pool per connection. Part 4 covers pools in detail.

A hook is a spot the core leaves open, saying "I'll accept functions that want to step in at this point." Modules register their functions on hooks, and when that point comes the core calls the registered functions in order. Part 6 shows how hooks are registered. If C function pointers and structs feel unfamiliar, Part 2 is a good place to start.

1. Five layers on one page

Split into layers, httpd looks like this.

The five layers of httpd. Modules and the MPM sit side by side at the top, and both register functions on the core's hooks. Below the core are APR and APR-util, and below them the operating system. Modules and the MPM also use APR directly. An arrow means "the upper box uses the lower one." The core, with the thick border, sits in the middle, defining hooks and calling them.

Besides these, include/ holds the headers every layer shares (httpd.h, http_config.h, etc.), and support/ holds tools such as ab and htpasswd. Part 1 has a more detailed folder map.

What APR does: hiding OS differences behind one function

One piece of code shows nicely why APR exists. When LoadModule loads a .so file, the function it ends up calling is APR's apr_dso_load().

APR_DECLARE(apr_status_t) apr_dso_load(apr_dso_handle_t **res_handle, 
                                       const char *path, apr_pool_t *pool)
{
#if defined(DSO_USE_SHL)
    shl_t os_handle = shl_load(path, BIND_IMMEDIATE, 0L);

#elif defined(DSO_USE_DYLD)
    /* ... */
#elif defined(DSO_USE_DLFCN)
    /* ... */
    os_handle = dlopen(path, flags);
#endif    
#endif /* DSO_USE_x */

dso/unix/dso.c lines 80–141 (APR 1.7.2)

#if defined(...), #elif and #endif are C preprocessor directives: before compiling, only the lines whose condition holds are kept. The same function contains code for HP-UX (shl_load), old macOS (NSLinkModule) and most Unix systems including Linux (dlopen), and only the branch for the OS being built survives. The Windows version lives in a different file entirely (dso/win32/dso.c, using LoadLibraryExW). Whatever the OS, httpd only has to call apr_dso_load().

You can also see for yourself that APR is a separate library and not part of the httpd binary. In the example container I used ldd to list the shared libraries the httpd binary depends on.

### 0. httpd 실행 파일이 기대는 공유 라이브러리 (APR 줄만)
	libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0
	libaprutil-1.so.0 => /lib/x86_64-linux-gnu/libaprutil-1.so.0
	libapr-1.so.0 => /lib/x86_64-linux-gnu/libapr-1.so.0

All examples in this post were run in a Docker container (Ubuntu 24.04) with httpd 2.5.1-dev built unchanged from the reference commit (e4af30fe6d). Everything is in a single script, ex/u03-architecture/run.sh, and the ### lines in the output are headings the script prints. Those headings are in Korean because the script is shared with the Korean edition; the first one here reads "shared libraries the httpd binary depends on (APR lines only)," and I translate each one as we go.

libapr-1 and libaprutil-1 are APR installed from Ubuntu packages. The regular-expression library PCRE2 is separate too.

Easy to get confused: the arrows don't all point one way

The layer diagram is drawn as "upper uses lower," but between the core and the MPM or modules, calls go both ways. Modules call functions the core provides (starting with ap_), and the core calls module functions through hooks. The core doesn't know any module by name; it only knows "the functions registered on this hook." That's why you can remove or add modules without recompiling the core. The same goes for the MPM: at the end, main() merely runs a hook called ap_run_mpm(), and the config file decides which MPM is registered on it (section 3).

2. The core is a module too

One thing that surprises people reading the httpd source: even the part we call "the core" is registered with the very same module struct as every other module.

One module = one module struct

Every module has one instance of this struct, defined in include/http_config.h.

struct module_struct {
    /* ... */
    int version;
    /* ... */
    int module_index;

    /* ... */
    const char *name;
    /* ... */
    unsigned long magic;

    /* ... */
    void *(*create_dir_config) (apr_pool_t *p, char *dir);
    /* ... */
    void *(*create_server_config) (apr_pool_t *p, server_rec *s);
    /* ... */
    const command_rec *cmds;

    /* ... */
    void (*register_hooks) (apr_pool_t *p);
    /* ... */
};

include/http_config.h lines 348–424

I kept only the fields this post needs.

Field Meaning More in
module_index A number given to each module, used to find "my slot" in config arrays (section 4) Part 6
name The name of the C file that defines the module (__FILE__) this section
magic A marker value meaning "this really is an httpd module struct" section 3
create_dir_config, create_server_config, etc. Functions that create and merge this module's config structs Part 7
cmds The list of directives this module handles Part 7
register_hooks The function that registers which hooks this module uses Part 6

A field written with (* before its name, like void *(*create_dir_config) (...), is a function pointer: a slot that holds the address of a function. Each module puts its own function in the slot, and the core calls that function through the slot. It is C's way of doing what you'd do in Java by implementing an interface method (Part 2).

The core's module: core_module

The core's module struct sits at the very end of server/core.c.

AP_DECLARE_MODULE(core) = {
    MPM20_MODULE_STUFF,
    AP_PLATFORM_REWRITE_ARGS_HOOK, /* hook to run before apache parses args */
    create_core_dir_config,       /* create per-directory config structure */
    merge_core_dir_configs,       /* merge per-directory config structures */
    create_core_server_config,    /* create per-server config structure */
    merge_core_server_configs,    /* merge per-server config structures */
    core_cmds,                    /* command apr_table_t */
    register_hooks                /* register hooks */
};

server/core.c lines 6216–6225

AP_DECLARE_MODULE(core) is a macro that declares a variable named module core_module (http_config.h lines 467–469). MPM20_MODULE_STUFF is a macro that fills in the leading fields in one go: version, module_index = -1, name = __FILE__, magic and so on (http_config.h lines 499–505). After that come the functions that create directory and server configs, the directive table core_cmds (basic directives such as DocumentRoot, <Directory> and <VirtualHost>), and register_hooks.

Open the core's register_hooks and you see the core registering functions on hooks just like any other module. A few lines:

static void register_hooks(apr_pool_t *p)
{
    /* ... */
    ap_hook_create_connection(core_create_conn, NULL, NULL,
                              APR_HOOK_REALLY_LAST);
    /* ... */
    ap_hook_translate_name(ap_core_translate,NULL,NULL,APR_HOOK_REALLY_LAST);
    /* ... */
    ap_hook_handler(core_upgrade_handler,NULL,NULL,APR_HOOK_REALLY_FIRST);
    ap_hook_handler(default_handler,NULL,NULL,APR_HOOK_REALLY_LAST);
    /* ... */
}

server/core.c lines 6141–6214

All three are registered with APR_HOOK_REALLY_LAST, meaning "run at the very end." If another module handles the job first, its result wins; only when nobody else does does the core apply its default behavior. The core gets no special treatment; it behaves as one module that supplies the defaults.

HTTP is a module too: http_module

Even more surprising: HTTP protocol handling is not part of the core either, but a separate module. Its file isn't even under server/; it's in modules/http/.

AP_DECLARE_MODULE(http) = {
    STANDARD20_MODULE_STUFF,
    NULL,              /* create per-directory config structure */
    NULL,              /* merge per-directory config structures */
    NULL,              /* create per-server config structure */
    NULL,              /* merge per-server config structures */
    http_cmds,         /* command apr_table_t */
    register_hooks     /* register hooks */
};

modules/http/http_core.c lines 396–404

This module's register_hooks uses ap_hook_process_connection(ap_process_http_connection, ...) to register "the function that reads and handles HTTP/1.x requests when a connection arrives" (http_core.c lines 355–359). Filters such as HTTP_HEADER (writes the response headers) and CHUNK (chunked encoding) are registered by this module too. The core only knows "the hook that processes a connection"; whether that connection speaks HTTP is up to this module. That's why a module speaking a different protocol can step in on the same hook.

Other "core-like" modules

Module variable File What it does How it's loaded
core_module server/core.c Basic directives, default hooks (creating connections, mapping file paths, the static file handler), core filters Compiled into the binary
http_module modules/http/http_core.c HTTP/1.x connection handling, HTTP filters Compiled into the binary
so_module modules/core/mod_so.c The LoadModule and LoadFile directives Compiled into the binary
mpm_event_module server/mpm/event/event.c Managing processes and threads, accepting connections LoadModule in this build
unixd_module modules/arch/unix/mod_unixd.c Dropping privileges with User and Group LoadModule

The MPM's module struct has exactly the same shape.

AP_DECLARE_MODULE(mpm_event) = {
    MPM20_MODULE_STUFF,
    NULL,                       /* hook to run before apache parses args */
    NULL,                       /* create per-directory config structure */
    NULL,                       /* merge per-directory config structures */
    NULL,                       /* create per-server config structure */
    NULL,                       /* merge per-server config structures */
    event_cmds,                 /* command apr_table_t */
    event_hooks                 /* register_hooks */
};

server/mpm/event/event.c lines 4535–4544

Inside event_hooks, ap_hook_mpm(event_run, ...) registers "the function that runs the server's main loop" on the mpm hook (event.c line 4381). Directives such as StartServers and ThreadsPerChild are in event_cmds. We read the MPM itself in Part 8.

Every module is one module struct of the same shape, and a *_MODULE_STUFF macro fills its front slots (version, module_index, name, magic). core_module in server/core.c (built into the binary) fills in four config functions and core_cmds (DocumentRoot, Directory and so on), and its register_hooks hangs core_create_conn on the create_connection hook, ap_core_translate on translate_name and default_handler on handler, all with APR_HOOK_REALLY_LAST. http_module in modules/http/http_core.c (built into the binary) has all four config functions NULL and has http_cmds; its register_hooks hangs ap_process_http_connection on the process_connection hook and registers the HTTP_HEADER and CHUNK filters. mpm_event_module in server/mpm/event/event.c (loaded with LoadModule) has all four config functions NULL and has event_cmds (StartServers and so on); its event_hooks hangs the main loop event_run on the mpm hook The core, HTTP and the MPM are each one struct cut from the same template. Yellow boxes are values written into the struct; blue boxes are functions its register_hooks hangs on hooks.

Easy to get confused

3. Modules compiled into the binary vs. modules loaded with LoadModule

There are two ways to put a module into the server: link it statically into the binary at build time, or load a .so file while running (shared, DSO).

The two ways a module enters the server. The ap_prelinked_modules array (core, so, http) in modules.c, generated by configure at build time, is registered first by ap_setup_prelinked_modules. Then ap_read_config reads the config file, and for each LoadModule line mod_so loads the .so with apr_dso_load and apr_dso_sym and adds it with ap_add_loaded_module. After the config is read, ap_check_mpm checks that exactly one function is registered on the mpm hook. The lower-left branch is how one LoadModule line is processed; the right branch is the MPM check. The red box is a config error that keeps the server from starting.

A file the build generates: modules.c

The list of statically linked modules lives in a file that isn't in the repository: modules.c. When you run ./configure, build/build-modules-c.awk generates it (configure.in lines 1118–1122). I opened /usr/src/httpd/modules.c in the container that built the example image (it's a build artifact, so there's no GitHub link).

/*
 * modules.c --- automatically generated by Apache
 * configuration script.  DO NOT HAND EDIT!!!!!
 */
/* ... */
extern module core_module;
extern module so_module;
extern module http_module;
/* ... */
AP_DECLARE_DATA module *ap_prelinked_modules[] = {
  &core_module,
  &so_module,
  &http_module,
  NULL
};

Further down the same file there is also ap_preloaded_modules[], holding the same three modules. The example image was built with --enable-mods-shared=most --enable-mpms-shared=all, so every other module and MPM became a .so, and only these three are compiled into the binary. There are reasons these three remain.

At startup: the linked modules are registered first

main() calls ap_setup_prelinked_modules() before it even reads the config file (server/main.c line 574).

AP_DECLARE(const char *) ap_setup_prelinked_modules(process_rec *process)
{
    /* ... */
    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;
    /* ... */

    /*
     *   Initialize chain of linked (=activate) 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();
    /* ... */
}

server/config.c lines 763–816

It numbers the linked modules 0, 1, 2 as their module_index, and reserves DYNAMIC_MODULE_LIMIT more slots (256 by default, include/httpd.h lines 119–121) for modules that LoadModule will bring in later. Then it adds the three to the list of active modules with ap_add_module(). At this point only core, so and http are alive.

While reading the config: one LoadModule line

When the config file contains LoadModule, mod_so's load_module() is called, because the LoadModule directive itself is in mod_so's directive table (mod_so.c lines 448–450). Here are the essential lines:

    error = dso_load(cmd, &modhandle, filename, &module_file);
    /* ... */
    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 "
                            /* ... */);
    }

    /* ... */
    error = ap_add_loaded_module(modp, cmd->pool, modname);

modules/core/mod_so.c lines 265–304

  1. dso_load() loads the .so file into the process using apr_dso_load(), which we saw in section 1.
  2. apr_dso_sym() looks inside that file for a variable named after LoadModule's first argument (for example mime_module). That variable is the module's module struct — the very variable AP_DECLARE_MODULE(mime) created.
  3. It checks the magic value. This filters out the wrong kind of file, or a module compiled for a different version.
  4. ap_add_loaded_module() adds it to the module list. From now on this module is treated exactly like core.

Exactly one MPM

After reading the whole config file, ap_read_config() calls ap_check_mpm() (config.c lines 2355–2360).

AP_DECLARE(const char *)ap_check_mpm(void)
{
    static const char *last_mpm_name = NULL;

    if (!_hooks.link_mpm || _hooks.link_mpm->nelts == 0)
        return "No MPM loaded.";
    else if (_hooks.link_mpm->nelts > 1)
        return "More than one MPM loaded.";

server/mpm_common.c lines 656–663

It doesn't check "is an MPM loaded?" by name, but by how many functions are registered on the mpm hook (nelts). In other words, an MPM is defined as "a module that registers a function on the mpm hook."

Example: httpd -l, -M, -V

I ran the three options in the example container. -M and -V read the default config file, /opt/httpd/conf/httpd.conf. The headings say "statically linked modules in the binary," "with the default httpd.conf, first 14 lines," and "default httpd.conf, first 9 lines."

### 1. httpd -l  (실행 파일 안에 정적으로 묶인 모듈)
Compiled in modules:
  core.c
  mod_so.c
  http_core.c

### 2. httpd -M  (기본 httpd.conf 로 실었을 때, 앞 14줄)
Loaded Modules:
 core_module (static)
 so_module (static)
 http_module (static)
 mpm_event_module (shared)
 autht_core_module (shared)
 authn_file_module (shared)
 authn_core_module (shared)
 authz_host_module (shared)
 authz_groupfile_module (shared)
 authz_user_module (shared)
 authz_core_module (shared)
 access_compat_module (shared)
 auth_basic_module (shared)
...
static: 3
shared: 24
.so files in /opt/httpd/modules: 101

### 3. httpd -V  (기본 httpd.conf, 앞 9줄)
Server version: Apache/2.5.1-dev (Unix)
Server built:   Oct 24 2026 11:41:11
Server's Module Magic Number: 20211221:35
Server loaded:  APR 1.7.2, APR-UTIL 1.6.3, PCRE 10.42 2022-12-11
Compiled using: APR 1.7.2, APR-UTIL 1.6.3, PCRE 10.42 2022-12-11
Architecture:   64-bit
Server MPM:     event
  threaded:     yes (fixed thread count)
    forked:     yes (variable process count)

Easy to get confused: why -V reads the config file

httpd -V shows build information, so you'd think the config file doesn't matter. But if the MPM was built as a .so, httpd has to read the config file to learn the MPM's name. The source says so:

        case 'V':
            if (strcmp(ap_show_mpm(), "")) { /* MPM built-in? */
                show_compile_settings();
                destroy_and_exit_process(process, 0);
            }
            else {
                showcompile = 1;
                ap_run_mode = AP_SQ_RM_CONFIG_DUMP;
            }
            break;

server/main.c lines 679–688

If the MPM is compiled into the binary, it prints and exits right away; otherwise it just sets showcompile and prints after reading the config. So with a config file that loads no MPM, you get this (heading: "httpd -V / -t with a config that loads no MPM"):

### 4. MPM 을 싣지 않은 설정으로 httpd -V / -t
AH00534: httpd: Configuration error: No MPM loaded.
Server MPM:     
AH00534: httpd: Configuration error: No MPM loaded.
(exit code 1)

-V prints the error but still shows the build information, with the Server MPM: field left empty. The syntax check -t stops with the same error and exit code 1. It's worth remembering that what httpd -V reports as the MPM on a live server depends on which config file it read.

4. The three big structs: server_rec, conn_rec, request_rec

Most functions in the httpd source take one of these three as their first argument. All three are in include/httpd.h.

Struct Represents one... Created / destroyed Memory (pool)
server_rec server: one main server plus one per <VirtualHost> Created when the config is read, gone on restart pconf
conn_rec TCP connection with a client Created when a connection is accepted, gone when it closes connection pool c->pool
request_rec HTTP request Created when the request line is read, gone once the response is fully sent request pool r->pool

On a keep-alive connection (one that stays open after a response and waits for the next request — KeepAlive On in httpd.conf) several requests travel over one connection, so one conn_rec sees several request_recs come and go in turn. A server_rec lives much longer than either.

How the three structs point to each other. request_rec points to conn_rec through r->connection and to server_rec through r->server. The next request on the same connection points to the same conn_rec. conn_rec points to server_rec through c->base_server. The main server's server_rec (ap_server_conf) links to the virtual hosts' server_recs through next, and every server_rec points to process_rec through process. Arrows are pointer fields inside the structs. There are paths from request to connection and from connection to server, but no field going back the other way appears in this picture.

request_rec: one request

The first fields of the struct are the paths to the other structs.

struct request_rec {
    /** The pool associated with the request */
    apr_pool_t *pool;
    /** The connection to the client */
    conn_rec *connection;
    /** The virtual host for this request */
    server_rec *server;

    /** Pointer to the redirected request if this is an external redirect */
    request_rec *next;
    /** Pointer to the previous request if this is an internal redirect */
    request_rec *prev;

    /** Pointer to the main request if this is a sub-request
     * (see http_request.h) */
    request_rec *main;

include/httpd.h lines 866–881

The whole struct runs to line 1169, more than 300 lines. Here are the fields you'll meet most often on a first read.

Field Meaning Line
pool, connection, server Request pool, connection, the server handling this request 868–872
next, prev, main The following and preceding requests in a redirect chain; for a subrequest (the server processing another URL internally while handling this request, Part 10), the original (main) request 875–881
the_request The first line of the request as received (e.g. GET /three HTTP/1.1) 887
method, protocol, hostname Method, protocol, host name taken from Host 921, 900, 904
status Response status code 912
headers_in, headers_out, err_headers_out Request headers, response headers, headers sent even with error responses 997–1002
notes, subprocess_env Notes passed between modules, environment variables for CGI and the like 1004–1006
content_type, handler Kind of response, name of the handler that will process the request 1013–1015
user Authenticated user name 1026
unparsed_uri, uri, filename, path_info, args Raw URL, path, file path on disk, PATH_INFO, query string 1035–1047
per_dir_config, request_config Directory config that applies to this request, per-request module notes 1068–1070
output_filters, input_filters This request's output and input filter chains 1091–1093

How uri becomes filename is in Part 10, how handler picks a handler in Part 12, and filter chains in Part 11.

conn_rec: one connection

struct conn_rec {
    /** Pool associated with this connection */
    apr_pool_t *pool;
    /** Physical vhost this conn came in on */
    server_rec *base_server;
    /** used by http_vhost.c */
    void *vhost_lookup_data;

include/httpd.h lines 1203–1209

Field Meaning Line
pool, base_server Connection pool, the server chosen by the IP and port the connection came in on 1205–1207
client_ip, local_ip The peer's IP (the next hop), the server-side IP 1222, 1233
id Connection number 1239
conn_config, notes Per-connection module data, notes passed between modules 1242–1245
input_filters, output_filters Connection-level filter chains (the side closer to the socket) 1247–1249
bucket_alloc The allocator used to create buckets 1253
cs Connection state used by the MPM (the event MPM's state machine) 1255
aborted, keepalive, keepalives Was it cut off, will it take another request, how many requests so far on this connection 1267–1274

server_rec: one server (virtual host)

struct server_rec {
    /** The process this server is running in */
    process_rec *process;
    /** The next server in the list */
    server_rec *next;

include/httpd.h lines 1401–1405

Field Meaning Line
process, next Process info, the next server (the virtual host list) 1403–1405
error_log This server's error log file 1412
module_config Server config array with one slot per module 1424
lookup_defaults This server's default directory config 1426
defn_name, is_virtual Where in the config file it was defined, whether it's a virtual host 1429, 1433
port, server_hostname Port and name from ServerName 1439, 1448
timeout, keep_alive_timeout, keep_alive_max, keep_alive Timeout, KeepAliveTimeout, MaxKeepAliveRequests, KeepAlive 1455–1461
names, wild_names ServerAlias lists 1464–1466

One server_rec is one <VirtualHost> block in the config file. The code that handles <VirtualHost> creates a new server_rec and slots it in right after the main server.

    s->next = main_server->next;
    main_server->next = s;

server/core.c lines 3065–3066

The order in which the virtual host list is built. At first there is only the main server localhost (ap_server_conf). Reading a.example at line 14 of the config file inserts a.example after localhost. Reading b.example at line 17 inserts b.example right after the main server, so the list becomes localhost, b.example, a.example. Because of the two lines that insert a new server_rec s, s->next = main_server->next; main_server->next = s;, a new virtual host always goes right after the main server How the two lines change the list, traced with the config from the example below (a.example at line 14, b.example at line 17). The green box is the virtual host just inserted.

Where module config lives: a void * array and module_index

module_config in server_rec and per_dir_config in request_rec hold config structs whose shape differs from module to module — mod_mime's config, mod_dir's config, and so on. The struct definitions have no such fields at all. Instead there is a void * array with one slot per module, and each module uses only slot number module_index.

#define ap_get_module_config(v,m)       \
    (((void **)(v))[(m)->module_index])
#define ap_set_module_config(v,m,val)   \
    ((((void **)(v))[(m)->module_index]) = (val))

include/http_config.h lines 553–556

void * is "a pointer that can point to anything." Each module takes out slot module_index and treats it as its own struct type. The mod_so code behind the -M output in section 3 does exactly that with ap_get_module_config(s->module_config, &so_module) (mod_so.c lines 394–395). Thanks to this, server_rec keeps the same shape no matter how many modules are loaded. How the slots are filled and merged per <Directory> is in Part 7; the downside, no type checking, is discussed in Part 2.

Example: printing the three structs for one request

To check that the three structs really are connected this way, I wrote a small module. For a request to /three, it prints a few fields of r, r->connection and r->server, the server list, and the parent relationship of the pools into the response body.

/* Example written for this post (not part of the httpd source) */
/* For one request, print a few fields of request_rec, conn_rec and server_rec,
 * how the three point to each other, and how the pools are parented. */
#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_main.h"     /* ap_server_conf */
#include "apr_pools.h"

static int bigthree_handler(request_rec *r)
{
    conn_rec *c = r->connection;     /* request -> connection */
    server_rec *s = r->server;       /* request -> the (virtual) server handling it */
    server_rec *v;

    if (!r->handler || strcmp(r->handler, "bigthree")) {
        return DECLINED;             /* not mine: let the next handler try */
    }
    ap_set_content_type(r, "text/plain");

    ap_rprintf(r, "[request_rec r=%pp]\n", r);
    ap_rprintf(r, "  the_request      = %s\n", r->the_request);
    ap_rprintf(r, "  uri              = %s\n", r->uri);
    ap_rprintf(r, "  hostname         = %s\n", r->hostname);
    ap_rprintf(r, "  handler          = %s\n", r->handler);

    ap_rprintf(r, "[conn_rec c=%pp]\n", c);
    ap_rprintf(r, "  client_ip        = %s\n", c->client_ip);
    ap_rprintf(r, "  local_ip         = %s\n", c->local_ip);
    ap_rprintf(r, "  keepalives       = %d\n", c->keepalives);
    ap_rprintf(r, "  base_server      = %s\n", c->base_server->server_hostname);

    ap_rprintf(r, "[server_rec s=%pp]\n", s);
    ap_rprintf(r, "  server_hostname  = %s\n", s->server_hostname);
    ap_rprintf(r, "  is_virtual       = %d\n", s->is_virtual);
    ap_rprintf(r, "  defined at       = %s:%u\n", s->defn_name, s->defn_line_number);
    ap_rprintf(r, "  process          = %s\n", s->process->short_name);

    /* The whole server list linked by s->next, starting at ap_server_conf (main server) */
    ap_rprintf(r, "[server list from ap_server_conf]\n");
    for (v = ap_server_conf; v; v = v->next) {
        ap_rprintf(r, "  %s%s\n", v->server_hostname, v == s ? "   <- r->server" : "");
    }

    /* Pool parent relationships */
    ap_rprintf(r, "[pools]\n");
    ap_rprintf(r, "  parent(r->pool) == c->pool : %s\n",
               apr_pool_parent_get(r->pool) == c->pool ? "yes" : "no");
    ap_rprintf(r, "  pconf is ancestor of r->pool : %s\n",
               apr_pool_is_ancestor(s->process->pconf, r->pool) ? "yes" : "no");
    return OK;
}

static void register_hooks(apr_pool_t *p)
{
    ap_hook_handler(bigthree_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

AP_DECLARE_MODULE(bigthree) = {
    STANDARD20_MODULE_STUFF,
    NULL,                  /* create directory config */
    NULL,                  /* merge directory config */
    NULL,                  /* create server config */
    NULL,                  /* merge server config */
    NULL,                  /* directive table */
    register_hooks         /* register hooks */
};

It creates a module struct shaped just like core_module and http_module from section 2, and in register_hooks puts one function on the handler hook. I compiled the module into a .so with apxs -c and loaded it with LoadModule bigthree_module /work/.libs/mod_bigthree.so. The config file (/tmp/three.conf) sets SetHandler bigthree inside <Location /three> and defines two virtual hosts on the same port 8080, in this order:

<VirtualHost *:8080>
  ServerName a.example
</VirtualHost>
<VirtualHost *:8080>
  ServerName b.example
</VirtualHost>

a.example starts on line 14 of the config file and b.example on line 17. The main server's ServerName is localhost. I sent Host: b.example with curl and requested the same URL twice. Given two URLs in one command, curl reuses a single connection. (The first line of output is a heading: "Host: b.example, twice over the same connection.")

--- curl: Host: b.example, 같은 연결로 두 번
[request_rec r=7f212c7620a0]
  the_request      = GET /three HTTP/1.1
  uri              = /three
  hostname         = b.example
  handler          = bigthree
[conn_rec c=7f212c797350]
  client_ip        = 127.0.0.1
  local_ip         = 127.0.0.1
  keepalives       = 0
  base_server      = a.example
[server_rec s=7f212d032330]
  server_hostname  = b.example
  is_virtual       = 1
  defined at       = /tmp/three.conf:17
  process          = httpd
[server list from ap_server_conf]
  localhost
  b.example   <- r->server
  a.example
[pools]
  parent(r->pool) == c->pool : yes
  pconf is ancestor of r->pool : yes
[request_rec r=7f212c7600a0]
...
[conn_rec c=7f212c797350]
...
  keepalives       = 1
...

For the second request I kept only the lines that differ from the first and replaced the rest with .... The addresses after r=, c= and s= change on every run. Here's what to look at:

  1. The two requests have different r addresses but the same c address. A new request_rec is created per request, but both point to the same conn_rec. keepalives went from 0 to 1.
  2. c->base_server is a.example, while r->server is b.example. When the connection arrives, only the IP and port (*:8080) are known, so the first virtual host for that address becomes base_server; after the request headers are read, Host: b.example decides r->server. We follow this selection through server/vhost.c in Part 9.
  3. The server_rec that r->server points to is the <VirtualHost> on line 17 of the config file. I checked this with defn_name and defn_line_number.
  4. The request pool's parent is the connection pool. And the process's pconf is an ancestor of the request pool (section 5).

The example output as a picture. The two requests sent over the same connection have different request_rec addresses, 7f212c7620a0 and 7f212c7600a0, but both point through r->connection to the same conn_rec 7f212c797350, whose keepalives went from 0 to 1. The conn_rec's c->base_server is a.example (three.conf line 14), the first virtual host for IP and port *:8080, while the first request's r->server is b.example (three.conf line 17, is_virtual=1), chosen after reading the Host header. The server list from ap_server_conf following next is localhost, b.example, a.example Addresses and line numbers are copied from the output above (addresses differ on every run). Blue is a request, yellow a connection, green a server.

If Host names no virtual host at all, r->server stays at c->base_server (a.example), the value ap_create_request() put there at the start (see the code in section 5). (Heading: "conn_rec and server_rec parts only.")

--- curl: Host: nosuch.example (conn_rec, server_rec 부분만)
[conn_rec c=7f212c797360]
  client_ip        = 127.0.0.1
  local_ip         = 127.0.0.1
  keepalives       = 0
  base_server      = a.example
[server_rec s=7f212d031ad0]
  server_hostname  = a.example
  is_virtual       = 1
  defined at       = /tmp/three.conf:14
  process          = httpd

Easy to get confused

5. The pool hierarchy at a glance

Each of the three structs lives as long as its pool. Pools are linked as parents and children, so destroying a parent pool destroys its children too.

The httpd pool hierarchy. Under the top-level process pool (ap_pglobal) are plog and pconf; under pconf are ptemp, pchild and a transaction pool per connection. Under transaction is master_conn (c->pool), under that request (r->pool), and under that small pools such as subrequest. Every yellow box is a pool. The further down and to the right, the shorter the lifetime. The parent of the transaction pool depends on the MPM (see the table).

Pool Created in Lifetime What goes in it
process (ap_pglobal) init_process(), main.c lines 352 and 375 The whole process A few global items
pconf main.c lines 336–337 Cleared on every restart server_recs, module config, loaded .so files
plog, ptemp main.c lines 705–708 For logs / only while reading the config Log-related data, temporary values
transaction (ptrans) event: event.c lines 2250–2253 (parent pconf) One connection The conn_rec struct itself
master_conn = c->pool core_create_conn(), core.c lines 5525–5527 One connection The connection's fields (IP strings, notes, etc.)
request = r->pool ap_create_request(), protocol.c lines 1063–1067 One request The request_rec, header tables, URI strings, etc.

The code that creates the request pool is short.

AP_DECLARE(request_rec *) ap_create_request(conn_rec *conn)
{
    request_rec *r;
    apr_pool_t *p;

    apr_pool_create(&p, conn->pool);
    apr_pool_tag(p, "request");
    r = apr_pcalloc(p, sizeof(request_rec));
    AP_READ_REQUEST_ENTRY((intptr_t)r, (uintptr_t)conn);
    r->pool            = p;
    r->connection      = conn;
    r->server          = conn->base_server;

server/protocol.c lines 1058–1069

It creates the request pool as a child of the connection pool (conn->pool) and builds the request_rec inside that pool (apr_pcalloc takes zero-filled memory from a pool). So destroying the request pool makes the request_rec itself, and every string and table in it, disappear at once. You can also see r->server being set to conn->base_server at first. The place that changes it after reading the Host header is server/vhost.c line 1075 (item 2 of the example in section 4).

The request pool is destroyed when a special bucket called EOR (End Of Request) is destroyed after the response has gone all the way out (apr_pool_destroy(r->pool) in server/eor_bucket.c lines 100–112). Buckets are covered in Part 11.

The APR side of creating and destroying pools, and registering "things to do when a pool is destroyed" with apr_pool_cleanup_register(), is the topic of Part 4. How pconf is cleared on every restart and the config re-read is in Part 5.

Easy to get confused: trunk and 2.4.x differ by one pool layer for connections

In the reference commit (trunk — the development line preparing the next release), the conn_rec struct is allocated in the transaction pool, and c->pool is a separate master_conn pool created beneath it (core.c line 5516 and lines 5525–5527). In the 2.4.x branch most of us install, core_create_conn() does c->pool = ptrans;, using the transaction pool itself as the connection pool. From a module's point of view "c->pool lives as long as the connection" holds either way, but when you read 2.4.x articles or code alongside trunk, the pool hierarchy can look one level different.

The parent of the transaction pool also depends on the MPM. event and worker create it under pconf (worker.c line 673), while prefork creates it under the child process's pchild (prefork.c line 469). The diagram follows event, the MPM used in this post's examples.

6. The path a single request takes

Now let's string the layers and structs together. When a client opens a connection and sends GET /index.html, it follows this path.

The path of one request. After the MPM listener accepts, ap_run_create_connection builds a conn_rec via core_create_conn, ap_pre_connection attaches filters to the connection, and ap_run_process_connection calls ap_process_http_connection. ap_read_request creates the request_rec and reads the request line and headers, ap_process_request_internal runs the phases from translate_name to fixups, ap_invoke_handler runs the handler hook, and the response goes out through the output filters and the CORE filter to the socket. Finally the EOR bucket destroys r->pool, and with keep-alive the flow returns for the next request on the same connection. The blue boxes are places where the core runs hooks; each box names the post that reads that step in detail.

Step Who Function File More in
Accept the connection MPM Listener thread, process_socket() server/mpm/event/event.c Part 8
Build the conn_rec core_module ap_run_create_connection() → core_create_conn() server/core.c Part 9
Prepare the connection core_module and others ap_pre_connection() → core_pre_connection() attaches the CORE_IN and CORE filters server/core.c Part 9, Part 15 (where TLS plugs in)
Process the connection http_module ap_run_process_connection() → ap_process_http_connection() modules/http/http_core.c Part 9
Read the request Core ap_read_request() → ap_create_request() server/protocol.c Part 9
Processing phases Core + many modules ap_process_request_internal(): translate_name → map_to_storage → access, authentication, authorization → type_checker → fixups server/request.c Part 10, Part 13
Build the response Handler module ap_invoke_handler() → the handler hook (default_handler for static files) server/config.c, server/core.c Part 12
Send it out Filters Output filter chain → the CORE filter writes to the socket server/util_filter.c, server/core_filters.c Part 11
Wrap up Core EOR bucket → access log entry, r->pool freed server/eor_bucket.c Part 16 (logging)

Read the table top to bottom and the layers from section 1 appear. Only the first row is the MPM; in all the others the core runs a hook and core_module, http_module and other modules do the work in turn. The core itself does less than you'd expect; mostly it "runs the next hook."

ap_process_http_connection(), in the "Process the connection" row, splits into two branches depending on how the MPM works.

static int ap_process_http_connection(conn_rec *c)
{
    if (async_mpm && !c->clogging_input_filters) {
        return ap_process_http_async_connection(c);
    }
    else {
        return ap_process_http_sync_connection(c);
    }
}

modules/http/http_core.c lines 247–255

With an MPM like prefork or worker, where one thread (or process) holds a connection to the end, the sync branch handles keep-alive requests one after another in a while ((r = ap_read_request(c)) != NULL) loop (http_core.c line 197). event takes the async branch: it handles a single request and gives the connection back to the MPM. When the next request arrives, the MPM runs the process_connection hook again. That's why the arrow looping back in the diagram points at ap_run_process_connection().

Easy to get confused: event never calls ap_process_connection()

server/connection.c has a function whose name says exactly what it does:

AP_CORE_DECLARE(void) ap_process_connection(conn_rec *c, void *csd)
{
    ap_update_vhost_given_ip(c);

    ap_pre_connection(c, csd);

    if (!c->aborted) {
        ap_run_process_connection(c);
    }
}

server/connection.c lines 244–253

prefork (prefork.c line 693) and worker (worker.c line 496) call this function. event's process_socket(), however, skips it and does the same work itself: it calls ap_run_create_connection(), ap_update_vhost_given_ip() and ap_pre_connection() in turn, and later ap_run_process_connection() (event.c lines 1075–1153), because the connection has to be passed back and forth between the MPM and the core several times. This is why, when following the source, you can set a breakpoint on ap_process_connection() under event and never hit it.

What this post leaves out

Things I left out to keep the map to one page:

Summary

Concept One-line summary
Five layers OS ← APR·APR-util ← core (server/) ← MPM (server/mpm/) and modules (modules/). The core and modules/MPM call each other through hooks
APR A separate library that hides OS differences behind apr_ functions: pools, sockets, DSO, threads, etc.
The core is a module core_module (server/core.c), http_module (modules/http/http_core.c), so_module and even the MPM all use the same module struct
Static vs. shared Modules in modules.c, generated by configure (core, so and http in the example build), go into the binary; the rest come in via LoadModule → mod_so → apr_dso_load()
MPM check ap_check_mpm() checks that exactly one function is registered on the mpm hook
server_rec One server (virtual host). Lives in pconf, linked by next. New virtual hosts are inserted right after the main server
conn_rec One TCP connection. c->pool, c->base_server (server chosen by IP and port)
request_rec One request. r->pool, r->connection, r->server (server chosen by Host)
Module config Each struct holds a void * array; a module takes out slot module_index (ap_get_module_config)
Pool hierarchy process → pconf → transaction → c->pool → r->pool. The EOR bucket destroys the request pool
Path of a request MPM accept → create_connection → pre_connection → process_connection → ap_read_request → ap_process_request_internal → handler → filters → socket

What's next

Time to fold the map and start at the floor. The next post covers what this post kept calling just "pools": APR memory pools. We'll see what happens inside APR when you create parent and child pools with apr_pool_create(), take memory with apr_palloc() and destroy it all at once with apr_pool_destroy(), and we'll run an example that registers a cleanup on the request pool to catch the moment a request ends.

Digging into Apache httpd's Source 4: APR Memory Pools — Managing Memory Without free()

The Spring series spread out a big-picture map at the same point. Spring splits its modules into jars at build time and lets a single container wire beans together at run time, while httpd loads .so files at run time and treats them all as the same module struct. The mechanism for stepping into other modules' behavior differs too: Spring uses extension points such as BeanPostProcessor, httpd uses hooks. Spring's DispatcherServlet, which processes a request step by step, is in Spring Part 21; put it next to the table in section 6 and you'll see both projects implementing the same idea in different languages: split a request into phases and leave a slot to plug into at each one.

More posts