├── .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 | 
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 | 
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 | 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 | 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 |
[Операционные системы] | [Компьютерные сети] | [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 |  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 |  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 |  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 |  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
1020 |