diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-31 15:59:41 +0100 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-31 15:59:41 +0100 |
| commit | 4d030871c004c917458b5e1c296edcfc689a2e66 (patch) | |
| tree | c6c057e47a17d8b03ea28a611ff029d950acfa8c /ot_tcp.c | |
| parent | 440cfeee71829308fa1cc9b5aef63beb7ab756dc (diff) | |
Make tcp worker multithreaded, too
This shamelessly steals the model established in the UDP workers, i.e.
if the kernel is able to bind multiple listening sockets to the same
endpoint, we will share them across threads. If it can't, all threads
will accept on the same socket.
The fast path handles most of the connections, only the rare case of
no data yet on the socket is offloaded to the slow path (which used to
be the mainloop) for completion. This scales nicely, as the higher the
load is, the higher chances are that an accept happens only after the
network roundtrip to the peer completes, leading to more connections
that already have data waiting for us.
We also consolidate the workstruct init and deinit code, preparation
for later unifying all thread state structs for all protocols (mca,
udc and tcp).
The config file gains a per tcp connection worker count and some docs
explaining what this knob buys and costs, nudging the user to either
keep it 0 or dial it up to at least 2.
Diffstat (limited to 'ot_tcp.c')
| -rw-r--r-- | ot_tcp.c | 83 |
1 files changed, 82 insertions, 1 deletions
| @@ -5,6 +5,10 @@ | |||
| 5 | 5 | ||
| 6 | /* System */ | 6 | /* System */ |
| 7 | #include <errno.h> | 7 | #include <errno.h> |
| 8 | #include <pthread.h> | ||
| 9 | #include <stdatomic.h> | ||
| 10 | #include <stdio.h> | ||
| 11 | #include <stdlib.h> | ||
| 8 | #include <string.h> | 12 | #include <string.h> |
| 9 | #include <unistd.h> | 13 | #include <unistd.h> |
| 10 | 14 | ||
| @@ -149,9 +153,86 @@ void tcp_accept(const int64_t serversocket, struct ot_workstruct *ws) { | |||
| 149 | if (fdstate_generation((int)sock) != generation) | 153 | if (fdstate_generation((int)sock) != generation) |
| 150 | continue; | 154 | continue; |
| 151 | 155 | ||
| 152 | if (http_adopt(sock, &cookie) == -1) | 156 | /* Handling it may already have given it a heap home and handed it to |
| 157 | another thread, and then it is no longer this frame's to touch */ | ||
| 158 | if (fdstate_cookie((int)sock) != &cookie) | ||
| 159 | fdstate_release((int)sock); | ||
| 160 | else if (!http_adopt(sock)) | ||
| 153 | http_close(sock); | 161 | http_close(sock); |
| 154 | else | 162 | else |
| 155 | fdstate_release((int)sock); | 163 | fdstate_release((int)sock); |
| 156 | } | 164 | } |
| 157 | } | 165 | } |
| 166 | |||
| 167 | /* One of these per worker, each allocated on its own so that growing the | ||
| 168 | array below never moves the one a running worker holds. */ | ||
| 169 | struct tcp_worker { | ||
| 170 | pthread_t thread; | ||
| 171 | int64_t sock; | ||
| 172 | _Atomic int left; | ||
| 173 | }; | ||
| 174 | |||
| 175 | static struct tcp_worker **g_workers; | ||
| 176 | static size_t g_worker_count; | ||
| 177 | |||
| 178 | /* A worker blocks in accept and nowhere else: the connection it is handed is | ||
| 179 | non blocking, so a client that stops talking is given to the event loop | ||
| 180 | rather than keeping this thread. */ | ||
| 181 | static void *tcp_worker(void *args) { | ||
| 182 | struct tcp_worker *me = args; | ||
| 183 | struct ot_workstruct ws; | ||
| 184 | |||
| 185 | if (workstruct_init(&ws) == -1) { | ||
| 186 | fprintf(stderr, "Warning: a tcp worker could not get its buffers and will not run.\n"); | ||
| 187 | atomic_store(&me->left, 1); | ||
| 188 | return NULL; | ||
| 189 | } | ||
| 190 | |||
| 191 | while (g_opentracker_running) | ||
| 192 | tcp_accept(me->sock, &ws); | ||
| 193 | |||
| 194 | workstruct_deinit(&ws); | ||
| 195 | atomic_store(&me->left, 1); | ||
| 196 | return NULL; | ||
| 197 | } | ||
| 198 | |||
| 199 | void tcp_worker_install(int64_t sock) { | ||
| 200 | struct tcp_worker **grown = realloc(g_workers, (g_worker_count + 1) * sizeof(*g_workers)); | ||
| 201 | struct tcp_worker *worker; | ||
| 202 | |||
| 203 | if (!grown || !(worker = calloc(1, sizeof(*worker)))) { | ||
| 204 | fprintf(stderr, "Warning: out of memory installing a tcp worker, this socket stays with the workers already running.\n"); | ||
| 205 | g_workers = grown ? grown : g_workers; | ||
| 206 | return; | ||
| 207 | } | ||
| 208 | g_workers = grown; | ||
| 209 | worker->sock = sock; | ||
| 210 | |||
| 211 | socket_set_blocking((int)sock, 1); | ||
| 212 | socket_defer_accept((int)sock); | ||
| 213 | |||
| 214 | if (pthread_create(&worker->thread, NULL, tcp_worker, worker)) | ||
| 215 | free(worker); | ||
| 216 | else | ||
| 217 | g_workers[g_worker_count++] = worker; | ||
| 218 | } | ||
| 219 | |||
| 220 | void tcp_workers_deinit(void) { | ||
| 221 | size_t i; | ||
| 222 | |||
| 223 | for (i = 0; i < g_worker_count; ++i) { | ||
| 224 | struct tcp_worker *worker = g_workers[i]; | ||
| 225 | |||
| 226 | /* Repeated because a signal only interrupts a call already under way */ | ||
| 227 | while (!atomic_load(&worker->left)) { | ||
| 228 | pthread_kill(worker->thread, OT_SIGNAL_WAKE); | ||
| 229 | usleep(1000); | ||
| 230 | } | ||
| 231 | pthread_join(worker->thread, NULL); | ||
| 232 | free(worker); | ||
| 233 | } | ||
| 234 | |||
| 235 | free(g_workers); | ||
| 236 | g_workers = NULL; | ||
| 237 | g_worker_count = 0; | ||
| 238 | } | ||
