master
c 569 lines 20.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "../libnetdata.h"
4
5 #define nd_thread_status_get(nti) __atomic_load_n(&((nti)->options), __ATOMIC_ACQUIRE)
6 #define nd_thread_status_check(nti, flag) (__atomic_load_n(&((nti)->options), __ATOMIC_ACQUIRE) & (flag))
7 #define nd_thread_status_set(nti, flag) __atomic_or_fetch(&((nti)->options), flag, __ATOMIC_RELEASE)
8 #define nd_thread_status_clear(nti, flag) __atomic_and_fetch(&((nti)->options), ~(flag), __ATOMIC_RELEASE)
9
10 typedef void (*nd_thread_canceller)(void *data);
11
12 typedef enum __attribute__((packed)) {
13 ND_THREAD_LIST_NONE = 0,
14 ND_THREAD_LIST_RUNNING,
15 ND_THREAD_LIST_EXITED,
16 } ND_THREAD_LIST;
17
18 struct nd_thread {
19 void *arg;
20 pid_t tid;
21 char tag[ND_THREAD_TAG_MAX + 1];
22 //void *ret; // the return value of start routine
23 void (*start_routine) (void *);
24 NETDATA_THREAD_OPTIONS options;
25 uv_thread_t thread;
26 bool cancel_atomic;
27
28 #ifdef NETDATA_INTERNAL_CHECKS
29 // keep track of the locks currently held
30 // used to detect locks that are left locked during exit
31 int rwlocks_read_locks;
32 int rwlocks_write_locks;
33 int mutex_locks;
34 int spinlock_locks;
35 int rwspinlock_read_locks;
36 int rwspinlock_write_locks;
37 #endif
38
39 struct {
40 SPINLOCK spinlock;
41 nd_thread_canceller cb;
42 void *data;
43 } canceller;
44
45 ND_THREAD_LIST list;
46 struct nd_thread *prev, *next;
47 };
48
49 static struct {
50 struct {
51 SPINLOCK spinlock;
52 ND_THREAD *list;
53 } exited;
54
55 struct {
56 SPINLOCK spinlock;
57 ND_THREAD *list;
58 } running;
59
60 pthread_attr_t attr;
61 } threads_globals = {
62 .exited = {
63 .spinlock = SPINLOCK_INITIALIZER,
64 .list = NULL,
65 },
66 .running = {
67 .spinlock = SPINLOCK_INITIALIZER,
68 .list = NULL,
69 },
70 };
71
72 static __thread ND_THREAD *_nd_thread_info = NULL;
73 static __thread char _nd_thread_os_name[ND_THREAD_TAG_MAX + 1] = "";
74 static __thread bool _nd_thread_can_run_sql = true;
75
76 void nd_thread_can_run_sql(bool run_sql) {
77 _nd_thread_can_run_sql = run_sql;
78 }
79
80 bool nd_thread_runs_sql(void) {
81 return _nd_thread_can_run_sql;
82 }
83
84
85 // --------------------------------------------------------------------------------------------------------------------
86 // O/S abstraction
87
88 // get the thread name from the operating system
89 static inline void os_get_thread_name(char *out, size_t size) {
90 #if defined(__FreeBSD__)
91 pthread_get_name_np(pthread_self(), out, size);
92 if(strcmp(_nd_thread_os_name, "netdata") == 0)
93 strncpyz(out, "MAIN", size - 1);
94 #elif defined(HAVE_PTHREAD_GETNAME_NP)
95 pthread_getname_np(pthread_self(), out, size - 1);
96 if(strcmp(out, "netdata") == 0)
97 strncpyz(out, "MAIN", size - 1);
98 #else
99 strncpyz(out, "MAIN", size - 1);
100 #endif
101 }
102
103 // set the thread name to the operating system
104 static inline void os_set_thread_name(const char *name) {
105 #if defined(__FreeBSD__)
106 pthread_set_name_np(pthread_self(), name);
107 #elif defined(__APPLE__)
108 pthread_setname_np(name);
109 #else
110 pthread_setname_np(pthread_self(), name);
111 #endif
112 }
113
114 // --------------------------------------------------------------------------------------------------------------------
115 // internal API for managing names
116
117 inline int nd_thread_has_tag(void) {
118 return (_nd_thread_info && _nd_thread_info->tag[0]);
119 }
120
121 // For threads created by netdata, return the tag of the thread.
122 // For threads created by others (libuv, webrtc, etc), return the tag of the operating system.
123 // This caches the response, so that it won't query the operating system multiple times.
124 static inline const char *nd_thread_get_name(bool recheck) {
125 if(nd_thread_has_tag())
126 return _nd_thread_info->tag;
127
128 if(!recheck && _nd_thread_os_name[0])
129 return _nd_thread_os_name;
130
131 os_get_thread_name(_nd_thread_os_name, sizeof(_nd_thread_os_name));
132
133 return _nd_thread_os_name;
134 }
135
136 const char *nd_thread_tag(void) {
137 return nd_thread_get_name(false);
138 }
139
140 const char *nd_thread_tag_async_safe(void) {
141 if(nd_thread_has_tag())
142 return _nd_thread_info->tag;
143
144 return _nd_thread_os_name;
145 }
146
147 void nd_thread_tag_set(const char *tag) {
148 if(!tag || !*tag) return;
149
150 if(_nd_thread_info)
151 strncpyz(_nd_thread_info->tag, tag, sizeof(_nd_thread_info->tag) - 1);
152
153 strncpyz(_nd_thread_os_name, tag, sizeof(_nd_thread_os_name) - 1);
154
155 os_set_thread_name(_nd_thread_os_name);
156 }
157
158 // --------------------------------------------------------------------------------------------------------------------
159
160 void uv_thread_set_name_np(const char *name) {
161 static __thread bool libuv_name_set = false;
162
163 if(!name || !*name || (libuv_name_set && _nd_thread_os_name[0]))
164 return;
165
166 strncpyz(_nd_thread_os_name, name, sizeof(_nd_thread_os_name) - 1);
167 os_set_thread_name(_nd_thread_os_name);
168 libuv_name_set = true;
169 }
170
171 // --------------------------------------------------------------------------------------------------------------------
172
173 static size_t webrtc_id = 0;
174 static __thread bool webrtc_name_set = false;
175 void webrtc_set_thread_name(void) {
176 if(_nd_thread_info || webrtc_name_set) return;
177
178 webrtc_name_set = true;
179
180 char tmp[ND_THREAD_TAG_MAX + 1] = "";
181 os_get_thread_name(tmp, sizeof(tmp));
182
183 if(!tmp[0] || strcmp(tmp, "netdata") == 0) {
184 char name[ND_THREAD_TAG_MAX + 1];
185 snprintfz(name, ND_THREAD_TAG_MAX, "WEBRTC[%zu]", __atomic_fetch_add(&webrtc_id, 1, __ATOMIC_RELAXED));
186 os_set_thread_name(name);
187 }
188
189 nd_thread_get_name(true);
190 }
191
192 // --------------------------------------------------------------------------------------------------------------------
193 // locks tracking
194
195 #ifdef NETDATA_INTERNAL_CHECKS
196 void nd_thread_rwlock_read_locked(void) { if(_nd_thread_info) _nd_thread_info->rwlocks_read_locks++; }
197 void nd_thread_rwlock_read_unlocked(void) { if(_nd_thread_info) _nd_thread_info->rwlocks_read_locks--; }
198 void nd_thread_rwlock_write_locked(void) { if(_nd_thread_info) _nd_thread_info->rwlocks_write_locks++; }
199 void nd_thread_rwlock_write_unlocked(void) { if(_nd_thread_info) _nd_thread_info->rwlocks_write_locks--; }
200 void nd_thread_mutex_locked(void) { if(_nd_thread_info) _nd_thread_info->mutex_locks++; }
201 void nd_thread_mutex_unlocked(void) { if(_nd_thread_info) _nd_thread_info->mutex_locks--; }
202 void nd_thread_spinlock_locked(void) { if(_nd_thread_info) _nd_thread_info->spinlock_locks++; }
203 void nd_thread_spinlock_unlocked(void) { if(_nd_thread_info) _nd_thread_info->spinlock_locks--; }
204 void nd_thread_rwspinlock_read_locked(void) { if(_nd_thread_info) _nd_thread_info->rwspinlock_read_locks++; }
205 void nd_thread_rwspinlock_read_unlocked(void) { if(_nd_thread_info) _nd_thread_info->rwspinlock_read_locks--; }
206 void nd_thread_rwspinlock_write_locked(void) { if(_nd_thread_info) _nd_thread_info->rwspinlock_write_locks++; }
207 void nd_thread_rwspinlock_write_unlocked(void) { if(_nd_thread_info) _nd_thread_info->rwspinlock_write_locks--; }
208 #endif
209
210 // --------------------------------------------------------------------------------------------------------------------
211 // early initialization
212
213 size_t netdata_threads_init(void) {
214 memset(&threads_globals.attr, 0, sizeof(threads_globals.attr));
215
216 if(pthread_attr_init(&threads_globals.attr) != 0)
217 fatal("pthread_attr_init() failed.");
218
219 // get the required stack size of the threads of netdata
220 size_t stacksize = 0;
221 if(pthread_attr_getstacksize(&threads_globals.attr, &stacksize) != 0)
222 fatal("pthread_attr_getstacksize() failed with code.");
223
224 return stacksize;
225 }
226
227 // ----------------------------------------------------------------------------
228 // late initialization
229
230 void netdata_threads_set_stack_size(size_t stacksize) {
231 int i;
232
233 // set pthread stack size
234 if(stacksize > (size_t)PTHREAD_STACK_MIN) {
235 i = pthread_attr_setstacksize(&threads_globals.attr, stacksize);
236 if(i != 0)
237 nd_log(NDLS_DAEMON, NDLP_WARNING, "pthread_attr_setstacksize() to %zu bytes, failed with code %d.", stacksize, i);
238 else
239 nd_log(NDLS_DAEMON, NDLP_DEBUG, "Set threads stack size to %zu bytes", stacksize);
240 }
241 else
242 nd_log(NDLS_DAEMON, NDLP_WARNING, "Invalid pthread stacksize %zu", stacksize);
243 }
244
245 // ----------------------------------------------------------------------------
246 // threads init for external plugins
247
248 void netdata_threads_init_for_external_plugins(size_t stacksize) {
249 size_t default_stacksize = netdata_threads_init();
250 if(default_stacksize < 1 * 1024 * 1024)
251 default_stacksize = 1 * 1024 * 1024;
252
253 netdata_threads_set_stack_size(stacksize ? stacksize : default_stacksize);
254 }
255
256 // ----------------------------------------------------------------------------
257 // Thread-cleanup callback registry.
258 //
259 // Registration is startup-only — callers register before any daemon
260 // thread is created. libnetdata calls every registered callback at
261 // thread-exit time, in registration order. The startup-only invariant
262 // lets nd_thread_run_cleanup_callbacks read the count without holding
263 // the spinlock; the __ATOMIC_RELEASE/_ACQUIRE pair handles any late
264 // edge.
265 //
266 // Ordering: all registered callbacks run before thread_cache_destroy
267 // and worker_unregister (called below). A callback that needs to
268 // observe thread_cache state must do so before that destroy.
269
270 #define ND_THREAD_CLEANUP_MAX 16
271 static struct {
272 SPINLOCK spinlock;
273 size_t count;
274 nd_thread_cleanup_fn fns[ND_THREAD_CLEANUP_MAX];
275 } nd_thread_cleanup_registry = { .spinlock = SPINLOCK_INITIALIZER };
276
277 void nd_thread_register_cleanup(nd_thread_cleanup_fn fn) {
278 if(!fn) return;
279
280 spinlock_lock(&nd_thread_cleanup_registry.spinlock);
281
282 // Idempotent: skip if this fn is already registered. Keeps the
283 // registry small under repeated rrd_init / unittest re-entry.
284 for(size_t i = 0; i < nd_thread_cleanup_registry.count; i++) {
285 if(nd_thread_cleanup_registry.fns[i] == fn) {
286 spinlock_unlock(&nd_thread_cleanup_registry.spinlock);
287 return;
288 }
289 }
290
291 if(nd_thread_cleanup_registry.count >= ND_THREAD_CLEANUP_MAX) {
292 spinlock_unlock(&nd_thread_cleanup_registry.spinlock);
293 fatal("nd_thread_register_cleanup: registry full (max %d)", ND_THREAD_CLEANUP_MAX);
294 }
295 nd_thread_cleanup_registry.fns[nd_thread_cleanup_registry.count] = fn;
296 __atomic_store_n(&nd_thread_cleanup_registry.count,
297 nd_thread_cleanup_registry.count + 1,
298 __ATOMIC_RELEASE);
299 spinlock_unlock(&nd_thread_cleanup_registry.spinlock);
300 }
301
302 static void nd_thread_run_cleanup_callbacks(void) {
303 size_t count = __atomic_load_n(&nd_thread_cleanup_registry.count, __ATOMIC_ACQUIRE);
304 for(size_t i = 0; i < count; i++) {
305 nd_thread_cleanup_fn fn = nd_thread_cleanup_registry.fns[i];
306 if(fn) fn();
307 }
308 }
309
310 // ----------------------------------------------------------------------------
311
312 void nd_thread_join_threads()
313 {
314 ND_THREAD *nti;
315 do {
316 spinlock_lock(&threads_globals.exited.spinlock);
317 nti = threads_globals.exited.list;
318 if(nti) {
319 // Remove from exited list while holding the lock to prevent race condition
320 // where another thread with a direct pointer calls nd_thread_join() and frees
321 // the nti before we get a chance to use it
322 DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(threads_globals.exited.list, nti, prev, next);
323 nti->list = ND_THREAD_LIST_NONE;
324 }
325 spinlock_unlock(&threads_globals.exited.spinlock);
326
327 // nd_thread_join() handles NULL and will skip list removal since we already did it
328 // The atomic CAS in nd_thread_join() still protects against direct callers racing with us
329 nd_thread_join(nti);
330
331 } while (nti);
332 }
333
334 static void nd_thread_exit(ND_THREAD *nti) {
335
336 if(nti != _nd_thread_info || !nti || !_nd_thread_info) {
337 nd_log(NDLS_DAEMON, NDLP_ERR,
338 "THREADS: internal error - thread local variable does not match the one passed to this function. "
339 "Expected thread '%s', passed thread '%s'",
340 _nd_thread_info ? _nd_thread_info->tag : "(null)", nti ? nti->tag : "(null)");
341
342 if(!nti) nti = _nd_thread_info;
343 }
344
345 if(!nti) return;
346
347 internal_fatal(nti->rwlocks_read_locks != 0,
348 "THREAD '%s' WITH PID %d HAS %d RWLOCKS READ ACQUIRED WHILE EXITING !!!",
349 (nti) ? nti->tag : "(unset)", gettid_cached(), nti->rwlocks_read_locks);
350
351 internal_fatal(nti->rwlocks_write_locks != 0,
352 "THREAD '%s' WITH PID %d HAS %d RWLOCKS WRITE ACQUIRED WHILE EXITING !!!",
353 (nti) ? nti->tag : "(unset)", gettid_cached(), nti->rwlocks_write_locks);
354
355 internal_fatal(nti->mutex_locks != 0,
356 "THREAD '%s' WITH PID %d HAS %d MUTEXES ACQUIRED WHILE EXITING !!!",
357 (nti) ? nti->tag : "(unset)", gettid_cached(), nti->mutex_locks);
358
359 internal_fatal(nti->spinlock_locks != 0,
360 "THREAD '%s' WITH PID %d HAS %d SPINLOCKS ACQUIRED WHILE EXITING !!!",
361 (nti) ? nti->tag : "(unset)", gettid_cached(), nti->spinlock_locks);
362
363 internal_fatal(nti->rwspinlock_read_locks != 0,
364 "THREAD '%s' WITH PID %d HAS %d RWSPINLOCKS READ ACQUIRED WHILE EXITING !!!",
365 (nti) ? nti->tag : "(unset)", gettid_cached(), nti->rwspinlock_read_locks);
366
367 internal_fatal(nti->rwspinlock_write_locks != 0,
368 "THREAD '%s' WITH PID %d HAS %d RWSPINLOCKS WRITE ACQUIRED WHILE EXITING !!!",
369 (nti) ? nti->tag : "(unset)", gettid_cached(), nti->rwspinlock_write_locks);
370
371 if(nd_thread_status_check(nti, NETDATA_THREAD_OPTION_DONT_LOG_CLEANUP) != NETDATA_THREAD_OPTION_DONT_LOG_CLEANUP)
372 nd_log(NDLS_DAEMON, NDLP_DEBUG, "thread with task id %d finished", nti->tid);
373
374 nd_thread_run_cleanup_callbacks();
375 thread_cache_destroy();
376 worker_unregister();
377
378 nd_thread_status_set(nti, NETDATA_THREAD_STATUS_FINISHED);
379
380 spinlock_lock(&threads_globals.running.spinlock);
381 if(nti->list == ND_THREAD_LIST_RUNNING) {
382 DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(threads_globals.running.list, nti, prev, next);
383 nti->list = ND_THREAD_LIST_NONE;
384 }
385 spinlock_unlock(&threads_globals.running.spinlock);
386
387 spinlock_lock(&threads_globals.exited.spinlock);
388 DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(threads_globals.exited.list, nti, prev, next);
389 nti->list = ND_THREAD_LIST_EXITED;
390 spinlock_unlock(&threads_globals.exited.spinlock);
391 }
392
393 static void nd_thread_starting_point(void *ptr) {
394 ND_THREAD *nti = _nd_thread_info = (ND_THREAD *)ptr;
395 nd_thread_status_set(nti, NETDATA_THREAD_STATUS_STARTED);
396
397 nti->tid = gettid_cached();
398 nd_thread_tag_set(nti->tag);
399
400 if(nd_thread_status_check(nti, NETDATA_THREAD_OPTION_DONT_LOG_STARTUP) != NETDATA_THREAD_OPTION_DONT_LOG_STARTUP)
401 nd_log(NDLS_DAEMON, NDLP_DEBUG, "thread created with task id %d", gettid_cached());
402
403 signals_block_all_except_deadly();
404
405 spinlock_lock(&threads_globals.running.spinlock);
406 DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(threads_globals.running.list, nti, prev, next);
407 nti->list = ND_THREAD_LIST_RUNNING;
408 spinlock_unlock(&threads_globals.running.spinlock);
409
410 // run the thread code
411 nti->start_routine(nti->arg);
412
413 nd_thread_exit(nti);
414 }
415
416 ND_THREAD *nd_thread_self(void) {
417 return _nd_thread_info;
418 }
419
420 bool nd_thread_is_me(ND_THREAD *nti) {
421 return nti && nti->thread == pthread_self();
422 }
423
424
425 // utils
426 #define MAX_THREAD_CREATE_RETRIES (10)
427 #define MAX_THREAD_CREATE_WAIT_MS (1000)
428
429 static int create_uv_thread(uv_thread_t *thread, uv_thread_cb thread_func, void *arg, int *retries)
430 {
431 int err;
432
433 do {
434 err = uv_thread_create(thread, thread_func, arg);
435 if (err == 0)
436 break;
437
438 sleep_usec(MAX_THREAD_CREATE_WAIT_MS * USEC_PER_MS);
439 } while (err == UV_EAGAIN && ++(*retries) < MAX_THREAD_CREATE_RETRIES);
440
441 return err;
442 }
443
444 ND_THREAD *nd_thread_create(const char *tag, NETDATA_THREAD_OPTIONS options, void (*start_routine)(void *), void *arg)
445 {
446 ND_THREAD *nti = callocz(1, sizeof(*nti));
447 spinlock_init(&nti->canceller.spinlock);
448 nti->arg = arg;
449 nti->start_routine = start_routine;
450 nti->options = (options & NETDATA_THREAD_OPTIONS_ALL);
451 strncpyz(nti->tag, tag, ND_THREAD_TAG_MAX);
452
453 int retries = 0;
454 int ret = create_uv_thread(&nti->thread, nd_thread_starting_point, nti, &retries);
455 if(ret != 0) {
456 nd_log(NDLS_DAEMON, NDLP_ERR,
457 "failed to create new thread for %s. uv_thread_create() failed with code %d",
458 tag, ret);
459
460 freez(nti);
461 return NULL;
462 }
463 if (retries)
464 nd_log_daemon(NDLP_WARNING, "nd_thread_create required %d attempts", retries);
465
466 return nti;
467 }
468
469 // --------------------------------------------------------------------------------------------------------------------
470
471 void nd_thread_register_canceller(nd_thread_canceller cb, void *data) {
472 ND_THREAD *nti = _nd_thread_info;
473 if(!nti) return;
474
475 spinlock_lock(&nti->canceller.spinlock);
476 nti->canceller.cb = cb;
477 nti->canceller.data = data;
478 spinlock_unlock(&nti->canceller.spinlock);
479 }
480
481 void nd_thread_signal_cancel(ND_THREAD *nti) {
482 if(!nti) return;
483
484 __atomic_store_n(&nti->cancel_atomic, true, __ATOMIC_RELAXED);
485
486 spinlock_lock(&nti->canceller.spinlock);
487 if(nti->canceller.cb)
488 nti->canceller.cb(nti->canceller.data);
489 spinlock_unlock(&nti->canceller.spinlock);
490 }
491
492 ALWAYS_INLINE
493 bool nd_thread_signaled_to_cancel(void) {
494 if(!_nd_thread_info) return false;
495 return __atomic_load_n(&_nd_thread_info->cancel_atomic, __ATOMIC_RELAXED);
496 }
497
498 // ----------------------------------------------------------------------------
499 // nd_thread_join
500
501 int nd_thread_join(ND_THREAD *nti) {
502 if(!nti)
503 return ESRCH;
504
505 // Atomically check and set JOINED flag to prevent race conditions
506 // where two threads both try to join the same thread
507 NETDATA_THREAD_OPTIONS old_options;
508 do {
509 old_options = __atomic_load_n(&nti->options, __ATOMIC_ACQUIRE);
510 if(old_options & NETDATA_THREAD_STATUS_JOINED)
511 return 0;
512 } while(!__atomic_compare_exchange_n(&nti->options, &old_options,
513 old_options | NETDATA_THREAD_STATUS_JOINED,
514 false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE));
515
516 int ret;
517
518 if((ret = uv_thread_join(&nti->thread))) {
519 // we can't join the thread
520
521 nd_log(NDLS_DAEMON, NDLP_WARNING,
522 "cannot join thread. uv_thread_join() failed with code %d. (tag=%s)",
523 ret, nti->tag);
524
525 // On Windows/MSYS2, if the thread exited very quickly, uv_thread_join() can fail with EINVAL (-22)
526 // because the thread handle becomes invalid before the join executes. However, the thread may
527 // still be finishing its cleanup. Wait for it to reach FINISHED state before cleaning up.
528 if(ret == -22) { // UV_EINVAL
529 nd_log(NDLS_DAEMON, NDLP_INFO,
530 "thread '%s' join returned EINVAL, waiting for thread to finish...", nti->tag);
531
532 // Spin-wait for the thread to mark itself as finished
533 size_t retries = 0;
534 while(!nd_thread_status_check(nti, NETDATA_THREAD_STATUS_FINISHED) && retries < 1000) {
535 sleep_usec(1 * USEC_PER_MS); // 1ms
536 retries++;
537 }
538
539 if (nd_thread_status_check(nti, NETDATA_THREAD_STATUS_FINISHED)) {
540 nd_log(NDLS_DAEMON, NDLP_INFO, "thread '%s' confirmed finished, cleaning up structure", nti->tag);
541 ret = 0;
542 } else {
543 nd_log(NDLS_DAEMON, NDLP_ERR, "thread '%s' did not reach FINISHED state after 1 second", nti->tag);
544 }
545 }
546 }
547
548 // Always clean up the thread structure - if uv_thread_join() failed,
549 // retrying won't help (thread doesn't exist, not joinable, or logic error)
550 // JOINED flag was already set atomically at the start of this function
551
552 spinlock_lock(&threads_globals.running.spinlock);
553 if(nti->list == ND_THREAD_LIST_RUNNING) {
554 DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(threads_globals.running.list, nti, prev, next);
555 nti->list = ND_THREAD_LIST_NONE;
556 }
557 spinlock_unlock(&threads_globals.running.spinlock);
558
559 spinlock_lock(&threads_globals.exited.spinlock);
560 if(nti->list == ND_THREAD_LIST_EXITED) {
561 DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(threads_globals.exited.list, nti, prev, next);
562 nti->list = ND_THREAD_LIST_NONE;
563 }
564 spinlock_unlock(&threads_globals.exited.spinlock);
565
566 freez(nti);
567
568 return ret;
569 }