Необычные системные вызовы на Linux
Что видит программист, начиная работать с языком 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
знает «в лицо», то он покажет не просто «передан указатель 0×12345678», а опишет, что в этой структуре передаётся в ту или иную сторону. Если strace
достаточно свежий, то с помощью параметра -k
его можно попросить выдавать стек вызовов.
$ strace -k sleep 1
execve("/bin/sleep", ["sleep", "1"], 0x7ffe9f9cce30 /* 60 vars */) = 0
> /lib/x86_64-linux-gnu/libc-2.30.so(execve+0xb) [0xe601b]
> /usr/bin/strace(+0x0) [0xa279c]
> /usr/bin/strace(+0x0) [0xa41d2]
> /usr/bin/strace(+0x0) [0x7090b]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3]
> /usr/bin/strace(+0x0) [0x7112a]
brk(NULL) = 0x558936ded000
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x20b) [0x1ccdb]
> /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1cd2) [0x1b872]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
arch_prctl(0x3001 /* ARCH_??? */, 0x7fff593c0070) = -1 EINVAL (Недопустимый аргумент)
> /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e25) [0x1b9c5]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Нет такого файла или каталога)
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x10cb) [0x1db9b]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c12]
> /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x1238) [0x1dd08]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_debug_state+0x73a) [0x11d4a]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_exception_free+0x908) [0x189c8]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0xa362]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55]
> /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
fstat(3, {st_mode=S_IFREG|0644, st_size=254851, ...}) = 0
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x1009) [0x1dad9]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_debug_state+0x761) [0x11d71]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_exception_free+0x908) [0x189c8]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0xa362]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55]
> /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
mmap(NULL, 254851, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc49621c000
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x1426) [0x1def6]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_debug_state+0x79d) [0x11dad]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_exception_free+0x908) [0x189c8]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0xa362]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55]
> /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
close(3) = 0
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x10fb) [0x1dbcb]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_debug_state+0x780) [0x11d90]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_exception_free+0x908) [0x189c8]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0xa362]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55]
> /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x1238) [0x1dd08]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x7d40]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0xa3a8]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55]
> /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360r\2\0\0\0\0\0"..., 832) = 832
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x12f8) [0x1ddc8]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x7d79]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0xa3a8]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85]
> /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55]
> /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c]
> /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
...
Много динамической линковки
...
brk(NULL) = 0x558936ded000
> /lib/x86_64-linux-gnu/libc-2.30.so(brk+0xb) [0x11755b]
> /lib/x86_64-linux-gnu/libc-2.30.so(__sbrk+0x67) [0x117617]
> /lib/x86_64-linux-gnu/libc-2.30.so(__default_morecore+0xd) [0x9fd3d]
> /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x2725) [0x9a745]
> /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x3943) [0x9b963]
> /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x3b2b) [0x9bb4b]
> /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x4d9e) [0x9cdbe]
> /lib/x86_64-linux-gnu/libc-2.30.so(textdomain+0x740) [0x3be70]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1d35) [0x35515]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5]
> /bin/sleep() [0x25f0]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3]
> /bin/sleep() [0x287e]
brk(0x558936e0e000) = 0x558936e0e000
> /lib/x86_64-linux-gnu/libc-2.30.so(brk+0xb) [0x11755b]
> /lib/x86_64-linux-gnu/libc-2.30.so(__sbrk+0x91) [0x117641]
> /lib/x86_64-linux-gnu/libc-2.30.so(__default_morecore+0xd) [0x9fd3d]
> /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x2725) [0x9a745]
> /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x3943) [0x9b963]
> /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x3b2b) [0x9bb4b]
> /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x4d9e) [0x9cdbe]
> /lib/x86_64-linux-gnu/libc-2.30.so(textdomain+0x740) [0x3be70]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1d35) [0x35515]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5]
> /bin/sleep() [0x25f0]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3]
> /bin/sleep() [0x287e]
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
> /lib/x86_64-linux-gnu/libc-2.30.so(__open64_nocancel+0x4c) [0x11679c]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1ce9) [0x354c9]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5]
> /bin/sleep() [0x25f0]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3]
> /bin/sleep() [0x287e]
fstat(3, {st_mode=S_IFREG|0644, st_size=8994080, ...}) = 0
> /lib/x86_64-linux-gnu/libc-2.30.so(__fxstat64+0x19) [0x1107b9]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1e33) [0x35613]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5]
> /bin/sleep() [0x25f0]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3]
> /bin/sleep() [0x287e]
mmap(NULL, 8994080, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc495795000
> /lib/x86_64-linux-gnu/libc-2.30.so(mmap64+0x26) [0x11baf6]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1e5d) [0x3563d]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5]
> /bin/sleep() [0x25f0]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3]
> /bin/sleep() [0x287e]
close(3) = 0
> /lib/x86_64-linux-gnu/libc-2.30.so(__close_nocancel+0xb) [0x1165bb]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1eab) [0x3568b]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf]
> /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5]
> /bin/sleep() [0x25f0]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3]
> /bin/sleep() [0x287e]
nanosleep({tv_sec=1, tv_nsec=0}, NULL) = 0
> /lib/x86_64-linux-gnu/libc-2.30.so(nanosleep+0x17) [0xe5d17]
> /bin/sleep() [0x5827]
> /bin/sleep() [0x5600]
> /bin/sleep() [0x27b0]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3]
> /bin/sleep() [0x287e]
close(1) = 0
> /lib/x86_64-linux-gnu/libc-2.30.so(__close_nocancel+0xb) [0x1165bb]
> /lib/x86_64-linux-gnu/libc-2.30.so(_IO_file_close_it+0x70) [0x92fc0]
> /lib/x86_64-linux-gnu/libc-2.30.so(fclose+0x166) [0x85006]
> /bin/sleep() [0x5881]
> /bin/sleep() [0x2d27]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_secure_getenv+0x127) [0x49ba7]
> /lib/x86_64-linux-gnu/libc-2.30.so(exit+0x20) [0x49d60]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xfa) [0x271ea]
> /bin/sleep() [0x287e]
close(2) = 0
> /lib/x86_64-linux-gnu/libc-2.30.so(__close_nocancel+0xb) [0x1165bb]
> /lib/x86_64-linux-gnu/libc-2.30.so(_IO_file_close_it+0x70) [0x92fc0]
> /lib/x86_64-linux-gnu/libc-2.30.so(fclose+0x166) [0x85006]
> /bin/sleep() [0x5881]
> /bin/sleep() [0x2d4d]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_secure_getenv+0x127) [0x49ba7]
> /lib/x86_64-linux-gnu/libc-2.30.so(exit+0x20) [0x49d60]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xfa) [0x271ea]
> /bin/sleep() [0x287e]
exit_group(0) = ?
+++ exited with 0 +++
> /lib/x86_64-linux-gnu/libc-2.30.so(_exit+0x36) [0xe5fe6]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_secure_getenv+0x242) [0x49cc2]
> /lib/x86_64-linux-gnu/libc-2.30.so(exit+0x20) [0x49d60]
> /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xfa) [0x271ea]
> /bin/sleep(+0x0) [0x287e]
Правда, тут не отображаются имена файлов с исходным кодом и номера строк. Вам поможет addr2line
(если эта информация в принципе присутствует, конечно).
Есть и второй вопрос: некоторые системные вызовы не имеют обёрток в libc
. Тогда можно воспользоваться универсальной обёрткой под названием syscall
:
syscall(SYS_kcmp, getpid(), getpid(), KCMP_FILE, 1, fd)
Файл — это очень уж странный предмет…
Системные вызовы — это не только способ попросить ядро обратиться к оборудованию от имени процесса. Это ещё и универсальное API, понятное всем библиотекам в системе. Значит, если в библиотеке не поддержали нужную вам функциональность, возможно, она автоматически получится, если правильно попросить ядро. К тому же, часть «настроек» процесса наследуется при execve
, поэтому таким образом можно попробовать обойтись без сложных костылей, просто правильно сформировав состояние перед запуском процесса (что-то вроде «зачем вручную перекладывать stderr
в файл, если можно просто открыть файл и сделать его FD #2 для дочернего процесса»).
Как-то раз мне потребовалось вычитать из файла последовательность сетевых пакетов. В какой-то момент количество костылей превысило все разумные пределы, и я решил, что вряд ли libpcap
будет сложнее, чем то, что я написал, к тому же, это стандарт, и для открытия этих файлов существуют общепринятые инструменты. Оказалось, что пользоваться libpcap
для чтения дампов примерно настолько же сложно, как и fopen
для чтения файлов: вы просто открываете дамп с помощью pcap_(f)open_offline
и вычерпываете пакеты через pcap_next_ex
. Всё! Ну, ещё стоит закрыть дамп по завершению работы…
Но вот незадача: похоже, libpcap
не умеет читать из памяти. Может, и умеет конечно, если покопаться, но для нашей «лабораторки» представим, что не умеет.
Итак, модельный пример: мы ждём на stdin
некую последовательность байтов, после которой идёт выровненный на 4 байта дамп. Я понимаю, что можно использовать буферизованный ввод и какой-нибудь ungetc
(поскольку libpcap
всё равно требует FILE *
), но в общем случае мы, может, на ходу распаковываем, например, или библиотека может непосредственно работать с read
/ write
.
Решение 1: memfd_create
Системный вызов memfd_create
позволяет создать «вообще анонимный» файловый дескриптор. Файл находится в памяти и существует, пока на него открыт хоть один дескриптор. В простейшем случае, вы просто получаете такой дескриптор, записываете в него данные через write
, перематываете lseek
, и с помощью fdopen
даёте о нём знать libc
:
int fd = memfd_create("pcap-dump-contents", 0);
write(fd, buf, length);
lseek(fd, 0, SEEK_SET);
FILE *file = fdopen(fd, "r");
Имя файла, передаваемое первым аргументом, будет отображаться в символьной ссылке в /proc/
:
$ ls -l /proc/31747/fd
итого 0
lr-x------ 1 trosinenko trosinenko 64 ноя 10 13:12 0 -> /path/to/128test.pcap
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 1 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 2 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 23 -> '/home/trosinenko/.cache/appstream-cache-AH3OA0.mdb (deleted)'
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 3 -> '/memfd:pcap-dump-contents (deleted)'
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 57 -> 'socket:[41036]'
Решение 2: open с флагом O_TMPFILE
В Linux, начиная с какой-то версии, при создании файла можно указать опцию O_TMPFILE
и имя каталога вместо имени файла. В итоге файл, как (приблизительно) говаривал один литературный персонаж, вроде он есть, но его как бы нет… Пишутся ли данные на диск — не знаю, но наверное, это зависит и от файловой системы (кстати, она должна поддерживать этот режим). Файл всё так же исчезает при закрытии последней ссылки, но его можно прикрепить к дереву каталогов с помощью linkat
:
int fd = open(".", O_RDWR | O_TMPFILE, S_IRUSR | S_IWUSR);
assert(fd != -1);
assert(write(fd, buffer + offset, len - offset) == len - offset);
assert(lseek(fd, 0, SEEK_SET) == 0);
const char *link_to = getenv("LINK_TO");
if (link_to != NULL) {
char path[128];
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
linkat(AT_FDCWD, path, AT_FDCWD, link_to, AT_SYMLINK_FOLLOW);
}
Кроме возможности не мучаться с именованием файла, это даёт возможность заполнить файл, настроить права и т. д., а потом атомарно прилинковать в дерево каталогов.
#define _GNU_SOURCE
#ifdef NDEBUG
// Чтобы спокойно использовать assert с сайд-эффектами в примерах
# undef NDEBUG
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// Идентификатор файла в формате PCAP (один из возможных -- см. спецификацию)
static const uint32_t pcap_mgc = 0xA1B2C3D4;
char buffer[1 << 20];
int main()
{
int len = read(0, buffer, sizeof(buffer));
// По какой-то причине у нас сначала идёт "мусор",
// который никогда не содержит pcap_mgc, а потом
// выровненный на 4 байта дамп. Просто для примера...
int offset = -1;
for (int i = 0; i < len; i += 4) {
if (*(uint32_t *)(buffer + i) == pcap_mgc) {
offset = i;
break;
}
}
if (offset >= 0) {
printf("Found PCAP dump at offset %d\n", offset);
} else {
fprintf(stderr, "No PCAP dump found.\n");
exit(1);
}
// Теперь сделаем так, чтобы libpcap считал, что читает
// данные из файла.
#if 0
int fd = memfd_create("pcap-dump-contents", 0);
assert(fd != -1);
assert(write(fd, buffer + offset, len - offset) == len - offset);
assert(lseek(fd, 0, SEEK_SET) == 0);
#else
int fd = open(".", O_RDWR | O_TMPFILE, S_IRUSR | S_IWUSR);
assert(fd != -1);
assert(write(fd, buffer + offset, len - offset) == len - offset);
assert(lseek(fd, 0, SEEK_SET) == 0);
const char *link_to = getenv("LINK_TO");
if (link_to != NULL) {
char path[128];
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
linkat(AT_FDCWD, path, AT_FDCWD, link_to, AT_SYMLINK_FOLLOW);
}
#endif
raise(SIGSTOP); // Чтобы посмотреть в /proc/PID/fd/
// Теперь попробуем открыть дамп и что-нибудь вывести...
FILE *file = fdopen(fd, "r");
char errbuf[PCAP_ERRBUF_SIZE];
pcap_t * dump = pcap_fopen_offline(file, errbuf);
assert(dump != NULL);
struct pcap_pkthdr *hdr;
const uint8_t *data;
while (pcap_next_ex(dump, &hdr, &data) == 1) {
printf("Read packet: full length = %d bytes, available %d bytes.\n", hdr->len, hdr->caplen);
}
return 0;
}
$ fallocate -l 128 zero128
$ cat zero128 test.pcap > 128test.pcap
$ ./memfd < 128test.pcap
Found PCAP dump at offset 128
Read packet: full length = 105 bytes, available 105 bytes.
Read packet: full length = 105 bytes, available 105 bytes.
Read packet: full length = 66 bytes, available 66 bytes.
Read packet: full length = 385 bytes, available 385 bytes.
Read packet: full length = 66 bytes, available 66 bytes.
...
userfaultfd: обработка ошибок памяти в userspace
Думаю, не будет чего-то сильно нового в том, чтобы сказать, что в UNIX-like системах файловые дескрипторы на что только не указывают. Например, на Linux это может быть сокет, pipe, eventfd или даже ссылка на ebpf-программу. Но, возможно, этот пример вас всё-таки удивит. В начале статьи я говорил о том, что для ядра page faults — обычное дело: своп, copy-on-write, вот это всё… Когда же пользовательский процесс «промахивается», ему отправляется SIGSEGV. Насколько я знаю, возврат управления из обработчика SIGSEGV, сгенерированного ядром, является undefined behavior, и тем не менее, существует библиотека GNU libsigsegv, обобщающая особенности обработки ошибки доступа к памяти на различных платформах, даже Windows (ВНИМАНИЕ: лицензия GPL, если не готовы под ней же распространять свою программу, то не используйте libsigsegv). Не так давно в Linux появился абсолютно задокументированный способ, называется userfaultfd
: с помощью одноимённого системного вызова вы открываете файловый дескриптор, чтение и запись в который специальных структур являются командами.
Имея такой файловый дескриптор, вы можете пометить некий диапазон виртуальных адресов вашего процесса. После этого при первом обращение к каждой помеченной странице памяти, обратившийся поток заснёт, а чтение из файлового дескриптора вернёт информацию о произошедшем. После чего обработчик заполнит ответную структуру с указателем на данные, которые нужно использовать для инициализации «проблемной» страницы, ядро её проинициализирует и разбудит обратившийся поток. При этом предполагается наличие отдельного потока, в чьи обязанности входит чтение команд из дескриптора и выдача ответов. Вообще, говоря, через userfaultfd
можно получать и другую информацию, например, некоторые уведомления об изменении карты виртуальной памяти процесса.
#define _GNU_SOURCE
#ifdef NDEBUG
// Чтобы спокойно использовать assert с сайд-эффектами в примерах
# undef NDEBUG
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// По-хорошему, нужно спросить sysconf...
#define PAGE_SIZE 4096
#define PAGE_MASK (PAGE_SIZE - 1)
static void *thread_fn(void * arg)
{
int uffd = (intptr_t)arg;
struct uffd_msg msg;
// Интересно, как заработает с hugepages...
uint8_t *replacement_page = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 ,0);
while(1)
{
assert(read(uffd, &msg, sizeof msg) > 0);
// Бывают разные типы сообщений, обрабатываем только одно
if (msg.event == UFFD_EVENT_PAGEFAULT) {
uintptr_t addr = msg.arg.pagefault.address;
fprintf(stderr, "Fault: addr = 0x%zx\n", addr);
// Округляем адрес вниз до размера страницы
uint8_t *page_addr = (uint8_t *)((uintptr_t)addr & ~PAGE_MASK);
// Заполняем "подменную" страницу, не "сбойную"!
memset(replacement_page, 0xAB, PAGE_SIZE);
// просим ядро инициализировать страницу
struct uffdio_copy copy;
copy.src = (uintptr_t)replacement_page;
copy.dst = (uintptr_t)page_addr;
copy.mode = 0; // флаги, здесь -- по умолчанию
copy.copy = 0; // возвращаемое значение -- сколько байтов скопировали или ошибка
copy.len = PAGE_SIZE;
assert(ioctl(uffd, UFFDIO_COPY, ©) != -1);
}
}
}
static int init_userfaultfd(void)
{
// Открываем дескриптор
int uffd = syscall(__NR_userfaultfd, 0);
// Говорим, поддержку чего мы от него ожидаем
struct uffdio_api api;
api.api = UFFD_API;
api.features = 0;
assert(ioctl(uffd, UFFDIO_API, &api) != -1);
fprintf(stderr, "UFFD open\n");
// Запускаем поток-обработчик
pthread_t thread;
memset(&thread, 0, sizeof(thread));
// Хмм... допустимо ли хранить int в void *?
pthread_create(&thread, 0, thread_fn, (void *)(intptr_t)uffd);
return uffd;
}
static void register_region(int uffd, void * aligned_addr, size_t size)
{
struct uffdio_register reg;
memset(®, 0, sizeof reg);
reg.range.start = (uintptr_t)aligned_addr;
reg.range.len = size;
reg.mode = UFFDIO_REGISTER_MODE_MISSING;
assert (ioctl(uffd, UFFDIO_REGISTER, ®) != -1);
}
int main()
{
void *addr = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int uffd = init_userfaultfd();
register_region(uffd, addr, PAGE_SIZE);
fprintf(stderr, "Before reading\n");
fprintf(stderr, "Data at %p: %x\n", addr, *(volatile int *)addr);
return 0;
}
$ ./userfaultfd
UFFD open
Before reading
Fault: addr = 0x7f46f40d5000
Data at 0x7f46f40d5000: abababab
«Ключевой вопрос математики: не всё ли равно» ©
Что, если вам нужно узнать, ссылается ли этот файловый дескриптор на stdin
? Казалось бы, if (fd == 0) ...
— и все дела. Ну, окей…
#define _GNU_SOURCE
#include
#include
int main()
{
int fd = dup(0);
printf("stdin is fd %d, too\n", fd);
if (fd == 0)
printf("stdin");
else
printf("not stdin");
return 0;
}
$ gcc kcmp.c -o kcmp
$ ./kcmp
stdin is fd 3, too
not stdin
Упс… Дескриптор-то вроде как один, но алиасы разные. Нам поможет CRIU — Checkpoint/Restore In Userspace. Без паники, я не предлагаю сдампить процесс, посмотреть на результат и загрузить обратно. Просто для нужд своих userspace-инструментов, разработчики из этого проекта, как я понял, добавили в ядро системный вызов kcmp
: ему передаются два PID, тип ресурса и, собственно, два ресурса, а он говорит, указывают ли они на одну и ту же сущность ядра:
#define _GNU_SOURCE
#include
#include
#include
#include
int main()
{
int fd = dup(0);
printf("stdin is fd %d, too\n", fd);
int pid = getpid();
if (syscall(SYS_kcmp,
pid, pid, KCMP_FILE /* не путать с _FILES! */,
0 /* stdin fd */, fd) == 0)
printf("stdin\n");
else
printf("not stdin\n");
if (syscall(SYS_kcmp,
pid, pid, KCMP_FILE,
1 /* stdout fd */, fd) == 0)
printf("stdout\n");
else
printf("not stdout\n");
return 0;
}
$ ./kcmp
stdin is fd 3, too
stdin
stdout
Опять двадцать пять! Подождите ка, а что если…
$ ls -l /proc/self/fd
итого 0
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 0 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 1 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 2 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 23 -> '/home/trosinenko/.cache/appstream-cache-AH3OA0.mdb (deleted)'
lr-x------ 1 trosinenko trosinenko 64 ноя 10 14:45 3 -> /proc/17265/fd
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 57 -> 'socket:[41036]'
Ага, тот неловкий момент, когда пытаешься понять, где ошибка, а она оказалась в твоём понимании происходящего. Логично считать, что bash точно так же поступает и с моей программой, как и с ls
!
$ ./kcmp < kcmp.c
stdin is fd 3, too
stdin
not stdout
Не буду утверждать, что это работает всегда и идеально — для этого нужно самому его использовать много и по-разному, но как best effort инструмент для каких-нибудь эвристик наверняка может пригодиться.
Обо всём и понемножку
Знаете ли вы, …
- … что ядро содержит в себе JIT-компилятор для байткода из userspace? Так вот, для целей фильтрации пакетов и прочей трассировки ядро поддерживает eBPF-байткод. В процессе загрузки он проверяется на безопасность, терминируемость (а значит, как я понимаю, даже теоретически не может быть Тьюринг-полным) и т. д., после чего либо JIT-ится, либо интерпретируется. Кстати, не путайте его с его предшественником, BPF.
- … что обработчики сигналов можно запускать на отдельном стеке? Если нет, то вот описание системного вызова
sigaltstack
. - … что можно просто предупредить ядро о том, что некоторый файл пригодится позже:
readahead
А ещё в процессе просмотра списка системных вызовов я нашёл oldolduname
…