-
Notifications
You must be signed in to change notification settings - Fork 100
Dev Docs AFP Daemon
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.
| 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.
%%{ 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
The parent process in main() performs initialization, then enters an event loop that manages listening sockets, child IPC, signals, and cache hint batching.
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 |
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.
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).
New connections are handled by dsi_getsession() called from dsi_start() (static function in main.c).
%%{ 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()
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.
%%{ 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
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-NULL -
postauth_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);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.
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
AFP file access is tracked through the open fork table defined in fork.h and ofork.c.
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
};-
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
| 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
|
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 |
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.
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
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.
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.
The idle worker is a background pthread in each child process, defined in idle_worker.c and declared in idle_worker.h.
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
%%{ 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
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.
| 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) |
-
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
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).
The child process table is managed by server_child.c with types in server_child.h.
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;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;CHILD_HASHSIZE = 32. Hash: ((pid >> 8) ^ pid) & (CHILD_HASHSIZE - 1) (defined as HASH() macro in server_child.c).
| 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
|
Resources
- Getting Started
- FAQ
- Troubleshooting
- Connect to AFP Server
- Webmin Module
- Benchmarks
- Interoperability with Samba
OS Specific Guides
- Installing Netatalk on Alpine Linux
- Installing Netatalk on Debian Linux
- Installing Netatalk on Fedora Linux
- Installing Netatalk on FreeBSD
- Installing Netatalk on macOS
- Installing Netatalk on NetBSD
- Installing Netatalk on OmniOS
- Installing Netatalk on OpenBSD
- Installing Netatalk on OpenIndiana
- Installing Netatalk on openSUSE
- Installing Netatalk on Raspberry Pi OS
- Installing Netatalk on Solaris
- Installing Netatalk on Ubuntu
Tech Notes
- Capturing AFP network traffic
- Kerberos
- Special Files and Folders
- Spotlight
- MySQL CNID Backend
- Slow AFP read performance
- Limiting Time Machine volumes
- Netatalk and ZFS nbmand property
Retro AFP
Development