├── .gitignore ├── assets ├── nodejs-layers.png ├── blocking_io_model.png ├── libuv_event_loop.png ├── operations_cpu_cost.png ├── asynchronous_io_model.png ├── io_models_comparison_1.png ├── libuv_design_overview.png ├── multiplexing_io_model.png ├── non-blocking_io_model.png └── signal-driven_io_model.png ├── .editorconfig ├── LICENSE ├── Multiplexing.MD └── README.MD /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /assets/nodejs-layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/nodejs-layers.png -------------------------------------------------------------------------------- /assets/blocking_io_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/blocking_io_model.png -------------------------------------------------------------------------------- /assets/libuv_event_loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/libuv_event_loop.png -------------------------------------------------------------------------------- /assets/operations_cpu_cost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/operations_cpu_cost.png -------------------------------------------------------------------------------- /assets/asynchronous_io_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/asynchronous_io_model.png -------------------------------------------------------------------------------- /assets/io_models_comparison_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/io_models_comparison_1.png -------------------------------------------------------------------------------- /assets/libuv_design_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/libuv_design_overview.png -------------------------------------------------------------------------------- /assets/multiplexing_io_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/multiplexing_io_model.png -------------------------------------------------------------------------------- /assets/non-blocking_io_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/non-blocking_io_model.png -------------------------------------------------------------------------------- /assets/signal-driven_io_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity182/I-O-Book/HEAD/assets/signal-driven_io_model.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Documentation config 2 | root = true 3 | 4 | [*.{md,MD}] 5 | trim_trailing_whitespace = true 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dmitry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Multiplexing.MD: -------------------------------------------------------------------------------- 1 | # Мультиплексирование 2 | 3 | **Содержание:** 4 | - [select](#select) 5 | - [poll](#poll) 6 | - [libuv](#libuv) 7 | - [Event Loop](#event-loop) 8 | - [libuv Internals](#libuv-internals) 9 | - [More about libuv](#more-about-libuv) 10 | 11 | --- 12 | 13 | ## select 14 | 15 | **select** - это *level-triggered* мультиплексор. 16 | 17 | Ключевой модуль данного мультиплексора - это структура **fd_set**. Мы загружаем в fd_set все наши сокеты и вызываем функцию **select()**, передавая в качестве параметра наше множество сокетов (структуру fd_set). На этом моменте программа блокируется, ожидая ответа от ядра. После возвращения контроля мы проходимся по всем нашим сокетам и определяем, содержится ли этот сокет в возвращенном множестве, то есть готов ли данный сокет принимать/отравлять данные. 18 | 19 | **Объявление структуры *fd_set*:** 20 | 21 | ```c 22 | #ifndef FD_SETSIZE 23 | #define FD_SETSIZE 1024 24 | #endif 25 | #define NBBY 8 /* number of bits in a byte */ 26 | typedef long fd_mask; 27 | #define NFDBITS (sizeof (fd_mask) * NBBY) /* bits per mask */ 28 | #define howmany(x,y) (((x)+((y)-1))/(y)) 29 | typedef struct _types_fd_set { 30 | fd_mask fds_bits[howmany(FD_SETSIZE, NFDBITS)]; 31 | } _types_fd_set; 32 | 33 | #define fd_set _types_fd_set 34 | ``` 35 | 36 | Здесь: 37 | - `FD_SETSIZE 1024` - это максимальное количество дескрипторов 38 | - `NBBY` - это количество битов в 1 байте. Это знание требуется закреплять, так как байт не обязательно должен состоять из 8 битов 39 | - `fd_mask` - это тип маски (bit array) для представления файловых дескрипторов, которая выражается в виде `long` 40 | - `NFDBITS` - это количество бит в 1 маске, что равно `sizeof (long)` байт помноженное на количество бит в 1 байте (NBBY) 41 | - `fd_mask fds_bits[howmany(FD_SETSIZE, NFDBITS)]` - это массив из масок, который и служит для регистрирования дескрипторов 42 | - `howmany(x,y)` - это функция, которая считает размер массива масок, требуемый для представления `FD_SETSIZE` дескрипторов. Например, если FD_SETSIZE=1024, а NFDBITS равен 64 (sizeof(long) = 8 байт), то функция вернет 16. То есть мы сохраним массив из 16 масок, которые позволят нам представить всего 1024 дескриптора, ведь полный размер массива - 1024 бит 43 | 44 | Для того, чтобы лучше понять работу с битмаской, рассмотрим на примере. Допустим, что есть дескрипторы с значениями 3, 7, 15 и . Пусть мы вызвали функцию *FD_SET()* и зарегистрировали все сокеты в нашу структуру с помощью `FD_SET()`. Тогда массив `fds_bits` будет выглядеть следующим образом: 45 | 46 | ```text 47 | fds_bits[0] fds_bits[1] 48 | 0001000100000001000000000000000000000000000000000000000000000000:010.... 49 | ^ ^ ^ ^ 50 | 3 7 15 65 51 | ``` 52 | 53 | Select полностью перезаписывает множество fd_set, не позволяя переиспользовать его на следующей итерации, то есть на каждой итерации или придется добавлять все сокеты снова в множество, или же восстанавливать предварительно самостоятельно сохраненную копию множества функцией *FD_COPY(&fdset_orig, &fdset_copy)*. 54 | 55 | Select не умеет проверять различные ивенты - чтение/запись/ошибка, а только фиксированный ивент на дескрипторе. Поэтому используется 3 отдельных множества для чтения, записи и ошибок - *fd_set readfds*, *fd_set writefds*, *fd_set errorfds* соответственно, что отражено в сигнатуре функции *select()*: 56 | 57 | ```C 58 | int select(int nfds, fd_set *restrict readfds, 59 | fd_set *restrict writefds, fd_set *restrict errorfds, 60 | struct timeval *restrict timeout); 61 | ``` 62 | 63 | --- 64 | 65 | ## poll 66 | 67 | **poll** - это *level-triggered* мультиплексор, который является преемником select и работает быстрее и эффективнее него. 68 | 69 | Для регистрирования сокетов используется *массив структур `pollfd`*. 70 | 71 | **Объявление структуры pollfd:** 72 | 73 | ```C 74 | struct pollfd { 75 | int fd; // the socket descriptor 76 | short events; // bitmap of events we're interested in 77 | short revents; // when poll() returns, bitmap of events that occurred 78 | }; 79 | ``` 80 | 81 | Здесь: 82 | - `fd` это значение файлового дескриптора 83 | - `events` - это список ивентов в виде битмаски, в которых заинтересована user-space программа. Возможно указать следующие ивенты: 84 | - `POLLIN` - на сокете доступны данные для чтения 85 | - `POLLOUT` - буфер сокета готов для записи 86 | - `POLLERR` - дескриптор вернул ошибку 87 | - `revents` - это список интересующих нас ивентов в виде битмаски, которые произошли на файловом дескрипторе. Этот список заполняет сам poll перед возвращением контроля программе 88 | - Помимо интересующих нас ивентов, poll заполняет также следующие ивенты: 89 | - `POLLHUP` - закрыто соединение на данном сокете 90 | - `POLLVAL` - некорректный файловый дескриптор 91 | - Данные ивенты не имеет смысла передавать в `events`, так как они всегда автоматически возвращаются в `revents` 92 | 93 | Ключевые особенности poll: 94 | 1) poll не требует повторного очищения/заполнения сокетов, что избавляет от лишних расходов на повторное копирование в user-space. Это достигается путем простой перезаписи поля *revents*. Поля `fd` и `events` всегда остаются неизменными 95 | 2) poll не имеет ограничения на количество регистрируемых сокетов 96 | 97 | --- 98 | 99 | ## libuv 100 | 101 | I/O Framework [libuv](https://libuv.org/) оборачивает системное I/O API (включая мультиплексор), предоставляя единое API и асинхронную event-driven парадигму работы с I/O. Именно libuv позволяет быть Node.js кросс-платформенным, так как Unix и Windows обладают совершенно разными примитивами работы с I/O. 102 | 103 | Более того, так как в JavaScript мы не имеем примитивов работы с тредами, но все равно хотим асинхронно выполнять долгие и другие блокирующие операции на фоне, то с помощью libuv мы имеем возможность асинхронного выполнения не только I/O, но и любого другого user кода с помощью *Worker Threads* - что под капотом является обычным Thread Pool, который построен на тех OS тредах, которые есть в системе. Да, имплементация трединга тоже зависит от платформы; например, в Unix это [pthreads](https://man7.org/linux/man-pages/man7/pthreads.7.html). 104 | 105 | Другие фичи libuv: 106 | - *TCP sockets*, which in Node represent the [net](https://nodejs.org/api/net.html) module. 107 | - *DNS resolution* (part of the functions inside the [dns](https://nodejs.org/api/dns.html) module are provided by Libuv and some of them are provided by c-ares) 108 | - *UDP sockets*, which represent the [dgram](https://nodejs.org/api/dgram.html) module in Node 109 | - *File I/O* which include file watching, file system operations (basically everything inside [fs](https://nodejs.org/api/fs.html) module in Node) 110 | - *Child processes* (the [child-process](https://nodejs.org/api/child_process.html) module in Node) 111 | - *A pool of worker threads* to handle blocking I/O tasks (also since Node 10.5 the [worker-threads](https://nodejs.org/api/worker_threads.html) module is available to execute Javascript in parallel) 112 | - *Synchronization primitives for threads* (e.g. locks, mutexes, semaphores, barriers) 113 | - *High-resolution clock* (if Date.now is not accurate enough, process.hrtime can be used) 114 | 115 | Libuv обрабатывает *Network I/O* с помощью epoll (Linux), kqueue (MacOS) или же с помощью IOCP на Windows: 116 | - > The event loop follows the rather usual single threaded asynchronous I/O approach: all (network) I/O is performed on non-blocking sockets which are polled using the best mechanism available on the given platform: epoll on Linux, kqueue on OSX and other BSDs, event ports on SunOS and IOCP on Windows. As part of a loop iteration the loop will block waiting for I/O activity on sockets which have been added to the poller and callbacks will be fired indicating socket conditions (readable, writable hangup) so handles can read, write or perform the desired I/O operation. 117 | 118 | Однако, *File I/O* обрабатывается с помощью Thread Pool: 119 | - > Unlike network I/O, there are no platform-specific file I/O primitives libuv could rely on, so the current approach is to run blocking file I/O operations in a thread pool. 120 | - > libuv uses a thread pool to make asynchronous file I/O operations possible, but network I/O is always performed in a single thread, each loop’s thread. 121 | 122 | Также доступ к этому Thread Pool открыт в API для выполнения пользовательских долгих или блокирующих операций - смотрите функцию [uv_queue_work()](http://docs.libuv.org/en/v1.x/threadpool.html#c.uv_queue_work). Она принимает коллбек `work_cb`, содержащий в себе необходимую пользовательскую работу, которую надо выполнить в Thread Pool, и коллбек `after_work_cb`, вызываемый по завершении работы в треде Event Loop: 123 | > Initializes a work request which will run the given work_cb in a thread from the threadpool. Once work_cb is completed, after_work_cb will be called on the loop thread. 124 | 125 | Составляющие части libuv: 126 | ![libuv Design Overview](assets/libuv_design_overview.png) 127 | 128 | Самый важный концепт libuv это *[Handle](http://docs.libuv.org/en/v1.x/guide/basics.html#handles-and-requests)*: - обработчик определенного вида ивента как I/O или timer. Например, handle для чтения данных с TCP сокета - [uv_tcp_t](http://docs.libuv.org/en/v1.x/tcp.html), который является сабклассом [uv_stream_t](http://docs.libuv.org/en/v1.x/stream.html). Смотрите другие виды handle в [API Documentation](http://docs.libuv.org/en/v1.x/api.html). 129 | 130 | ### Event Loop 131 | 132 | Event Loop в libuv работает следующим образом: 133 | 134 | ![Event Loop](assets/libuv_event_loop.png) 135 | 136 | 1. The loop concept of ‘now’ is updated. Event Loop кеширует текущее время перед началом итерации чтобы снизить количество time-related 137 | 2. *Due timers* are run. All active timers scheduled for a time before the loop’s concept of now get their callbacks called. (В Node.js это `setTimeout()` и `setInterval()`) 138 | 3. *Pending callbacks are called*. Обычно все коллбеки, связанные с I/O операциями, выполняются на стадии Poll. Однако, иногда выполнение коллбека откладывается на следующую итерацию. Именно здесь будут выполнены ранее отложенные коллбеки 139 | 4. *Idle handle callbacks* are called. Despite the unfortunate name, idle handles are run on every loop iteration, if they are active. 140 | 5. *Prepare handle callbacks* are called. Prepare handles get their callbacks called right before the loop will block for I/O. 141 | 6. *Poll timeout is calculated*. Before blocking for I/O the loop calculates for how long it should block 142 | 7. *The loop blocks for I/O*. Именно на этой стадии мы блокируемся на ожидании I/O с помощью epoll (на Linux). После возврата из epoll будут выполнены handles, которые мониторили какой-то готовый файловый дескриптор. Также именно здесь будут выполнены все связанные пользовательские I/O коллбеки. В node.js здесь например выполняется коллбек сетевого запроса 143 | 8. *Check handle callbacks* are called. Check handles get their callbacks called right after the loop has blocked for I/O. (В node.js это `setImmediate()`) 144 | 9. *Close callbacks* are called. If a handle was closed by calling uv_close() it will get the close callback called. (В node.js это например `socket.on('close', ...).`) 145 | 10. Iteration ends 146 | 147 | Больше можно прочитать в [документации libuv](https://docs.libuv.org/en/v1.x/design.html). 148 | 149 | ### libuv Internals 150 | 151 | Здесь мы разберем внутреннее устройство libuv. Будет много соурс кода, но я постараюсь сокращать его только до самой полезной и важной информации, и давать пояснительные комментарии. 152 | 153 | Например, мы хотим подписаться на ожидание данных на TCP сокете. Сделать это можно вот так - я взял уже готовый пример из официальной документации ([docs/code/tcp-echo-server/main.c](https://github.com/libuv/libuv/blob/master/docs/code/tcp-echo-server/main.c)): 154 | 155 | ```C 156 | // коллбек для аллокации буфера, куда запишутся данные с сокета 157 | void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { 158 | buf->base = (char*) malloc(suggested_size); 159 | buf->len = suggested_size; 160 | } 161 | 162 | // пользовательский коллбек при готовности данных на сокете 163 | void echo_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) { 164 | if (nread > 0) { 165 | write_req_t *req = (write_req_t*) malloc(sizeof(write_req_t)); 166 | req->buf = uv_buf_init(buf->base, nread); 167 | uv_write((uv_write_t*) req, client, &req->buf, 1, echo_write); 168 | return; 169 | } 170 | if (nread < 0) { 171 | if (nread != UV_EOF) 172 | fprintf(stderr, "Read error %s\n", uv_err_name(nread)); 173 | uv_close((uv_handle_t*) client, on_close); 174 | } 175 | 176 | free(buf->base); 177 | } 178 | 179 | void on_new_connection(uv_stream_t *server, int status) { 180 | if (status < 0) { 181 | fprintf(stderr, "New connection error %s\n", uv_strerror(status)); 182 | // error! 183 | return; 184 | } 185 | 186 | uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t)); 187 | uv_tcp_init(loop, client); 188 | if (uv_accept(server, (uv_stream_t*) client) == 0) { 189 | uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read); 190 | } 191 | else { 192 | uv_close((uv_handle_t*) client, on_close); 193 | } 194 | } 195 | 196 | int main() { 197 | loop = uv_default_loop(); 198 | 199 | uv_tcp_t server; 200 | uv_tcp_init(loop, &server); 201 | 202 | uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr); 203 | 204 | uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0); 205 | int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection); 206 | if (r) { 207 | fprintf(stderr, "Listen error %s\n", uv_strerror(r)); 208 | return 1; 209 | } 210 | return uv_run(loop, UV_RUN_DEFAULT); 211 | } 212 | ``` 213 | 214 | Код по подписанию на чтение с сокета находится в коллбеке `on_new_connection`, который вызывается при принятии нового соединения. В нем нас интересуют функции `uv_tcp_init`, `uv_accept` и `uv_read_start`. Первые две функции являются синхронными, так как они выполняют CPU-bound работу по инициализации handle и принятию соединения. Функция `uv_read_start` же подписывает нас на асинхронное чтение с сокета. 215 | 216 | Разберем **uv_tcp_init**: 217 | 1. Функция [uv_tcp_init](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/tcp.c#L143) определена так: 218 | 219 | ```C 220 | int uv_tcp_init(uv_loop_t* loop, uv_tcp_t* tcp) { 221 | return uv_tcp_init_ex(loop, tcp, AF_UNSPEC); 222 | } 223 | ``` 224 | 225 | -> 226 | 227 | ```C 228 | int uv_tcp_init_ex(uv_loop_t* loop, uv_tcp_t* tcp, unsigned int flags) { 229 | int domain; 230 | 231 | /* Use the lower 8 bits for the domain */ 232 | domain = flags & 0xFF; 233 | if (domain != AF_INET && domain != AF_INET6 && domain != AF_UNSPEC) 234 | return UV_EINVAL; 235 | 236 | uv__stream_init(loop, (uv_stream_t*)tcp, UV_TCP); 237 | 238 | return 0; 239 | } 240 | ``` 241 | 242 | Здесь нас интересует функция `uv__stream_init`. 243 | 2. Разберем [uv__stream_init](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/stream.c#L72) подробнее: 244 | 245 | ```C 246 | void uv__stream_init(uv_loop_t* loop, 247 | uv_stream_t* stream, 248 | uv_handle_type type) { 249 | int err; 250 | 251 | uv__handle_init(loop, (uv_handle_t*)stream, type); 252 | stream->read_cb = NULL; 253 | stream->alloc_cb = NULL; 254 | stream->close_cb = NULL; 255 | stream->connection_cb = NULL; 256 | stream->connect_req = NULL; 257 | stream->shutdown_req = NULL; 258 | stream->accepted_fd = -1; 259 | stream->queued_fds = NULL; 260 | stream->delayed_error = 0; 261 | QUEUE_INIT(&stream->write_queue); 262 | QUEUE_INIT(&stream->write_completed_queue); 263 | stream->write_queue_size = 0; 264 | 265 | uv__io_init(&stream->io_watcher, uv__stream_io, -1); 266 | } 267 | ``` 268 | 269 | Обратите внимание на строчку `uv__io_init(&stream->io_watcher, uv__stream_io, -1)` - данная функция инициализирует io_watcher внутри `uv_stream_t` и устанавливает в него (internal) callback, который ответственнен за обработку ивента на дескрипторе (для stream handle это `uv__stream_io`): 270 | 271 | ```C 272 | void uv__io_init(uv__io_t* w, uv__io_cb cb, int fd) { 273 | QUEUE_INIT(&w->pending_queue); 274 | QUEUE_INIT(&w->watcher_queue); 275 | w->cb = cb; /* устанавливаем watcher callback */ 276 | w->fd = fd; /* устанавливаем файловый дескриптор */ 277 | w->events = 0; 278 | w->pevents = 0; 279 | } 280 | ``` 281 | 282 | Пока что здесь fd равен -1, но мы заполним его позднее, когда примем соединение. 283 | 3. Сам коллбек [uv__stream_io](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/stream.c#L1209) определен так: 284 | 285 | ```C 286 | static void uv__stream_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) { 287 | uv_stream_t* stream; 288 | stream = container_of(w, uv_stream_t, io_watcher); /* Получаем handle, в котором сохранен пользовательский read callback */ 289 | 290 | /* Ignore POLLHUP here. Even if it's set, there may still be data to read. */ 291 | if (events & (POLLIN | POLLERR | POLLHUP)) 292 | uv__read(stream); 293 | 294 | if (events & (POLLOUT | POLLERR | POLLHUP)) { 295 | uv__write(stream); 296 | uv__write_callbacks(stream); 297 | } 298 | } 299 | ``` 300 | 301 | 1. В нем функция [uv__read](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/stream.c#L1036) отвечает за чтение данных с сокета: 302 | 303 | ```C 304 | static void uv__read(uv_stream_t* stream) { 305 | uv_buf_t buf; 306 | ssize_t nread; 307 | 308 | buf = uv_buf_init(NULL, 0); 309 | stream->alloc_cb((uv_handle_t*)stream, 64 * 1024, &buf); /* Аллоцируем буфер для чтения */ 310 | 311 | nread = read(uv__stream_fd(stream), buf.base, buf.len); /* Читаем данные с сокета */ 312 | 313 | if (nread < 0) { 314 | /* Обрабатываем ошибку */ 315 | } else if (nread == 0) { 316 | /* Соединение закрыто (EOF) */ 317 | uv__stream_eof(stream, &buf); 318 | return 319 | } else { 320 | /* Успешное чтение */ 321 | stream->read_cb(stream, nread, &buf); /* Вызываем пользовательский коллбек, куда передаем handle, количество прочитанных байт и сам буфер с прочитанными данными */ 322 | } 323 | } 324 | ``` 325 | 326 | Именно watcher и его коллбек (который был установлен в `uv__stream_io`) играют ключевую роль при обработке файлового дескриптора, на котором появились данные. Это мы увидим позднее, когда будем разбирать Event Loop. 327 | 328 | Теперь разберем `uv_accept`: 329 | 1. Функция [uv_accept](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/stream.c#L526) определена так: 330 | 331 | ```C 332 | int uv_accept(uv_stream_t* server, uv_stream_t* client) { 333 | int err; 334 | assert(server->loop == client->loop); 335 | 336 | if (server->accepted_fd == -1) 337 | return UV_EAGAIN; 338 | 339 | switch (client->type) { 340 | case UV_TCP: 341 | uv__stream_open(client, server->accepted_fd, UV_HANDLE_READABLE | UV_HANDLE_WRITABLE); 342 | } 343 | ``` 344 | 345 | Здесь нас интересует функция [uv__stream_open](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/stream.c#L393), куда мы передали файловый дескриптор для принятого соединения (`server->accepted_fd`): 346 | 347 | ```C 348 | int uv__stream_open(uv_stream_t* stream, int fd, int flags) { 349 | if (!(stream->io_watcher.fd == -1 || stream->io_watcher.fd == fd)) /* Ранее мы установили io_watcher.fd равным -1 в функции uv__stream_init */ 350 | return UV_EBUSY; 351 | 352 | assert(fd >= 0); 353 | stream->io_watcher.fd = fd; /* Устанавливаем файловый дескриптор принятого соединения в watcher */ 354 | 355 | return 0; 356 | } 357 | ``` 358 | 359 | Итак, теперь мы установили в watcher файловый дескриптор, на котором слушаем новое соединение. 360 | 361 | Теперь разберем последнюю функцию **uv_read_start**. 362 | 1. Функция [uv_read_start](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/stream.c#L1459) определена так: 363 | 364 | ```C 365 | int uv__read_start(uv_stream_t* stream, 366 | uv_alloc_cb alloc_cb, 367 | uv_read_cb read_cb) { 368 | stream->read_cb = read_cb; // сохраняем пользовательский read callback 369 | stream->alloc_cb = alloc_cb; // сохраняем callback, который аллоцирует буфер для чтения 370 | 371 | uv__io_start(stream->loop, &stream->io_watcher, POLLIN); /* Выражаем интерес в ивенте POLLIN */ 372 | } 373 | ``` 374 | 375 | Нас очень интересует функция [uv__io_start](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/core.c#L873): 376 | 377 | ```C 378 | void uv__io_start(uv_loop_t* loop, uv__io_t* w, unsigned int events) { 379 | w->pevents |= events; /* Устанавливаем нужные ивенты (мы передали POLLIN из uv__read_start) */ 380 | 381 | if (loop->watchers[w->fd] == NULL) { 382 | loop->watchers[w->fd] = w; /* Регистрируем watcher для этого файлового дескриптора */ 383 | loop->nfds++; /* Увеличиваем счетчик отслеживаемых файловых дескрипторов */ 384 | } 385 | } 386 | ``` 387 | 388 | Здесь мы установили пользовательский коллбек в handle и зарегистрировали watcher для данного файлового дескриптора в структуре Event Loop. 389 | 390 | Итак, давайте резюмируем, что произошло по окончании вызова всех 3-х методов: 391 | 1. При инициализации handle (`uv_tcp_init`) мы инициализировали watcher (`uv__io_init`) и сохранили в него коллбек `uv__stream_io`, который ответственнен за обработку ивентов на файловом дескрипторе, в том числе и за чтение данных с дескриптора 392 | 2. При вызове `uv_accept` мы приняли соединение и проставили файловый дескриптор, на котором слушаем новое соединение, в watcher (`stream->io_watcher.fd = fd`) 393 | 3. При вызове `uv__read_start` мы сохранили наш пользовательский коллбек в handle (`stream->read_cb = read_cb`), а также зарегистрировали watcher в структуре Event Loop (`loop->watchers[w->fd] = w`) 394 | 395 | Теперь в Event Loop, когда epoll вернет ивент для нашего файлового дескриптора, будет взят данный зарегистрированный watcher и выполнен его коллбек `w->cb`. Watcher коллбек `uv__stream_io` достает handle и выполняет чтение на файловом дескрипторе (`uv__read`), где после успешного чтения вызывается пользовательский коллбек `stream->read_cb`. 396 | 397 | Давайте посмотрим наконец на сам всемогущий Event Loop. Он определен в функции [uv_run](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/core.c#L382): 398 | 399 | ```C 400 | int uv_run(uv_loop_t* loop, uv_run_mode mode) { 401 | int timeout; 402 | int r; 403 | int can_sleep; 404 | 405 | r = uv__loop_alive(loop); 406 | if (!r) 407 | uv__update_time(loop); 408 | 409 | while (r != 0 && loop->stop_flag == 0) { 410 | uv__update_time(loop); 411 | uv__run_timers(loop); 412 | 413 | can_sleep = 414 | QUEUE_EMPTY(&loop->pending_queue) && QUEUE_EMPTY(&loop->idle_handles); 415 | 416 | uv__run_pending(loop); 417 | uv__run_idle(loop); 418 | uv__run_prepare(loop); 419 | 420 | timeout = 0; 421 | if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT) 422 | timeout = uv__backend_timeout(loop); 423 | 424 | uv__io_poll(loop, timeout); 425 | 426 | /* Process immediate callbacks (e.g. write_cb) a small fixed number of 427 | * times to avoid loop starvation.*/ 428 | for (r = 0; r < 8 && !QUEUE_EMPTY(&loop->pending_queue); r++) 429 | uv__run_pending(loop); 430 | 431 | /* Run one final update on the provider_idle_time in case uv__io_poll 432 | * returned because the timeout expired, but no events were received. This 433 | * call will be ignored if the provider_entry_time was either never set (if 434 | * the timeout == 0) or was already updated b/c an event was received. 435 | */ 436 | uv__metrics_update_idle_time(loop); 437 | 438 | uv__run_check(loop); 439 | uv__run_closing_handles(loop); 440 | 441 | r = uv__loop_alive(loop); 442 | } 443 | return r; 444 | } 445 | ``` 446 | 447 | Как видите, код полностью матчится в описание Event Loop, которое вы могли видеть в документации libuv или Node.js. Я приводил визуализацию стейджей Event Loop выше. 448 | 449 | Нас здесь интересует строчка `uv__io_poll(loop, timeout)`, которая ответственна за стадию "Poll for I/O", где происходит ожидание новых IO событий с помощью epoll и их обработка. Функция `uv__io_poll` определена по-разному для разных систем. Мы возьмем имплементацию на основе epoll из файла [src/unix/epoll.c](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/epoll.c#L103): 450 | 451 | ```C 452 | void uv__io_poll(uv_loop_t* loop, int timeout) { 453 | int nfds; 454 | struct epoll_event events[1024]; 455 | nfds = epoll_wait(loop->backend_fd, events, ARRAY_SIZE(events), timeout); 456 | for (i = 0; i < nfds; i++) { 457 | pe = events + i; 458 | fd = pe->data.fd; /* достаем файловый дескриптор */ 459 | 460 | w = loop->watchers[fd]; /* достаем watcher */ 461 | 462 | /* Give users only events they're interested in. Prevents spurious 463 | * callbacks when previous callback invocation in this loop has stopped 464 | * the current watcher. Also, filters out events that users has not 465 | * requested us to watch. 466 | */ 467 | pe->events &= w->pevents | POLLERR | POLLHUP; 468 | 469 | if (pe->events != 0) { 470 | w->cb(loop, w, pe->events); /* вызываем watcher callback (uv__stream_io) */ 471 | } 472 | } 473 | } 474 | ``` 475 | 476 | Как видите, обработка готового I/O и вызов пользовательского коллбека происходит на этой же стадии I/O Poll, где мы ожидаем новых собыий. Во многих статьях будут писать, что обработка коллбеков происходит на стадии Pending Callback, но это только отчасти правда. Коллбек может быть отложен до следующей итерации Event Loop, если мы не успеваем по времени обработать все ивенты, т.к. нас ждут время-зависимые timers. Это же подтверждает и официальная документация libuv: 477 | > Pending callbacks are called. All I/O callbacks are called right after polling for I/O, for the most part. There are cases, however, in which calling such a callback is deferred for the next loop iteration. If the previous iteration deferred any I/O callback it will be run at this point. 478 | 479 |
480 | Bonus: Код функции `uv__run_pending`, ответственной за стадию "Call pending callbacks" 481 | 482 | ```C 483 | static void uv__run_pending(uv_loop_t* loop) { 484 | QUEUE* q; 485 | QUEUE pq; 486 | uv__io_t* w; 487 | 488 | QUEUE_MOVE(&loop->pending_queue, &pq); 489 | 490 | while (!QUEUE_EMPTY(&pq)) { 491 | q = QUEUE_HEAD(&pq); 492 | QUEUE_REMOVE(q); 493 | QUEUE_INIT(q); 494 | w = QUEUE_DATA(q, uv__io_t, pending_queue); 495 | w->cb(loop, w, POLLOUT); 496 | } 497 | } 498 | ``` 499 | 500 | Как видите, здесь мы все также вызываем watcher callback, как в стадии "Poll for I/O". 501 | 502 |
503 | 504 |
505 | Bonus: Инициализция Event Loop 506 | 507 | Давайте также посмотрим на функцию `uv_loop_init`, которая инициализирует Event Loop. Эта функция определена в файле [src/unix/loop.c](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/loop.c#L30): 508 | 509 | ```C 510 | int uv_loop_init(uv_loop_t* loop) { 511 | uv__loop_internal_fields_t* lfields; 512 | void* saved_data; 513 | int err; 514 | 515 | saved_data = loop->data; 516 | memset(loop, 0, sizeof(*loop)); 517 | loop->data = saved_data; 518 | 519 | lfields = (uv__loop_internal_fields_t*) uv__calloc(1, sizeof(*lfields)); 520 | loop->internal_fields = lfields; 521 | 522 | uv_mutex_init(&lfields->loop_metrics.lock); 523 | 524 | heap_init((struct heap*) &loop->timer_heap); 525 | QUEUE_INIT(&loop->wq); 526 | QUEUE_INIT(&loop->idle_handles); 527 | QUEUE_INIT(&loop->async_handles); 528 | QUEUE_INIT(&loop->check_handles); 529 | QUEUE_INIT(&loop->prepare_handles); 530 | QUEUE_INIT(&loop->handle_queue); 531 | 532 | loop->active_handles = 0; 533 | loop->active_reqs.count = 0; 534 | loop->nfds = 0; 535 | loop->watchers = NULL; 536 | loop->nwatchers = 0; 537 | QUEUE_INIT(&loop->pending_queue); 538 | QUEUE_INIT(&loop->watcher_queue); 539 | 540 | loop->closing_handles = NULL; 541 | uv__update_time(loop); 542 | loop->async_io_watcher.fd = -1; 543 | loop->async_wfd = -1; 544 | loop->signal_pipefd[0] = -1; 545 | loop->signal_pipefd[1] = -1; 546 | loop->backend_fd = -1; 547 | loop->emfile_fd = -1; 548 | 549 | loop->timer_counter = 0; 550 | loop->stop_flag = 0; 551 | 552 | err = uv__platform_loop_init(loop); 553 | } 554 | ``` 555 | 556 | Здесь содержится общая логика по инициализации полей в структуре `uv_loop_t`. Нас же больше всего интересует функция `uv__platform_loop_init`, которая выполняет инициализацию под определенную платформу. 557 | 558 | Давайте взглянем на инициализацию под Linux - смотрим на файл [src/unix/linux-core.c](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/linux-core.c#L85): 559 | 560 | ```C 561 | int uv__platform_loop_init(uv_loop_t* loop) { 562 | 563 | loop->inotify_fd = -1; 564 | loop->inotify_watchers = NULL; 565 | 566 | return uv__epoll_init(loop); 567 | } 568 | ``` 569 | 570 | Мы видим, что имплементация под Linux использует epoll. Давайте взглянем на [uv__epoll_init](https://github.com/libuv/libuv/blob/6c692ad1cbcc5083ec90954a4b091b660aedfc10/src/unix/epoll.c#L27): 571 | 572 | ```C 573 | int uv__epoll_init(uv_loop_t* loop) { 574 | int fd; 575 | fd = epoll_create1(O_CLOEXEC); 576 | 577 | /* epoll_create1() can fail either because it's not implemented (old kernel) 578 | * or because it doesn't understand the O_CLOEXEC flag. 579 | */ 580 | if (fd == -1 && (errno == ENOSYS || errno == EINVAL)) { 581 | fd = epoll_create(256); 582 | 583 | if (fd != -1) 584 | uv__cloexec(fd, 1); 585 | } 586 | 587 | loop->backend_fd = fd; 588 | if (fd == -1) 589 | return UV__ERR(errno); 590 | 591 | return 0; 592 | } 593 | ``` 594 | 595 | Здесь происходит довольно обычная инициализации epoll с помощью [epoll_create1](https://man7.org/linux/man-pages/man2/epoll_create.2.html). 596 | 597 |
598 | 599 | ### More about libuv 600 | 601 | Здесь приведу дополнительные ресурсы про libuv. Некоторые рассказывают о libuv в контексте Node.js, так как там используется libuv под капотом. 602 | 603 | Доклады: 604 | - [NodeConf EU | A deep dive into libuv](https://www.youtube.com/watch?v=sGTRmPiXD4Y) 605 | - [Node's Event Loop From the Inside Out by Sam Roberts, IBM](https://www.youtube.com/watch?v=P9csgxBgaZ8) 606 | 607 | Статьи и документация: 608 | - [libuv documentation](https://docs.libuv.org/en/v1.x/index.html) 609 | - [Node.js Internals: Libuv and the event loop behind the curtain](https://medium.com/softup-technologies/node-js-internals-libuv-and-the-event-loop-behind-the-curtain-30708c5ca83) 610 | 611 | Source code: 612 | - [https://github.com/libuv/libuv](https://github.com/libuv/libuv) 613 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Помощник в освоении I/O и сетевого программирования 2 | 3 |

[Операционные системы] | [Компьютерные сети] | [I/O и сетевое программирование]

4 | 5 | **Содержание:** 6 | - [Sockets Basics](#sockets-basics) 7 | - [Модели I/O](#модели-io) 8 | - [Blocking I/O](#blocking-io) 9 | - [Non-Blocking I/O](#non-blocking-io) 10 | - [I/O Multiplexing](#io-multiplexing) 11 | - [Signal-Driven I/O](#signal-driven-io) 12 | - [Signal execution](#signal-execution) 13 | - [Multiple signals execution](#multiple-signals-execution) 14 | - [Async-signal-safe functions](#async-signal-safe-functions) 15 | - [Synchronously accepting a signal](#synchronously-accepting-a-signal) 16 | - [Multiplexing signals](#multiplexing-signals) 17 | - [Summary](#summary) 18 | - [Больше про Signal-Driven I/O](#больше-про-signal-driven-io) 19 | - [Asynchronous I/O](#asynchronous-io) 20 | - [POSIX AIO](#posix-aio) 21 | - [Linux io\_uring](#linux-io_uring) 22 | - [Концептуальное сравнение I/O моделей](#концептуальное-сравнение-io-моделей) 23 | - [Синхронное/Асинхронное I/O](#синхронноеасинхронное-io) 24 | - [Designing An Efficient Web Server](#designing-an-efficient-web-server) 25 | - [Single-Threaded Blocking I/O Server](#single-threaded-blocking-io-server) 26 | - [Multi-Threaded Blocking I/O Server (Thread-per-connection)](#multi-threaded-blocking-io-server-thread-per-connection) 27 | - [Non-Blocking I/O Server](#non-blocking-io-server) 28 | - [Non-Blocking Multiplexing I/O Server \[select\]](#non-blocking-multiplexing-io-server-select) 29 | - [Больше о select](#больше-о-select) 30 | - [Non-Blocking Multiplexing I/O Server \[poll\]](#non-blocking-multiplexing-io-server-poll) 31 | - [Больше о poll](#больше-о-poll) 32 | - [Non-Blocking Multiplexing I/O Server \[epoll\]](#non-blocking-multiplexing-io-server-epoll) 33 | - [Больше об epoll](#больше-об-epoll) 34 | - [Аналоги epoll в других системах](#аналоги-epoll-в-других-системах) 35 | - [Signal-Driven I/O Server](#signal-driven-io-server) 36 | - [Asynchronous I/O Server (io\_uring)](#asynchronous-io-server-io_uring) 37 | - [select vs poll vs epoll](#select-vs-poll-vs-epoll) 38 | - [Why Single-Threaded Multiplexing Model Works](#why-single-threaded-multiplexing-model-works) 39 | - [N kernel calls in loop](#n-kernel-calls-in-loop) 40 | - [Leveraging Multi-Threading](#leveraging-multi-threading) 41 | - [The C10K Problem](#the-c10k-problem) 42 | - [Use non-blocking I/O in case of multiplexing](#use-non-blocking-io-in-case-of-multiplexing) 43 | - [Edge-Triggered vs Level-Triggered Mode](#edge-triggered-vs-level-triggered-mode) 44 | - [I/O Frameworks](#io-frameworks) 45 | - [libuv](#libuv) 46 | - [Low-Level Asynchronous I/O vs Asynchronous I/O Request Processing](#low-level-asynchronous-io-vs-asynchronous-io-request-processing) 47 | - [Asynchronous HTTP Client Example](#asynchronous-http-client-example) 48 | - [Многопоточность (Multithreading)](#многопоточность-multithreading) 49 | - [Что такое многопоточность](#что-такое-многопоточность) 50 | - [Что такое concurrency, parallelism](#что-такое-concurrency-parallelism) 51 | - [Как связаны многопоточность и concurrency](#как-связаны-многопоточность-и-concurrency) 52 | - [Как связаны многопоточность и parallelism](#как-связаны-многопоточность-и-parallelism) 53 | - [Что такое асинхронность](#что-такое-асинхронность) 54 | - [Как связаны асинхронность и многопоточность](#как-связаны-асинхронность-и-многопоточность) 55 | - [Как связаны асинхронность и concurrency](#как-связаны-асинхронность-и-concurrency) 56 | - [Ресурсы](#ресурсы) 57 | 58 | --- 59 | 60 | ## Sockets Basics 61 | 62 | **Сокет** - это интерфейс между user-space программой и сетевым стеком различных протоколов в ядре. 63 | 64 | При открытии сокета мы указываем семейство протоколов и тип сокета. Например, семейство `AF_UNIX` используется для IPC (inter-process communication), а `AF_INET` используется для сетевого общения по IPv4. Два основных типа сокетов это `SOCK_STREAM` (TCP) и `SOCK_DGRAM` (UDP). 65 | 66 | Функции для I/O на сокетах: 67 | - [recv()](https://man7.org/linux/man-pages/man2/recv.2.html) для чтения. Если данных в *read buffer* нет, то есть другая сторона соединения еще не прислала данные, то чтение блокируется 68 | - [send()](https://man7.org/linux/man-pages/man2/send.2.html) для записи. Если сообщение не помещается в *send buffer* сокета, то есть другая сторона соединения еще не прочитала данные, то запись блокируется 69 | 70 | В Unix используется парадигма ["Everything is a file"](https://en.wikipedia.org/wiki/Everything_is_a_file), поэтому сокет представлен в системе обычным файловым дескриптором. Благодаря этому, например, можно использовать общие функции [read()](https://man7.org/linux/man-pages/man2/read.2.html)/[write()](https://man7.org/linux/man-pages/man2/write.2.html), предназначенные для работы с файловыми дескрипторами. В дальнейшем я буду использовать понятия "сокет" и "файловый дескриптор" взаимозаменяемо. 71 | 72 | Читайте больше: 73 | - [man socket(7, overview)](https://man7.org/linux/man-pages/man7/socket.7.html) 74 | - [man socket(2, syscall)](https://man7.org/linux/man-pages/man2/socket.2.html) 75 | 76 | --- 77 | 78 | ## Модели I/O 79 | 80 | ### Blocking I/O 81 | 82 | Данная модель использует сокеты в блокирующем режиме. Поток блокируется на вызове I/O сисколла в ожидании ответа, т.е. не происходит никакой работы, пока системный вызов I/O (read/write) не будет завершен. 83 | - Со стороны программы и планировщика это выглядит так, что поток был заблокирован и полезной работы в нем не совершалось 84 | - Со стороны потока это выглядит так, что ответ пришел немедленно, потому что поток заблокировался во время вызова и не работал все время ожидания ответа 85 | 86 | Такая модель является синхронной, потому что результат приходит сразу после вызова I/O метода. Важно отметить, что сразу **не означает немедленно, в то же абсолютное время**; это означает то, что результат придет **сразу же, как будет готов ответ**, и работа программы продолжится дальше. 87 | 88 | ![Blocking I/O](assets/blocking_io_model.png) 89 | 90 | **Что нужно для имплементации данной модели I/O:** 91 | - Блокирующие сокеты 92 | 93 | **Преимущества:** 94 | - Модель очень простая и не требует сложного кода на стороне сервера 95 | 96 | **Недостатки:** 97 | - Поток блокируется в ожидании I/O, хотя мог делать полезную работу 98 | 99 | --- 100 | 101 | ### Non-Blocking I/O 102 | 103 | В данной модели мы используем сокеты в неблокирующем режиме. Поток никогда не блокируется: в зависимости от готовности I/O на вызове [read](https://man7.org/linux/man-pages/man2/read.2.html)/[write](https://man7.org/linux/man-pages/man2/write.2.html) в качестве ответа придет или количество прочитанных байт (полезный результат), или же ошибка `EAGAIN` означающая что: 104 | - read: *Receive Buffer* пуст, то есть клиент еще не прислал нам новые данные 105 | - write: *Send Buffer* заполнен, то есть клиент еще не прочитал ранее отправленные данные 106 | 107 | Такая модель все еще остается синхронной, ведь результат приходит сразу же после I/O вызова, как и в случае с блокирующим синхронным I/O. Разница лишь в том, что здесь мы не всегда гарантированно получим полезный результат. 108 | 109 | Вот как можно перевести сокет в неблокирующий режим с помощью [fcntl](https://man7.org/linux/man-pages/man2/fcntl.2.html) команды `F_SETFL`: 110 | 111 | ```C 112 | void set_nonblocking(int sockfd) { 113 | int flags; 114 | if ((flags = fcntl(fd, F_GETFL, 0)) < 0) 115 | err_sys("F_GETFL error"); 116 | if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) 117 | err_sys("F_SETFL error"); 118 | } 119 | ``` 120 | 121 | В данной модели нам необходимо постоянно опрашивать сокет на наличие данных, что также называется *[polling](https://en.wikipedia.org/wiki/Polling_(computer_science))*. Эта модель менее эффективна, чем блокирующий синхронный I/O, потому что программа будет тратить процессорное время попусту в отличие от блокирующего I/O, где поток просто спал в ожидании результата, освобождая ресурсы CPU под другие потоки. 122 | 123 | ![Non-Blocking IO](assets/non-blocking_io_model.png) 124 | 125 | **Что нужно для имплементации данной модели I/O:** 126 | - Неблокирующие сокеты 127 | 128 | **Преимущества:** 129 | - Поток не блокируется 130 | 131 | **Недостатки:** 132 | - Необходимо постоянно опрашивать дескриптор, что означает: 133 | - пустая трата ресурсов CPU 134 | - постоянный переход между user/kernel space 135 | 136 | --- 137 | 138 | ### I/O Multiplexing 139 | 140 | Итак, как мы увидели, мы имеем возможность не блокировать поток, но это слишком дорого и неэффективно. 141 | 142 | Мы можем решить эту проблему, *отдав работу по опрашиванию дескрипторов ядру*, которое намного эффективнее справляется с этой работой. Для этого OS предоставляет специальный механизм - *мультиплексор*. Например, в Unix-системах это [select](http://manpages.debian.net/cgi-bin/man.cgi?query=select), [poll](http://manpages.debian.net/cgi-bin/man.cgi?query=poll) и [epoll](http://www.xmailserver.org/linux-patches/epoll.txt). 143 | 144 | Каждый мультиплексор дает как минимум следующее API: 145 | - Регистрирование файловых дескрипторов, на которых необходимо слушать I/O операции 146 | - Блокирующее синхронное ожидание новых событий на зарегистрированных дескрипторах. При появлении результата на каких-либо дескрипторах мультиплексор отдает управление программе, чтобы та смогла обработать все результаты 147 | 148 | Таким образом, на стороне приложения мы регистрируем все интересующие нас файловые дескрипторы, а затем вызываем мультиплексор и засыпаем в ожидании новых данных. 149 | 150 | Больше об использовании мультиплексоров я расскажу в разделе про дизайнинг веб-сервера. 151 | 152 | ![I/O Multiplexing](assets/multiplexing_io_model.png) 153 | 154 | **Что нужно для имплементации данной модели I/O:** 155 | - Блокирующие/неблокирующие сокеты 156 | - Мультиплексор 157 | 158 | **Преимущества:** 159 | - Мы больше не тратим процессорное время на polling сокетов, поток просто спит в ожидании результатов 160 | - Не происходит постоянного переключения между user и kernel space 161 | - Хорошо масштабируемая модель, которая поддерживает регистрацию сразу множества сокетов 162 | 163 | **Недостатки:** 164 | - Нет 165 | 166 | --- 167 | 168 | ### Signal-Driven I/O 169 | 170 | В этой модели используются *[POSIX сигналы](https://en.wikipedia.org/wiki/Signal_(IPC))* для уведомления процесса о готовности I/O на файловом дескрипторе. Мы говорим ядру мониторить указанный файловый дескриптор и уведомить процесс с помощью сигнала о готовности данных на нем. 171 | 172 | POSIX определяет два вида сигналов: reliable (standard) и real-time. По-умолчанию в модели используется стандартный сигнал `SIGIO` (сообщает о том, что I/O возможно), но можно использовать и real-time сигналы. 173 | 174 | Давайте сразу перейдем к делу. Для того, чтобы заиспользовать данную модель, мы должны проделать следующие шаги: 175 | 1. Зарегистрировать signal handler для `SIGIO` сигнала с помощью функции [sigaction](https://man7.org/linux/man-pages/man2/sigaction.2.html): 176 | 177 | 1. ```C 178 | void signal_handler(int signum, siginfo_t *info, void *context) { 179 | int sockfd = info->si_fd; 180 | long revents = info->si_band; 181 | if (revents & POLLIN) { 182 | handle_input(sockfd) 183 | } 184 | } 185 | 186 | struct sigaction act = { 0 }; 187 | 188 | act.sa_flags = SA_RESTART | SA_SIGINFO; 189 | act.sa_handler = &signal_handler; 190 | if (sigaction(SIGIO, &act, NULL) == -1) { 191 | errExit("sigaction") 192 | } 193 | ``` 194 | 195 | 2. Обратите внимание на флаг `SA_RESTART`. Когда сигнал происходит во время того, как программа была заблокирована на системном вызове, сисколл завершается с ошибкой `EINTR`. Однако возможно сделать автоматических перезапуск многих (но не всех) сисколлов с помощью указания этого флага 196 | > **SA_RESTART** 197 | > 198 | > Provide behavior compatible with BSD signal semantics by 199 | making certain system calls restartable across signals. 200 | This flag is meaningful only when establishing a signal 201 | handler. See signal(7) for a discussion of system call 202 | restarting. 203 | 3. Мы указываем флаг `SA_SIGINFO` для того, чтобы при срабатывании сигнала в handler была передана дополнительная информация о сработанном сигнале в виде структуры `siginfo_t` 204 | 1. > **SA_SIGINFO (since Linux 2.2)** 205 | > 206 | > The signal handler takes three arguments, not one. In 207 | this case, `sa_sigaction` should be set instead of 208 | `sa_handler`. This flag is meaningful only when 209 | establishing a signal handler. 210 | 2. В этой структуре поля `si_signo`, `si_errno` and `si_code` заполняются всегда. Остальная часть структуры является union: будут заполнены только те поля, которые относятся к конкретному сигналу. Для `SIGIO` это: 211 | > **SIGIO/SIGPOLL** (the two names are synonyms on Linux) fills in 212 | `si_band` and `si_fd`. The `si_band` event is a bit mask containing 213 | the same values as are filled in the `revents` field by poll(2). 214 | The `si_fd` field indicates the file descriptor for which the I/O 215 | event occurred 216 | 2. Установить PID процесса-владельца, которому следует отправлять сигнал, с помощью fcntl команды `F_SETOWN`: `fcntl(sockfd, F_SETOWN, getpid())` 217 | 3. Включить генерацию сигнала `SIGIO` при готовности данных на слушаемом сокете, установив флаг `O_ASYNC` с помощью fcntl команды `F_SETFL`: 218 | 219 | ```C 220 | void set_async(int sockfd) { 221 | int flags; 222 | if ((flags = fcntl(fd, F_GETFL, 0)) < 0) 223 | err_sys("F_GETFL error"); 224 | if (fcntl(fd, F_SETFL, flags | O_ASYNC) < 0) 225 | err_sys("F_SETFL error"); 226 | } 227 | ``` 228 | 229 | После выполнения вышеперечисленных шагов программа может работать как обычно и делать полезную работу. При появлении сигнала он будет автоматически обработан в зарегистрированном signal handler. Мы также можем сделать так, чтобы генерировался не дефолтный `SIGIO` сигнал, а какой-нибудь другой (включая real-time) сигнал с помощью fcntl команды `F_SETSIG`. Соответственно, все так же необходимо зарегистрировать signal_handler для этого сигнала. 230 | 231 | Эта модель является edge-triggered, то есть сообщает только о новых ивентах на дескрипторе. Поэтому с данной моделью необходимо использовать неблокирующие сокеты. Также нужно знать, что существуют spurious сигналы, то есть ложные срабатывания сигналов, и это вторая причина для использования неблокирующих сокетов вместе с данной моделью. 232 | 233 | ![Signal-Driven IO Model](assets/signal-driven_io_model.png) 234 | 235 | #### Signal execution 236 | 237 | Давайте более подробно рассмотрим процесс обработки сигналов (выполнения signal handler). 238 | 239 | Когда приходит сигнал, то выполнение программы прерывается. Если мы были заблокированы на сисколле, то сисколл завершится с ошибкой `EINTR`. Если же мы работали в user-space, то выполнение приостановится для обработки сигнала и затем возобновится с этого же места. 240 | 241 | Каждый тред в процессе имеет так называемую *signal mask* - битовую маску сигналов, которые в данный момент этот тред блокирует. Если сигнал в данный момент блокируется тредом, то инстансы этого сигнала не будут доставлены до треда, пока сигнал не будет разблокирован. *Pending signal* - это такой сигнал, который был сгенерирован, но не может быть доставлен до треда из-за блокировки, то есть он ожидает разблокировки. 242 | 243 | Мы можем посмотреть или изменить свою *signal mask* с помощью сисколлов [pthread_sigmask()](https://man7.org/linux/man-pages/man3/pthread_sigmask.3.html)/[sigprocmask()](https://man7.org/linux/man-pages/man2/sigprocmask.2.html). Например, вот как мы можем заблокировать определенный тип сигналов из программы: 244 | 245 | ```C 246 | #include 247 | #include 248 | 249 | sigset_t set; 250 | sigemptyset(&set); 251 | sigaddset(&set, SIGQUIT); 252 | sigaddset(&set, SIGUSR1); 253 | if (pthread_sigmask(SIG_BLOCK, &set, NULL) < 0) { 254 | errExit("pthread_sigmask") 255 | } 256 | ``` 257 | 258 | Этот механизм используется и при прерывании программы обработчиком: когда приходит сигнал, то данный тип сигнала блокируется, чтобы следующий инстанс сигнала ожидал завершения обработки предыдущего инстанса. Таким образом, в один момент треда может обрабатываться только один инстанс сигнала. 259 | 260 | #### Multiple signals execution 261 | 262 | Стандартные сигналы имеют большой недостаток, который делает невозможным обработку множество сокетов: если было сгенерировано несколько инстансов одного типа сигнала (несколько `SIGIO` для разных сокетов), то до программы дойдет только один инстанс, причем порядок доставки не определен. Из [man signal(7)](https://man7.org/linux/man-pages/man7/signal.7.html): 263 | > **Queueing and delivery semantics for standard signals** 264 | > 265 | > Standard signals do not queue. If multiple instances of a 266 | standard signal are generated while that signal is blocked, then 267 | only one instance of the signal is marked as pending (and the 268 | signal will be delivered just once when it is unblocked). In the 269 | case where a standard signal is already pending, the `siginfo_t` 270 | structure (see sigaction(2)) associated with that signal is not 271 | overwritten on arrival of subsequent instances of the same 272 | signal. Thus, the process will receive the information 273 | associated with the first instance of the signal. 274 | 275 | Real-time сигналы такой проблемой не обладают и более того они встают в очередь: 276 | > **Real-time signals are distinguished by the following:** 277 | > 278 | > 1. Multiple instances of real-time signals can be queued. By contrast, if multiple instances of a standard signal are delivered while that signal is currently blocked, then only one instance is queued 279 | > 2. Real-time signals are delivered in a guaranteed order. 280 | Multiple real-time signals of the same type are delivered in 281 | the order they were sent. 282 | 283 | Поэтому, если мы хотим обрабатывать сразу множество сокетов, то необходимо установить генерацию real-time сигнала вместо дефолтного `SIGIO` с помощью fcntl команды `F_SETSIG`: `fcntl(sockfd, F_SETSIG, signum)`. Из fcntl(2) manual page: 284 | > **F_SETSIG (int)** 285 | > 286 | > By selecting a real time signal (value >= `SIGRTMIN`), multiple I/O events may be queued using the same signal numbers. (Queuing is dependent on available memory.) Extra information is available if `SA_SIGINFO` is set for the signal handler, as above. 287 | > 288 | > Note that Linux imposes a limit on the number of real-time signals that may be queued to a process (see getrlimit(2) and signal(7)) and if this limit is reached, then the kernel reverts to delivering `SIGIO`, and this signal is delivered to the entire process rather than to a specific thread. 289 | 290 | --- 291 | 292 | #### Async-signal-safe functions 293 | 294 | Из-за асинхронной природы сигналов и прерывания посреди программы необходимо очень аккуратно писать код обработчика. 295 | 296 | 1. В обработчике безопасно вызывать только *async-signal-safe* функции, так как многие сисколлы выполняются в несколько действий (не являются атомарными) и изменяют глобальные данные. А так как при происхождении сигнала программа прерывается handler_ом, где мы снова можем вызвать этот сисколл, то второй вызов будет оперировать неконсистентными данными. Читайте об этом в man [signal-safety(7)](https://man7.org/linux/man-pages/man7/signal-safety.7.html) 297 | 2. В обработчике нужно очень осторожно работать с shared данными, ведь ведь сигнал может произойти посреди выполнения user-space инструкций и есть риск возникновения гонки 298 | 299 | ##### Synchronously accepting a signal 300 | 301 | Работать с сигналами можно и синхронно с помощью [sigwaitinfo()](https://man7.org/linux/man-pages/man2/sigwaitinfo.2.html) или [signalfd()](https://man7.org/linux/man-pages/man2/signalfd.2.html). 302 | 303 | ##### Multiplexing signals 304 | 305 | Мультиплексоры имеют альтернативную версию, которая позволяет автомарно ожидать или готовности данных, или происхождения сигнала на файловом дескрипторе. Смотрите: 306 | - [pselect(2)](https://man7.org/linux/man-pages/man2/select.2.html) 307 | - [ppoll(2)](https://man7.org/linux/man-pages/man2/poll.2.html) 308 | - [epoll_pwait(2)](https://man7.org/linux/man-pages/man2/epoll_wait.2.html) 309 | 310 | --- 311 | 312 | #### Summary 313 | 314 | **Преимущества:** 315 | - Мы не блокируем поток, пока ждем готовности данных на сокете. В какой-то ситуации это может быть преимуществом по сравнению с мультиплексированием 316 | - Может выступить заменой edge-triggered мультиплексора, если такого не имеется в системе 317 | 318 | **Недостатки:** 319 | - Ловить сигналы - дорого, поэтому эта модель плохо масштабируется 320 | - Работать с сигналами сложно 321 | 322 | #### Больше про Signal-Driven I/O 323 | 324 | Signal-Driven I/O: 325 | - [Unix Network Programming, Volume 1 - 25.2 Signal-Driven I/O for Sockets](https://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch25lev1sec2.html) 326 | - [GNU C Library - 13.19 Interrupt-Driven Input](https://www.gnu.org/software/libc/manual/html_node/Interrupt-Input.html) 327 | - [GNU C Library - 24.2.4 Asynchronous I/O Signals](https://www.gnu.org/software/libc/manual/html_node/Asynchronous-I_002fO-Signals.html) 328 | 329 | Документация по сигналам: 330 | - [man signal(2, syscall)](https://man7.org/linux/man-pages/man2/signal.2.html) 331 | - [man signal(7, overview)](https://man7.org/linux/man-pages/man7/signal.7.html) 332 | - [man signal-safety(7, overview)](https://man7.org/linux/man-pages/man7/signal.7.html) 333 | - [man sigaction(2)](https://man7.org/linux/man-pages/man2/sigaction.2.html) 334 | - [man fcntl(2)](https://man7.org/linux/man-pages/man2/fcntl.2.html) 335 | 336 | Синхронная обработка сигналов: 337 | - [https://unixism.net/2021/02/making-signals-less-painful-under-linux/](https://unixism.net/2021/02/making-signals-less-painful-under-linux/) 338 | - [https://ldpreload.com/blog/signalfd-is-useless](https://ldpreload.com/blog/signalfd-is-useless) 339 | 340 | --- 341 | 342 | ### Asynchronous I/O 343 | 344 | В асинхронной модели I/O мы просим ядро начать операцию и уведомить нас, когда вся I/O операция завершится, включая копирование данных между user/kernel-space буферами. Это отличается от прошлых моделей тем, что теперь ядро уведомляет нас не о готовности данных на сокете, а о полном окончании операции чтения/записи. Таким образом, теперь операции I/O выполняются полностью на стороне ядра. 345 | 346 | #### POSIX AIO 347 | 348 | **[POSIX AIO](https://man7.org/linux/man-pages/man7/aio.7.html)** - это асинхронное I/O API, закрепленное в стандарте POSIX. 349 | 350 | Две основные функции данного API - это `int aio_read(struct aiocb *aiocbp)` и `int aio_write(struct aiocb *aiocbp)`. Они принимают в качестве аргумента структуру *aiocb* ("asynchronous I/O control block"), которая определяет I/O операцию: 351 | 352 | ```C 353 | #include 354 | 355 | struct aiocb { 356 | /* The order of these fields is implementation-dependent */ 357 | 358 | int aio_fildes; /* File descriptor */ 359 | off_t aio_offset; /* File offset */ 360 | volatile void *aio_buf; /* Location of buffer */ 361 | size_t aio_nbytes; /* Length of transfer */ 362 | int aio_reqprio; /* Request priority */ 363 | struct sigevent aio_sigevent; /* Notification method */ 364 | int aio_lio_opcode; /* Operation to be performed; 365 | lio_listio() only */ 366 | 367 | /* Various implementation-internal fields not shown */ 368 | }; 369 | ``` 370 | 371 | Например, чтобы выполнить асинхронное чтение (Input), мы должны вызвать функцию `aio_read` передав следующие параметры: 372 | - файловый дескриптор (`aio_fildes`), указатель на буфер (`aio_buf`), размер буфера (`aio_nbytes`), что соответствует сигнатуре `read` 373 | - file offset (`aio_offset`) 374 | - способ уведомления об окончании I/O операции (`aio_sigevent`): с помощью уже знакомого нам *сигнала*, с помощью создания треда, или же вообще без уведомления. Валидные значения для `aio_sigevent.sigev_notify` это `SIGEV_NONE`, `SIGEV_SIGNAL`, или `SIGEV_THREAD` соответственно 375 | 376 | Системный вызов `aio_read` возвращает управление потоку сразу не блокируясь. Сама I/O операция будет выполняться на стороне ядра. Когда программа получит сигнал, мы знаем, что I/O операция уже завершена. 377 | 378 | ![Asynchronous I/O Model](assets/asynchronous_io_model.png) 379 | 380 | **Преимущества:** 381 | - Мы не блокируем поток и программа все время может делать полезную работу 382 | - Требует всего лишь 1 syscall: вся операция происходит на стороне ядра; то есть нет повторного перехода из user в kernel space для чтения данных 383 | 384 | **Недостатки:** 385 | - Сложен в использовании 386 | - Не эффективен 387 | - Почти не используется в реальном мире 388 | 389 | Больше информации про AIO: 390 | - [man aio(7)](https://man7.org/linux/man-pages/man7/aio.7.html). 391 | - [asynchronous disk I/O](https://blog.libtorrent.org/2012/10/asynchronous-disk-io/) - статья про неудачные попытки использования POSIX AIO/Windows overlapped I/O для файловых I/O операций 392 | - [Lord of the io_uring: Asynchronous Programming Under Linux](https://unixism.net/loti/async_intro.html) - отличная вводная статья про Asynchronous I/O в Linux; здесь также рассказывается про недостатки AIO 393 | 394 | #### Linux io_uring 395 | 396 | **io_uring** - это новое API для асинхронных I/O операций в Linux, которое призвано решить недостатки aio. Доступен для использования с версии Linux Kernel 5.1 397 | 398 | Скорее всего вы захотите использовать библиотеку [liburing](https://github.com/axboe/liburing), так как она сильно облегчает использование io_uring, предоставляя более высокоуровневое и удобное API для доступа к io_uring. 399 | 400 | Больше информации про io_uring: 401 | - [Lord of the io_uring](https://unixism.net/loti/index.html) и Цикл статей [io_uring By Example](https://unixism.net/2020/04/io-uring-by-example-article-series/) 402 | - [whitepaper: Efficient IO with io_uring](https://kernel.dk/io_uring.pdf) - whitepaper про ui_uring 403 | - [https://kernel.dk/axboe-kr2022.pdf](https://kernel.dk/axboe-kr2022.pdf) - доклад про io_uring от [Jens Axboe](https://en.wikipedia.org/wiki/Jens_Axboe) 404 | - [IO_URING. Часть 1. Введение](https://habr.com/ru/post/589389/) - статья на Habr про ui_uring 405 | 406 | --- 407 | 408 | ### Концептуальное сравнение I/O моделей 409 | 410 | ![I/O Models Comparison](assets/io_models_comparison_1.png) 411 | 412 | Как мы видим, в первых четырех моделях - Blocking/Non-Blocking, Multiplexing, Signal-Driven - отличается первая фаза, где мы ожидаем данных на сокете; вторая фаза, где мы блокируемся и читаем данные из сокеты, одинакова. Асинхронное I/O же обрабатывает обе фазы и этим отличается от первых четырех моделей. 413 | 414 | --- 415 | 416 | ### Синхронное/Асинхронное I/O 417 | 418 | Какой же является каждая из рассмотренных I/O моделей - синхронной или асинхронной? 419 | 420 | POSIX определяет эти термины как следующие: 421 | > - A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes 422 | > - An asynchronous I/O operation does not cause the requesting process to be blocked 423 | 424 | IO вызов является синхронным, если при вызове IO операции контроль программе не возвращается, пока операция не будет завершена, или пока не пройдет некоторое время по которому операция упадет по таймауту. Во всех первых четырех моделях мы вызываем `read`/`recv` синхронно и ждем, пока операция не будет завершена. Не важно, в каком режиме сокет - блокирующем, неблокирующем (O_NONBLOCK) или асинхронном (signal-driven, O_ASYNC) - мы все равно делаем чтение синхронно. 425 | 426 | Используя эти определения, первые четыре модели I/O является *синхронными*, потому что I/O операция блокирует тред. И только асинхронная I/O модель удовлетворяет определению *асинхронного I/O*, ведь мы только инициируем I/O и не ожидаем ее завершения. 427 | 428 | --- 429 | 430 | ## Designing An Efficient Web Server 431 | 432 | В данной главе мы будем дизайнить веб-сервер и в ходе этого: 433 | 1. Поговорим побольше про практическое применение всех рассмотренных моделей I/O 434 | 2. Рассмотрим преимущества и недостатки каждой из моделей 435 | 3. Рассмотрим разные мультиплексоры - select, poll, epoll 436 | 4. Пройдем путь от самой простой и неэффективной до самой эффективной имплементации веб сервера 437 | 438 | Сразу оговорюсь, что мы говорим про высоконагруженный сервер, обрабатывающий одновременно сотни тысяч соединений. 439 | 440 | ### Single-Threaded Blocking I/O Server 441 | 442 | Дизайн: 443 | - *I/O*: 444 | - Блокирующие сокеты (Blocking I/O) 445 | - *Concurrency Model*: 446 | - Single-Threaded 447 | 448 | В данной модели мы постоянно слушаем новые соединения на мастер сокете и добавляем их в список слушаемых соединений. 449 | 450 | Затем в цикле мы слушаем и читаем данные из открытых блокирующих сокетов: 451 | 452 | ```C 453 | char buf[1024]; 454 | int i, n; 455 | while (1) { 456 | for (i=0; i 560 | 561 | int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict timeout); 562 | ``` 563 | 564 | Тогда код будет выглядеть так: 565 | 566 | ```C 567 | int n_sockets, fd[n_sockets]; /* sockets */ 568 | int i, n; 569 | char buf[1024]; 570 | 571 | fd_set readset; 572 | while (1) { 573 | int maxfd = -1; 574 | FD_ZERO(&readset); 575 | 576 | /* Add all of the interesting fds to readset */ 577 | for (i=0; i < n_sockets; ++i) { 578 | if (fd[i]>maxfd) maxfd = fd[i]; 579 | FD_SET(fd[i], &readset); 580 | } 581 | 582 | /* Wait until one or more fds are ready to read */ 583 | select(maxfd+1, &readset, NULL, NULL, NULL); 584 | 585 | /* Process all of the fds that are still set in readset */ 586 | for (i=0; i < n_sockets; ++i) { 587 | if (FD_ISSET(fd[i], &readset)) { 588 | n = recv(fd[i], buf, sizeof(buf), 0); 589 | if (n == 0) { 590 | handle_close(fd[i]); 591 | } else if (n < 0) { 592 | if (errno == EAGAIN) 593 | ; /* The kernel didn't have any data for us to read. */ 594 | else 595 | handle_error(fd[i], errno); 596 | } else { 597 | handle_input(fd[i], buf, n); 598 | } 599 | } 600 | } 601 | } 602 | ``` 603 | 604 | Мы заполняем сутруктуру fd_set интересующими нас сокетами и блокируемся на вызове сисколла `select`. Мультиплексор не отдаст контроль программе, пока не появятся данные на сокетах. После возвращения контроля в переданном `readset` будут лежать те файловые дескрипторы, на которых появились данные. Нам остается только пройтись по сету и собрать данные с них. 605 | 606 | Кажется, уже лучше. Однако, `select` - очень старый и неэффективный механизм: 607 | - Плохая масштабируемость: select на стороне ядра работает за `O(n)` от значения самого большого файлового дескриптора `maxfd`, который обычно равен суммарному количеству всех открытых файловых дескрипторов во всей программе, в независимости от того, сколько fd было добавлено в отслеживаемый сет 608 | - Перед каждым новым вызовом `select()` нам необходимо обнулять (`FD_ZERO`) и снова зполнять fd_set нужными дескрипторами (`FD_SET`), так как select изменяет переданный ему сет, оставляя в нем только те сокеты, на которых появились данные 609 | - `select` имеет лимит на количество сокетов, равное `FD_SETSIZE`, которое забито в библиотеке. Некоторые версии GNU C Library позволяют вам изменить лимит в compile-time. В ранних версиях Linux этот лимит был равен 1024 610 | - Между kernel и user-space постоянно копируется fd_set 611 | 612 | У нас есть более лучший мультиплексор - `poll`. Давайте рассмотрим модель построенную на нем. 613 | 614 | #### Больше о select 615 | 616 | - [man select](https://man7.org/linux/man-pages/man2/select.2.html) 617 | - [UNIX Network Programming Book: Section 6.3 select Function](https://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec3.html) 618 | - Расширенные заметки о select - [Multiplexing.MD#select](./Multiplexing.MD#select) 619 | 620 | --- 621 | 622 | ### Non-Blocking Multiplexing I/O Server \[poll\] 623 | 624 | Дизайн: 625 | - *I/O*: 626 | - Неблокирующие сокеты (Non-Blocking I/O) 627 | - I/O Multiplexing - [poll](https://man7.org/linux/man-pages/man2/poll.2.html) 628 | - *Concurrency Model*: 629 | - Single-Threaded 630 | 631 | В этой модели мы возьмем пультиплексор *poll*: 632 | 633 | ```C 634 | #include 635 | 636 | int poll(struct pollfd *fds, nfds_t nfds, int timeout); 637 | ``` 638 | 639 | Poll обладает следующими преимуществами по сравнению с select: 640 | - все так же работает за `O(n)`, но теперь эта работа зависит от количества переданных сокетов, а не от значения максимального файлового дескриптора 641 | - не имеет ограничения на количество сокетов 642 | 643 | Список файловых дескрипторов передается в аргументе `fds`, который является массивом структур `pollfd`: 644 | 645 | ```C 646 | struct pollfd { 647 | int fd; /* file descriptor */ 648 | short events; /* requested events */ 649 | short revents; /* returned events */ 650 | }; 651 | ``` 652 | 653 | Также мы должны указать количество файловых дескрипторов в аргументе `nfds` и таймаут ожидания в `timeout`. 654 | 655 | Пример использования poll: 656 | 657 | ```C 658 | int n_sockets, fd[n_sockets]; /* sockets */ 659 | int i, n; 660 | char buf[1024]; 661 | 662 | struct pollfd pfds[n_sockets]; 663 | /* Add all of the interesting fds to pollfd */ 664 | for (i=0; i < n_sockets; ++i) { 665 | pfds[i].fd = fd[i]; 666 | pfds[i].events = POLLIN; // we are interested in input operation 667 | } 668 | 669 | while (1) { 670 | /* Wait until one or more fds are ready to read */ 671 | poll(pfds, n_sockets, -1); 672 | 673 | /* Process all of the fds that have POLLIN set in returned events*/ 674 | for (i=0; i < n_sockets; ++i) { 675 | if (pfds[i].revents != 0) { 676 | if (pfds[i].revents & POLLIN) { 677 | n = recv(pfds[i].fd, buf, sizeof(buf), 0); 678 | if (n == -1) 679 | if (errno == EAGAIN) 680 | ; /* The kernel didn't have any data for us to read. */ 681 | else 682 | handle_error(pfds[i].fd, errno); 683 | handle_input(pfds[i].fd, buf, n); 684 | } else { /* POLLERR | POLLHUP */ 685 | handle_close(pfds[i].fd); 686 | } 687 | } 688 | } 689 | } 690 | ``` 691 | 692 | Можно ли улучшить эту модель веб сервера? Да, можно, ведь есть еще более эффективный новый мультиплексор - *epoll*. 693 | 694 | #### Больше о poll 695 | 696 | - [man poll](https://man7.org/linux/man-pages/man2/poll.2.html) 697 | - [UNIX Network Programming Book: Section 6.10 poll Function](https://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec10.html) 698 | - Расширенные заметки о poll - [Multiplexing.MD#poll](./Multiplexing.MD#poll) 699 | 700 | --- 701 | 702 | ### Non-Blocking Multiplexing I/O Server \[epoll\] 703 | 704 | Дизайн: 705 | - *I/O*: 706 | - Неблокирующие сокеты (Non-Blocking I/O) 707 | - I/O Multiplexing - [epoll](https://man7.org/linux/man-pages/man7/epoll.7.html) 708 | - *Concurrency Model*: 709 | - Single-Threaded 710 | 711 | *Epoll* - это самый эффективный из всех возможных мультиплексоров в Linux, который работает за время O(1) в независимости от количества сокетов, в отличии от select и poll, которые на уровне ядра делают поллинг всех сокетов. 712 | 713 | - epoll не делает поллинг сокетов, а работает на основе сигналов в ядре: каждый раз, когда на файловый дескриптор поступают данные (через ядро), epoll отслеживает это и сохраняет этот файловый дескриптор. Когда в следующий раз мы вызовем мультиплексор, то он вернет все файловые дескрипторы, для которых сработали коллбеки на момент вызова. То есть, мультиплексор больше не делает поллинг файловых дескрипторов, а просто мониторит коллбеки. Именно поэтому epoll работает за время O(1), так как не важно сколько у нас файловых дескрипторов - мультиплексор всегда отдаст ответ за константное время, ведь он постоянно в пассивном режиме поддерживает у себя список готовых к I/O файловых дескрипторов. 714 | - epoll отслеживает закрытие файлового дескриптора и автоматически удаляет его из списка отслеживаемых 715 | - epoll позволяет эффективно добавлять сокеты динамически в множество; и позволяет это делать даже тогда, когда поток заблокирован в ожидании ивентов - это может пригодиться, если мы принимаем соединения в параллельном треде 716 | 717 | Для управления epoll предоставляется следующее API: 718 | 1. [epoll_create](https://man7.org/linux/man-pages/man2/epoll_create.2.html) создает новый инстанс epoll и возвращает файловый дескриптор, который служит как интерфейс к epoll 719 | 2. [epoll_ctl](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) служит для регистрирования интересующего нас файлового дескриптора - epoll будет отслеживать этот файловый дескриптор 720 | 3. [epoll_wait](https://man7.org/linux/man-pages/man2/epoll_wait.2.html) - блокирующий вызов ожидания новых I/O ивентов. Если таковых еще не имеется, то поток блокируется. Этот сисколл возвращает все файловые дескрипторы, на которых имеются данные 721 | 722 | Пример использования epoll: 723 | 724 | ```C 725 | #define MAX_EVENTS 1000 726 | 727 | int n_sockets, fd[n_sockets]; /* sockets */ 728 | int i, n; 729 | char buf[1024]; 730 | 731 | int epfd = epoll_create(1); 732 | 733 | /* Add all of the interesting fds to epoll */ 734 | for (i=0; i < n_sockets; ++i) { 735 | struct epoll_event ev; 736 | ev.data.fd = fd[i]; 737 | ev.events = EPOLLIN; // we are interested in input operation 738 | epoll_ctl(epfd, EPOLL_CTL_ADD, fd[i], &ev); 739 | } 740 | 741 | struct epoll_event events[MAX_EVENTS]; 742 | 743 | while (1) { 744 | nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); 745 | if (nfds == -1) { 746 | errExit("epoll_wait"); 747 | } 748 | 749 | /* Process all of the returned fds*/ 750 | for (i=0; i < nfds; ++i) { 751 | n = recv(events[i].data.fd, buf, sizeof(buf), 0); 752 | if (n == -1) 753 | if (errno == EAGAIN) 754 | ; /* The kernel didn't have any data for us to read. */ 755 | else 756 | handle_error(events[i].data.fd, errno); 757 | handle_input(events[i].data.fd, buf, n); 758 | } 759 | } 760 | ``` 761 | 762 | Чтобы сделать эту модель еще эффективнее, можно заиспользовать edge-triggered режим epoll - об этом расскажу далее. 763 | 764 | Примеры: 765 | - Node.js 766 | - Netty (на самом деле netty использует сразу несколько тредов, чтобы распределить нагрузку между несколькими параллельными тредами) 767 | 768 | #### Больше об epoll 769 | 770 | - [man epoll](https://man7.org/linux/man-pages/man7/epoll.7.html) 771 | - [https://github.com/frevib/epoll-echo-server](https://github.com/frevib/epoll-echo-server) - пример echo сервера на epoll 772 | 773 | #### Аналоги epoll в других системах 774 | 775 | Epoll существует только в Linux, однако другие системы имеют его аналоги работающие также за O(1) - например `kqueue` в BSD системах, `event ports` или `/dev/poll` в Solaris. 776 | 777 | --- 778 | 779 | ### Signal-Driven I/O Server 780 | 781 | Signal-Driven I/O очень дорогая и сложная модель, которая не имеет преимуществ по сравнению с мультиплексированием при дизайне веб-сервера. Поэтому такой веб-сервер рассматривать не имеет смысла. 782 | 783 | --- 784 | 785 | ### Asynchronous I/O Server (io_uring) 786 | 787 | Пока что io_uring не так хорошо адаптирован в Linux и нет популярных серверов построенных на нем. 788 | 789 | Однако, io_uring вполне может составить конкуренцию epoll - смотрите [бенчмарки](https://github.com/frevib/io_uring-echo-server/blob/io-uring-feat-fast-poll/benchmarks/benchmarks.md) echo серверов на epoll vs io_uring: 790 | 791 | **io_uring with IORING_FEAT_FAST_POLL** (requests/second) 792 | 793 | | clients | 1 | 50 | 150 | 300 | 500 | 1000 | 794 | | :--------: | :---: | :----: | :----: | :----: | :----: | :----: | 795 | | 128 bytes | 13093 | 147078 | 190054 | 216637 | 211280 | 173343 | 796 | | 512 bytes | 13140 | 150444 | 193019 | 203360 | 194701 | 156880 | 797 | | 1000 bytes | 14024 | 140248 | 178638 | 200853 | 183845 | 143810 | 798 | 799 | **epoll** (requests/second) 800 | 801 | | clients | 1 | 50 | 150 | 300 | 500 | 1000 | 802 | | :--------: | :---: | :----: | :----: | :----: | :----: | :----: | 803 | | 128 bytes | 13177 | 139863 | 152561 | 145517 | 125402 | 108380 | 804 | | 512 bytes | 13190 | 135973 | 147153 | 142518 | 124584 | 107257 | 805 | | 1000 bytes | 13172 | 131773 | 142481 | 131748 | 123287 | 102474 | 806 | 807 | Пример сервера на io_uring - [https://github.com/frevib/io_uring-echo-server](https://github.com/frevib/io_uring-echo-server). 808 | 809 | --- 810 | 811 | ### select vs poll vs epoll 812 | 813 | В какой ситуации выбирать каждый из мультиплексоров? 814 | 815 | 1. **epoll** 816 | - Всегда, если доступен 817 | 2. **poll** 818 | - Требуется поддержка платформ кроме Linux, а значит, невозможна работа с epoll, который эксклюзивен для Linux 819 | 3. **select** 820 | - Портативность приложения. Select - самый старый механизм мультиплексирования из доступных, поэтому можно быть полностью уверенным, что на каждой машине, где будет запускаться программа, данный механизм мультиплексирования будет поддерживаться 821 | 822 | --- 823 | 824 | ### Why Single-Threaded Multiplexing Model Works 825 | 826 | Вы наверное спросите, но разве это эффективно обрабатывать все дескрипторы в единственном потоке с использованием мультиплексора? 827 | 828 | Да, это эффективно по следующим причинам: 829 | 1. Мы больше не блокируемся на вызовах чтения/записи на сокетах, т.к. точно знаем, что данные там есть. А значит тред делает всегда полезную работу 830 | 2. Да, мы блокируемся на вызове мультиплексора, но это наоборот эффективно, так как мы не тратим ресурсы CPU, пока новых данных нет 831 | 3. У нас больше нет таких огромных трат циклов CPU на context switch по сравнению с моделью thread-per-request. А ведь чем больше запросов, тем больше возрастает и конкурретность, а соответственно и траты на context switch 832 | 833 | Посмотрите на стоимость различных операций в циклах CPU: 834 | ![CPU Cycles Cost](assets/operations_cpu_cost.png) 835 | 836 | Траты на context switch просто огромны - он стоит как минимум в 10 раз дороже чем kernel call! Избавление от context switch позволяет нам очень сильно выиграть в производительности программы. 837 | 838 | #### N kernel calls in loop 839 | 840 | Итак, мы получили ивенты от мультиплексора, проходимся по всем ним и читаем с них данные, вызывая сисколл [read()](https://man7.org/linux/man-pages/man2/read.2.html). Возникает закономерный вопрос: разве это эффективно делать N довольно дорогих сисколлов в цикле? 841 | 842 | Да, сисколлы действительно дорогие. И хоть они стоят на порядок меньше чем context switch, но траты все равно есть. 843 | 844 | Есть ли решение? В Single-Threaded Multiplexing I/O модели сервера нет. С этим ничего не сделать: читать данные с сокетов все равно нужно, а значит всегда будут и сисколлы. 845 | 846 | Однако, с использованием *io_uring* возможно сделать сервер, который будет делать ровно 0 сисколлов на каждый запрос - читайте [Stupid tricks with io_uring: a server that does zero syscalls per request](https://wjwh.eu/posts/2021-10-01-no-syscall-server-iouring.html). 847 | 848 | --- 849 | 850 | #### Leveraging Multi-Threading 851 | 852 | Я привел эффективную, но очень простую однопоточную модель веб сервера с использованием epoll. На самом деле, мы можем получить выигрыш в производительности от многопоточности, если имеем несколько процессорных ядер. В реальной практике сервер обычно будет спроектирован следующим образом: 853 | - **Listener** - это поток, который постоянно слушает новые соединения и отправляет этот коннекшен в *I/O Worker* 854 | - **I/O Worker(s)** - это один или несколько тредов, в которых запущен epoll - эти воркеры постоянно крутятся в лупе (event loop), ожидая новых ивентов на дескрипторах и обрабатывая их. Они принимают новые соединения от Listener и динамически добавляют их в epoll с помощью `epoll_ctl`. Таким образом, мы можем использовать не один поток с мультиплексором, а несколько, распараллеливая обработку соединений на несколько тредов 855 | - **Thread Pool** - это один или несколько тредов, которые предназначены для выполнения долгих или блокирующих операций 856 | 857 | --- 858 | 859 | ### The C10K Problem 860 | 861 | *The C10K Problem* - проблема обработки большого количества одновременных соединений. Это понятие, которое возникло когда-то давно с ростом нагрузки веб серверных приложений. 862 | 863 | На эту тему есть отличная статья - [The C10K Problem](http://www.kegel.com/c10k.html),- советую почитать, в ней рассказывается про варианты дизайна веб-сервера, с преимуществами и недостатками каждой модели, а также есть и про модели I/O. 864 | 865 | --- 866 | 867 | ### Use non-blocking I/O in case of multiplexing 868 | 869 | Во всех своих моделях с мультиплексором я использовал неблокирующие сокеты. Почему так? 870 | 871 | Первая причина заключается в том, что нотификация от edge-triggered мультиплексора о готовности данных на сокете - это всего лишь подсказка (hint). На самом деле дескриптор может быть больше не готов к I/O, когда вы попытаетесь прочитать из него. Поэтому важно использовать неблокирующий режим I/O при использовании мультиплексоров - при отсуствии данных мы получим ошибку `EAGAIN`, а не заблокируем тред. 872 | 873 | Вторая причина заключается в том, что обычно мы хотим прочитать как можно больше или полностью все данные из сокета, которые там присутствуют. Дело в том, что если на сокете все еще доступны данные для чтения, то мультиплексор всегда будет возвращать ивент на данном сокете, хотя других ивентов могло не быть. Мы не хотим постоянного возвращения из ожидания мультиплексора - это просто неэффективно, так как слишком часто переключаемся между user/kernel space, хотя на других дескрипторах могло не быть новых данных. Мы хотим максимально эффективно использовать мультиплексор и за один сисколл вызова мультиплексора принимать как много больше готовых дескрипторов. Например, для чтения 64 kB данных с помощью буфера размером 1 kb у нас может уйти или 128 сисколла (64 вызова мультиплексора и 64 чтения по 1 kb после каждого вызова), если не читать все данные, или же 65 сисколлов (1 вызов мультиплексора и 64 сискола для чтения). 874 | 875 | Итак, мы хотим прочитать все данные из сокета. Однако, при чтении мы используем буфер определенного размера, и поэтому нам может понадобиться несколько вызовов [read](https://man7.org/linux/man-pages/man2/read.2.html)/[recv](https://man7.org/linux/man-pages/man2/recv.2.html) для чтения, если размер данных больше размера буфера. Если использовать сокет в блокирующем режиме, то мы точно заблокируемся на каком-нибудь чтении, когда данных для чтения больше не окажется. Но этого можно избежать, если использовать неблокирующие сокеты - мы просто читаем до тех пор, пока вызов не вернет ошибку `EAGAIN`, которая означает, что буфер чтения пуст, то есть данных для чтения не осталось. 876 | 877 | Также неблокирующие сокеты необходимо обязательно использовать с edge-triggered режимом epoll. 878 | 879 | --- 880 | 881 | ### Edge-Triggered vs Level-Triggered Mode 882 | 883 | Еще одна особенность epoll в том, что он может работать как в edge-triggered режиме, так и level-triggered аналогично поведению select и poll. 884 | 885 | В **edge-triggered** режиме epoll возвращает только те дескрипторы, на которых произошли новые ивенты; то есть epoll отслеживает только тот факт, что файловый дескриптор *стал доступен для I/O*. 886 | А в **level-triggered** режиме ядро постоянно отслеживает статус файловых дескрипторов и возвращает те дескрипторы, на которых *в настоящий момент доступно I/O* - это поведение полностью соответствует поведению select и poll и является дефолтным поведением epoll. 887 | 888 | Edge triggered режим имеет преимущество по перформансу: ядру больше не нужно отслеживать состояние файловых дескрипторов, доступны ли для них еще данные для чтения, а оно отслеживает только новые коллбеки. 889 | 890 | Edge-triggered режим можно включить, передав специальный флаг `EPOLLET` при регистрировании сокета: 891 | 892 | ```C 893 | struct epoll_event ev; 894 | ev.data.fd = fd[i]; 895 | ev.events = EPOLLIN | EPOLLET; 896 | epoll_ctl(epfd, EPOLL_CTL_ADD, fd[i], &ev); 897 | ``` 898 | 899 | Для большего понимания давайте разберем на примере: 900 | 1. Зарегистрирован файловый дескриптор rfd, на котором мы ожидаем данные для чтения (EPOLLIN) 901 | 2. Клиент пишет в сокет 2 kB данных 902 | 3. Выполнено ожидание новых событий `epoll_wait`, и нам вернулся этот файловый дескриптор 903 | 4. Программа читает 1 kB данных из rfd 904 | 5. Снова выполняется вызов `epoll_wait` 905 | 906 | В зависимости от выбранного режима, мы можем или сразу вернуться из ожидания или же заблокироваться на шаге 5: 907 | - *edge-triggered*: мы блокируемся на вызове `epoll_wait`, не смотря на то, что данные для чтения все еще присутствуют, ведь в user space мы прочитали не все данные из дескриптора 908 | - *level-triggered*: мы сразу же возвращаемся из ожидания, так как на сокете все еще есть данные для чтения 909 | 910 | Хотя edge-triggered режим производительнее, чем level-triggered, он требует специальной обработки в user space: 911 | 1. Необходимо использовать неблокирующий режим сокета (`O_NONBLOCK`) 912 | 2. Необходимо писать ([write](https://man7.org/linux/man-pages/man2/write.2.html)) или читать ([read](https://man7.org/linux/man-pages/man2/read.2.html) данные до тех пор, пока вызов не вернет `EAGAIN` 913 | 914 | Если этого не сделать, то может возникнуть такая ситуация, когда мы не прочитали все данные и заблокировались снова, но в то же время клиент уже мог отправить все данные и ожидает ответа. То есть, новых ивентов на этом дескрипторе не наступит, а значит мы никогда не обработаем этот сокет - возникает starvation. 915 | 916 | --- 917 | 918 | ### I/O Frameworks 919 | 920 | Для написания собственного веб-сервера мы можем не использовать напрямую все эти мультиплексоры, а воспользоваться I/O фреймворками, которые предоставляют удобный высокоуровневый toolkit для обработки I/O. 921 | 922 | I/O фреймворки решают сразу множество проблем: 923 | - Скрытие деталей имплементации и предоставление единого API для обработки I/O: нам не нужно думать о блокирующем/неблокирующем режиме сокета или используемом мультиплексоре. I/O фреймворк под капотом всегда постарается использовать самую производительную модель I/O и как можно эффективнее работать с I/O 924 | - Платформо-переносимость. К сожалению, не существует единого эффективного мультиплексера, который существовал бы на каждой платформе. Linux имеет `epoll`, BSD-системы имеют `kqueue`, а Solaris имеет `evports` и `/dev/poll`. Более того, на какой-то платформе может вообще не существовать даже `poll`. И только select как самый древний мультиплексор может быть поддержан везде. Если мы хотим, чтобы наш сервер работал сразу на множестве платформ, то нам придется писать Platform-specific код, используя различные мультиплексоры в зависимости от платформы 925 | - Предоставление удобной парадигмы работы с I/O. Чаще всего это асинхронная event-driven парадигма, так как она хорошо ложится на обработку I/O, а не императивная, которую мы использовали в примерах выше. Важно заметить, что это не означает то, что под капотом будет использоваться асихронная модель I/O (как io_uring), а только то, что наш I/O запрос будет обработан каким-то образом асинхронно. Мы говорим, что хотим слушать данный сокет и при появлении данных на нем необходимо выполнить указанный event handler. Обычно все фреймворки используют под капотом epoll (или аналоги) и при готовности данных на сокете вызывают event handler 926 | 927 | Примеры I/O фреймворков: 928 | - [libevent](https://libevent.org/) 929 | - Описание с официальной страницы: 930 | > Currently, libevent supports /dev/poll, kqueue(2), event ports, POSIX select(2), Windows select(), poll(2), and epoll(4). The internal event mechanism is completely independent of the exposed event API, and a simple update of libevent can provide new functionality without having to redesign the applications. As a result, Libevent allows for portable application development and provides the most scalable event notification mechanism available on an operating system. 931 | - [libev](https://github.com/enki/libev) 932 | - Описание с официальной страницы: 933 | > libev is a high-performance event loop/event model with lots of features. 934 | - [Бенчмарк сравнения libevent и libev](http://libev.schmorp.de/bench.html) 935 | - [libuv](https://github.com/libuv/libuv) 936 | - > libuv is a multi-platform support library with a focus on asynchronous I/O 937 | - libuv был создан специально для Node.js с целью обеспечения работы на Windows: 938 | - > The node.js project began in 2009 as a JavaScript environment decoupled from the browser. Using Google’s V8 and Marc Lehmann’s libev, node.js combined a model of I/O – evented – with a language that was well suited to the style of programming; due to the way it had been shaped by browsers. As node.js grew in popularity, it was important to make it work on Windows, but libev ran only on Unix. The Windows equivalent of kernel event notification mechanisms like kqueue or (e)poll is IOCP. libuv was an abstraction around libev or IOCP depending on the platform, providing users an API based on libev. In the node-v0.9.0 version of libuv libev was removed. 939 | - [ASIO](http://think-async.com/Asio/) 940 | 941 | #### libuv 942 | 943 | Я разобрал libuv подробно в отдельной заметке. Смотрите [Multiplexing.MD#libuv](./Multiplexing.MD#libuv) 944 | 945 | --- 946 | 947 | ### Low-Level Asynchronous I/O vs Asynchronous I/O Request Processing 948 | 949 | Здесь я бы хотел обсудить понятия асинхронного I/O в высокоуровневых фреймворках и клиентах. Много высокоуровневых I/O фреймворков и HTTP клиентов предоставляют возможность асинхронной обработки I/O запросов, и часто это именуется как "асинхронное I/O". 950 | 951 | Однако, это не совсем корректно так говорить: следует отличать низкоуровневую асинхронную модель I/O по типу aio/io_uring в Linux и *асинхронную обработку I/O запросов*. Когда мы говорим про асинхронное I/O в высокоуровневых фреймворках, мы имеем в виду *асинхронную обработку*. Например, если взять асинхронный HTTP клиент, то мы не знаем, какая low-level модель I/O используется под капотом - нам лишь гарантируется, что запрос будет обработан асинхронно, не блокируя текущий тред. Однако, обычно там под капотом будет epoll + non-blocking сокеты (в Linux), потому что если нам приходится обращаться за помощью к асинхронному клиенту, то обычный блокирующий клиент с тред пулом нас уже не устраивают по причине низкого throughput, а именно асинхронные клиенты призваны решить проблему блокирующих клиентов, поэтому в здравом уме никто не будет использовать блокирующее I/O с тред пулом под капотом в таком клиенте. 952 | 953 | Существует 3 популярных способа обработки готового результата: коллбеки, promise и reactive streams. 954 | 955 | Но вообще, понятно почему это именуют асинхронным I/O - ведь со стороны разработчика (программы), который использует асинхронный http client, это так и выглядит - есть синхронный (блокирующий) http client, а есть асинхронный - и последний позволяет сделать I/O (сетевой запрос) асинхронно. 956 | 957 | #### Asynchronous HTTP Client Example 958 | 959 | Давайте рассмотрим на примере, как использовать асинхронный HTTP Client. Я возьму [HTTP Client](https://openjdk.org/groups/net/httpclient/)) из Java 11, который предоставляет возможность асинхронной обработки HTTP запросов. Данный HTTP Client использует под капотом Selector из Java NIO API, который является абстракцией над системным мультиплексором (epoll). 960 | 961 | Полный пример: 962 | 963 | ```kotlin 964 | val executor: ExecutorService = Executors.newSingleThreadExecutor { r -> 965 | val thread = Thread(r) 966 | thread.name = "http-client-executor" 967 | thread 968 | } 969 | val httpClient: HttpClient = HttpClient.newBuilder() 970 | .executor(executor) 971 | .build() 972 | 973 | val request: HttpRequest = HttpRequest.newBuilder() 974 | .uri(URI("https://example.com")) 975 | .GET() 976 | .build() 977 | 978 | val completableFuture: CompletableFuture = httpClient 979 | .sendAsync(request) { 980 | println("${Thread.currentThread().name}: handling response body") 981 | BodySubscribers.ofString(StandardCharsets.UTF_8) 982 | } 983 | .thenAcceptAsync { println("${Thread.currentThread().name}: got response") } 984 | completableFuture.join() 985 | ``` 986 | 987 | Если вызвать данную программу, то мы получим такой лог: 988 | 989 | ```text 990 | http-client-executor: handling response body 991 | ForkJoinPool.commonPool-worker-1: got response 992 | ``` 993 | 994 | Как мы видим, HttpClient обрабатывает тело ответа в отдельном Executor (`http-client-executor`), чтобы обезопасить event loop тред от потенциально долгой работы, но это детали работы данного клиента. 995 | 996 | Самое главное, что результат запроса автоматически обрабатывается в `ForkJoinPool.commonPool` благодаря `CompletableFuture`, и все это происходит асинхронно. 997 | 998 | --- 999 | --- 1000 | 1001 | ## Многопоточность (Multithreading) 1002 | 1003 | Здесь расскажу немного про важные базовые концепты многопоточности. 1004 | 1005 | ### Что такое многопоточность 1006 | 1007 | **Многопоточность** (Multithreading) - это технология, которая позволяет сосуществовать множеству тредов внутри одного процесса. 1008 | 1009 | Треды, принадлежащие к одному процессу, имеют общий доступ к памяти процесса (shared memory). Такой общий доступ и полезен, и опасен одновременно, потому что если два треда работают с одной и той же переменной или объектом, это может вызывать ошибки в программе. Поэтому мы используем *примитивы синхронизации* для синхронизирования тредов так, чтобы они изменяли ресурс последовательно, а не параллельно. 1010 | 1011 | ### Что такое concurrency, parallelism 1012 | 1013 | - **Concurrency** - означает то, что треды **работают вместе, прерываясь** - какой-то промежуток времени работает один тред, затем другой. Отсюда и название - треды *конкурируют* между собой 1014 | - **Parallelism** - означает то, что треды работают **одновременно**. Для этого нужен многоядерный процессор, на одноядерном такое невозможно 1015 | 1016 | Обе технологии на примере: 1017 | 1018 |
1019 | concurrency and parallelism comparison 1020 |
Сверху Concurrency, снизу Parallelism
1021 |
1022 | 1023 | Замечу, parallelism не исключает Concurrency: треды могут конкурировать между собой и на многоядерном процессоре, если тредов больше чем ядер, что является вполне обычной ситуацией для приложений. 1024 | 1025 | ### Как связаны многопоточность и concurrency 1026 | 1027 | В многопоточном приложении обычно количество тредов больше чем количество ядер процессора, поэтому треды обязательно будут конкурировать между собой. Соответственно, мы получаем concurrency: один промежуток времени выполняется один тред, затем другой, и таким образом треды будут сменять друг друга в пределах одного ядра процессора. 1028 | 1029 | То есть, если имеется 4-ядерный процессор - то выполняется 4 треда параллельно, а остальные ожидают своей очереди. Ни один тред не получит больше времени, чем положено - за это отвечает планировщик ОСи. 1030 | 1031 | ### Как связаны многопоточность и parallelism 1032 | 1033 | Если мы имеем многоядерный процессор, то треды могут выполняться одновременно (параллельно) на нескольких ядрах. 1034 | 1035 | --- 1036 | 1037 | ### Что такое асинхронность 1038 | 1039 | **Асинхронность** можно определить по-разному, но смысл тот же: 1040 | - Мы выражаем интерес в какой-то операции в один момент программы, а обрабатываем результат в другой момент в программе, и между этими двумя инструкциями мы выполняли другие инструкции 1041 | - Это выполнение задачи в фоне, в каком-то внешнем для текущего потока обработчике, будь то другой тред, процесс или даже компьютер. В любом случае, инициировавший асинхронную задачу поток продолжит работать дальше, делая полезную работу. Мы можем или проверить окончание выполнения задачи позже, или же получить результат выполнения задачи в виде ивента, в зависимости от используемой парадигмы, библиотеки и языка 1042 | 1043 | Сравните: в синхронной операции мы выражаем интерес в один момент программы и получаем результат в тот же момент 1044 | 1045 | #### Как связаны асинхронность и многопоточность 1046 | 1047 | Ассинхронная работа задач может быть достигнута путем передачи задачи другому треду. 1048 | 1049 | #### Как связаны асинхронность и concurrency 1050 | 1051 | Так как асинхронных операций может быть множество, то они будут выполняться concurrently. 1052 | 1053 | --- 1054 | 1055 | ## Ресурсы 1056 | 1057 | Книги: 1058 | - [Unix Network Programming, Volume 1: The Sockets Networking API](https://www.amazon.com/Unix-Network-Programming-Sockets-Networking/dp/0131411551) 1059 | - [The Linux Programming Interface](https://man7.org/tlpi/index.html) 1060 | - [Advanced Programming in the UNIX Environment](https://www.amazon.com/Advanced-Programming-UNIX-Environment-3rd/dp/0321637739) 1061 | 1062 | Статьи: 1063 | - [www.wangafu.net/~nickm/libevent-book/01_intro.html](http://www.wangafu.net/~nickm/libevent-book/01_intro.html) - вводная статья к libevent, в которой также обозреваются разные модели I/O 1064 | - [The C10K problem](http://www.kegel.com/c10k.html) - старая, но все еще актуальная статья про построение эффективного веб-сервера 1065 | - Цикл статей [Linux Applications Performance](https://unixism.net/2019/04/linux-applications-performance-introduction/) про использование различных моделей I/O для построения веб-сервера 1066 | - [Kernel Queue: The Complete Guide On The Most Essential Technology For High-Performance I/O](https://habr.com/ru/post/600123/) - обозревает различные I/O API такие как kqueue, epoll и Windows I/O Completion Ports 1067 | - [Lord of the io_uring](https://unixism.net/loti/index.html) - сайт про io_uring 1068 | - [Lord of the io_uring: Asynchronous Programming Under Linux](https://unixism.net/loti/async_intro.html) - про асинхронное I/O в Linux 1069 | 1070 | Docs: 1071 | - [Linux man-pages](https://www.kernel.org/doc/man-pages/) - Linux manual pages 1072 | --------------------------------------------------------------------------------