Document Version: 1.0
Analysis Date: 2026-03-20
Coverage: F-Stack v1.26 Public API, Configuration System, Development Guidelines (FreeBSD 15.0 port; KBI/KPI deltas captured)
Target Audience: Application Developers, System Integration Engineers, Performance Optimization Engineers
F-Stack exports 80+ public symbols, divided into the following major categories:
┌─────────────────────────────────────────────────┐
│ F-Stack Public API (ff_api.h 491 lines) │
├─────────────────────────────────────────────────┤
│ 1. Lifecycle Management (3) │
│ ff_init / ff_run / ff_stop_run │
│ │
│ 2. Socket API (25+) │
│ ff_socket / ff_bind / ff_listen / ff_accept │
│ ff_connect / ff_close / ff_dup / ff_dup2 │
│ ... │
│ │
│ 3. I/O Operations (10+) │
│ ff_read / ff_write / ff_readv / ff_writev │
│ ff_send / ff_sendto / ff_sendmsg │
│ ff_recv / ff_recvfrom / ff_recvmsg │
│ ... │
│ │
│ 4. Event Multiplexing (5) │
│ ff_kqueue / ff_kevent / ff_kevent_do_each │
│ ff_select / ff_poll │
│ │
│ 5. Epoll Compatibility Layer (3) │
│ ff_epoll_create / ff_epoll_ctl / ff_epoll_wait│
│ │
│ 6. Control Operations (10+) │
│ ff_setsockopt / ff_getsockopt │
│ ff_ioctl / ff_fcntl │
│ ... │
│ │
│ 7. Route Management (1) │
│ ff_route_ctl │
│ │
│ 8. Zero-Copy Mbuf (3) │
│ ff_zc_mbuf_get / ff_zc_mbuf_write │
│ ff_zc_mbuf_read (not yet implemented) / ... │
│ │
│ 9. Multi-Threading Support (2) │
│ ff_pthread_create / ff_pthread_join │
│ │
│ 10. System Interfaces (10+) │
│ ff_gettimeofday / ff_clock_gettime │
│ ff_log_open_set / ff_log / ff_vlog │
│ ... │
│ │
│ 11. Graceful Reload (ff_reload.h) │
│ ff_reload_attach_state / ff_reload_set_gen │
│ ff_reload_active_gen / ff_reload_hw_locked │
│ ff_reload_gendir_* (epoch directory) │
│ │
│ 12. Software Flow Map (ff_flow_map.h) │
│ ff_flow_map_admit / insert / revoke │
│ ff_flow_map_lookup / stats2 │
│ │
│ 13. Drain Rings (ff_drain_ring.h) │
│ ff_drain_ring_init / rx_enqueue / tx_enqueue│
│ ff_drain_ring_rx_dequeue / stats │
│ │
└─────────────────────────────────────────────────┘
Initialization & Startup:
int ff_init(int argc, char * const argv[]);
// Initialize F-Stack
// Parameters: Command-line arguments in DPDK style
// Returns: 0 on success, -1 on failure
// Must be called first by the primary process
void ff_run(loop_func_t loop, void *arg);
// Start the main polling loop
// Parameters: User callback function, user argument
// Does not return (unless ff_stop_run() is called)
// Called once per lcore
void ff_stop_run(void);
// Stop the polling loop
// Graceful shutdown, waits for all packet processing to completeSocket Operations (POSIX-Compatible):
int ff_socket(int domain, int type, int protocol);
// Create a socket
// domain: AF_INET (IPv4) or AF_INET6 (IPv6)
// type: SOCK_STREAM (TCP) or SOCK_DGRAM (UDP)
// Returns: File descriptor (>= 0)
int ff_bind(int sockfd, const struct linux_sockaddr *addr, socklen_t addrlen);
// Bind a local address
// addr: Pointer to address structure
// Note: Uses struct linux_sockaddr, not FreeBSD format
int ff_listen(int sockfd, int backlog);
// Listen for connections (TCP only)
// backlog: Connection queue size
int ff_accept(int sockfd, struct linux_sockaddr *addr, socklen_t *addrlen);
// Accept a connection (TCP only)
// Returns: fd of the new connection
int ff_connect(int sockfd, const struct linux_sockaddr *addr, socklen_t addrlen);
// Establish a connection (TCP only)
// Returns: 0 on success, -1 on failure (non-blocking, may return EINPROGRESS)
int ff_close(int sockfd);
// Close a socket
// Waits for all pending data to be sentI/O Operations (Non-Blocking):
ssize_t ff_read(int fd, void *buf, size_t nbytes);
// Read data
// Returns: > 0 bytes read, 0 connection closed, -1 error
// Non-blocking: Returns -1 + errno=EAGAIN when no data available
ssize_t ff_write(int fd, const void *buf, size_t nbytes);
// Write data
// Returns: > 0 bytes sent, -1 buffer full or error
// Note: Returns -1 when buffer is full, NOT partial send
// Should monitor EVFILT_WRITE events to handle
ssize_t ff_readv(int fd, const struct iovec *iov, int iovcnt);
// Scatter-gather read
ssize_t ff_writev(int fd, const struct iovec *iov, int iovcnt);
// Scatter-gather write
ssize_t ff_send(int fd, const void *buf, size_t len, int flags);
ssize_t ff_sendto(int fd, const void *buf, size_t len, int flags,
const struct linux_sockaddr *to, socklen_t tolen);
ssize_t ff_sendmsg(int fd, const struct msghdr *msg, int flags);
// Various send methods (flags support MSG_MORE, etc.)
ssize_t ff_recv(int fd, void *buf, size_t len, int flags);
ssize_t ff_recvfrom(int fd, void *buf, size_t len, int flags,
struct linux_sockaddr *from, socklen_t *fromlen);
ssize_t ff_recvmsg(int fd, struct msghdr *msg, int flags);
// Various receive methodsEvent Multiplexing (Kqueue):
int ff_kqueue(void);
// Create an event object (similar to epoll_create)
// Returns: Event object fd
int ff_kevent(int kq, const struct kevent *changelist, int nchanges,
struct kevent *eventlist, int nevents,
const struct timespec *timeout);
// Register events and wait for events
// changelist: Array of events to register
// eventlist: Array of returned ready events
// timeout: NULL for blocking, 0 for non-blocking, > 0 for timeout wait
// Returns: Number of returned events
int ff_kevent_do_each(int kq, const struct kevent *changelist, int nchanges,
void *eventlist, int nevents,
const struct timespec *timeout,
void (*do_each)(void **, struct kevent *));
// Convenience function: internally calls ff_kevent and invokes do_each for each event
int ff_select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// select() implementation (for compatibility only)
int ff_poll(struct pollfd *fds, nfds_t nfds, int timeout);
// poll() implementation (for compatibility only)Epoll Compatibility Layer (Linux API):
int ff_epoll_create(int size);
// Create an epoll object (actually calls ff_kqueue)
int ff_epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// Register/modify/delete epoll events
// op: EPOLL_CTL_ADD / EPOLL_CTL_MOD / EPOLL_CTL_DEL
int ff_epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// Wait for events
// Supported epoll event flags:
#define EPOLLIN 0x001 // Readable
#define EPOLLOUT 0x004 // Writable
#define EPOLLET 0x80000000 // Edge-triggered (EV_CLEAR)
#define EPOLLONESHOT 0x40000000 // One-shot trigger (EV_ONESHOT)Control Operations:
int ff_setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
// Set socket options
// level: SOL_SOCKET / IPPROTO_IP / IPPROTO_TCP / ...
int ff_getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);
// Get socket options
int ff_ioctl(int fd, unsigned long request, ...);
// I/O control
// Common: FIONBIO (set non-blocking), FIONREAD (query readable bytes)
int ff_fcntl(int fd, int cmd, ...);
// File control
// Common: F_SETFL O_NONBLOCK, F_GETFL
struct hostent * ff_gethostbyname(const char *name);
// DNS resolution (basic implementation)
struct hostent * ff_gethostbyname2(const char *name, int af);
// DNS resolution (specify AF_INET or AF_INET6)Route Management:
int ff_route_ctl(enum FF_ROUTE_CTL req, enum FF_ROUTE_FLAG flag,
struct linux_sockaddr *dst, struct linux_sockaddr *gw,
struct linux_sockaddr *netmask);
// Routing operations
// req: FF_ROUTE_ADD / FF_ROUTE_DEL / FF_ROUTE_CHANGE
// flag: FF_RTF_HOST / FF_RTF_GATEWAYZero-Copy Mbuf:
struct ff_mbuf * ff_mbuf_gethdr(void);
struct ff_mbuf * ff_mbuf_get(const void *data, uint16_t len);
void ff_mbuf_free(struct ff_mbuf *mbuf);
ssize_t ff_mbuf_copydata(struct ff_mbuf *mbuf, uint16_t off, uint16_t len, void *buf);
int ff_zc_mbuf_get(struct ff_zc_mbuf *m, int len);
// Get a zero-copy mbuf (0 on success, -1 on failure)
ssize_t ff_zc_mbuf_write(int fd, struct ff_zc_mbuf *zm);
ssize_t ff_zc_mbuf_read(int fd, struct ff_zc_mbuf *zm);
// Zero-copy I/O [Note] ff_zc_mbuf_read is not yet implementedLogging Interface:
#define FF_LOGTYPE_EAL 0
#define FF_LOGTYPE_MALLOC 1
#define FF_LOGTYPE_RING 2
#define FF_LOGTYPE_MEMPOOL 3
#define FF_LOGTYPE_USER1 20
#define FF_LOGTYPE_USER8 27
int ff_log_open_set(void);
int ff_log_set_level(uint32_t logtype, uint32_t level);
int ff_log(uint32_t level, uint32_t logtype, const char *format, ...);
// Log levels (values start from 1, 0 means disabled)
#define FF_LOG_EMERG 1U
#define FF_LOG_ALERT 2U
#define FF_LOG_CRIT 3U
#define FF_LOG_ERR 4U
#define FF_LOG_WARNING 5U
#define FF_LOG_NOTICE 6U
#define FF_LOG_INFO 7U
#define FF_LOG_DEBUG 8UMulti-Threading Support:
int ff_pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
int ff_pthread_join(pthread_t thread, void **retval);System Interfaces:
int ff_gettimeofday(struct timeval *tv, struct timezone *tz);
int ff_clock_gettime(clockid_t clock_id, struct timespec *tp);
// High-precision time (supports CLOCK_REALTIME / CLOCK_MONOTONIC)Wraps ff_kqueue as epoll interface for Linux application compatibility.
Core Structure:
struct ff_config {
char *filename;
struct { // DPDK configuration
char *proc_type;
char *lcore_mask; // CPU core mask (hexadecimal)
char *proc_mask;
char *base_virtaddr;
char *file_prefix;
char *allow; // PCI whitelist
int nb_channel; // Number of memory channels
int memory; // Reserved memory (MB)
int no_huge;
int nb_procs;
int proc_id;
int promiscuous; // Promiscuous mode
int nb_vdev;
int nb_bond;
int numa_on; // NUMA support
int tso;
int tx_csum_offoad_skip;
int vlan_strip;
int nb_vlan_filter;
uint16_t vlan_filter_id[DPDK_MAX_VLAN_FILTER];
int symmetric_rss;
unsigned idle_sleep;
unsigned pkt_tx_delay;
uint16_t *proc_lcore;
int nb_ports;
uint16_t max_portid;
uint16_t *portid_list;
uint16_t log_level;
struct ff_port_cfg *port_cfgs;
struct ff_vlan_cfg *vlan_cfgs;
struct ff_vdev_cfg *vdev_cfgs;
struct ff_bond_cfg *bond_cfgs;
struct ff_rss_check_cfg *rss_check_cfgs;
} dpdk;
struct { // KNI configuration
int enable;
int console_packets_ratelimit;
int general_packets_ratelimit;
int kernel_packets_ratelimit;
char *kni_action;
char *method;
char *tcp_port;
char *udp_port;
} kni;
struct {
int level;
const char *dir;
void *f; /* FILE * */
} log;
struct { // FreeBSD boot parameters
struct ff_freebsd_cfg *boot;
struct ff_freebsd_cfg *sysctl;
long physmem;
int hz; // Clock frequency (1000 = 1kHz)
int fd_reserve; // Reserved fd count
int mem_size;
} freebsd;
struct {
uint16_t enable;
uint16_t snap_len;
uint32_t save_len;
char* save_path;
} pcap;
};struct kevent {
uintptr_t ident; // Event ID (socket fd)
int16_t filter; // Event filter (values are negative)
uint16_t flags; // Event flags
uint32_t fflags; // Filter flags
__int64_t data; // Data (ready count/error, fixed 64-bit)
void *udata; // User data pointer
__uint64_t ext[4]; // FreeBSD 13/15 extended fields (KBI unchanged across 13.0 → 15.0; M2 verify-only)
};
// Supported filters (values are negative!)
#define EVFILT_READ (-1) // Socket readable
#define EVFILT_WRITE (-2) // Socket writable
#define EVFILT_TIMER (-7) // Timer
// ... 10 more types, see freebsd/sys/event.h
// Event flags
#define EV_ADD 0x0001 // Add event
#define EV_DELETE 0x0002 // Delete event
#define EV_ONESHOT 0x0010 // One-shot trigger
#define EV_CLEAR 0x0020 // Auto-clear (edge-triggered)
#define EV_EOF 0x8000 // Connection closedProvides POSIX/FreeBSD-compatible error numbers:
#define ff_EPERM 1 // Operation not permitted
#define ff_ENOENT 2 // No such file or directory
#define ff_ECONNREFUSED 61 // Connection refused
#define ff_ETIMEDOUT 60 // Connection timed out
#define ff_ECONNRESET 54 // Connection reset
#define ff_EAGAIN 35 // Try again
#define ff_EINPROGRESS 36 // Operation in progress
// ... etc.A structured logging system supporting multiple log types and levels.
F-Stack provides Linux-compatible system call interfaces, but relies on the FreeBSD protocol stack underneath. Key mappings:
| Operation | F-Stack API | POSIX Standard | Key Differences |
|---|---|---|---|
| Create | ff_socket() | socket() | Address format: linux_sockaddr |
| Bind | ff_bind() | bind() | Non-blocking enforced |
| Listen | ff_listen() | listen() | TCP only |
| Accept | ff_accept() | accept() | Returns new connection fd |
| Connect | ff_connect() | connect() | May return EINPROGRESS |
| Close | ff_close() | close() | Graceful close |
| Operation | F-Stack API | POSIX Standard | Key Differences |
|---|---|---|---|
| Read | ff_read() | read() | Non-blocking, returns -1 when no data |
| Write | ff_write() | write() | Returns -1 when buffer full, no partial send |
| Scatter read | ff_readv() | readv() | Same as above |
| Scatter write | ff_writev() | writev() | Same as above |
| Send | ff_send() | send() | flags: MSG_MORE, etc. |
| Receive | ff_recv() | recv() | Same as above |
| Send to | ff_sendto() | sendto() | UDP only |
| Send message | ff_sendmsg() | sendmsg() | Supports msghdr control info |
| Receive from | ff_recvfrom() | recvfrom() | UDP only |
| Receive message | ff_recvmsg() | recvmsg() | Supports msghdr control info |
| Operation | F-Stack API | POSIX Standard | Key Differences |
|---|---|---|---|
| Create queue | ff_kqueue() | epoll_create() | Uses BSD kqueue |
| Register events | ff_kevent() | epoll_ctl() | kevent format |
| Wait for events | ff_kevent() + ff_epoll_wait() | epoll_wait() | Supports timeout |
| Iterate events | ff_kevent_do_each() | N/A | Convenience function |
#define LINUX_SO_REUSEADDR 2 → SO_REUSEADDR
#define LINUX_SO_TYPE 3 → SO_TYPE
#define LINUX_SO_ERROR 4 → SO_ERROR
#define LINUX_SO_DONTROUTE 5 → SO_DONTROUTE
#define LINUX_SO_BROADCAST 6 → SO_BROADCAST
#define LINUX_SO_SNDBUF 7 → SO_SNDBUF
#define LINUX_SO_RCVBUF 8 → SO_RCVBUF
#define LINUX_SO_KEEPALIVE 10 → SO_KEEPALIVE
#define LINUX_SO_OOBINLINE 11 → SO_OOBINLINE
#define LINUX_SO_REUSEPORT 15 → SO_REUSEPORT#define LINUX_IP_TOS 1 → IP_TOS
#define LINUX_IP_TTL 2 → IP_TTL
#define LINUX_IP_HDRINCL 3 → IP_HDRINCL
#define LINUX_IP_OPTIONS 4 → IP_OPTIONS
#define LINUX_IP_ROUTER_ALERT 5 → IP_ROUTER_ALERT
#define LINUX_IP_RECVOPTS 6 → IP_RECVOPTS
#define LINUX_IP_RETOPTS 7 → IP_RETOPTS
#define LINUX_IP_MULTICAST_IF 32 → IP_MULTICAST_IF
#define LINUX_IP_MULTICAST_TTL 33 → IP_MULTICAST_TTL#define LINUX_TCP_NODELAY 1 → TCP_NODELAY
#define LINUX_TCP_MAXSEG 2 → TCP_MAXSEG
#define LINUX_TCP_CORK 3 → TCP_CORK
#define LINUX_TCP_KEEPIDLE 4 → TCP_KEEPIDLE
#define LINUX_TCP_KEEPINTVL 5 → TCP_KEEPINTVL
#define LINUX_TCP_KEEPCNT 6 → TCP_KEEPCNT#define LINUX_FIONBIO 0x5421 // Set non-blocking
#define LINUX_FIONREAD 0x541B // Query readable bytes
#define LINUX_SIOCGIFFLAGS 0x8913 // Get NIC flags
#define LINUX_SIOCGIFADDR 0x8915 // Get NIC IP
#define LINUX_SIOCGIFNETMASK 0x891B // Get NIC netmaskF-Stack uses INI format configuration files, loaded via ff_load_config():
# Example configuration
[dpdk]
lcore_mask = 0xf # Use cores 0-3 (hexadecimal)
channel = 4 # Number of memory channels
memory = 4096 # Reserved memory (MB)
promiscuous = 1 # Promiscuous mode
numa_on = 1 # NUMA support
port_list = 0,1 # Use NICs 0 and 1
nb_vdev = 0 # Number of virtual devices
nb_bond = 0 # Number of bonding NICs
tso = 1 # Enable TSO (hardware segmentation)
vlan_strip = 1 # VLAN hardware tag stripping
symmetric_rss = 0 # Bidirectional RSS symmetry
idle_sleep = 0 # Idle sleep (microseconds)
pkt_tx_delay = 100 # Packet TX delay (set 0 for immediate send)
enable_kni = 1 # Enable virtual NIC
mbuf_low_watermark = 0 # mbuf water-level threshold (0=disabled; issue #1076)
primary_slim = 0 # Primary runs control-plane only (0=disabled; issue #1078)
primary_slim_idle_sleep = 1000 # Sleep (us) when primary_slim=1 (default 1000)
graceful_reload = 0 # Two generations per lcore_id (0=off; needs nginx
# master; all-secondary workers + resident primary)
reload_heartbeat_timeout_ms = 1000 # G_old stall detection on G_new (0 -> default)
[port0]
addr = 10.0.0.1
netmask = 255.255.255.0
gateway = 10.0.0.254
broadcast = 10.0.0.255
vip_addr = 10.0.0.100; 10.0.0.101; 10.0.0.102
addr6 = 2001:db8::1
prefix_len = 64
gateway6 = 2001:db8::ff
ipfw_pr = 10.0.0.100 255.255.255.0;192.168.0.0 255.255.255.0
lcore_list = 0,1
[port1]
addr = 192.168.1.1
netmask = 255.255.255.0
gateway = 192.168.1.254
[vlan<vlan id>]
# VLAN configuration (higher priority than [portN])
portid = 0
addr = 172.16.0.1
netmask = 255.255.0.0
gateway = 172.16.0.1
broadcast = 172.16.255.255
[kni]
enable=1
method=reject
tcp_port=80,443
udp_port=53
[stack]
kernel_coexist = 0 # Kernel-stack coexistence (only when built with
# FF_KERNEL_COEXIST=1). 0=off (pure F-Stack, default),
# 1=on (SOCK_KERNEL sockets also use the host kernel stack)
[freebsd.boot]
hz = 100 # Clock frequency (Hz)
fd_reserve = 1204
kern.ipc.maxsockets = 1000000
[freebsd.sysctl]
kern.ipc.somaxconn = 32768
net.inet.tcp.syncache.hashsize = 4096
net.inet.tcp.syncache.bucketlimit = 100
net.inet.tcp.cc.algorithm = cubic
net.inet.tcp.functions_default=freebsd # freebsd/rack/bbr
net.inet.tcp.sendspace = 16384 # Send buffer (bytes)
net.inet.tcp.recvspace = 8192 # Receive buffer
net.inet.tcp.sack.enable = 1 # SACK selective acknowledgment
net.inet.tcp.rfc1323 = 1 # Timestamp option
net.inet.tcp.delayed_ack = 1 # Delayed ACK: 1 for high throughput, 0 for low latencyConfiguration priority order (highest to lowest):
1. VLAN configuration ([vlanN]) - Used if defined
2. Port configuration ([portN])
3. Command-line arguments (e.g., DPDK -l option)
4. Compile-time defaults
5. Hardcoded values in source code
Note: If both [port0] and [vlan0] are defined, [vlan0] takes effect and [port0] is ignored
// ff_config.c
ff_load_config(argc, argv)
├─ 1. Parse command-line arguments
│ (e.g., --config-file /path/config.ini)
├─ 2. Open configuration file
├─ 3. Parse INI format line by line
│ [section] markers
│ key = value pairs
├─ 4. Validate configuration
│ (address ranges, core count, etc.)
├─ 5. Convert to struct ff_config
└─ 6. Generate DPDK parameter array
(passed to rte_eal_init)When the library is built with make FF_KERNEL_COEXIST=1 and config.ini [stack] kernel_coexist=1, the application can place individual sockets on the host Linux kernel stack while everything else keeps using the F-Stack fast path — all from the same process and event loop. The contract is intentionally small and additive (no existing signature changes):
Per-socket stack markers (defined in ff_api.h, only under FF_KERNEL_COEXIST):
#define SOCK_FSTACK 0x01000000 // force the F-Stack user-space stack
#define SOCK_KERNEL 0x02000000 // force the host Linux kernel stack
int fk = ff_socket(AF_INET, SOCK_STREAM | SOCK_KERNEL, 0); // kernel-stack socket
int ff = ff_socket(AF_INET, SOCK_STREAM | SOCK_FSTACK, 0); // F-Stack socket
int du = ff_socket(AF_INET, SOCK_STREAM, 0); // dual-created (default)type flag |
Behaviour when kernel_coexist=1 |
Returned fd |
|---|---|---|
SOCK_KERNEL (no SOCK_FSTACK) |
host kernel socket only | managed kernel fd = host_fd + 0x40000000 |
SOCK_FSTACK |
F-Stack socket only | F-Stack fd |
| neither (default) | dual-created: F-Stack + paired host socket | F-Stack fd (host fd kept in ff_native_fd_map) |
Priority: per-socket marker > config.ini [stack] kernel_coexist > F-Stack. When the macro is not compiled in, the markers are undefined and ff_socket behaves exactly as before.
Transparent routing. All standard ff_* calls accept a managed kernel fd transparently: ff_bind/listen/connect/accept[4]/close/read/write/recv*/send*/sendmsg/recvmsg/getpeername/getsockname/shutdown/setsockopt/getsockopt/fcntl, and ff_epoll_ctl/wait. Internally each kernel fd is forwarded to a thin ff_host_* host-libc bridge (lib/ff_host_interface.c). Since R10 also accepted transparently: ff_readv / ff_writev / ff_ioctl / ff_dup / ff_dup2 (kernel fd via the matching ff_host_* bridge; ff_ioctl uses the raw Linux request; ff_dup2 cross-stack rejected errno=EINVAL). Full design and tests: docs/kernel_event_support_spec/.
R9 — kqueue/kevent coexistence + IPv6. The unified-event support now also covers the native ff_kqueue / ff_kevent interface (previously only ff_epoll_*): each kqueue lazily pairs one host epoll (shared ff_epoll_host_ep, reusing the ff_epoll_pairs table). ff_kevent registers a kernel/dual-stack fd's EVFILT_READ/WRITE into that host epoll (kernel-only changes are not forwarded to the F-Stack kqueue), and on wait it synthesizes struct kevent (ident=the app-side fd, EV_EOF↔EPOLLHUP|ERR) from a non-blocking host-epoll poll before merging F-Stack kqueue events. This makes a pure-kqueue application (e.g. example/main.c) reach the kernel-side listener — measured curl 127.0.0.1:80 = 200 size=438. Kernel fds via kqueue support EVFILT_READ/WRITE only. On the IPv6 side, a dual-built AF_INET6 socket has its host counterpart set to IPV6_V6ONLY=1 (ff_host_set_v6only) so a -DINET6 build starts cleanly with v4+v6 on the same port (fixes the prior host-IPv6 errno=98 EADDRINUSE).
R10 — residual-entry coexistence. ff_readv/ff_writev (kernel fd → ff_host_readv/writev, mimic read/write, connection fds single-stack hot path), ff_ioctl (kernel fd uses the raw Linux request straight to ff_host_ioctl, NOT via linux2freebsd_ioctl; dual-stack fd same-driver since R10.1 syncs FIONBIO/FIOASYNC to the paired host fd (query ioctls like FIONREAD not forwarded, to avoid clobbering argp)), ff_dup (kernel fd → ff_host_dup+encode), ff_dup2 (both-kernel → ff_host_dup2+encode; cross-stack rejected errno=EINVAL). Adds 5 host bridges ff_host_readv/writev/ioctl/dup/dup2. Known limitation: ff_select (encode kernel fd ≫ FD_SETSIZE hard limit) / ff_poll (conservatively not implemented) do not support kernel-fd coexistence — use ff_epoll_*/ff_kqueue.
R-F/R-G — RSS connect-side reverse path + IPv6 reverse-proxy fix (internal, no public API change). The internal RSS source-port reverse-computation helpers ff_rss_adjust_sport / ff_rss_adjust_sport6 gained two range parameters uint16_t first, uint16_t last (ephemeral range from freebsd/netinet/in_pcb.c), and now build the tuple in reply (inbound SYN-ACK) field order so the reply lands on the local RX queue; the NIC RSS KEY_FINAL is built and published before dev_configure by ff_rss_thash_build_key. Gated by [rss_check] thash_adjust (default on, decoupled from rss_check.enable); diagnostics gated by compile macro FF_RSS_DIAG (default off). No public ff_* socket API signature changes. Separately, lib/ff_veth.c fixes IPv6 reverse-proxy addressing on FreeBSD 15 (VIP6 as /128 host addr, link-local gateway in6_setscope, ND6_IFF_NO_DAD to skip DAD since ip6_input drops unicast to NOTREADY/TENTATIVE). Details: docs/ff_rss_check_opt_spec/zh_cn/.
When maxsockets is configured very high (e.g. 1,048,576) but the DPDK mbuf pool is relatively small (~8K–32K entries), mbufs can exhaust before sockets under a SYN flood or high connection rate, causing the entire stack to become unresponsive — no mbufs left to receive even ACK/FIN packets for existing connections.
Configuration (config.ini [dpdk]):
| Parameter | Type | Default | Description |
|---|---|---|---|
mbuf_low_watermark |
unsigned | 0 (disabled) | When > 0, TCP SYN packets are dropped at process_packets() entry if rte_mempool_avail_count() < mbuf_low_watermark. Non-SYN TCP and non-TCP packets are not affected. Set to ~10-20% of mbuf pool size. |
Implementation: is_tcp_syn() (lib/ff_dpdk_if.c) parses Ethernet/VLAN/IPv4/IPv6/TCP headers to identify SYN packets. The water-level check in process_packets() calls rte_mempool_avail_count() before processing; below the threshold, SYN packets are silently dropped (no SYN-ACK), preserving mbufs for established connections.
Four FreeBSD native CC limiting mechanisms (maxsockets, ipfw limit rules, somaxconn, syncache) were confirmed functional at code level, but they operate at the socket layer and cannot prevent mbuf exhaustion when maxsockets is oversized. The water-level backpressure is a complementary safety net.
See docs/issue_1076/zh_cn/ for full analysis.
When running multi-process F-Stack (nb_procs > 1), the primary process can be configured to run only the control plane (NIC initialization, KNI init, IPC server, heap-expansion proxy) and skip the data-plane RX loop, offloading all packet processing to secondary processes.
Configuration (config.ini [dpdk]):
| Parameter | Type | Default | Description |
|---|---|---|---|
primary_slim |
int | 0 (disabled) | When 1, the primary runs control-plane only; no RX queues are assigned. Queue count derives from lcore_list length, so removing the primary's lcore shrinks queues/RSS reta consistently. |
primary_slim_idle_sleep |
unsigned | 1000 (µs) | rte_pause() sleep duration when the slim primary has no work. |
DPDK hard constraints (primary must stay resident even when slim):
- The primary is the sole IPC server (secondaries connect via DPDK multi-process IPC).
- Secondary heap expansion (
rte_malloc_heap_memory_add) must be proxied by the primary. - All interrupt callbacks fire only on the primary.
New API: ff_is_slim_primary() (lib/ff_api.h) — returns true when this process is the primary and primary_slim=1.
Validation chain (V2–V6 in ff_config.c): validates primary_slim=1 requires multi-process, proc_type=primary, no lcore_list overlap with secondaries, all data-plane lcores covered by secondaries, and idle_sleep sanity.
KNI runtime owner (K4): a secondary process can be the runtime KNI owner when the primary is slim. ff_kni_is_runtime_owner() (lib/ff_dpdk_kni.c) replaces the hard-coded primary check with an owner_proc_id-based check.
See docs/issue_1078/zh_cn/ for full spec and verification.
F-Stack adopts DPDK's Primary-Secondary process model:
// Command-line setting at startup
proc_type=primary proc_id=0
ff_init(argc, argv)
├─ Initialize DPDK EAL (rte_eal_init)
│ ├─ Allocate hugepage memory
│ ├─ Create shared memory mapping
│ └─ Initialize core 0
├─ Create mempool (shared by all processes)
├─ Initialize NIC (rte_eth_dev_configure)
├─ Start NIC (rte_eth_dev_start)
└─ Enter polling loop (ff_run)
├─ Receive/process packets
└─ Handle IPC messages from workers// Command-line setting at startup
proc_type=secondary proc_id=1 // Different secondary processes use different IDs
ff_init(argc, argv)
├─ Connect to DPDK shared memory (created by primary process)
│ ├─ Map hugepage memory
│ ├─ Connect to mempool
│ └─ Access NIC configuration
├─ Initialize local FreeBSD stack instance
└─ Enter polling loop (ff_run)
├─ Receive/process packets belonging to this process (via RSS)
└─ Optional: Send IPC messages to primary process# Use F-Stack's built-in start.sh to launch (recommended)
# start.sh parameter description:
# -c [conf] Configuration file path (default: config.ini)
# -b [bin] Application path (default: ./example/helloworld)
# -o [args] Extra arguments passed to the application
# Example: Launch a custom application with config.ini
bash start.sh -c config.ini -b ./app
# start.sh will automatically:
# 1. Read lcore_mask from config.ini to determine the number of processes
# 2. Start primary process: ./app --conf config.ini --proc-type=primary --proc-id=0
# 3. Wait 5 seconds then sequentially start secondary processes:
# ./app --conf config.ini --proc-type=secondary --proc-id=1
# ./app --conf config.ini --proc-type=secondary --proc-id=2
# To pass extra arguments to the application:
bash start.sh -c config.ini -b ./app -o "--extra-arg value"F-Stack implements efficient inter-process message passing via DPDK Ring:
struct ff_msg {
uint32_t msg_type; // Message type
uint32_t msg_id; // Message ID (for matching request/response)
uint32_t msg_len; // Message length
union {
struct {
uint32_t sysctl_id;
uint32_t value;
} sysctl;
struct {
uint32_t route_cmd; // ROUTE_CMD_ADD/DEL/CHANGE
struct sockaddr addr;
struct sockaddr mask;
} route;
struct {
uint64_t tx_packets;
uint64_t rx_packets;
uint64_t tx_bytes;
uint64_t rx_bytes;
} traffic;
char data[256]; // Generic data
} msg_body;
};
// Message types
#define FF_MSG_SYSCTL 1
#define FF_MSG_IOCTL 2
#define FF_MSG_ROUTE 3
#define FF_MSG_TOP 4
#define FF_MSG_TRAFFIC 5
#define FF_MSG_NGCTL 6
#define FF_MSG_IPFW_CTL 7
#define FF_MSG_KNICTL 8// Secondary process querying parameters from primary process
// Step 1: Connect to primary process
ff_ipc_init(); // Initialize IPC (connect to primary process's Ring)
// Step 2: Allocate message
struct ff_msg *msg = ff_ipc_msg_alloc();
// Step 3: Fill in request
msg->msg_type = FF_MSG_TRAFFIC;
msg->msg_id = 1;
// Step 4: Send to primary process
ff_ipc_send(msg);
// Step 5: Wait for response
struct ff_msg *retmsg = NULL;
ff_ipc_recv(&retmsg, FF_MSG_TRAFFIC);
// Step 6: Read response
printf("TX packets: %lu\n", retmsg->msg_body.traffic.tx_packets);
// Step 7: Free messages
ff_ipc_msg_free(msg);
ff_ipc_msg_free(retmsg);Reload control does not use a new channel: it rides the existing per-generation
msg rings as message type FF_RELOAD, with the subcommand in ff_reload_args.
enum FF_RELOAD_CMD {
FF_RELOAD_CMD_UNKNOWN = 0,
FF_RELOAD_CMD_READY, /* worker: generation bound and running */
FF_RELOAD_CMD_HANDOVER_REQ, /* master: park rx, hand hardware over */
FF_RELOAD_CMD_HANDOVER_ACK,
FF_RELOAD_CMD_DRAIN_PROGRESS, /* worker: conns / snd_pending / syncache */
FF_RELOAD_CMD_DRAIN_DONE,
FF_RELOAD_CMD_REJECT, /* reload refused, old generation resumes */
FF_RELOAD_CMD_QUERY, /* observability: current reload view */
};
struct ff_reload_args {
uint32_t cmd; /* enum FF_RELOAD_CMD */
uint32_t gen; /* sender or target generation */
uint32_t status; /* command result / progress */
uint32_t active_gen; /* serving generation */
};Ring naming is generation- and epoch-aware, so a tool has to name the generation
it wants to talk to: ff_ipc accepts "<proc>[:<gen>[:<epoch>]]" and resolves
AUTO on the first send. With graceful_reload=1 the resident slim primary
(proc_id 0) serves both generations and never leaves, so it is always reachable.
F-Stack provides basic multi-threading support, but sockets are not shared between threads:
// Create a thread
pthread_t thread;
int ret = ff_pthread_create(&thread, NULL, thread_func, arg);
// Each thread must:
// 1. Call ff_init() - Create an independent FreeBSD stack instance
// 2. Obtain an independent lcore number
// 3. Use ff_run() to enter the polling loop
// Thread safety
// ✓ Socket operations within the same thread are safe
// ✗ Sockets cannot be shared across threads
// ✗ Cannot create new threads after ff_run()Native Multi-Thread Mode (thread_mode=1, v1.26+):
Since v1.26, F-Stack supports thread_mode=1 in config.ini for single-process multi-thread operation. Each worker thread gets its own dense pcpu slot with per-thread curcpu, UMA/SMR per-CPU cache slot, and callout callwheel. The uma_crit_lock global spinlock that previously serialized UMA access has been removed (slot isolation is the sole protection). See docs/native_mt_spec/ for the full design and runtime test data.
Note:
ff_pthread_create()-created application threads do not get a pcpu slot (they havepcpup == NULL). Calling anyff_*stack API from such threads is unsupported and will crash (fail-fast by design). Only threads created via theff_stack_thread_init()path (i.e.,main_loopworkers inthread_mode=1) have valid pcpu slots.
Example - Multi-Threaded Application:
struct thread_arg {
int thread_id;
const char *bind_addr;
int bind_port;
};
void *thread_func(void *arg) {
struct thread_arg *ta = (struct thread_arg *)arg;
ff_init(0, NULL); // Obtain independent lcore
int sockfd = ff_socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(ta->bind_port),
.sin_addr.s_addr = inet_addr(ta->bind_addr)
};
ff_bind(sockfd, (struct linux_sockaddr *)&addr, sizeof(addr));
ff_listen(sockfd, 128);
ff_run(my_loop, (void *)(intptr_t)sockfd);
return NULL;
}
int main() {
pthread_t threads[4];
struct thread_arg args[4] = {
{0, "10.0.0.1", 80}, {1, "10.0.0.2", 80},
{2, "10.0.0.3", 80}, {3, "10.0.0.4", 80}
};
for (int i = 0; i < 4; i++)
ff_pthread_create(&threads[i], NULL, thread_func, &args[i]);
for (int i = 0; i < 4; i++)
ff_pthread_join(threads[i], NULL);
return 0;
}#include <ff_api.h>
int main_loop(void *arg) {
// Event-ready handler function
// Called once per polling cycle; must return within 100μs
int kq = (int)(intptr_t)arg;
struct kevent events[64];
int nevents = ff_kevent(kq, NULL, 0, events, 64, NULL);
for (int i = 0; i < nevents; i++) {
if (events[i].flags & EV_EOF) {
ff_close((int)events[i].ident);
} else if (events[i].filter == EVFILT_READ) {
char buf[4096];
ssize_t n = ff_read((int)events[i].ident, buf, sizeof(buf));
// Process the read data
} else if (events[i].filter == EVFILT_WRITE) {
// Write buffer has space, can continue sending
}
}
return 0;
}
int main(int argc, char *argv[]) {
ff_init(argc, argv);
int sockfd = ff_socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {...};
ff_bind(sockfd, (struct linux_sockaddr *)&addr, sizeof(addr));
ff_listen(sockfd, 128);
int kq = ff_kqueue();
struct kevent kev;
EV_SET(&kev, sockfd, EVFILT_READ, EV_ADD, 0, 0, NULL);
ff_kevent(kq, &kev, 1, NULL, 0, NULL);
ff_run(main_loop, (void *)(intptr_t)kq);
return 0;
}#include <ff_api.h>
int main_loop(void *arg) {
int epfd = (int)(intptr_t)arg;
struct epoll_event events[64];
int nevents = ff_epoll_wait(epfd, events, 64, 0); // Non-blocking
for (int i = 0; i < nevents; i++) {
int fd = events[i].data.fd;
if (events[i].events & EPOLLIN)
ff_read(fd, buf, sizeof(buf));
if (events[i].events & EPOLLOUT)
ff_write(fd, data, len);
}
return 0;
}
int main(int argc, char *argv[]) {
ff_init(argc, argv);
int sockfd = ff_socket(AF_INET, SOCK_STREAM, 0);
// ... bind/listen ...
int epfd = ff_epoll_create(0);
struct epoll_event ev = { .events = EPOLLIN | EPOLLOUT, .data.fd = sockfd };
ff_epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
ff_run(main_loop, (void *)(intptr_t)epfd);
return 0;
}Rule 1: Mandatory Non-Blocking
int flags = 1;
ff_ioctl(sockfd, FIONBIO, &flags); // Must set!Rule 2: Main Loop Time Limit
// ✗ Wrong: Blocks too long
int main_loop(void *arg) { sleep(1); return 0; }
// ✓ Correct: Keep within 100μs
int main_loop(void *arg) {
ff_kevent(...); // Non-blocking query
return 0;
}Rule 3: Write Buffer Management
// ✗ Wrong: Assume ff_write always succeeds
ssize_t n = ff_write(fd, data, len);
// ✓ Correct: Monitor EVFILT_WRITE events
ssize_t n = ff_write(fd, data, len);
if (n < 0) {
struct kevent kev;
EV_SET(&kev, fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL);
ff_kevent(kq, &kev, 1, NULL, 0, NULL);
}Rule 4: Connection Close Handling
// ✓ Correct: Check EV_EOF first
if (events[i].flags & EV_EOF) {
ff_close(fd);
} else if (events[i].filter == EVFILT_READ) {
ff_read(fd, buf, size);
}Rule 5: Socket Thread Isolation
// ✗ Wrong: Share socket across threads
int sockfd = ff_socket(...); // Created in main thread
ff_pthread_create(&tid, NULL, worker_thread, (void *)(intptr_t)sockfd);
// ✓ Correct: Each thread creates its own socket
void *worker_thread(void *arg) {
int sockfd = ff_socket(...); // Created within thread
}| Pitfall | Symptom | Root Cause | Solution |
|---|---|---|---|
| FIONBIO not set | Deadlock/latency | Socket in blocking mode | ff_ioctl(fd, FIONBIO, &on) |
| Main loop too long | Packet loss/latency | Polling blocked | Keep loop function < 100μs |
| Cross-thread socket sharing | Crash/race condition | FreeBSD stack not thread-safe | Thread-isolate sockets |
| EV_EOF ignored | fd leak/anomaly | Unhandled connection close | Check ev.flags & EV_EOF |
| Write buffer full unhandled | Data loss | ff_write returns -1 | Monitor EVFILT_WRITE and retry |
| Config file missing | Init failure | Wrong path | Check config.ini location |
| Insufficient memory (hugepage) | Init failure | Hugepage allocation failed | sysctl vm.nr_hugepages=2048 |
| Inadequate CPU isolation | Performance degradation | CPU occupied by other processes | Use CPU affinity tools |
1. Adjust Socket Buffers
[freebsd.sysctl]
net.inet.tcp.sendspace = 65536 # 64KB send buffer
net.inet.tcp.recvspace = 65536 # 64KB receive buffer2. Enable TSO (TCP Segmentation Offload)
[dpdk]
tso = 1 # Enable hardware TSO3. Optimize Packet TX Delay
[dpdk]
pkt_tx_delay = 0 # Send immediately, do not wait for batch4. Enable Symmetric RSS
[dpdk]
symmetric_rss = 1 # Bidirectional connections to same queue5. TCP Algorithm Selection
[freebsd.sysctl]
# High-latency network: bbr (Bottleneck Bandwidth and Round-trip time)
# hz=1000000
# net.inet.tcp.functions_default=bbr
# Low-latency network: cubic (default, balanced)
# hz=100
# net.inet.tcp.functions_default=freebsd
# net.inet.tcp.cc.algorithm = cubic
# Specific scenarios: rack (TCP Selective Acknowledgment)
# hz=1000000
# net.inet.tcp.functions_default=rack| Tool | Purpose | Underlying Message Type | Example Command |
|---|---|---|---|
| top | Real-time CPU stats | FF_TOP | top -p <pid> |
| sysctl | Parameter query/modify | FF_SYSCTL | sysctl net.inet.tcp.sendspace |
| ifconfig | NIC status | Direct config read | ifconfig -a |
| route | Routing table management | FF_ROUTE | route add/del |
| netstat | Network statistics | FF_TRAFFIC | netstat -s |
| arp | ARP table query | Direct memory read | arp -a |
| ipfw | Firewall rules | FF_IPFW_CTL | ipfw add ... |
| knictl | Virtual NIC control | FF_KNICTL | knictl set-rate ... |
| traffic | Traffic statistics export | FF_TRAFFIC | traffic -p <proc_id> -d <secs> |
| ndp | IPv6 Neighbor Discovery | ioctl (SIOCGNBRINFO_IN6) | ndp -C <proc_id> -a |
| ngctl | Netgraph control | FF_NGCTL | ngctl -p <proc_id> list |
| ff_ipc | Reload/generation-aware IPC for all tools | FF_RELOAD (query) | ff_ipc -p 0:auto |
Direct Call Mode: Application directly links libfstack.a and calls ff_* APIs.
LD_PRELOAD Mode: Application is preloaded with libff_syscall.so (built from
adapter/syscall/); it runs as a separate process from the fstack instance
binary and the two communicate over Hugepage shared memory. The default IPC path is
semaphore-based; setting FF_USE_RING_IPC=1 switches to a lock-free DPDK SPSC ring.
The fstack instance must be started before the LD_PRELOAD application.
# Start the fstack instance(s) first
./fstack --conf config.ini --proc-type=primary --proc-id=0 &
# Nginx integration
LD_PRELOAD=/path/to/libff_syscall.so /usr/sbin/nginx -g "daemon off;"
# Redis integration
LD_PRELOAD=/path/to/libff_syscall.so /usr/bin/redis-server /etc/redis.conf
# Custom application
LD_PRELOAD=/path/to/libff_syscall.so ./my_appHooked POSIX entries include socket / bind / connect / accept / accept4 / listen / close / read / write / send / sendto / sendmsg / recv / recvfrom / recvmsg / __read_chk / __recv_chk / __recvfrom_chk / ioctl / epoll_create|ctl|wait / fork.
Common runtime / compile switches:
| Switch | Default | Effect |
|---|---|---|
FF_KERNEL_EVENT |
off | Forward kernel-only fds to host epoll in parallel |
FF_MULTI_SC |
off | SO_REUSEPORT-style multi-sc, one sc per worker fd |
FF_USE_RING_IPC |
off | Switch IPC to lock-free DPDK SPSC ring (v3.4 opts on by default) |
For the full design, supported modes, environment variables, performance notes and
known limitations, see adapter/syscall/README.md.
Key Interface Features:
- POSIX-Compatible - Linux applications can migrate without modification
- Mandatory Non-Blocking - All I/O operations are non-blocking
- Event-Driven - Multiple modes: Kqueue/Epoll/Select
- Multi-Process Support - Primary-Secondary model with IPC communication
- Hardware Acceleration - Full use of RSS/TSO/Checksum and other offloads
Development Path:
- Write a main_loop callback to handle events
- Register sockets to kqueue/epoll
- Call ff_run() to enter the polling loop
- Use IPC tools for operations management
Related Documents: