io_uring учится подменять личности потоков ради скорости
Подсистема io_uring построена на обещании не блокировать вызывающий поток без явной на то просьбы. Проблема в том, что многие пути внутри ядра изначально не рассчитаны на асинхронное исполнение. Сегодня любую операцию, способную заблокироваться (например, аналоги fdatasync(), statx() или части openat()), io_uring передаёт отдельному рабочему потоку. Это гарантирует, что io_uring_enter() не зависнет, но обходится дорого: пробуждение потока и переключение контекста. Обидно, что расплачиваться приходится всегда, даже когда операция в итоге и не блокируется, — а именно ради производительности за io_uring обычно и берутся.
Обмен личностями
Йенс Аксбо опубликовал RFC-набор патчей с решением, которое сам называет пугающим. Идея: начинать потенциально блокирующую операцию прямо в вызывающем потоке и платить за рабочий поток только тогда, когда блокировка действительно произошла.
Чтобы поймать этот момент, потребовалась правка планировщика. В task_struct добавлен флаг PF_IO_HANDOFF; если помеченный им поток собирается заснуть где угодно в ядре, планировщик вызывает новую функцию io_uring_task_sleeping(). Так io_uring узнаёт о блокировке, не переписывая сами кодовые пути.
Дальше происходит "передача личности потока". Отматывать уже выполненную работу нельзя — блокировка может случиться почти где угодно, поэтому операцию надо доводить до конца. Вместо этого io_uring берёт поток из своего пула и меняет двум потокам идентичности: рабочий поток получает идентификатор, настройки обработки сигналов и прочие атрибуты исходного, продолжает обработку очереди и в итоге возвращается в пользовательское пространство. А исходный поток, приняв личность рабочего, спокойно блокируется, по пробуждении завершает операцию и уходит в пул.
Много мин на пути
Главная опасность — ссылки на task_struct. Перед обменом ядро обязано убедиться, что структуру никто больше не держит, иначе кто-то обратится не к тому потоку, пополнив поток кернельных CVE. Отсюда длинный список запретов на передачу: трассировка через ptrace() (в обе стороны), perf-события, отслеживаемое ядром владение futex, realtime-планировщик, cookie core scheduling, vfork() и другие. Именно эта проверка выглядит самой хрупкой частью: task_struct используется повсюду, и трудно поручиться, что учтены все ссылки — тем более будущие, добавленные разработчиками, вовсе не думающими про io_uring.
Выигрыш при этом велик. В бенчмарках fsync() на tmpfs дал прирост почти на 700%. Другие улучшения скромнее, а тесты, где блокировка происходит всегда, местами показывают регрессию — особенно при большой глубине очереди, когда последовательная обработка проигрывает параллельному запуску в воркерах. Аксбо считает, что приложения с такими операциями обычно и не держат длинных очередей.
В ближайшее время патчи вливать никто не собирается — автор лишь проверяет жизнеспособность подхода. Петер Зейлстра указал, что запрет на передачу при активном shadow stack закроет фичу для большинства систем; Аксбо надеется переносить и его. Гавриэль Крисман Бертацци уложил общее настроение в формулу: "очень круто и выглядит очень опасно".
Источник: Lobsters
Комментарии
Войдите, чтобы комментировать.