УДК 004.051

Исследование технологии ввода-вывода в ядре Linux для оптимизации многопоточного ввода-вывода в асинхронном режиме

Штатнов Артемий Александрович – магистрант кафедры «Информационные системы и технологии» Казанского национального исследовательского технологического университета

Аннотация: Данная статья посвящена анализу io_uring технологии ввода-вывода в ядре Linux, предназначенной для оптимизации многопоточного ввода-вывода в асинхронном режиме. Она обеспечивает эффективную обработку набора задач в единой очереди, которая находится в ядре операционной системы, позволяя достигать высокой производительности и сокращать накладные расходы на управление системными вызовами.

Keywords: automated system, unauthorized access, AnyDesk, Ammyy Admin, RAdmin, TeamViewer.

 

Что такое io_uring

Io_uring – это интерфейс системного вызова для Linux. Он был впервые представлен в версии ядра Linux 5.1 в 2019 году. Он позволяет приложению инициировать системные вызовы, которые могут выполняться асинхронно. Изначально io_uring поддерживал только простые системные вызовы ввода-вывода, такие как read() и write(), но поддержка других вызовов постоянно растет и быстро развивается. В конечном итоге он может обладать поддержкой для большинства системных вызовов.

Зачем это используется

Основной мотивацией при создании io_uring была улучшение производительности. Несмотря на то, что он до сих пор относительно новый, его производительность быстро улучшается. Создатель и основной разработчик Jens Axboe похвастался пиковой производительностью в 13 миллионов IOPS на ядро. У io_uring есть несколько ключевых элементов дизайна, которые снижают накладные расходы и повышают производительность.

С io_uring системные вызовы могут быть завершены асинхронно. Это означает, что поток приложения не должен блокироваться во время ожидания завершения системного вызова ядром. Он может просто отправить запрос на системный вызов и получить результаты позже блокировка не требуется, и время не теряется.

Кроме того, можно отправить все запросы на системный вызов единовременно. Задача, которая обычно требует нескольких системных вызовов, может быть сокращена до одного. Также есть новая функция, которая может снизить количество системных вызовов до нуля. Это существенно снижает количество контекстных переключений между пользовательским пространством и ядром. Каждое контекстное переключение добавляет накладные расходы, поэтому их сокращение имеет преимущества для производительности.

В io_uring большая часть связи между пользовательским пространством приложения и ядром осуществляется через общие буферы. Это снижает большое количество накладных расходов при выполнении системных вызовов, которые передают данные между ядром и пользовательским пространством. По этой причине io_uring может быть системой нулевого копирования.

Также есть функция "фиксированных" файлов, которая может улучшить производительность. До того, как операция чтения или записи может быть выполнена с файловым дескриптором, ядро должно ссылаться на файл. Поскольку ссылка на файл происходит атомарно, это вызывает накладные расходы. С фиксированным файлом эта ссылка открыта, и нет необходимости получать ссылку для каждой операции.

Накладные расходы на блокировку, контекстные переключения или копирование байтов могут быть незаметны для большинства случаев, но в высокопроизводительных приложениях это может иметь значение. Также стоит отметить, что производительность системных вызовов ухудшилась после патчей для Spectre и Meltdown, поэтому сокращение количества системных вызовов может быть важной оптимизацией.

Для чего это используется

Как отмечено выше, приложения с высокой производительностью могут получить выгоду от использования io_uring. Он может быть особенно полезен для приложений, связанных с сервером/бэкэндом, где значительная доля времени приложения тратится на ожидание ввода-вывода.

Как его использовать

Использовать io_uring можно делая вызовы системных функций io_uring непосредственно. Это достаточно трудоемкое занятие, так как io_uring довольно сложен, а пользовательское приложение отвечает за множество работы для его правильного функционирования. Вместо этого разработчик может использовать liburing, если бы хотел, чтобы его приложение использовало io_uring.

liburing представляет собой библиотеку пользовательского пространства, которая обеспечивает упрощенный API для взаимодействия с компонентом ядра io_uring. Она разрабатывается и поддерживается главным разработчиком io_uring, поэтому она обновляется при изменениях с ядровой стороны.

Следует отметить одну вещь: io_uring не реализует версионирование своих структур. Поэтому, если приложение использует новую функцию, сначала необходимо проверить, поддерживает ли ядро системы, на которой оно работает, данную функцию. К счастью, системный вызов io_uring_setup возвращает эту информацию.

io_uring и liburing быстро развиваются, документация постоянно нуждается в обновлении. Образцы кода и примеры, находящиеся в свободном доступе в сети Интернет, не согласованы: новые функции влияют на актуальность более ранних.

Это распространенная проблема для OSS, но она не является показателем качества библиотеки. Все это важно иметь в виду, особенно, при первичном использовании. При этом, встречаются случаи фундаментального изменения реакций в разных версиях ядра, что не всегда отражено и задокументировано.

Как это работает

Как следует из названия, центральной частью модели io_uring являются два кольцевых буфера, находящиеся в памяти, общей для пространства пользователя и ядра. Экземпляр io_uring инициализируется вызовом системного вызова io_uring_setup. Ядро вернет дескриптор файла, который приложение пространства пользователя будет использовать для создания отображений общей памяти.

Создаются отображения:

Очередь отправки (SQ) – кольцевой буфер, где размещаются запросы системного вызова.

Очередь завершения (CQ) – кольцевой буфер, куда помещаются завершенные запросы системного вызова. Массив элементов очереди отправки (SQE), размер которого выбирается при настройке.

1

Рисунок 1. Сопоставления создаются для совместного использования памяти между пространством пользователя и ядром.

Для каждого запроса заполняется SQE и помещается в кольцевую очередь отправки. Один SQE описывает операцию системного вызова, которую необходимо выполнить. Ядро уведомляет о наличии работы в SQ, когда приложение делает системный вызов io_uring_enter. В качестве альтернативы, если используется функция IORING_SETUP_SQPOLL, создается поток ядра для опроса SQ на наличие новых записей, что исключает необходимость в вызове системного вызова io_uring_enter.

2

Рисунок 2. Приложение, отправляющее запрос на операцию чтения в io_uring.

При завершении каждого SQE ядро сначала определяет, будет ли выполнена операция асинхронно. Если операция может быть выполнена без блокировки, она будет выполнена синхронно в контексте вызывающего потока. В противном случае она помещается в очередь асинхронной работы ядра и выполнение передается потоку io_wrk асинхронно. В обоих случаях вызывающий поток не будет заблокирован, разница в том, будет ли операция завершена немедленно вызывающим потоком или позже потоком io_wrk:

2

Рисунок 3.

При завершении операции для каждого SQE в CQ помещается запись очереди завершения (CQE). Приложение может опрашивать CQ на наличие новых CQE. В этом случае приложение узнает, что соответствующая операция выполнена. SQE могут завершаться в любом порядке, но могут быть связаны друг с другом, если нужен определенный порядок завершения.

4

 

Рисунок 4.

Список некоторый функции io_uring

#include <stdio.h>

#include <stdlib.h>

#include <fcntl.h>

#include <unistd.h>

#include <string.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <liburing.h>

#define BLOCK_SIZE 1024

int main(int argc, char *argv[]) {

struct io_uring ring;

struct io_uring_sqe *sqe;

struct io_uring_cqe *cqe;

int fd, ret;

char buffer[BLOCK_SIZE];

if (io_uring_queue_init(2, &ring, 0) < 0) {

perror("io_uring_queue_init failed");

exit(1);

}

fd = open(argv[1], O_RDONLY | O_DIRECT);

if (fd < 0) {

perror("open failed");

exit(1);

}

memset(buffer, 0, BLOCK_SIZE);

sqe = io_uring_get_sqe(&ring);

io_uring_prep_read(sqe, fd, buffer, BLOCK_SIZE, 0);

io_uring_submit(&ring);

ret = 0;

while (ret != 1) {

ret = io_uring_wait_cqe(&ring, &cqe);

if (ret < 0) {

perror("io_uring_wait_cqe failed");

exit(1);

}

if (cqe->res < 0) {

perror("io_uring operation failed");

exit(1);

}

io_uring_cqe_seen(&ring, cqe);

}

printf("%s", buffer);

close(fd);

io_uring_queue_exit(&ring);

return 0;

}

  • вызывается io_uring_queue_init, чтобы инициализировать io_uring;
  • открывается файл, который будем читать;
  • создается буфер для чтения;
  • вызывается io_uring_get_sqe, чтобы получить структуру sqe для записи операции чтения;
  • вызывается io_uring_prep_read, чтобы предварительно настроить sqe для чтения из файла;
  • вызывается io_uring_submit, чтобы отправить запрос на чтение в ядро;
  • вызывается io_uring_wait_cqe, чтобы дождаться завершения операции чтения;
  • проверяется, успешно ли завершилась операция чтения;
  • вызывается io_uring_cqe_seen, чтобы наконец освободить использованный CQE;
  • закрывается файл и освобождается io_urign с использованием io_uring_queue_exit. 

В результате получается вывод прочитанной строки.

Заключение

Введение в io_uring представляет собой уникальную технологию, которая позволяет улучшить производительность при работе с вводом/выводом в операционных системах Linux. Она обладает рядом преимуществ по сравнению с другими подходами, такими как epoll, и может быть использована в самых разных приложениях от веб-серверов до баз данных и распределенных систем. С помощью io_uring можно эффективно обрабатывать большие объемы данных и делать это с минимальной задержкой. Эта технология активно развивается и улучшается, поэтому ее применение будет только дальнейшим расширяться. Если вы ищете новые и инновационные подходы к улучшению производительности ваших приложений, io_uring то, что вам нужно.

Список литературы

  1. Linux: сайт. – URL: https://www.linux.com/training-tutorials/understanding-and-using-io_uring-ultrafast-linux-io-interface/ (дата обращения: 10.03.2023)
  2. Ringing in a new asynchronous I/O API // lwn : сайт. – URL: https://lwn.net/Articles/776703/ (дата обращения: 10.03.2023)
  3. Linux io_uring how does it work // redhat : сайт. – URL: https://developers.redhat.com/blog/2019/05/15/linux-io_uring-how-does-it-work/ (дата обращения: 10.03.2023)
  4. Understanding and using io_uring ultrafast linux io interface // linux : сайт. – URL: https://www.linux.com/training-tutorials/understanding-and-using-io_uring-ultrafast-linux-io-interface/ (дата обращения: 10.03.2023)
  5. io-uring // cloudflare : сайт. – URL: https://blog.cloudflare.com/io-uring-overcoming-io-demux-misery/ (дата обращения: 10.03.2023)
  6. io_uring exploiting the linux kernel // graplsecurity : сайт. – URL: https://www.graplsecurity.com/post/iou-ring-exploiting-the-linux-kernel (дата обращения: 10.03.2023)
  7. what is io_uring // unixism : сайт. – URL: https://unixism.net/loti/what_is_io_uring.html (дата обращения: 10.03.2023)
  8. what is io_uring // unixism : сайт. – URL: https://unixism.net/loti/what_is_io_uring.html (дата обращения: 10.03.2023)
  9. io_uring manual // archlinux : сайт. – URL: https://man.archlinux.org/man/io_uring.7.en (дата обращения: 10.03.2023)
  10. The rapid growth of io_uring // lwn : сайт. – URL: https://lwn.net/Articles/810414/ (дата обращения: 10.03.2023)
  11. Часть 1. Введение // habr : сайт. – URL: https://habr.com/ru/post/589389/ (дата обращения: 10.03.2023).
  12. Liburing // github: сайт. – URL: https://github.com/axboe/liburing (дата обращения: 10.03.2023).

Интересная статья? Поделись ей с другими: