Что видит программист, начиная работать с языком C? Он видит fopen, printf, scanf и ещё много других функций. Видит он и всякие open и mmap — казалось бы, зачем их выделять? Но, в отличие от первой группы, эти две функции при выполнении на ядре Linux являются системными вызовами (на самом деле нет, почти никогда системный вызов нельзя просто вызвать как функцию, и поэтому libc содержит обёртки, перепаковывающие аргументы и иногда, как в случае с тем же open, заменяющие старые системные вызовы более общими новыми). Вообще, в отличие от тысяч библиотечных функций, доступных на типичной GNU/Linux системе, интерфейс ядра имеет довольно ограниченное количество точек входа — порядка нескольких сотен, зато то, что для user space — crash (например, обращение к отсутствующей странице), для ядра — default mode of operation. В этой статье я расскажу некоторые интересные на мой взгляд факты. В ней не будет futex-ов и прочих скучных (наверное) деталей реализации. Будет преимущественно то, что вызывало у меня реакцию «А что, так можно было?!?». Во первых, некоторые комментарии к тексту до ката: некоторые системные вызовы имеют опциональный интерфейс в виде функции из shared object под названием vDSO, подкладываемого ядром в процесс. Таких функций немного (что-то около четырёх, но конкретное количество, видимо, может зависеть от версии ядра и архитектуры) — это всякие time и gettimeofday, которые, с одной стороны, часто используются, а с другой — их удалось реализовать без переключения в контекст ядра. Во-вторых, не всегда SIGSEGV заканчивается крешем процесса, но об этом мы ещё поговорим, когда речь зайдёт о userfaultfd. DISCLAIMER: помните, что используя большинство из представленных здесь возможностей, вы завязываете свою программу на Linux. Это нормально, если таким способом вы делаете опциональную оптитимизацию для конкретного типа систем или дополнительную возможность, которой иначе бы просто не было. Но в противном случае рекомендую подумать о том, как сделать кросс-платформенный fallback. Для начала, как можно всё это отлаживать? Конечно же, нам поможет strace! Поскольку набор системных вызовов ограничен, и большинство strace знает «в лицо», то он покажет не просто «передан указатель 0x12345678», а опишет, что в этой структуре передаётся в ту или иную сторону. Если strace достаточно свежий, то с помощью параметра -k его можно попросить выдавать стек вызовов. Правда, тут не отображаются имена файлов с исходным кодом и номера строк. Вам поможет addr2line (если эта информация в принципе присутствует, конечно). Есть и второй вопрос: некоторые системные вызовы не имеют обёрток в libc. Тогда можно воспользоваться универсальной обёрткой под названием syscall: Системные вызовы — это не только способ попросить ядро обратиться к оборудованию от имени процесса. Это ещё и универсальное API, понятное всем библиотекам в системе. Значит, если в библиотеке не поддержали нужную вам функциональность, возможно, она автоматически получится, если правильно попросить ядро. К тому же, часть «настроек» процесса наследуется при execve, поэтому таким образом можно попробовать обойтись без сложных костылей, просто правильно сформировав состояние перед запуском процесса (что-то вроде «зачем вручную перекладывать stderr в файл, если можно просто открыть файл и сделать его FD #2 для дочернего процесса»). Как-то раз мне потребовалось вычитать из файла последовательность сетевых пакетов. В какой-то момент количество костылей превысило все разумные пределы, и я решил, что вряд ли libpcap будет сложнее, чем то, что я написал, к тому же, это стандарт, и для открытия этих файлов существуют общепринятые инструменты. Оказалось, что пользоваться libpcap для чтения дампов примерно настолько же сложно, как и fopen для чтения файлов: вы просто открываете дамп с помощью pcap_(f)open_offline и вычерпываете пакеты через pcap_next_ex. Всё! Ну, ещё стоит закрыть дамп по завершению работы... Но вот незадача: похоже, libpcap не умеет читать из памяти. Может, и умеет конечно, если покопаться, но для нашей «лабораторки» представим, что не умеет. Итак, модельный пример: мы ждём на stdin некую последовательность байтов, после которой идёт выровненный на 4 байта дамп. Я понимаю, что можно использовать буферизованный ввод и какой-нибудь ungetc (поскольку libpcap всё равно требует FILE *), но в общем случае мы, может, на ходу распаковываем, например, или библиотека может непосредственно работать с read / write. Системный вызов memfd_create позволяет создать «вообще анонимный» файловый дескриптор. Файл находится в памяти и существует, пока на него открыт хоть один дескриптор. В простейшем случае, вы просто получаете такой дескриптор, записываете в него данные через write, перематываете lseek, и с помощью fdopen даёте о нём знать libc: Имя файла, передаваемое первым аргументом, будет отображаться в символьной ссылке в /proc/<PID>/fd: В Linux, начиная с какой-то версии, при создании файла можно указать опцию O_TMPFILE и имя каталога вместо имени файла. В итоге файл, как (приблизительно) говаривал один литературный персонаж, вроде он есть, но его как бы нет… Пишутся ли данные на диск — не знаю, но наверное, это зависит и от файловой системы (кстати, она должна поддерживать этот режим). Файл всё так же исчезает при закрытии последней ссылки, но его можно прикрепить к дереву каталогов с помощью linkat: Кроме возможности не мучаться с именованием файла, это даёт возможность заполнить файл, настроить права и т. д., а потом атомарно прилинковать в дерево каталогов. Думаю, не будет чего-то сильно нового в том, чтобы сказать, что в UNIX-like системах файловые дескрипторы на что только не указывают. Например, на Linux это может быть сокет, pipe, eventfd или даже ссылка на ebpf-программу. Но, возможно, этот пример вас всё-таки удивит. В начале статьи я говорил о том, что для ядра page faults — обычное дело: своп, copy-on-write, вот это всё… Когда же пользовательский процесс «промахивается», ему отправляется SIGSEGV. Насколько я знаю, возврат управления из обработчика SIGSEGV, сгенерированного ядром, является undefined behavior, и тем не менее, существует библиотека GNU libsigsegv, обобщающая особенности обработки ошибки доступа к памяти на различных платформах, даже Windows (ВНИМАНИЕ: лицензия GPL, если не готовы под ней же распространять свою программу, то не используйте libsigsegv). Не так давно в Linux появился абсолютно задокументированный способ, называется userfaultfd: с помощью одноимённого системного вызова вы открываете файловый дескриптор, чтение и запись в который специальных структур являются командами. Имея такой файловый дескриптор, вы можете пометить некий диапазон виртуальных адресов вашего процесса. После этого при первом обращение к каждой помеченной странице памяти, обратившийся поток заснёт, а чтение из файлового дескриптора вернёт информацию о произошедшем. После чего обработчик заполнит ответную структуру с указателем на данные, которые нужно использовать для инициализации «проблемной» страницы, ядро её проинициализирует и разбудит обратившийся поток. При этом предполагается наличие отдельного потока, в чьи обязанности входит чтение команд из дескриптора и выдача ответов. Вообще говоря, через userfaultfd можно получать и другую информацию, например, некоторые уведомления об изменении карты виртуальной памяти процесса. Что, если вам нужно узнать, ссылается ли этот файловый дескриптор на stdin? Казалось бы, if (fd == 0) ... — и все дела. Ну, окей... Упс… Дескриптор-то вроде как один, но алиасы разные. Нам поможет CRIU — Checkpoint/Restore In Userspace. Без паники, я не предлагаю сдампить процесс, посмотреть на результат и загрузить обратно. Просто для нужд своих userspace-инструментов, разработчики из этого проекта, как я понял, добавили в ядро системный вызов kcmp: ему передаются два PID, тип ресурса и, собственно, два ресурса, а он говорит, указывают ли они на одну и ту же сущность ядра: Опять двадцать пять! Подождите ка, а что если... Ага, тот неловкий момент, когда пытаешься понять, где ошибка, а она оказалась в твоём понимании происходящего. Логично считать, что bash точно так же поступает и с моей программой, как и с ls! Не буду утверждать, что это работает всегда и идеально — для этого нужно самому его использовать много и по-разному, но как best effort инструмент для каких-нибудь эвристик наверняка может пригодиться. Знаете ли вы, ... А ещё в процессе просмотра списка системных вызовов я нашёл oldolduname... Программист