179 lines
13 KiB
Markdown
179 lines
13 KiB
Markdown
# u_async — Центральный цикл событий (event loop)
|
||
|
||
## 1. Назначение
|
||
|
||
u_async — центральный планировщик проекта uTun. Каждый поток, которому нужна асинхронная обработка сокетов, таймеров и отложенных задач, создаёт свой экземпляр `struct UASYNC` и запускает `uasync_mainloop()` / `uasync_poll()`. Модуль объединяет:
|
||
|
||
- **Сокеты** — мониторинг fd/socket на чтение/запись/ошибки через Linux epoll (предпочтительно) с fallback на poll/select.
|
||
- **Таймеры** — однократные таймеры с точностью 0.1 мс, реализованные на базе `timeout_heap` (min-heap) и `memory_pool`.
|
||
- **Немедленное выполнение** — FIFO-очередь `uasync_call_soon` для отложенного запуска callback в ближайшей итерации цикла.
|
||
- **Межпоточную связь** — `uasync_post` позволяет другому потоку безопасно запланировать callback в главном потоке (wakeup через pipe/UDP-сокет).
|
||
|
||
На Linux используется epoll, на FreeBSD/Windows — poll/select. Платформенная абстракция прозрачна для вызывающего кода.
|
||
|
||
## 2. Как пользоваться
|
||
|
||
### Правила
|
||
|
||
- **Один UASYNC на поток.** Нельзя создать несколько экземпляров и вызывать их из разных потоков.
|
||
- **Никаких sleep/usleep.** Если поток заблокирован на sleep, цикл событий стоит. Все ожидания — только через таймеры `uasync_set_timeout`.
|
||
- **Закрытие из callback.** Можно удалять сокет и освобождать его пользовательский контекст из callback. Перед следующим callback модуль повторно проверяет регистрацию, поколение и разрешённые события. Для завершения цикла вызывайте `uasync_stop`; `uasync_destroy` выполняйте после возврата из `uasync_poll` / `uasync_mainloop`, вне callbacks.
|
||
- **Указатель `user_arg`** передаётся во все callback и позволяет передать контекстную структуру (например, `UTUN_INSTANCE`).
|
||
|
||
### Типовой сценарий
|
||
|
||
```c
|
||
// 1. Создать uasync
|
||
struct UASYNC* ua = uasync_create();
|
||
if (!ua) { /* ошибка */ }
|
||
|
||
// 2. Зарегистрировать сокет
|
||
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, my_ctx);
|
||
// или для socket_t:
|
||
void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, my_ctx);
|
||
|
||
// 3. Зарегистрировать таймер (timebase = 0.1 мс, т.е. timeout_tb=100 = 10 мс)
|
||
void* t_id = uasync_set_timeout(ua, 100, my_ctx, my_timer_cb, "my_timer");
|
||
|
||
// 4. Отменить таймер (обязательно обнулить дескриптор!)
|
||
uasync_cancel_timeout(ua, t_id);
|
||
t_id = NULL;
|
||
|
||
// 5. Отложенное выполнение в следующей итерации
|
||
void* soon_id = uasync_call_soon(ua, my_ctx, deferred_cb);
|
||
|
||
// 6. Динамически включить/отключить мониторинг записи на сокете
|
||
uasync_set_socket_write(ua, sock_id, 1); // включить
|
||
uasync_set_socket_write(ua, sock_id, 0); // отключить
|
||
|
||
// 7. Запустить главный цикл (блокирующий, выход через uasync_stop)
|
||
uasync_mainloop(ua);
|
||
|
||
// 8. Или один шаг с таймаутом (timebase, -1 = бесконечно)
|
||
uasync_poll(ua, 500); // ждать до 50 мс или первого события
|
||
|
||
// 9. Завершение
|
||
uasync_destroy(ua, 1); // close_fds=1 — закрыть все сокеты
|
||
```
|
||
|
||
### Таймеры: правильный паттерн
|
||
|
||
```c
|
||
void* reconnect_timer = NULL; // дескриптор всегда обнуляем
|
||
|
||
// При запуске:
|
||
if (!reconnect_timer) {
|
||
reconnect_timer = uasync_set_timeout(ua, 5000, ctx, reconnect_cb, "reconnect");
|
||
}
|
||
|
||
// В callback или при отмене:
|
||
void reconnect_cb(void* arg) {
|
||
reconnect_timer = NULL; // сработал — обнулили
|
||
// ...
|
||
}
|
||
|
||
// При ручной отмене:
|
||
if (reconnect_timer) {
|
||
uasync_cancel_timeout(ua, reconnect_timer);
|
||
reconnect_timer = NULL;
|
||
}
|
||
```
|
||
|
||
### Межпоточное взаимодействие (uasync_post)
|
||
|
||
```c
|
||
// Из другого потока:
|
||
void notify_main(void* arg) {
|
||
UTUN_INSTANCE* u = arg;
|
||
// работаем в главном потоке — можно вызывать любые функции uasync, трогать сокеты и т.д.
|
||
uasync_set_timeout(u->ua, 10, u, handle_work, "work");
|
||
}
|
||
|
||
void thread_func(UTUN_INSTANCE* u) {
|
||
// …
|
||
uasync_post(u->ua, notify_main, u);
|
||
}
|
||
|
||
// Для гарантии видимости памяти из другого потока перед uasync_post:
|
||
uasync_memsync(u->ua);
|
||
```
|
||
|
||
### Получение времени
|
||
|
||
```c
|
||
uint64_t now_tb = get_time_tb(); // timebase 0.1 мс (монотонные часы)
|
||
uint64_t now_us = get_time_us(); // микросекунды (для burst-измерений)
|
||
```
|
||
|
||
## 3. API
|
||
|
||
### Жизненный цикл
|
||
|
||
| Функция | Описание |
|
||
|---------|----------|
|
||
| `uasync_create()` | Создать экземпляр: аллоцирует структуру, `socket_array`, `timeout_heap`, `memory_pool`, epoll/poll, wakeup pipe/сокет. |
|
||
| `uasync_destroy(ua, close_fds)` | Уничтожить экземпляр. При `close_fds=1` закрывает все зарегистрированные fd. Перед уничтожением выводит диагностику ресурсов и проверяет на утечки (abort при несовпадении аллокаций). |
|
||
| `uasync_stop(ua)` | Установить флаг `stop = 1` — на следующей итерации `uasync_mainloop` выйдет. |
|
||
| `uasync_mainloop(ua)` | Бесконечный цикл `while(!stop) uasync_poll(ua, -1)`. |
|
||
| `uasync_poll(ua, timeout_tb)` | Одна итерация: обработать сокеты + таймеры. `timeout_tb` = максимальное ожидание в timebase; `-1` = ждать следующего таймера (или бесконечно, если нет таймеров). |
|
||
|
||
### Таймеры (timebase = 0.1 мс)
|
||
|
||
| Функция | Описание |
|
||
|---------|----------|
|
||
| `uasync_set_timeout(ua, timeout_tb, arg, cb, name)` | Запланировать однократный таймер. Возвращает дескриптор `void*` для отмены. `name` — до 15 символов, используется в логах. |
|
||
| `uasync_cancel_timeout(ua, t_id)` | Отменить таймер по дескриптору. После отмены дескриптор нужно обнулить — повторный cancel даст ошибку. |
|
||
|
||
### Немедленное выполнение (FIFO)
|
||
|
||
| Функция | Описание |
|
||
|---------|----------|
|
||
| `uasync_call_soon(ua, arg, cb)` | Запланировать callback на ближайшую итерацию цикла. Используется, когда нужно отложить выполнение на «сразу после текущих событий». Узел выделяется из того же `timeout_pool`. |
|
||
| `uasync_call_soon_cancel(ua, t_id)` | Отменить — просто зануляет callback, узел будет освобождён при обработке очереди (O(1)). |
|
||
|
||
### Сокеты
|
||
|
||
| Функция | Описание |
|
||
|---------|----------|
|
||
| `uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, arg)` | Добавить fd (pipe, file). `r_cb/w_cb/e_cb` могут быть NULL. Возвращает дескриптор `void*`. |
|
||
| `uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, arg)` | Добавить `socket_t` (кросс-платформенный сокет). |
|
||
| `uasync_remove_socket(ua, s_id)` | Удалить сокет по дескриптору. Помечает слот неактивным, не освобождает индексную ячейку (защита от stale epoll-событий через gen). |
|
||
| `uasync_remove_socket_t(ua, sock)` | Удалить по значению `socket_t`. |
|
||
| `uasync_set_socket_read(ua, s_id, enable)` | Динамически включить/отключить мониторинг чтения (EPOLL_CTL_MOD). |
|
||
| `uasync_set_socket_write(ua, s_id, enable)` | Динамически включить/отключить мониторинг записи. |
|
||
| `uasync_lookup_socket(ua, fd, &s_id)` | Найти handle сокета по fd (кодированный индекс, устойчивый к расширению массива). |
|
||
|
||
### Межпоточное взаимодействие
|
||
|
||
| Функция | Описание |
|
||
|---------|----------|
|
||
| `uasync_post(ua, cb, arg)` | **Потокобезопасно.** Запланировать callback в главном потоке. Выделяет `posted_task`, добавляет в связный список под мьютексом, будит главный поток через `uasync_wakeup`. |
|
||
| `uasync_memsync(ua)` | **Потокобезопасно.** Барьер памяти (lock/unlock `posted_lock`) для гарантии видимости данных, записанных из другого потока перед `uasync_post`. |
|
||
| `uasync_wakeup(ua)` | Разбудить `poll`/`epoll_wait` записью байта в wakeup pipe (POSIX) или send в UDP-сокет (Windows). Можно вызывать из обработчика сигналов. |
|
||
| `uasync_get_wakeup_fd(ua)` | Получить write-fd wakeup pipe для использования в `signalfd` или кастомных механизмах. |
|
||
|
||
### Время
|
||
|
||
| Функция | Описание |
|
||
|---------|----------|
|
||
| `get_time_tb()` | Монотонное время в timebase (0.1 мс). `clock_gettime(CLOCK_MONOTONIC)` на POSIX, `QueryPerformanceCounter` на Windows. |
|
||
| `get_time_us()` | Монотонное время в микросекундах (для burst-измерений производительности). |
|
||
|
||
### Диагностика
|
||
|
||
| Функция | Описание |
|
||
|---------|----------|
|
||
| `uasync_get_stats(ua, ...)` | Получить счётчики аллокаций/освобождений таймеров и сокетов. |
|
||
| `uasync_print_resources(ua, prefix)` | Вывести в лог все активные таймеры (имя, оставшееся время) и сокеты. Вызывается также в `uasync_destroy` перед очисткой. |
|
||
|
||
### Внутренние структуры
|
||
|
||
- **`struct timeout_node`** — узел таймера: name, arg, callback, expiration_ms, heap_index. Используется и для таймеров в heap, и для FIFO-очереди immediate (через поле `next`). Выделяется из `timeout_pool`.
|
||
- **`struct socket_node`** — узел сокета: fd/sock, тип (FD/SOCK), колбэки, user_data, флаги active/enable_read/enable_write, gen (защита от stale epoll-событий после переиспользования fd).
|
||
- **`struct socket_array`** — массив сокетов: O(1) доступ по fd через `fd_to_index[]`, обход активных через `active_indices[]`, динамическое расширение.
|
||
- **`struct posted_task`** — задача из другого потока: callback + arg + next, защищена `posted_lock`.
|
||
|
||
Таймеры используют монотонное время: коррекция календарных часов не меняет срок срабатывания. Положительные интервалы округляются вверх до миллисекунд; нулевой интервал готов немедленно. Каждый проход обрабатывает исходную порцию `call_soon`, затем проверяет таймеры и сокеты; новые callbacks остаются на следующий проход. `uasync_drain_immediate` явно обрабатывает все порции до опустошения очереди.
|
||
|
||
Для fallback `poll` массив дескрипторов резервируется при создании экземпляра и до регистрации нового сокета. Отказ памяти возвращает `NULL` из `uasync_create` / `uasync_add_socket*`; существующие регистрации сохраняются. Перестроение массива в `uasync_poll` не выделяет память. Расширение из callback сохраняет события текущего прохода, новый сокет опрашивается со следующего прохода.
|