summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-26 13:35:58 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-10-26 13:35:58 +0100
commit0194b1b8a00d6646f03291e6045918aaaf7cfdc4 (patch)
treea9ec6437af9b487595bfe4afe96a949a8556f409
parent2e5b19945eb03fe4e3fa47562888401c7cbae71d (diff)
Introduce a sub system that can store io cookies for file descriptors, thread safely so
-rw-r--r--Makefile8
-rw-r--r--ot_fdstate.c164
-rw-r--r--ot_fdstate.h52
-rw-r--r--tests/runner.c1
-rw-r--r--tests/t_fdstate.c224
-rw-r--r--tests/test.h1
6 files changed, 446 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index c7105d4..3bf254a 100644
--- a/Makefile
+++ b/Makefile
@@ -68,8 +68,8 @@ CFLAGS+=-I$(LIBOWFAT_HEADERS) -DGIT_VERSION=$(GIT_VERSION) -Wall -pipe -pthread
68LDFLAGS+=-L$(LIBOWFAT_LIBRARY) -lowfat -pthread 68LDFLAGS+=-L$(LIBOWFAT_LIBRARY) -lowfat -pthread
69 69
70BINARY =opentracker 70BINARY =opentracker
71HEADERS=trackerlogic.h scan_urlencoded_query.h ot_batch.h ot_buf.h ot_hex.h ot_ip.h ot_config.h ot_scan.h ot_socket.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h 71HEADERS=trackerlogic.h scan_urlencoded_query.h ot_batch.h ot_buf.h ot_fdstate.h ot_hex.h ot_ip.h ot_config.h ot_scan.h ot_socket.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h
72SOURCES=opentracker.c trackerlogic.c scan_urlencoded_query.c ot_batch.c ot_buf.c ot_hex.c ot_ip.c ot_config.c ot_scan.c ot_socket.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c 72SOURCES=opentracker.c trackerlogic.c scan_urlencoded_query.c ot_batch.c ot_buf.c ot_fdstate.c ot_hex.c ot_ip.c ot_config.c ot_scan.c ot_socket.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c
73SOURCES_proxy=proxy.c ot_batch.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c 73SOURCES_proxy=proxy.c ot_batch.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c
74 74
75OBJECTS = $(SOURCES:%.c=%.o) 75OBJECTS = $(SOURCES:%.c=%.o)
@@ -119,8 +119,8 @@ $(OBJECTS_proxy_debug): $(HEADERS)
119# has to. tests/stubs.c supplies the few symbols those modules reference but 119# has to. tests/stubs.c supplies the few symbols those modules reference but
120# the tests never reach. 120# the tests never reach.
121TESTBIN=tests/runner 121TESTBIN=tests/runner
122TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_scan_urlencoded_query.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_batch.c 122TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_scan_urlencoded_query.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_batch.c tests/t_fdstate.c
123TEST_UNITS=scan_urlencoded_query.c ot_batch.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c 123TEST_UNITS=scan_urlencoded_query.c ot_batch.c ot_fdstate.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c
124TEST_HEADERS=tests/test.h $(HEADERS) 124TEST_HEADERS=tests/test.h $(HEADERS)
125# Tests reach code that sits behind feature flags, so the units under test are 125# Tests reach code that sits behind feature flags, so the units under test are
126# built with those features enabled. A test binary is not a production binary. 126# built with those features enabled. A test binary is not a production binary.
diff --git a/ot_fdstate.c b/ot_fdstate.c
new file mode 100644
index 0000000..6ddbea3
--- /dev/null
+++ b/ot_fdstate.c
@@ -0,0 +1,164 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6/* System */
7#include <stdatomic.h>
8#include <stdint.h>
9#include <stdlib.h>
10#include <sys/mman.h>
11#include <sys/resource.h>
12#include <sys/types.h>
13
14/* Opentracker */
15#include "ot_fdstate.h"
16
17enum { FD_FREE, FD_IDLE, FD_BUSY, FD_CLOSING };
18
19/* Generation and state share one word so that taking the state is also a
20 check that the generation has not moved */
21#define FD_GEN(w) ((uint32_t)((w) >> 32))
22#define FD_ST(w) ((uint32_t)(((w) >> 24) & 0xff))
23#define FD_WORD(g, s) (((uint64_t)(g) << 32) | ((uint64_t)(s) << 24))
24#define FD_SET_ST(w, s) (((w) & ~(uint64_t)0xff000000) | ((uint64_t)(s) << 24))
25
26typedef struct {
27 _Atomic uint64_t state;
28 _Atomic(void *) cookie;
29 _Atomic ot_time expiry;
30} ot_fdslot;
31
32/* A descriptor the kernel can hand out is below the limit it was handed out
33 under, but the limit can be raised after we sized the table, so an out of
34 range descriptor is rejected */
35#define FD_SLOTS_MAX (1u << 20)
36
37static ot_fdslot *g_slots;
38static size_t g_slot_count;
39
40int fdstate_init(void) {
41 struct rlimit limit;
42 size_t count = FD_SLOTS_MAX;
43
44 if (getrlimit(RLIMIT_NOFILE, &limit) == 0 && limit.rlim_cur != RLIM_INFINITY && limit.rlim_cur < FD_SLOTS_MAX)
45 count = (size_t)limit.rlim_cur;
46
47 g_slots = mmap(NULL, count * sizeof(ot_fdslot), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
48 if (g_slots == MAP_FAILED) {
49 g_slots = NULL;
50 return -1;
51 }
52
53 g_slot_count = count;
54 return 0;
55}
56
57void fdstate_deinit(void) {
58 if (g_slots)
59 munmap(g_slots, g_slot_count * sizeof(ot_fdslot));
60 g_slots = NULL;
61 g_slot_count = 0;
62}
63
64size_t fdstate_slots(void) { return g_slot_count; }
65
66static ot_fdslot *fdstate_slot(int fd) {
67 if (fd < 0 || (size_t)fd >= g_slot_count)
68 return NULL;
69 return g_slots + fd;
70}
71
72uint32_t fdstate_open(int fd, void *cookie, ot_time expiry) {
73 ot_fdslot *slot = fdstate_slot(fd);
74 uint64_t word;
75
76 if (!slot)
77 return 0;
78
79 word = atomic_load_explicit(&slot->state, memory_order_relaxed);
80 atomic_store_explicit(&slot->cookie, cookie, memory_order_relaxed);
81 atomic_store_explicit(&slot->expiry, expiry, memory_order_relaxed);
82 /* Publishes the two above to whoever claims the slot next */
83 atomic_store_explicit(&slot->state, FD_WORD(FD_GEN(word), FD_IDLE), memory_order_release);
84
85 return FD_GEN(word);
86}
87
88int fdstate_claim(int fd, uint32_t generation) {
89 ot_fdslot *slot = fdstate_slot(fd);
90 uint64_t word;
91
92 if (!slot)
93 return 0;
94
95 word = atomic_load_explicit(&slot->state, memory_order_acquire);
96 for (;;) {
97 if (FD_GEN(word) != generation || FD_ST(word) != FD_IDLE)
98 return 0;
99 if (atomic_compare_exchange_weak_explicit(&slot->state, &word, FD_SET_ST(word, FD_BUSY), memory_order_acquire,
100 memory_order_relaxed))
101 return 1;
102 /* word now holds what was really there, so the test above runs again */
103 }
104}
105
106void fdstate_release(int fd) {
107 ot_fdslot *slot = fdstate_slot(fd);
108 uint64_t word;
109
110 if (!slot)
111 return;
112
113 word = atomic_load_explicit(&slot->state, memory_order_relaxed);
114 atomic_store_explicit(&slot->state, FD_SET_ST(word, FD_IDLE), memory_order_release);
115}
116
117int fdstate_claim_expired(int fd, ot_time now) {
118 ot_fdslot *slot = fdstate_slot(fd);
119 uint64_t word;
120 ot_time expiry;
121
122 if (!slot)
123 return 0;
124
125 /* Reading before asking for the slot keeps the common case, a descriptor
126 that has not expired, off the bus entirely */
127 word = atomic_load_explicit(&slot->state, memory_order_relaxed);
128 if (FD_ST(word) != FD_IDLE)
129 return 0;
130 expiry = atomic_load_explicit(&slot->expiry, memory_order_relaxed);
131 if (!expiry || expiry > now)
132 return 0;
133
134 /* Nothing can move the expiry while the slot sits at FD_IDLE, so winning
135 the slot is the last word */
136 return atomic_compare_exchange_strong_explicit(&slot->state, &word, FD_SET_ST(word, FD_CLOSING), memory_order_acquire,
137 memory_order_relaxed);
138}
139
140void fdstate_close(int fd) {
141 ot_fdslot *slot = fdstate_slot(fd);
142 uint64_t word;
143
144 if (!slot)
145 return;
146
147 word = atomic_load_explicit(&slot->state, memory_order_relaxed);
148 atomic_store_explicit(&slot->cookie, NULL, memory_order_relaxed);
149 atomic_store_explicit(&slot->expiry, 0, memory_order_relaxed);
150 atomic_store_explicit(&slot->state, FD_WORD(FD_GEN(word) + 1, FD_FREE), memory_order_release);
151}
152
153void *fdstate_cookie(int fd) {
154 ot_fdslot *slot = fdstate_slot(fd);
155
156 return slot ? atomic_load_explicit(&slot->cookie, memory_order_relaxed) : NULL;
157}
158
159void fdstate_set_expiry(int fd, ot_time expiry) {
160 ot_fdslot *slot = fdstate_slot(fd);
161
162 if (slot)
163 atomic_store_explicit(&slot->expiry, expiry, memory_order_relaxed);
164}
diff --git a/ot_fdstate.h b/ot_fdstate.h
new file mode 100644
index 0000000..2693a3b
--- /dev/null
+++ b/ot_fdstate.h
@@ -0,0 +1,52 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6#ifndef OT_FDSTATE_H__
7#define OT_FDSTATE_H__
8
9#include <stdint.h>
10
11#include "trackerlogic.h"
12
13/* What the tracker knows about a file descriptor: who owns it, when it
14 should be given up on, and whether anyone is working on it right now.
15
16 A descriptor number says nothing about identity, since the kernel hands
17 the same number out again as soon as it is closed. Every slot therefore
18 carries a generation that is bumped when the descriptor is given up, and
19 every caller that was told about a descriptor has to say which generation
20 it was told about. A caller holding a stale generation is turned away
21 instead of being handed somebody else's connection.
22
23 The table is sized once from RLIMIT_NOFILE and never moves, so looking a
24 descriptor up needs no synchronisation at all. */
25
26int fdstate_init(void);
27void fdstate_deinit(void);
28size_t fdstate_slots(void);
29
30/* Take a freshly accepted descriptor into the table and return the
31 generation to quote when asking for it back. expiry of 0 never expires. */
32uint32_t fdstate_open(int fd, void *cookie, ot_time expiry);
33
34/* Ask for exclusive use. Returns 0 if the descriptor has been recycled since
35 generation was handed out, or if somebody else is using it. */
36int fdstate_claim(int fd, uint32_t generation);
37void fdstate_release(int fd);
38
39/* The cleaner's side of the same thing: takes the descriptor only if nobody
40 is working on it and it really has expired. Never waits. */
41int fdstate_claim_expired(int fd, ot_time now);
42
43/* Give the descriptor up. The generation is bumped here, which has to happen
44 before close(2) so that the kernel cannot reissue the number while events
45 quoting the old generation are still in flight. */
46void fdstate_close(int fd);
47
48/* Both only valid while the caller holds the descriptor. */
49void *fdstate_cookie(int fd);
50void fdstate_set_expiry(int fd, ot_time expiry);
51
52#endif
diff --git a/tests/runner.c b/tests/runner.c
index 148b928..335d83d 100644
--- a/tests/runner.c
+++ b/tests/runner.c
@@ -25,6 +25,7 @@ static const struct module modules[] = {{"scan_urlencoded_query", test_scan_urle
25 {"ot_socket", test_socket}, 25 {"ot_socket", test_socket},
26 {"ot_mutex", test_mutex}, 26 {"ot_mutex", test_mutex},
27 {"ot_batch", test_batch}, 27 {"ot_batch", test_batch},
28 {"ot_fdstate", test_fdstate},
28 {"ot_accesslist", test_accesslist}, 29 {"ot_accesslist", test_accesslist},
29 {NULL, NULL}}; 30 {NULL, NULL}};
30 31
diff --git a/tests/t_fdstate.c b/tests/t_fdstate.c
new file mode 100644
index 0000000..bf8724a
--- /dev/null
+++ b/tests/t_fdstate.c
@@ -0,0 +1,224 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6/* System */
7#include <pthread.h>
8#include <stdatomic.h>
9#include <stdint.h>
10#include <stdlib.h>
11#include <string.h>
12
13/* Opentracker */
14#include "ot_fdstate.h"
15#include "test.h"
16
17/* ------------------------------------------------------- one thread first */
18
19static void case_open_claim_release(void) {
20 char cookie[4];
21 uint32_t generation;
22
23 test_case("a descriptor taken into the table can be claimed and gives back its cookie");
24 generation = fdstate_open(7, cookie, 0);
25 CHECK_INT(fdstate_claim(7, generation), 1);
26 CHECK_PTR(fdstate_cookie(7), cookie);
27 fdstate_release(7);
28
29 test_case("and can be claimed again once released");
30 CHECK_INT(fdstate_claim(7, generation), 1);
31 fdstate_release(7);
32 fdstate_close(7);
33}
34
35static void case_one_holder_at_a_time(void) {
36 uint32_t generation = fdstate_open(8, NULL, 0);
37
38 test_case("a descriptor somebody holds cannot be claimed again");
39 CHECK_INT(fdstate_claim(8, generation), 1);
40 CHECK_INT(fdstate_claim(8, generation), 0);
41 fdstate_release(8);
42 CHECK_INT(fdstate_claim(8, generation), 1);
43 fdstate_release(8);
44 fdstate_close(8);
45}
46
47static void case_stale_generation_is_refused(void) {
48 char first[4], second[4];
49 uint32_t old, new;
50
51 test_case("a caller quoting the generation of a closed descriptor is turned away");
52 old = fdstate_open(9, first, 0);
53 fdstate_close(9);
54 new = fdstate_open(9, second, 0);
55 CHECK(old != new);
56 CHECK_INT(fdstate_claim(9, old), 0);
57
58 test_case("while the one quoting the new generation gets the new cookie");
59 CHECK_INT(fdstate_claim(9, new), 1);
60 CHECK_PTR(fdstate_cookie(9), second);
61 fdstate_release(9);
62 fdstate_close(9);
63}
64
65static void case_expiry(void) {
66 uint32_t generation = fdstate_open(10, NULL, 100);
67
68 test_case("a descriptor past its expiry is collected");
69 CHECK_INT(fdstate_claim_expired(10, 99), 0);
70 CHECK_INT(fdstate_claim_expired(10, 100), 1);
71 fdstate_close(10);
72
73 test_case("one that never expires is never collected");
74 generation = fdstate_open(10, NULL, 0);
75 CHECK_INT(fdstate_claim_expired(10, 1 << 30), 0);
76
77 test_case("and one somebody is working on is left alone");
78 fdstate_set_expiry(10, 1);
79 CHECK_INT(fdstate_claim(10, generation), 1);
80 CHECK_INT(fdstate_claim_expired(10, 1 << 30), 0);
81 fdstate_release(10);
82 CHECK_INT(fdstate_claim_expired(10, 1 << 30), 1);
83 fdstate_close(10);
84}
85
86static void case_out_of_range(void) {
87 test_case("a descriptor outside the table is refused rather than trusted");
88 CHECK_INT(fdstate_claim(-1, 0), 0);
89 CHECK_INT(fdstate_claim((int)fdstate_slots(), 0), 0);
90 CHECK_INT(fdstate_claim_expired(-1, 0), 0);
91 CHECK_PTR(fdstate_cookie(-1), NULL);
92}
93
94/* --------------------------------------------------------- then in anger */
95
96/* The race this is about only exists while a descriptor is being recycled,
97 so recycling is driven by work done rather than by a clock: every slot is
98 given up after RECYCLE_AFTER claims, whatever the machine's speed. */
99
100#define STRESS_SLOTS 64
101#define RECYCLE_AFTER 32
102#define STRESS_CLAIMS 200000
103#define STRESS_GIVEUP (STRESS_CLAIMS * 100)
104
105static _Atomic long stress_claims, stress_recycles, stress_violations, stress_attempts;
106static _Atomic int stress_lives[STRESS_SLOTS];
107static _Atomic int stress_live[STRESS_SLOTS];
108
109/* Generation and cookie are published in one word so a worker can never read
110 a cookie that belongs to a different generation than the one it claimed
111 with. Each connection gets a cookie nobody else ever gets. */
112static _Atomic uint64_t stress_conn[STRESS_SLOTS];
113static _Atomic uint64_t stress_next_cookie;
114
115#define STRESS_CONN(cookie, gen) (((uint64_t)(cookie) << 32) | (uint32_t)(gen))
116#define STRESS_CONN_COOKIE(w) ((void *)(uintptr_t)((w) >> 32))
117#define STRESS_CONN_GEN(w) ((uint32_t)(w))
118
119static int stress_running(void) {
120 return atomic_load(&stress_claims) < STRESS_CLAIMS && atomic_load(&stress_attempts) < STRESS_GIVEUP;
121}
122
123static void *stress_worker(void *arg) {
124 unsigned seed = (unsigned)(uintptr_t)arg;
125
126 while (stress_running()) {
127 int fd = (int)(rand_r(&seed) % STRESS_SLOTS);
128 uint64_t conn = atomic_load(&stress_conn[fd]);
129
130 atomic_fetch_add(&stress_attempts, 1);
131 if (!fdstate_claim(fd, STRESS_CONN_GEN(conn)))
132 continue;
133
134 if (fdstate_cookie(fd) != STRESS_CONN_COOKIE(conn))
135 atomic_fetch_add(&stress_violations, 1);
136 if (atomic_fetch_sub(&stress_lives[fd], 1) <= 1)
137 fdstate_set_expiry(fd, 1); /* retired by its holder, as a handler would */
138 if (fdstate_cookie(fd) != STRESS_CONN_COOKIE(conn))
139 atomic_fetch_add(&stress_violations, 1);
140
141 fdstate_release(fd);
142 atomic_fetch_add(&stress_claims, 1);
143 }
144 return NULL;
145}
146
147static void *stress_keeper(void *arg) {
148 (void)arg;
149 while (stress_running()) {
150 int fd;
151
152 for (fd = 0; fd < STRESS_SLOTS; ++fd) {
153 if (atomic_load(&stress_live[fd])) {
154 if (!fdstate_claim_expired(fd, 1))
155 continue; /* a worker has it, come back */
156 fdstate_close(fd);
157 atomic_store(&stress_live[fd], 0);
158 atomic_fetch_add(&stress_recycles, 1);
159 }
160 {
161 void *cookie = (void *)(uintptr_t)(atomic_fetch_add(&stress_next_cookie, 1) + 1);
162 uint32_t gen = fdstate_open(fd, cookie, 0);
163
164 atomic_store(&stress_lives[fd], RECYCLE_AFTER);
165 atomic_store(&stress_conn[fd], STRESS_CONN((uintptr_t)cookie, gen));
166 atomic_store(&stress_live[fd], 1);
167 }
168 }
169 }
170 return NULL;
171}
172
173static void case_recycling_under_load(void) {
174 pthread_t worker[4], keeper;
175 size_t i;
176
177 test_case("no worker is ever handed a connection from another generation");
178 atomic_store(&stress_claims, 0);
179 atomic_store(&stress_recycles, 0);
180 atomic_store(&stress_violations, 0);
181 atomic_store(&stress_attempts, 0);
182 atomic_store(&stress_next_cookie, 0);
183 /* Every slot is opened and published before any worker runs: a worker only
184 ever learns of a descriptor from whoever registered it, and never
185 invents a generation of its own. */
186 for (i = 0; i < STRESS_SLOTS; ++i) {
187 void *cookie = (void *)(uintptr_t)(atomic_fetch_add(&stress_next_cookie, 1) + 1);
188 uint32_t gen = fdstate_open((int)i, cookie, 0);
189
190 atomic_store(&stress_lives[i], RECYCLE_AFTER);
191 atomic_store(&stress_conn[i], STRESS_CONN((uintptr_t)cookie, gen));
192 atomic_store(&stress_live[i], 1);
193 }
194
195 pthread_create(&keeper, NULL, stress_keeper, NULL);
196 for (i = 0; i < sizeof(worker) / sizeof(worker[0]); ++i)
197 pthread_create(&worker[i], NULL, stress_worker, (void *)(uintptr_t)(i + 1));
198 for (i = 0; i < sizeof(worker) / sizeof(worker[0]); ++i)
199 pthread_join(worker[i], NULL);
200 pthread_join(keeper, NULL);
201
202 CHECK_INT(atomic_load(&stress_violations), 0);
203
204 test_case("and the run actually recycled descriptors, or it proved nothing");
205 CHECK(atomic_load(&stress_claims) >= STRESS_CLAIMS);
206 CHECK(atomic_load(&stress_recycles) > STRESS_CLAIMS / (RECYCLE_AFTER * 4));
207
208 for (i = 0; i < STRESS_SLOTS; ++i)
209 fdstate_close((int)i);
210}
211
212void test_fdstate(void) {
213 CHECK_INT(fdstate_init(), 0);
214 CHECK(fdstate_slots() > STRESS_SLOTS);
215
216 case_open_claim_release();
217 case_one_holder_at_a_time();
218 case_stale_generation_is_refused();
219 case_expiry();
220 case_out_of_range();
221 case_recycling_under_load();
222
223 fdstate_deinit();
224}
diff --git a/tests/test.h b/tests/test.h
index fd574c2..bc8c3bd 100644
--- a/tests/test.h
+++ b/tests/test.h
@@ -88,5 +88,6 @@ void test_ip(void);
88void test_socket(void); 88void test_socket(void);
89void test_mutex(void); 89void test_mutex(void);
90void test_batch(void); 90void test_batch(void);
91void test_fdstate(void);
91 92
92#endif 93#endif