Dev Docs AFP Daemon
AFP Daemon (afpd)
1. AFP Daemon Overview
The afpd daemon is the core component of Netatalk that implements the Apple Filing Protocol (AFP) server. It uses a process-per-connection architecture where a single parent process accepts connections and forks a dedicated child worker for each client session.
Key Source Files
| Component | File |
|---|---|
| Parent process & event loop | etc/afpd/main.c |
| Child worker event loop | etc/afpd/afp_dsi.c |
| Session forking | libatalk/dsi/dsi_getsess.c |
| AFP command dispatch | etc/afpd/switch.c |
| Open fork management | etc/afpd/fork.h, etc/afpd/ofork.c, etc/afpd/fork.c |
| Child process table | libatalk/util/server_child.c, include/atalk/server_child.h |
| Idle worker thread | etc/afpd/idle_worker.c, etc/afpd/idle_worker.h |
Caching subsystem (directory cache, AD metadata cache, resource fork cache, IPC cache hint system) is documented in the dedicated Caching Architecture page.
Process Architecture Diagram
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph TB
subgraph "afpd Parent Process"
A["poll() event loop<br/>(main.c: main())"]:::yellow
B["Signal handler<br/>afp_goaway()"]:::orange
C["Child table<br/>server_child_t"]:::cyan
D["Hint buffer<br/>hint_buf[128]"]:::purple
end
subgraph "afpd Child Workers"
E["Child 1<br/>afp_over_dsi()"]:::green
F["Child 2<br/>afp_over_dsi()"]:::green
G["Child N<br/>afp_over_dsi()"]:::green
end
subgraph "Each Child Process"
H["AFP command<br/>dispatch"]:::purple
I["Idle worker<br/>pthread"]:::green
J["Dircache<br/>(ARC)"]:::cyan
K["AD cache<br/>(Tier 1)"]:::cyan
L["Rfork cache<br/>(Tier 2)"]:::cyan
end
Mac1["Mac Client 1"]:::blue
Mac2["Mac Client 2"]:::blue
MacN["Mac Client N"]:::blue
CNID["CNID<br/>Database"]:::salmon
Mac1 --> E
Mac2 --> F
MacN --> G
A -->|"fork()"| E
A -->|"fork()"| F
A -->|"fork()"| G
A --> C
A --> D
D -->|"hint pipe"| E
D -->|"hint pipe"| F
D -->|"hint pipe"| G
E -->|"IPC socketpair"| A
F -->|"IPC socketpair"| A
E --- H
E --- I
H --> J
J --> K
K --> L
H --> CNID
classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
classDef grey fill:#dfe6e9,stroke:#333,rx:10,ry:10
classDef salmon fill:#fab1a0,stroke:#333,rx:10,ry:10
classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10
2. Parent Process Architecture
The parent process in main() performs initialization, then enters an event loop that manages listening sockets, child IPC, signals, and cache hint batching.
Global State
Defined as file-scope statics in etc/afpd/main.c:
| Variable | Type | Purpose |
|---|---|---|
dsi_obj |
AFPObj |
DSI protocol configuration object |
asp_obj |
AFPObj |
ASP protocol configuration object (DDP) |
server_children |
server_child_t * |
Hash table of active child processes |
reloadconfig |
sig_atomic_t |
Flag set by SIGHUP handler |
gotsigchld |
sig_atomic_t |
Flag set by SIGCHLD handler |
asev |
struct asev * |
Poll fd set and metadata |
Event Loop
The main event loop in main() uses poll() with a dynamic timeout:
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
flowchart TD
START["while (1)"]:::yellow
TIMEOUT{"hint_buf_count()<br/>> 0?"}:::cream
POLL["poll(asev→fdset,<br/>asev→used,<br/>poll_timeout)"]:::yellow
SIGCHLD{"gotsigchld?"}:::orange
CH["child_handler():<br/>waitpid loop,<br/>server_child_remove,<br/>asev_del_fd"]:::orange
ERR{"ret < 0?"}:::red
EINTR{"EINTR?"}:::cream
RELOAD{"reloadconfig?"}:::orange
DOCFG["Reset sockets,<br/>re-parse config,<br/>re-init listeners,<br/>server_child_kill<br/>(SIGHUP)"]:::orange
FDEVT{"ret > 0?"}:::cream
FDLOOP["for each fd<br/>with revents"]:::purple
LISTEN["LISTEN_FD:<br/>dsi_start() →<br/>fork child,<br/>add IPC_FD"]:::cyan
IPC["IPC_FD:<br/>ipc_server_read()"]:::purple
FLUSH{"hint_buf_count > 0<br/>AND (ret==0 OR<br/>count ≥ 128)?"}:::cream
DOFLUSH["hint_flush_pending<br/>(server_children)"]:::purple
START --> TIMEOUT
TIMEOUT -->|"Yes: 50ms"| POLL
TIMEOUT -->|"No: -1 (block)"| POLL
POLL --> SIGCHLD
SIGCHLD -->|"Yes"| CH --> ERR
SIGCHLD -->|"No"| ERR
ERR -->|"Yes"| EINTR
EINTR -->|"Yes"| START
EINTR -->|"No"| BREAK["break (fatal)"]:::red
ERR -->|"No"| RELOAD
RELOAD -->|"Yes"| DOCFG --> START
RELOAD -->|"No"| FDEVT
FDEVT -->|"Yes"| FDLOOP
FDLOOP --> LISTEN
FDLOOP --> IPC
LISTEN --> FLUSH
IPC --> FLUSH
FDEVT -->|"No (timeout)"| FLUSH
FLUSH -->|"Yes"| DOFLUSH --> START
FLUSH -->|"No"| START
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
classDef cream fill:#f8f4e8,stroke:#333,rx:10,ry:10
classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10
classDef red fill:#ee5a5a,stroke:#333,color:#fff,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
Step ordering is critical (in main() event loop):
- SIGCHLD first — removes dead children from the table before hint flush or fd events can reference them
- Error handling — uses
saveerrnobecausechild_handler()clobbers errno viawaitpid/close/asev_del_fd - Config reload — resets listening sockets, re-parses config, broadcasts SIGHUP to children
- FD events — dispatches
LISTEN_FD(new connections) andIPC_FD(child messages) events - Hint flush — flushes buffered hints when timeout expires (ret == 0) or buffer is full (count ≥
HINT_BUF_SIZE= 128). See Caching Architecture: IPC Hint System for details.
Signal Mask Handling
Signals are unblocked before poll() and blocked immediately after (in the main() event loop):
pthread_sigmask(SIG_UNBLOCK, &sigs, NULL);
ret = poll(asev->fdset, asev->used, poll_timeout);
pthread_sigmask(SIG_BLOCK, &sigs, NULL);
The signal set sigs includes SIGALRM, SIGHUP, SIGUSR1, and SIGCHLD (configured in main() before the event loop).
3. Session Establishment
New connections are handled by dsi_getsession() called from dsi_start() (static function in main.c).
Fork and IPC Setup
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
participant P as Parent (main.c)
participant G as dsi_getsession()
participant C as Child Process
P->>G: dsi_start() → dsi_getsession()
Note over G: socketpair(PF_UNIX, SOCK_STREAM)<br/>→ ipc_fds[0], ipc_fds[1]
Note over G: pipe() → hint_pipe[0], hint_pipe[1]
Note over G: setnonblock() on all 4 fds
G->>G: dsi->proto_open(dsi) [fork()]
rect rgb(255, 238, 170)
Note over P: Parent path
P->>P: close(ipc_fds[1])<br/>close(hint_pipe[0])
P->>P: server_child_add(pid,<br/>ipc_fds[0], hint_pipe[1])
P->>P: dsi->proto_close(dsi)
P->>P: asev_add_fd(child→ipc_fd,<br/>IPC_FD)
end
rect rgb(85, 239, 196)
Note over C: Child path
C->>C: obj→ipc_fd = ipc_fds[1]<br/>obj→hint_fd = hint_pipe[0]
C->>C: close(ipc_fds[0])<br/>close(hint_pipe[1])<br/>close(serversock)
C->>C: server_child_free()
C->>C: dsi_opensession(dsi)
C->>C: → return to dsi_start()
C->>C: configfree() → afp_over_dsi()
end
Key details from dsi_getsession():
- IPC socketpair (
ipc_fds): bidirectionalSOCK_STREAMfor session control (state updates, reconnect transfer viasend_fd/recv_fd+SIGURG) - Hint pipe (
hint_pipe): unidirectional parent→child pipe for dircache invalidation hints. Separate from IPC to avoid interfering withsend_fd()/recv_fd()+SIGURGsignaling. See Caching Architecture: Communication Channels for the rationale. - Pipe atomicity: hint messages are 22 bytes, well within
PIPE_BUF(4096), guaranteeing atomic delivery (verified by_Static_assertinserver_ipc.c) - Child setup: the child closes the parent-side fds, frees the child table, and returns to
dsi_start()which callsafp_over_dsi()
4. Child Worker Architecture
Each child process runs afp_over_dsi() which never returns. It initializes the dircache, idle worker thread, and TCP socket options, then enters a command processing loop.
Child Event Loop
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
flowchart TD
INIT["dircache_init()<br/>idle_worker_init()<br/>TCP options<br/>ipc_child_state(RUNNING)"]:::green
OUTER["while (1)"]:::green
JMPCHK{"sigsetjmp<br/>recon_jmp?"}:::cream
INNER["Inner poll loop"]:::green
subgraph "Inner Poll Loop (idle wait)"
ABUF{"[A] DSI buffer<br/>has data?"}:::cream
BSTATE{"[B] Sleep/<br/>Disconnect/<br/>Die?"}:::cream
CPOLL["[C] Setup pfds:<br/>dsi→socket +<br/>obj→hint_fd"]:::green
ISTART["idle_worker_start()"]:::green
DPOLL["[D] poll(pfds, nfds, -1)"]:::green
ISTOP["idle_worker_stop()"]:::green
EHINT["[F] Hint pipe:<br/>process_cache_hints()"]:::purple
GSOCK{"[G-H] Socket<br/>ready?"}:::cream
end
RECV["dsi_stream_receive()"]:::green
CMD{"DSI command?"}:::cream
CLOSE["DSIFUNC_CLOSE:<br/>shutdown + exit"]:::red
TICKLE["DSIFUNC_TICKLE:<br/>clear DSI_DATA"]:::cyan
AFPCMD["DSIFUNC_CMD:<br/>afp_switch[fn]()"]:::purple
AFPWRT["DSIFUNC_WRITE:<br/>afp_switch[fn]()"]:::purple
REPLAY{"Replay cache<br/>hit?"}:::cream
CACHED["Return cached<br/>result"]:::cyan
EXEC["Execute AFP<br/>command"]:::purple
REPLY["dsi_cmdreply()"]:::green
PEND["pending_request()<br/>fce_pending_events()"]:::purple
INIT --> OUTER
OUTER --> JMPCHK
JMPCHK -->|"Normal"| INNER
JMPCHK -->|"Reconnect"| INNER
INNER --> ABUF
ABUF -->|"Yes"| RECV
ABUF -->|"No"| BSTATE
BSTATE -->|"Yes"| RECV
BSTATE -->|"No"| CPOLL --> ISTART --> DPOLL --> ISTOP
ISTOP --> EHINT --> GSOCK
GSOCK -->|"Yes"| RECV
GSOCK -->|"No (hint only)"| CPOLL
RECV --> CMD
CMD --> CLOSE
CMD --> TICKLE --> PEND
CMD --> AFPCMD
CMD --> AFPWRT --> PEND
AFPCMD --> REPLAY
REPLAY -->|"Hit"| CACHED --> REPLY --> PEND --> OUTER
REPLAY -->|"Miss"| EXEC --> REPLY
classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
classDef cream fill:#f8f4e8,stroke:#333,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
classDef red fill:#ee5a5a,stroke:#333,color:#fff,rx:10,ry:10
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10
AFP Command Dispatch Table
The command dispatch uses a 256-entry function pointer array in switch.c. There are two tables:
preauth_switch[]: onlyafp_login(index 18),afp_logincont(19),afp_logout(20), andafp_login_ext(63) are non-NULLpostauth_switch[]: full AFP command set includingafp_openfork(26),afp_read(27),afp_write(33),afp_enumerate(9),afp_delete(8), etc.
The global pointer afp_switch starts pointing to preauth_switch and is switched to postauth_switch after successful authentication. UAMs can register custom handlers via uam_afpserver_action().
Dispatch in afp_over_dsi():
err = (*afp_switch[function])(obj,
(char *)dsi->commands, dsi->cmdlen,
(char *)&dsi->data, &dsi->datalen);
Replay Cache
A fixed-size replay cache prevents duplicate command execution (defined in afp_dsi.c):
typedef struct {
uint16_t DSIreqID;
uint8_t AFPcommand;
uint32_t result;
} rc_elem_t;
static rc_elem_t replaycache[REPLAYCACHE_SIZE];
REPLAYCACHE_SIZE = 128 (defined in include/atalk/afp.h). Each entry is indexed by dsi->clientID % REPLAYCACHE_SIZE.
Tickle Handling
The SIGALRM-based alarm_handler() (in afp_dsi.c) fires at the configured tickle interval:
- If
DSI_DATAflag is set (recent traffic): clear it and return - Otherwise increment
dsi->ticklecounter - Sleeping: if
tickle > options.sleep, terminate - Disconnected: if
tickle > options.disconnected, terminate - Active: if
tickle >= options.timeout, enter disconnected state; otherwise senddsi_tickle()to client
5. Open Fork Management
AFP file access is tracked through the open fork table defined in fork.h and ofork.c.
struct ofork
Defined in fork.h:
struct file_key {
dev_t dev;
ino_t inode;
};
struct ofork {
struct file_key key; // dev_t dev + ino_t inode
struct adouble *of_ad; // AppleDouble metadata handle
struct vol *of_vol; // volume
cnid_t of_did; // parent directory CNID
uint16_t of_refnum; // AFP fork reference number
int of_flags; // AFPFORK_* flags
const unsigned char *of_virtual_data; // virtual file data pointer
off_t of_virtual_len; // virtual file length
struct ofork **prevp, *next; // hash chain pointers
};
Hash Table
OFORK_HASHSIZE: 64 buckets (defined inofork.c)- Hash function
hashfn():key->inode & (OFORK_HASHSIZE - 1) - Linear array
oforks[]: indexed byrefnum % nforks, wherenforks = min(getdtablesize() - 10, 0xffff) - Dual indexing: hash chain for dev/ino lookup, array for refnum lookup
Key Operations
| Function | File | Purpose |
|---|---|---|
of_alloc() |
ofork.c |
Allocate fork, hash by dev/ino, assign refnum |
of_find() |
ofork.c |
Lookup by refnum (array index + refnum verification) |
of_findname() |
ofork.c |
Lookup by dev/ino from stat() result |
of_dealloc() |
ofork.c |
Unhash, decrement ad_refcount, free if zero |
of_closefork() |
ofork.c |
Full close: flush dirty metadata, send IPC cache hints, ad_close |
AFPFORK Flags
Defined in fork.h:
| Flag | Value | Purpose |
|---|---|---|
AFPFORK_DATA |
1<<0 |
Open data fork |
AFPFORK_RSRC |
1<<1 |
Open resource fork |
AFPFORK_META |
1<<2 |
Open metadata |
AFPFORK_DIRTY |
1<<3 |
AD metadata modified (set in write_fork() in fork.c when ad_meta_fileno != -1) |
AFPFORK_ACCRD |
1<<4 |
Read access |
AFPFORK_ACCWR |
1<<5 |
Write access |
AFPFORK_MODIFIED |
1<<6 |
Data written (set in write_fork(), triggers FCE and IPC hints) |
AFPFORK_ERROR |
1<<7 |
Error opening fork |
AFPFORK_VIRTUAL |
1<<8 |
Virtual file fork |
Refcounting
The struct adouble has a reference count (ad_refcount). When multiple forks of the same file are open simultaneously, they share the same adouble via ad_ref(). of_dealloc() decrements the refcount and only frees the adouble when it reaches zero.
6. Caching Subsystem
Full documentation: Caching Architecture
Each afpd worker child maintains its own private, per-process three-tier cache hierarchy. There is no shared memory between workers — cross-process coherence is handled by the IPC cache hint relay system.
| Tier | What | Key File |
|---|---|---|
| Dircache | struct dir entries (files & dirs) with stat fields, ARC eviction |
dircache.c |
| AD cache | AppleDouble metadata (FinderInfo, dates, attributes) inline in struct dir |
ad_cache.c |
| Rfork cache | Resource fork content buffers with byte-budget LRU | ad_cache.c |
| IPC hints | Cross-process invalidation via parent relay | server_ipc.c |
The dircache supports both ARC (Adaptive Replacement Cache) and LRU modes. The IPC hint system uses batched, priority-sorted, PIPE_BUF-safe writes via dedicated hint pipes. See the Caching Architecture page for complete details including:
- ARC algorithm (T1/T2/B1/B2 lists, adaptation parameter p)
- AD metadata cache (Tier 1b):
ad_metadata_cached()with strict/non-strict validation - Resource fork content cache (Tier 2): budget management, LRU eviction
- IPC hint wire format, batching, rate limiting, receiver logic
struct dirfield layout and cache validation
7. Signal Handling
Parent Process Signals
All signals are handled by afp_goaway() in main.c:
| Signal | Action | Details |
|---|---|---|
| SIGTERM/SIGQUIT | Shutdown | server_child_kill(SIGTERM), _exit(0) |
| SIGUSR1 | Disable logins | Increments nologin, calls auth_unload(), forwards SIGUSR1 to children |
| SIGHUP | Reload config | Sets reloadconfig = 1 (deferred to event loop) |
| SIGCHLD | Child died | Sets gotsigchld = 1 (deferred to event loop) |
| SIGPIPE | Ignored | SIG_IGN |
| SIGXFSZ | Ignored | SIG_IGN (vfat O_LARGEFILE workaround) |
Signal masks are cross-configured: each handler’s sa_mask blocks all other handled signals (configured in main() before the event loop) to prevent re-entrant handler execution.
The child_handler() function (called from the event loop, not directly from the signal handler) loops with waitpid(WAIT_ANY, &status, WNOHANG) to reap all dead children, calling server_child_remove() (in server_child.c) and asev_del_fd() for each.
Child Process Signals
Installed by afp_over_dsi_sighandlers() in afp_dsi.c:
| Signal | Handler | Purpose |
|---|---|---|
| SIGHUP | afp_dsi_reload() |
Sets reload_request = 1 → load_volumes() |
| SIGURG | afp_dsi_transfer_session() |
Primary reconnect: receive new socket fd via IPC, siglongjmp() back to event loop |
| SIGTERM/SIGQUIT | afp_dsi_die() |
Send AFPATTN_SHUTDOWN, close session, exit |
| SIGUSR1 | afp_dsi_timedown() |
5-minute shutdown: send attention message, set 300s timer |
| SIGUSR2 | afp_dsi_getmesg() |
Server message delivery |
| SIGINT | afp_dsi_debug() |
Toggle max_debug logging to /tmp/afpd.PID.XXXXXX |
| SIGALRM | alarm_handler() |
Tickle timer, idle/disconnect timeout management |
| SIGCHLD | child_handler() (child-side) |
Simple wait(NULL) for any sub-processes |
All child signal handlers use sigfillset(&action.sa_mask) with SA_RESTART, ensuring no signal re-entrancy.
8. Idle Worker
The idle worker is a background pthread in each child process, defined in idle_worker.c and declared in idle_worker.h.
Purpose
The idle worker performs deferred cleanup tasks only while the main thread is blocked in poll(), avoiding contention with AFP command processing:
- Free invalidated dircache entries — dequeues from
invalid_dircache_entries(populated bydir_remove()during AFP commands) and callsdir_free() - Deferred child removal scans — processes one hash chain per iteration via
dircache_process_deferred_chain(), cleaning stale dircache entries including ARC ghost entries
Coordination Protocol
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
participant M as Main Thread
participant W as Idle Worker Thread
Note over M: About to enter poll()
M->>W: idle_worker_start()<br/>atomic_store(is_idle, 1)
M->>M: poll(pfds, nfds, -1)
Note over W: Wakes from timedwait<br/>sees is_idle==1
W->>W: atomic_store(bg_running, 1)
W->>W: Process work items<br/>(checking is_idle per item)
Note over M: poll() returns
M->>W: idle_worker_stop()<br/>atomic_store(is_idle, 0)
Note over M: Spins until<br/>bg_running==0
Note over W: Sees is_idle==0<br/>atomic_store(bg_running, 0)
M->>M: Continue processing<br/>AFP command
Atomic Coordination
Two atomic_int variables provide lock-free coordination:
| Variable | Purpose |
|---|---|
is_idle |
Set to 1 by main thread before poll(), cleared after poll() returns |
bg_running |
Set to 1 by worker while processing, cleared when done or interrupted |
The implementation requires ATOMIC_INT_LOCK_FREE == 2 (verified at compile time). On platforms without lock-free atomics, the idle worker is disabled entirely with stub functions that fall back to synchronous cleanup.
API
| Function | Safety | Purpose |
|---|---|---|
idle_worker_init() |
Normal | Create worker pthread (called once in afp_over_dsi() after dircache_init()) |
idle_worker_start() |
Async-signal-safe | Signal main thread entering poll() (single atomic store) |
idle_worker_stop() |
NOT signal-safe | Reclaim exclusive access (atomic store + spin on bg_running) |
idle_worker_stop_signal_safe() |
Async-signal-safe | Signal-safe variant: sets is_idle=0 without spin (caller ends in exit()) |
idle_worker_shutdown() |
Normal | Clean shutdown: set shutdown_flag, signal condvar, pthread_join() |
idle_worker_is_active() |
Normal | Returns worker_started flag |
idle_worker_log_stats() |
Normal | Log cycle statistics (started/completed/interrupted) |
Worker Thread Details
- Self-wake interval:
IDLE_WORKER_WAKE_MS= 1ms viapthread_cond_timedwait()withCLOCK_MONOTONIC(when available) - Signal blocking: all signals blocked via
sigfillset()+pthread_sigmask(SIG_BLOCK)— worker must not receive process signals - Work predicate (
idle_worker_has_work()): checks bothis_idleflag AND whether actual work exists (non-emptyinvalid_dircache_entriesqueue or deferred dircache cleanup pending) - Statistics tracked per session:
cycles_started,cycles_completed,cycles_interrupted
Integration with AFP Commands
When the idle worker is active (idle_worker_is_active() returns true), the main thread skips synchronous dir_free_invalid_q() after AFP command execution. Instead, the worker handles it during the next idle cycle. If the worker is not active (init failed), dir_free_invalid_q() is called synchronously after each command (see afp_over_dsi() DSIFUNC_CMD handling in afp_dsi.c).
9. Child Process Table
The child process table is managed by server_child.c with types in server_child.h.
server_child_t
typedef struct {
pthread_mutex_t servch_lock; // Lock (used with DBUS)
int servch_count; // Current active session count
int servch_nsessions; // Maximum allowed sessions
afp_child_t *servch_table[CHILD_HASHSIZE]; // Hash table (32 buckets)
} server_child_t;
afp_child_t
typedef struct afp_child {
pid_t afpch_pid; // Worker process PID
uid_t afpch_uid; // Connected user ID
int afpch_valid; // 1 if clientid is set
int afpch_killed; // 1 if SIGTERM already sent
uint32_t afpch_boottime; // Client boot time
time_t afpch_logintime; // Time child was added
uint32_t afpch_idlen; // Client ID length
char *afpch_clientid; // Client ID string
int afpch_ipc_fd; // IPC socketpair (parent end)
int afpch_hint_fd; // Hint pipe (parent write end)
char *afpch_hostname; // Server hostname
int16_t afpch_state; // Session state (active/sleeping/disconnected)
char *afpch_volumes; // Mounted volumes string
struct afp_child **afpch_prevp;
struct afp_child *afpch_next;
} afp_child_t;
Hash Function
CHILD_HASHSIZE = 32. Hash: ((pid >> 8) ^ pid) & (CHILD_HASHSIZE - 1) (defined as HASH() macro in server_child.c).
Key Operations
| Function | Purpose |
|---|---|
server_child_alloc() |
Allocate and initialize server_child_t with session limit |
server_child_add() |
Add child with PID, IPC fd, hint fd; enforces session limit |
server_child_remove() |
Unhash child, close IPC + hint fds, free memory, return IPC fd |
server_child_free() |
Free all children (called by child process after fork) |
server_child_kill() |
Send signal to all children |
server_child_resolve() |
Find child by PID |
server_child_transfer_session() |
Primary reconnect: write DSI ID + send_fd() + SIGURG |
Footnotes
This is a mirror of the Netatalk GitHub Wiki
Last updated 2026-04-06