Что такое системный вызов
Перейти к содержимому

Что такое системный вызов

  • автор:

Unix2019b/Системные вызовы

При программировании на C мы используем средства стандартной библиотеки языка, такие как тип FILE*, функции fopen(), fread(), fwrite(), fclose(). Эти функции кроссплатформенные и основываются внутри на более низкоуровневых функциях.

Например, C-функция fread и fwrite:

#include size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

На POSIX-совместимых системах (в том числе Linux) она сводится к функции

#include ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);

На Windows она сводится к функциям из WinAPI:

#include BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ); BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped );

Если использовать только функции из библиотеки C, то можно писать переносимые программы, которые будут работать под разными ОС. Но если использовать API операционной системы, то можно получить больше возможностей и несколько лучшую производительность за счёт меньшего числа копирований данных в промежуточные буферы.

Системные вызовы

Как же устроены те самые POSIX-функции read() и write()?

Они реализуются посредством системных вызовов. Системный вызов (англ. system call) — обращение прикладной программы к ядру операционной системы для выполнения какой-либо операции.

AppInVat.png

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

Syscall.png

Обычное пользовательское приложение работает в непривилегированном режиме в своём виртуальном адресном пространстве. В обычном случае для выполнения вычислительной работы, для доступа к памяти и пр. не требуются системные вызовы. Например, функции библиотеки C, такие как strlen() и memcpy(), не имеют ничего общего с ядром и всегда выполняются целиком в приложении. Однако такие функции, как malloc() и printf(), могут делать внутри системные вызовы.

Набор системных вызовов разный в разных операционных системах. Итого в ядре Linux около 310 системных вызовов. С ними можно познакомиться в таблице. Для сравнения, в ОС Windows системных вызовов около 460.

GNU C Library

Когда мы компилируем обычную программу на C под Linux, она автоматически линкуется с библиотекой glibc.

Библиотека GNU C Library (часто используется название glibc) — это вариант реализации стандартной библиотеки С от проекта GNU. Является одним из основных компонентов операционной системы GNU/Linux.

Реализует как стандартные C-функции типа malloc(), strcpy(), fopen() (они являются частью стандарта языка программирования C и доступны на всех платформах), так и POSIX-функции типа getpid(), open() (эти функции не входят в стандарт C и, как правило, скажем, под Windows не реализованы).

Библиотека GNU C Library предоставляет программисту удобный интерфейс для работы с ОС в виде интерфейсных функций. Многие функции в libc являются тонкими обёртками над системными вызовами. Однако не каждая POSIX-функция является системным вызовом. Так и наоборот, не для каждого системного вызова есть соответствующая C-функция.

Размещение

Библиотека libc является одним файлом (динамическая so и статическая a), размещается в каталоге /usr/lib. Кроме того, в состав glibc (GNU libc) входят ещё несколько библиотек:

  • libm — математическая библиотека (там реализованы функции вида sin(), cos(). )
  • libpthread — POSIX Threads — библиотека для работы с потоками (мы обратимся к ней на следующих занятиях)

Всего функций в glibc много, мы рассмотрим только несколько.

Пример: fwrite

Вот шаги, которые включает в себя вызов C-функции fwrite:

Image002.png

  1. fwrite вместе с остальной частью стандартной библиотеки C реализован в glibc.
  2. fwrite вызывает более низкоуровневую функцию write.
  3. write загружает идентификатор системного вызова (который равен 1 для write) и аргументы в регистры процессора, а затем заставит процессор переключиться на уровень ядра. То, как это делается, зависит от архитектуры процессора, а иногда и от модели процессора. Например, процессоры x86 обычно вызывают прерывание 80, а процессоры x86-64 используют инструкцию процессора syscall.
  4. Процессор, который теперь работает в режиме ядра, передает идентификатор системного вызова в таблицу системных вызовов, извлекает указатель функции со смещением 1 и вызывает функцию. Эта функция, sys_write, является реализацией записи в файл.

Как работают системные вызовы

Из пользовательского пространства (ring 3) нельзя просто так вызвать функцию из ядра (ring 0), как обычную функцию. На шаге №3 в предыдущем примере используется тот или иной механизм перехода в режим ядра в зависимости от архитектуры компьютера. На компьютерах самой популярной архитектуры x86 для системный вызов делается тем или иным методом:

  • через программное прерывание,
  • через инструкцию sysenter,
  • через инструкцию syscall.

Программное прерывание

Прерывания (англ. interrupts) — это как бы сигнал процессору, что надо прервать выполнение (их поэтому и назвали прерываниями) текущего кода и срочно сделать то, что указано в обработчике.

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

Программное прерывание — синхронное прерывание, которое может осуществить программа с помощью специальной инструкции.

В процессорах архитектуры x86 для явного вызова синхронного прерывания имеется инструкция int, аргументом которой является номер прерывания (от 0 до 255). В защищённом и длинном режиме обычные программы не могут обслуживать прерывания, эта функция доступна только системному коду (операционной системе).

В ОС Linux номер прерывания 0x80 (в десятичной системе — 128) используется для выполнения системных вызовов. Обработчиком прерывания 0x80 является ядро Linux. Программа перед выполнением прерывания помещает в регистр eax номер системного вызова, который нужно выполнить. Когда управление переходит в ring 0, то ядро считывает этот номер и вызывает нужную функцию.

Метод этот широко применялся на 32-битных системах, на 64-битных он считается устаревшим и не применяется, но тоже работает, хотя с целым рядом ограничений (например, нельзя в качестве параметра передать 64-битный указатель).

  • Поместить номер системного вызова в eax.
  • Поместить аргументы в регистры ebx, ecx, edx, esi, edi, ebp.
  • Вызвать инструкцию int 0x80.
  • Получить результат из eax.

Пример реализации mygetpid() (получение PID текущего процесса) на ассемблере (для системного вызова getpid используется номер 20):

.intel_syntax noprefix .globl mygetpid .text mygetpid: mov eax, 20 int 0x80 ret

Инструкция sysenter

Спустя некоторое время, ещё когда не было x86-64, в Intel поняли, что можно ускорить системные вызовы, если создать специальную инструкцию системного вызова, тем самым минуя некоторые издержки прерывания. Ускорение достигается за счёт того, что на аппаратном уровне при выполнении инструкции sysenter опускается множество проверок на валидность дескрипторов, а так же проверок, зависящих от уровня привилегий.

На сегодня эти инструкции (sysenter и sysexit) поддерживаются процессорами Intel в 32- и 64-битных режимах, процессорами AMD — только в 32-битном (на 64-битном приводит к исключению неизвестного опкода).

Поскольку 32-битные архитектуры теряют популярность, рассматривать не будем.

Инструкция syscall

Так как именно AMD разработали x86-64 архитектуру, которая и называется AMD64, то они решили создать свою собственную инструкцию для системных вызовов.

Эти инструкции (syscall и парная sysret) поддерживаются процессорами Intel только в 64-битном режиме, процессорами AMD — во всех режимах.

Системные вызовы при помощи этой инструкции делаются в современных версиях 64-битного Linux.

  • Номер системного вызова помещается в rax.
  • Аргументы записываются в rdi, rsi, rdx, r10, r8 и r9.
  • Затем вызывается syscall.
  • Когда управление возвращается, результат находится в rax.
  • Значения всех регистров, кроме r11, rcx и rax, системным вызовом не изменяются, дополнительно сохранять их не требуется.

Пример реализации mygetpid() (получение PID текущего процесса) на ассемблере (для системного вызова getpid по таблице используется номер 39):

.intel_syntax noprefix .globl mygetpid .text mygetpid: mov rax, 39 syscall ret

Производительность

Системные вызовы требуют переключения контекста и перехода процессора в режим с высоким уровнем привилегий. Поэтому системный вызов выполняется относительно медленно по сравнению с вызовом обычной C-функции. Ещё хуже стало после обнаружения уязвимости Meltdown: патч KPTI (kernel page-table isolation), помогающий против уязвимости, приводит к сбросу TLB-кешей и дополнительному падению производительности.

Ориентировочные цифры, сколько занимает системный вызов на конкретном процессоре [1]:

  • int 80h — 500 нс,
  • sysenter — 340 нс,
  • syscall — 280 нс,
  • патч KPTI увеличивает эти числа на 180 нс,
  • вызов обычной C-функции — единицы нс.

Есть приёмы под названием vsyscall (уже устарел) и vDSO (virtual dynamic shared object) [2], которые позволяют в некоторых случаях избежать переключения контекста и ускорить выполнение. Помогает для системных вызовов, которым реально не нужны высокие привилегии, например gettimeofday. Удобно, если надо часто получать таймстемпы, например, для логов.

Мониторинг системных вызовов

Существует несколько инструментов, которые можно использовать для просмотра системных вызовов, которые выполняются программами. Самый известный из них, strace, доступен во многих операционных системах, и, вероятно, он уже установлен на вашем компьютере.

strace может запустить новый процесс или подключиться к уже запущенному. Вы можете многое узнать, подглядывая за системными вызовами, сделанными различными программами.

Использование в коде на C

Заголовочный файл unistd.h — основной, его наличие и содержимое обеспечивается стандартом POSIX.1. Видимо, «uni» пошло от UNIX.

Типы данных

Примеры типов данных POSIX (тем не менее, они не являются стандартными типами в языке C).

  • pid_t — идентификаторы процессов.
  • ssize_t — аналогичен size_t, но обязан быть знаковым и, главное, обязан уметь хранить минус единицу (это число используется как возвращаемое значение в случае ошибки во многих функциях).
  • off_t и off64_t — знаковый тип для хранения смещения в файле.

Получение справки

Сигнатуры функций и информацию об использовании можно почерпнуть из man-страниц:

man 2 read man 3 printf

Бонус: пишем «Hello, world!» на ассемблере

.intel_syntax noprefix .text .globl _start _start: # write(1, message, 14) mov rax, 1 # system call 1 is write mov rdi, 1 # file handle 1 is stdout lea rsi, [rip+msg] # address of string to output mov rdx, 14 # number of bytes syscall # invoke operating system to do the write # exit(0) mov rax, 60 # system call 60 is exit mov rdi, 0 # we want return code 0 syscall # invoke operating system to exit .data msg: .ascii "Hello, world!\n"
$ gcc main.s -nostdlib $ ./a.out Hello, world!

Теперь посмотрим, что, кроме служебных вызовов при загрузке бинарника, наша программа выполнила именно ожидаемые два системных вызова.

$ strace ./a.out > /dev/null execve("./a.out", ["./a.out"], 0x7fffd263a9b0 /* 19 vars */) = 0 brk(NULL) = 0x7fffd2101000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8595c50000 arch_prctl(ARCH_SET_FS, 0x7f8595c50a80) = 0 mprotect(0x7f8596000000, 4096, PROT_READ) = 0 write(1, "Hello, world!\n\0", 14) = 14 exit(0) = ? +++ exited with 0 +++

Литература

  • System Calls Make the World Go Round
  • Creating a vDSO: the Colonel’s Other Chicken
  • Эволюция системных вызовов архитектуры x86

Что такое системный вызов

Обычно, системные вызовы не вызываются напрямую, это делается через обёрточную функцию из glibc (или другой библиотеки). Подробней о непосредственном вызове системного вызова, см. intro(2). Часто, но не всегда, имя обёрточной функции совпадает с именем системного вызова, который она осуществляет. Например, в glibc есть функция truncate(), которая осуществляет делающий всё работу системный вызов «truncate». Часто, обёрточная функция glibc очень маленькая, она просто копирует аргументы в нужные регистры перед запуском системного вызова, а затем присваивает переменной errno значение, которое было возвращено системным вызовом. (Эти те же шаги выполняет syscall(2), её можно использовать для осуществления системных вызовов, для которых нет обёрточных функций.) Замечание: системные вызовы указывают, что произошла ошибка возвращая отрицательное целое число вызывающей стороне; когда это происходит, обёрточная функция меняет знак у возвращённого значения (на положительный), копирует его в errno и возвращает -1 вызвавшей обёртку функции. Иногда, однако, обёрточная функция производит дополнительную работу до осуществления системного вызова. Например, в настоящее время существует (по причинам, описанным далее) два похожих системных вызова — truncate(2) и truncate64(2); обёрточная функция glibc truncate() проверяет какой из системных вызовов предоставляет ядро и решает какой нужно задействовать.

Список системных вызовов

Далее приведён список список системных вызовов Linux. В колонке Ядро указана версия ядра для системных вызовов, которые появились в Linux 2.2, и с какой именно версии. Также заметим следующее: * Если версия ядра не указана, то системный вызов появился в ядре 1.0 или более раннем. * Системные вызовы, помеченные «1.2», скорее всего появились в в версиях ядра 1.1.x, и впервые появились в стабильном ядре 1.2. (Разработка ядра 1.2 началась с ответвления от ядра 1.0.6 и прошла несколько выпусков ядер 1.1.x.) * Системные вызовы, помеченные «1.2», скорее всего появились в в версиях ядра 1.3.x, и впервые появились в стабильном ядре 2.0. (Разработка ядра 2.0 началась с ответвления от ядра 1.2.x, начиная, приблизительно, с 1.2.10, и прошла несколько выпусков ядер 1.3.x.) * Системные вызовы, помеченные «2.2», скорее всего появились в в версиях ядра 2.1.x, и впервые появились в стабильном ядре 2.2.0. (Разработка ядра 2.2 началась с ответвления от ядра 2.0.21, и прошла несколько выпусков ядер 2.1.x.) * Системные вызовы, помеченные «2.4», скорее всего появились в в версиях ядра 2.3.x, и впервые появились в стабильном ядре 2.4.0. (Разработка ядра 2.4 началась с ответвления от ядра 2.2.8, и прошла несколько выпусков ядер 2.3.x.) * Системные вызовы, помеченные «2.6», скорее всего появились в в версиях ядра 2.5.x, и впервые появились в стабильном ядре 2.6.0. (Разработка ядра 2.6 началась с ответвления от ядра 2.4.15, и прошла несколько выпусков ядер 2.5.x.) * Начиная с ядра 2.6.0 порядок разработки был изменён, и новые системные вызовы могут появляться в каждом выпуске 2.6.x. В этом случае для системного вызова указан точный номер версии. 2.6.39. Это соглашение продолжает действовать и в ядрах серии 3.x, которая началась с ядра 2.6.39, и в ядрах серии 4.х, которая началась с ядра 3.19. * Иногда системный вызов, добавленный в текущую на тот момент стабильную ветвь ядра, переносился в предыдущие стабильные ветви ядра. Например, некоторые системные вызовы, которые появились в в 2.6.x были перенесены и в выпуски 2.4.x, начиная с 2.4.15. Если это производилось, будут показаны основные версии обеих ветвей ядра. Список системных вызовов, доступных в ядре версии 4.4 (или, в некоторых случаях, только в более старых ядрах):

Системный вызов Ядро Примечания
_llseek(2) 1.2
_newselect(2) 2.0
_sysctl(2) 2.0
accept(2) 2.0 смотрите замечания по socketcall(2)
accept4(2) 2.6.28
access(2) 1.0
acct(2) 1.0
add_key(2) 2.6.11
adjtimex(2) 1.0
alarm(2) 1.0
alloc_hugepages(2) 2.5.36 удалён в 2.5.44
bdflush(2) 1.2 устарел (ничего не делает)
начиная с 2.6
bind(2) 2.0 смотрите замечания по socketcall(2)
bpf(2) 3.18
brk(2) 1.0
cacheflush(2) 1.2 не для x86
capget(2) 2.2
capset(2) 2.2
chdir(2) 1.0
chmod(2) 1.0
chown(2) 2.2 Смотрите в chown(2)
подробности по версии
chown32(2) 2.4
chroot(2) 1.0
clock_adjtime(2) 2.6.39
clock_getres(2) 2.6
clock_gettime(2) 2.6
clock_nanosleep(2) 2.6
clock_settime(2) 2.6
clone(2) 1.0
close(2) 1.0
connect(2) 2.0 0
creat(2) 1.0
create_module(2) 1.0 удалён в 2.6
delete_module(2) 1.0
dup(2) 1.0
dup2(2) 1.0
dup3(2) 2.6.27
epoll_create(2) 2.6
epoll_create1(2) 2.6.27
epoll_ctl(2) 2.6
epoll_pwait(2) 2.6.19
epoll_wait(2) 2.6
eventfd(2) 2.6.22
eventfd2(2) 2.6.27
execve(2) 1.0
execveat(2) 3.19
exit(2) 1.0
exit_group(2) 2.6
faccessat(2) 2.6.16
fadvise64(2) 2.6
fadvise64_64(2) 2.6
fallocate(2) 2.6.23
fanotify_init(2) 2.6.37
fanotify_mark(2) 2.6.37
fchdir(2) 1.0
fchmod(2) 1.0
fchmodat(2) 2.6.16
fchown(2) 1.0
fchown32(2) 2.4
fchownat(2) 2.6.16
fcntl(2) 1.0
fcntl64(2) 2.4
fdatasync(2) 2.0
fgetxattr(2) 2.6; 2.4.18
finit_module(2) 3.8
flistxattr(2) 2.6; 2.4.18
flock(2) 2.0
fork(2) 1.0
free_hugepages(2) 2.5.36 удалён в 2.5.44
fremovexattr(2) 2.6; 2.4.18
fsetxattr(2) 2.6; 2.4.18
fstat(2) 1.0
fstat64(2) 2.4
fstatat64(2) 2.6.16
fstatfs(2) 1.0
fstatfs64(2) 2.6
fsync(2) 1.0
ftruncate(2) 1.0
ftruncate64(2) 2.4
futex(2) 2.6
futimesat(2) 2.6.16
get_kernel_syms(2) 1.0 удалён в 2.6
get_mempolicy(2) 2.6.6
get_robust_list(2) 2.6.17
get_thread_area(2) 2.6
getcpu(2) 2.6.19
getcwd(2) 2.2
getdents(2) 2.0
getdents64(2) 2.4
getegid(2) 1.0
getegid32(2) 2.4
geteuid(2) 1.0
geteuid32(2) 2.4
getgid(2) 1.0
getgid32(2) 2.4
getgroups(2) 1.0
getgroups32(2) 2.4
getitimer(2) 1.0
getpeername(2) 2.0 смотрите замечания по socketcall(2)
getpagesize(2) 2.0 не для x86
getpgid(2) 1.0
getpgrp(2) 1.0
getpid(2) 1.0
getppid(2) 1.0
getpriority(2) 1.0
getrandom(2) 3.17
getresgid(2) 2.2
getresgid32(2) 2.4
getresuid(2) 2.2
getresuid32(2) 2.4
getrlimit(2) 1.0
getrusage(2) 1.0
getsid(2) 2.0
getsockname(2) 2.0 смотрите замечания по socketcall(2)
getsockopt(2) 2.0 смотрите замечания по socketcall(2)
gettid(2) 2.4.11
gettimeofday(2) 1.0
getuid(2) 1.0
getuid32(2) 2.4
getxattr(2) 2.6; 2.4.18
init_module(2) 1.0
inotify_add_watch(2) 2.6.13
inotify_init(2) 2.6.13
inotify_init1(2) 2.6.27
inotify_rm_watch(2) 2.6.13
io_cancel(2) 2.6
io_destroy(2) 2.6
io_getevents(2) 2.6
io_setup(2) 2.6
io_submit(2) 2.6
ioctl(2) 1.0
ioperm(2) 1.0
iopl(2) 1.0
ioprio_get(2) 2.6.13
ioprio_set(2) 2.6.13
ipc(2) 1.0
kcmp(2) 3.5
kern_features(2) 3.7 Sparc64
kexec_file_load(2) 3.17
kexec_load(2) 2.6.13
keyctl(2) 2.6.11
kill(2) 1.0
lchown(2) 1.0 Смотрите в chown(2)
подробности по версии
lchown32(2) 2.4
lgetxattr(2) 2.6; 2.4.18
link(2) 1.0
linkat(2) 2.6.16
listen(2) 2.0 смотрите замечания по socketcall(2)
listxattr(2) 2.6; 2.4.18
llistxattr(2) 2.6; 2.4.18
lookup_dcookie(2) 2.6
lremovexattr(2) 2.6; 2.4.18
lseek(2) 1.0
lsetxattr(2) 2.6; 2.4.18
lstat(2) 1.0
lstat64(2) 2.4
madvise(2) 2.4
mbind(2) 2.6.6
memfd_create(2) 3.17
migrate_pages(2) 2.6.16
mincore(2) 2.4
mkdir(2) 1.0
mkdirat(2) 2.6.16
mknod(2) 1.0
mknodat(2) 2.6.16
mlock(2) 2.0
mlock2(2) 4.4
mlockall(2) 2.0
mmap(2) 1.0
mmap2(2) 2.4
modify_ldt(2) 1.0
mount(2) 1.0
move_pages(2) 2.6.18
mprotect(2) 1.0
mq_getsetattr(2) 2.6.6
mq_notify(2) 2.6.6
mq_open(2) 2.6.6
mq_timedreceive(2) 2.6.6
mq_timedsend(2) 2.6.6
mq_unlink(2) 2.6.6
mremap(2) 2.0
msgctl(2) 2.0 смотрите замечания по ipc(2)
msgget(2) 2.0 смотрите замечания по ipc(2)
msgrcv(2) 2.0 смотрите замечания по ipc(2)
msgsnd(2) 2.0 смотрите замечания по ipc(2)
msync(2) 2.0
munlock(2) 2.0
munlockall(2) 2.0
munmap(2) 1.0
name_to_handle_at(2) 2.6.39
nanosleep(2) 2.0
nfsservctl(2) 2.2 удалён в 3.1
nice(2) 1.0
oldfstat(2) 1.0
oldlstat(2) 1.0
oldolduname(2) 1.0
oldstat(2) 1.0
olduname(2) 1.0
open(2) 1.0
open_by_handle_at(2) 2.6.39
openat(2) 2.6.16
pause(2) 1.0
pciconfig_iobase(2) 2.2.15; 2.4 не в x86
pciconfig_read(2) 2.0.26; 2.2 не в x86
pciconfig_write(2) 2.0.26; 2.2 не в x86
perf_event_open(2) 2.6.31 Был perf_counter_open() в
2.6.31; в 2.6.32 переименовано
personality(2) 1.2
perfctr(2) 2.2 Sparc; удалён в 2.6.34
perfmonctl(2) 2.4 ia64
pipe(2) 1.0
pipe2(2) 2.6.27
pivot_root(2) 2.4
poll(2) 2.0.36; 2.2
ppc_rtas(2) 2.6.2 Только для PowerPC
ppoll(2) 2.6.16
prctl(2) 2.2
pread64(2) добавлен под именем «pread» в 2.2
переименован в «pread64» в 2.6
preadv(2) 2.6.30
prlimit64(2) 2.6.36
process_vm_readv(2) 3.2
process_vm_writev(2) 3.2
pselect6(2) 2.6.16
ptrace(2) 1.0
pwrite64(2) добавлен под именем «pwrite» в 2.2
переименован в «pwrite64» в 2.6
pwritev(2) 2.6.30
query_module(2) 2.2 удалён в 2.6
quotactl(2) 1.0
read(2) 1.0
readahead(2) 2.4.13
readdir(2) 1.0
readlink(2) 1.0
readlinkat(2) 2.6.16
readv(2) 2.0
reboot(2) 1.0
recv(2) 2.0 смотрите замечания по socketcall(2)
recvfrom(2) 2.0 смотрите замечания по socketcall(2)
recvmsg(2) 2.0 смотрите замечания по socketcall(2)
recvmmsg(2) 2.6.33
remap_file_pages(2) 2.6 устарел начиная с 3.16
removexattr(2) 2.6; 2.4.18
rename(2) 1.0
renameat(2) 2.6.16
renameat2(2) 3.15
request_key(2) 2.6.11
restart_syscall(2) 2.6
rmdir(2) 1.0
rt_sigaction(2) 2.2
rt_sigpending(2) 2.2
rt_sigprocmask(2) 2.2
rt_sigqueueinfo(2) 2.2
rt_sigreturn(2) 2.2
rt_sigsuspend(2) 2.2
rt_sigtimedwait(2) 2.2
rt_tgsigqueueinfo(2) 2.6.31
s390_runtime_instr(2) 3.7 только для s390
s390_pci_mmio_read(2) 3.19 только для s390
s390_pci_mmio_write(2) 3.19 только для s390
sched_get_priority_max(2) 2.0
sched_get_priority_min(2) 2.0
sched_getaffinity(2) 2.6
sched_getattr(2) 3.14
sched_getparam(2) 2.0
sched_getscheduler(2) 2.0
sched_rr_get_interval(2) 2.0
sched_setaffinity(2) 2.6
sched_setattr(2) 3.14
sched_setparam(2) 2.0
sched_setscheduler(2) 2.0
sched_yield(2) 2.0
seccomp(2) 3.17
select(2) 1.0
semctl(2) 2.0 смотрите замечания по ipc(2)
semget(2) 2.0 смотрите замечания по ipc(2)
semop(2) 2.0 смотрите замечания по ipc(2)
semtimedop(2) 2.6; 2.4.22
send(2) 2.0 смотрите замечания по socketcall(2)
sendfile(2) 2.2
sendfile64(2) 2.6; 2.4.19
sendmmsg(2) 3.0
sendmsg(2) 2.0 смотрите замечания по socketcall(2)
sendto(2) 2.0 смотрите замечания по socketcall(2)
set_mempolicy(2) 2.6.6
set_robust_list(2) 2.6.17
set_thread_area(2) 2.6
set_tid_address(2) 2.6
setdomainname(2) 1.0
setfsgid(2) 1.2
setfsgid32(2) 2.4
setfsuid(2) 1.2
setfsuid32(2) 2.4
setgid(2) 1.0
setgid32(2) 2.4
setgroups(2) 1.0
setgroups32(2) 2.4
sethostname(2) 1.0
setitimer(2) 1.0
setns(2) 3.0
setpgid(2) 1.0
setpriority(2) 1.0
setregid(2) 1.0
setregid32(2) 2.4
setresgid(2) 2.2
setresgid32(2) 2.4
setresuid(2) 2.2
setresuid32(2) 2.4
setreuid(2) 1.0
setreuid32(2) 2.4
setrlimit(2) 1.0
setsid(2) 1.0
setsockopt(2) 2.0 смотрите замечания по socketcall(2)
settimeofday(2) 1.0
setuid(2) 1.0
setuid32(2) 2.4
setup(2) 1.0 удалён в 2.2
setxattr(2) 2.6; 2.4.18
sgetmask(2) 1.0
shmat(2) 2.0 смотрите замечания по ipc(2)
shmctl(2) 2.0 смотрите замечания по ipc(2)
shmdt(2) 2.0 смотрите замечания по ipc(2)
shmget(2) 2.0 смотрите замечания по ipc(2)
shutdown(2) 2.0 смотрите замечания по socketcall(2)
sigaction(2) 1.0
sigaltstack(2) 2.2
signal(2) 1.0
signalfd(2) 2.6.22
signalfd4(2) 2.6.27
sigpending(2) 1.0
sigprocmask(2) 1.0
sigreturn(2) 1.0
sigsuspend(2) 1.0
socket(2) 2.0 смотрите замечания по socketcall(2)
socketcall(2) 1.0
socketpair(2) 2.0 смотрите замечания по socketcall(2)
splice(2) 2.6.17
spu_create(2) 2.6.16 только для PowerPC
spu_run(2) 2.6.16 только для PowerPC
ssetmask(2) 1.0
stat(2) 1.0
stat64(2) 2.4
statfs(2) 1.0
statfs64(2) 2.6
stime(2) 1.0
subpage_prot(2) 2.6.25 только для PowerPC
swapoff(2) 1.0
swapon(2) 1.0
symlink(2) 1.0
symlinkat(2) 2.6.16
sync(2) 1.0
sync_file_range(2) 2.6.17
sync_file_range2(2) 2.6.22
syncfs(2) 2.6.39
sysfs(2) 1.2
sysinfo(2) 1.0
syslog(2) 1.0
tee(2) 2.6.17
tgkill(2) 2.6
time(2) 1.0
timer_create(2) 2.6
timer_delete(2) 2.6
timer_getoverrun(2) 2.6
timer_gettime(2) 2.6
timer_settime(2) 2.6
timerfd_create(2) 2.6.25
timerfd_gettime(2) 2.6.25
timerfd_settime(2) 2.6.25
times(2) 1.0
tkill(2) 2.6; 2.4.22
truncate(2) 1.0
truncate64(2) 2.4
ugetrlimit(2) 2.4
umask(2) 1.0
umount(2) 1.0
umount2(2) 2.2
uname(2) 1.0
unlink(2) 1.0
unlinkat(2) 2.6.16
unshare(2) 2.6.16
uselib(2) 1.0
ustat(2) 1.0
userfaultfd(2) 4.2
utime(2) 1.0
utimensat(2) 2.6.22
utimes(2) 2.2
utrap_install(2) 2.2 только для SPARC
vfork(2) 2.2
vhangup(2) 1.0
vm86old(2) 1.0 ранее «vm86»; переименован в 2.0.28/2.2
vm86(2) 2.0.28; 2.2
vmsplice(2) 2.6.17
wait4(2) 1.0
waitid(2) 2.6.10
waitpid(2) 1.0
write(2) 1.0
writev(2) 2.0

Для многих платформ, включая x86-32, все сокетные вызовы мультиплексируются (с помощью обёрточных функций glibc) через socketcall(2), а подобные IPC вызовы System V мультиплексируются через ipc(2). Хотя для них и зарезервированы места в таблице системных вызовов, следующие системные вызовы не реализованы в стандартном ядре: afs_syscall(2), break(2), ftime(2), getpmsg(2), gtty(2), idle(2), lock(2), madvise1(2), mpx(2), phys(2), prof(2), profil(2), putpmsg(2), security(2), stty(2), tuxcall(2), ulimit(2) и vserver(2) (см. также unimplemented(2)). Однако ftime(3), profil(3) и ulimit(3) есть среди библиотечных функций. Место для phys(2) занято начиная с ядра 2.1.116 под umount(2); phys(2) никогда не будет реализован. Вызовы getpmsg(2) и putpmsg(2) есть в ядрах с заплатами, обеспечивающими поддержку STREAMS, и могут никогда не появиться в стандартном ядре. На короткое время появлялся set_zone_reclaim(2), добавленный в Linux 2.6.13 и удалённый в 2.6.16; данный системный вызов никогда не был доступен из пользовательского пространства.

ЗАМЕЧАНИЯ

Чаще всего, код системного вызова с номером __NR_xxx, определённого в /usr/include/asm/unistd.h, можно найти в исходном коде ядра Linux в функции sys_xxx(). (Таблицу вызовов для i386 можно найти в /usr/src/linux/arch/i386/kernel/entry.S.) Есть много исключений из этого правила, в основном из-за того, что большинство старых системных вызовов заменена на новые, при чём без всякой системы. На платформах с эмуляцией собственнических ОС, таких как parisc, sparc, sparc64 и alpha, существует много дополнительных системных вызовов; для mips64 также есть полный набор 32-битных системных вызовов. С течением времени при необходимости происходили изменения в интерфейсе некоторых системных вызовов. Одной из причин таких изменений была необходимость увеличения размера структур или скалярных значений передаваемых системному вызову. Из-за этих изменений на некоторых архитектурах (а именно на старых 32-битных i386) появились различные группы похожих системных вызовов (например, truncate(2) и truncate64(2)), которые выполняют одинаковые задачи, но отличаются размером своих аргументов. (Как уже отмечалось, на приложения это не влияет: обёрточные функции glibc выполняют некоторые действия по запуску правильного системного вызова, и это обеспечивает совместимость по ABI для старых двоичных файлов.) Примеры системных вызовов, у которых есть несколько версий: * В настоящее время есть три различные версии stat(2): sys_stat() (место __NR_oldstat), sys_newstat() (место __NR_stat) и sys_stat64() (место __NR_stat64), последняя используется в в данный момент. Похожая ситуация с lstat(2) и fstat(2). * Похожим образом определены __NR_oldolduname, __NR_olduname и__NR_uname для вызовов sys_olduname(), sys_uname() и sys_newuname(). * В Linux 2.0 появилась новая версия vm86(2), новая и старая версии ядерных процедур называются sys_vm86old() и sys_vm86(). * В Linux 2.4 появилась новая версия getrlimit(2) новая и старая версии ядерных процедур называются sys_old_getrlimit() (место __NR_getrlimit) и sys_getrlimit() (место __NR_ugetrlimit). * В Linux 2.4 увеличено размер поля ID пользователей и групп с 16 до 32 бит. Для поддержки этого изменения добавлено несколько системных вызовов (например, chown32(2), getuid32(2), getgroups32(2), setresuid32(2)), упраздняющих ранние вызовы с теми же именами, но без суффикса «32». * В Linux 2.4 добавлена поддержка доступа к большим файлам (у которых размеры и смещения не умещаются в 32 бита) в приложениях на 32-битных архитектурах. Для этого потребовалось внести изменения в системные вызовы, работающие с размерами и смещениями по файлам. Были добавлены следующие системные вызовы: fcntl64(2), getdents64(2), stat64(2), statfs64(2), truncate64(2) и их аналоги, которые обрабатывают файловые дескрипторы или символьные ссылки. Эти системные вызовы упраздняют старые системные вызовы, которые, за исключением вызовов «stat», называются также, но не имеют суффикса «64». На новых платформах, имеющих только 64-битный доступ к файлам и 32-битные UID/GID (например, alpha, ia64, s390x, x86-64), есть только одна версия системных вызовов для UID/GID и файлового доступа. На платформах (обычно это 32-битные платформы) где имеются *64 и *32 вызовы, другие версии устарели. * Вызовы rt_sig* добавлены в ядро 2.2 для поддержки дополнительных сигналов реального времени (см. signal(7)). Эти системные вызовы упраздняют старые системные вызовы с теми же именами, но без префикса «rt_». * В системных вызовах select(2) и mmap(2) используется пять или более аргументов, что вызывало проблемы определения способа передачи аргументов на i386. В следствии этого, тогда как на других архитектурах вызовы sys_select() и sys_mmap() соответствуют __NR_select и __NR_mmap, на i386 они соответствуют old_select() и old_mmap() (процедуры, использующие указатель на блок аргументов). В настоящее время больше нет проблемы с передачей более пяти аргументов и есть __NR__newselect, который соответствует именно sys_select(), и такая же ситуация с __NR_mmap2.

Разбираемся с системными вызовами в Linux с помощью strace

Системный вызов — это механизм взаимодействия пользовательских программ с ядром Linux, а strace — мощный инструмент, для их отслеживания. Для лучшего понимания работы операционной системы полезно разобраться с тем, как они работают.

В операционной системе можно выделить два режима работы:

  • Режим ядра (kernel mode) — привилегированный режим, используемый ядром операционной системы.
  • Пользовательский режим (user mode) — режим, в котором выполняется большинство пользовательских приложений.

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

Большая часть этих деталей скрыта от пользователя в системных библиотеках (glibc в Linux-системах). Системные вызовы по своей природе являются универсальными, но несмотря на это, механика их выполнения во многом аппаратно-зависима.

В этой статье рассматривается несколько практических примеров анализа системных вызовов с помощью strace . В примерах используется Red Hat Enterprise Linux, но все команды должны работать и в других дистрибутивах Linux:

[root@sandbox ~]# cat /etc/redhat-release Red Hat Enterprise Linux Server release 7.7 (Maipo) [root@sandbox ~]# [root@sandbox ~]# uname -r 3.10.0-1062.el7.x86_64 [root@sandbox ~]#

Для начала убедитесь, что в вашей системе установлены необходимые инструменты. Проверить установлен ли strace можно с помощью приведенной ниже команды. Для просмотра версии strace запустите ее с параметром -V:

[root@sandbox ~]# rpm -qa | grep -i strace strace-4.12-9.el7.x86_64 [root@sandbox ~]# [root@sandbox ~]# strace -V strace -- version 4.12 [root@sandbox ~]#

Если strace не установлен, то установите запустив:

yum install strace

Для примера создайте тестовый каталог в /tmp и два файла с помощью команды touch :

[root@sandbox ~]# cd /tmp/ [root@sandbox tmp]# [root@sandbox tmp]# mkdir testdir [root@sandbox tmp]# [root@sandbox tmp]# touch testdir/file1 [root@sandbox tmp]# touch testdir/file2 [root@sandbox tmp]#

(Я использую каталог /tmp только потому, что доступ к нему есть у всех, но вы можете использовать любой другой.)

С помощью команды ls проверьте, что в каталоге testdir создались файлы:

[root@sandbox tmp]# ls testdir/ file1 file2 [root@sandbox tmp]#

Вероятно, вы используете команду ls каждый день, не осознавая того, что под капотом работают системные вызовы. Здесь в игру вступает абстракция. Вот как работает эта команда:

Утилита командной строки -> Функции системных библиотек (glibc) -> Системные вызовы

Команда ls вызывает функции из системных библиотек Linux (glibc). Эти библиотеки, в свою очередь, вызывают системные вызовы, которые выполняют большую часть работы.

Если вы хотите узнать, какие функции вызывались из библиотеки glibc, то используйте команду ltrace со следующей за ней командой ls testdir/ :

ltrace ls testdir/

Если ltrace не установлен, то установите:

yum install ltrace

На экране будет много информации, но не беспокойтесь — мы это рассмотрим далее. Вот некоторые из важных библиотечных функций из вывода ltrace :

opendir("testdir/") = < 3 >readdir(< 3 >) = < 101879119, "." >readdir(< 3 >) = < 134, ".." >readdir(< 3 >) = < 101879120, "file1" >strlen("file1") = 5 memcpy(0x1665be0, "file1\0", 6) = 0x1665be0 readdir(< 3 >) = < 101879122, "file2" >strlen("file2") = 5 memcpy(0x166dcb0, "file2\0", 6) = 0x166dcb0 readdir(< 3 >) = nil closedir(< 3 >) 

Изучив этот вывод, вы, вероятно, поймете, что происходит. Каталог с именем testdir открывается с помощью библиотечной функции opendir , после чего следуют вызовы функций readdir , читающих содержимое каталога. В конце происходит вызов функции closedir , которая закрывает каталог, открытый ранее. Пока проигнорируйте остальные функции, такие как strlen и memcpy .

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

Для просмотра системных вызовов используйте strace с командой ls testdir , как показано ниже. И вы снова получите кучу бессвязной информации:

[root@sandbox tmp]# strace ls testdir/ execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0 brk(NULL) = 0x1f12000 >> write(1, "file1 file2\n", 13file1 file2 ) = 13 close(1) = 0 munmap(0x7fd002c8d000, 4096) = 0 close(2) = 0 exit_group(0) = ? +++ exited with 0 +++ [root@sandbox tmp]# 

В результате выполнения strace вы получите список системных вызовов, выполненных при работе команды ls . Все системные вызовы можно разделить на следующие категории:

  • Управление процессами
  • Управление файлами
  • Управление каталогами и файловой системой
  • Прочие
[root@sandbox tmp]# strace -o trace.log ls testdir/ file1 file2 [root@sandbox tmp]#

На этот раз на экране не будет никаких данных — команда ls отработает, как и ожидается, показав список файлов и записав весь вывод strace в файл trace.log . Для простой команды ls файл содержит почти 100 строк:

[root@sandbox tmp]# ls -l trace.log -rw-r--r--. 1 root root 7809 Oct 12 13:52 trace.log [root@sandbox tmp]# [root@sandbox tmp]# wc -l trace.log 114 trace.log [root@sandbox tmp]#

Взгляните на первую строку в файле trace.log :

execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
  • В начале строки находится имя выполняемого системного вызова — это execve.
  • Текст в круглых скобках — это аргументы, передаваемые системному вызову.
  • Число после знака = (в данном случае 0) — это значение, возвращаемое системным вызовом.

Обратите внимание на ту единственную команду, которую вы вызвали — ls testdir . Вам известно имя каталога, используемое командой ls , так почему бы не воспользоваться grep для testdir в файле trace.log и не посмотреть, что найдется? Посмотрите внимательно на результат:

[root@sandbox tmp]# grep testdir trace.log execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0 stat("testdir/", ) = 0 openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3 [root@sandbox tmp]#

Возвращаясь к приведенному выше анализу execve , можете ли вы сказать, что делает следующий системный вызов?

execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0

Не нужно запоминать все системные вызовы и то, что они делают: все есть в документации. Man-страницы спешат на помощь! Перед запуском команды man убедитесь, что установлен пакет man-pages :

[root@sandbox tmp]# rpm -qa | grep -i man-pages man-pages-3.53-5.el7.noarch [root@sandbox tmp]#

Помните, что вам нужно добавить «2» между командой man и именем системного вызова. Если вы прочитаете в man про man ( man man ), то увидите, что раздел 2 зарезервирован для системных вызовов. Аналогично если вам нужна информация о библиотечных функциях, то нужно добавить 3 между man и именем библиотечной функции.

Ниже приведены номера разделов man :

1. Выполняемые программы или команды для командной оболочки. 2. Системные вызовы (функции, предоставляемые ядром). 3. Библиотечные вызовы (функции программных библиотек). 4. Специальные файлы (которые обычно находятся в /dev).

Для просмотра документации по системному вызову запустите man с именем этого системного вызова.

man 2 execve

В соответствии с документацией системный вызов execve выполняет программу, которая передается ему в параметрах (в данном случае это ls ). В него также передаются дополнительные параметры для ls. В этом примере это testdir . Следовательно, этот системный вызов просто запускает ls с testdir в качестве параметра:

'execve - execute program' 'DESCRIPTION execve() executes the program pointed to by filename'

В следующий системный вызов stat передается параметр testdir :

stat("testdir/", ) = 0

Для просмотра документации используйте man 2 stat . Системный вызов stat возвращает информацию об указанном файле. Помните, что все в Linux — файл, включая каталоги.

Далее системный вызов openat открывает testdir . Обратите внимание, что возвращается значение 3. Это дескриптор файла, который будет использоваться в последующих системных вызовах:

openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3

Теперь откройте файл

trace.log

и обратите внимание на строку, следующую после системного вызова openat . Вы увидите системный вызов getdents , который делает большую часть необходимой работы для выполнения команды ls testdir . Теперь выполним grep getdents для файла trace.log :

[root@sandbox tmp]# grep getdents trace.log getdents(3, /* 4 entries */, 32768) = 112 getdents(3, /* 0 entries */, 32768) = 0 [root@sandbox tmp]#

В документации ( man getdents ) говорится, что getdents читает записи каталога, это, собственно, нам и нужно. Обратите внимание, что аргумент для getdent равен 3 — это дескриптор файла, полученный ранее от системного вызова openat .

Теперь, когда получено содержимое каталога, нужен способ отобразить информацию в терминале. Итак, делаем grep для другого системного вызова write , который используется для вывода на терминал:

[root@sandbox tmp]# grep write trace.log write(1, "file1 file2\n", 13) = 13 [root@sandbox tmp]#

В аргументах вы можете видеть имена файлов, которые будут выводится: file1 и file2 . Что касается первого аргумента (1), вспомните, что в Linux для любого процесса по умолчанию открываются три файловых дескриптора:

  • 0 — стандартный поток ввода
  • 1 — стандартный поток вывода
  • 2 — стандартный поток ошибок

Теперь вы знаете, какие системные вызовы сделали большую часть работы для команды ls testdir/ . Но что насчет других 100+ системных вызовов в файле trace.log ?

Операционная система выполняет много вспомогательных действий для запуска процесса, поэтому многое из того, что вы видите в файле trace.log — это инициализация и очистка процесса. Посмотрите файл trace.log полностью и попытайтесь понять, что происходит во время запуска команды ls .

Теперь вы можете анализировать системные вызовы для любых программ. Утилита strace так же предоставляет множество полезных параметров командной строки, некоторые из которых описаны ниже.

По умолчанию strace отображает не всю информацию о системных вызовах. Однако у нее есть опция -v verbose , которая покажет дополнительную информацию о каждом системном вызове:

strace -v ls testdir

Хорошая практика использовать параметр -f для отслеживания дочерних процессов, созданных запущенным процессом:

strace -f ls testdir

А если вам нужны только имена системных вызовов, количество их запусков и процент времени, затраченного на выполнение? Вы можете использовать опцию -c , чтобы получить эту статистику:

strace -c ls testdir/

Если вы хотите отследить определенный системный вызов, например, open , и проигнорировать другие, то можно использовать опцию -e с именем системного вызова:

[root@sandbox tmp]# strace -e open ls testdir open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3 open("/lib64/libcap.so.2", O_RDONLY|O_CLOEXEC) = 3 open("/lib64/libacl.so.1", O_RDONLY|O_CLOEXEC) = 3 open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3 open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3 open("/lib64/libattr.so.1", O_RDONLY|O_CLOEXEC) = 3 open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3 open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3 file1 file2 +++ exited with 0 +++ [root@sandbox tmp]#

А что, если нужно отфильтровать по нескольким системным вызовам? Не волнуйтесь, можно использовать ту же опцию -e и разделить необходимые системные вызовы запятой. Например, для write и getdent :

[root@sandbox tmp]# strace -e write,getdents ls testdir getdents(3, /* 4 entries */, 32768) = 112 getdents(3, /* 0 entries */, 32768) = 0 write(1, "file1 file2\n", 13file1 file2 ) = 13 +++ exited with 0 +++ [root@sandbox tmp]#

До сих пор мы отслеживали только явный запуск команд. Но как насчет команд, которые были запущены ранее? Что, если вы хотите отслеживать демонов? Для этого у strace есть специальная опция -p , которой вы можете передать идентификатор процесса.

Мы не будем запускать демона, а используем команду cat , которая отображает содержимое файла, переданного ему в качестве аргумента. Но если аргумент не указать, то команда cat будет просто ждать ввод от пользователя. После ввода текста она выведет введенный текст на экран. И так до тех пор, пока пользователь не нажмет Ctrl+C для выхода.

Запустите команду cat на одном терминале.

[root@sandbox tmp]# cat

На другом терминале найдите идентификатор процесса (PID) с помощью команды ps :

[root@sandbox ~]# ps -ef | grep cat root 22443 20164 0 14:19 pts/0 00:00:00 cat root 22482 20300 0 14:20 pts/1 00:00:00 grep --color=auto cat [root@sandbox ~]#

Теперь запустите strace с опцией -p и PID’ом, который вы нашли с помощью ps . После запуска strace выведет информацию о процессе, к которому он подключился, а также его PID. Теперь strace отслеживает системные вызовы, выполняемые командой cat . Первый системный вызов, который вы увидите — это read, ожидающий ввода от потока с номером 0, то есть от стандартного ввода, который сейчас является терминалом, на котором запущена команда cat :

[root@sandbox ~]# strace -p 22443 strace: Process 22443 attached read(0,

Теперь вернитесь к терминалу, где вы оставили запущенную команду cat , и введите какой-нибудь текст. Для демонстрации я ввел x0x0 . Обратите внимание, что cat просто повторил то, что я ввел и x0x0 на экране будет дважды.

[root@sandbox tmp]# cat x0x0 x0x0

Вернитесь к терминалу, где strace был подключен к процессу cat . Теперь вы видите два новых системных вызова: предыдущий read , который теперь прочитал x0x0 , и еще один для записи write , который записывает x0x0 обратно в терминал, и снова новый read , который ожидает чтения с терминала. Обратите внимание, что стандартный ввод (0) и стандартный вывод (1) находятся на одном и том же терминале:

[root@sandbox ~]# strace -p 22443 strace: Process 22443 attached read(0, "x0x0\n", 65536) = 5 write(1, "x0x0\n", 5) = 5 read(0,

Представляете, какую пользу может принести вам запуск strace для демонов: вы можете увидеть все, что делается в фоне. Завершите команду

Ctrl+C

Это также прекратит сеанс

strace

так как отслеживаемый процесс был прекращен.

Для просмотра отметок времени системных вызовов используйте опцию -t :

[root@sandbox ~]#strace -t ls testdir/ 14:24:47 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0 14:24:47 brk(NULL) = 0x1f07000 14:24:47 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2530bc8000 14:24:47 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) 14:24:47 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

А если вы хотите узнать время, проведенное между системными вызовами? Есть удобная опция -r , которая показывает время, затраченное на выполнение каждого системного вызова. Довольно полезно, не так ли?

[root@sandbox ~]#strace -r ls testdir/ 0.000000 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0 0.000368 brk(NULL) = 0x1966000 0.000073 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb6b1155000 0.000047 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) 0.000119 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

Заключение

Утилита strace очень удобна для изучения системных вызовов в Linux. Чтобы узнать о других параметрах командной строки, обратитесь к man и онлайн-документации.

Что такое системный вызов

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

UNIX — многозадачная система (мультипрограммная). Это означает, что одновременно может быть запущено много процессов. Процессор выполняет их в режиме разделения времени — выделяя по очереди квант времени одному процессу, затем другому, третьему. В результате создается впечатление параллельного выполнения всех процессов (на многопроцессорных машинах параллельность истинная). Процессам, ожидающим некоторого события, время процессора не выделяется. Более того, «спящий» процесс может быть временно откачан (т.е. скопирован из памяти машины) на диск, чтобы освободить память для других процессов. Когда «спящий» процесс дождется события, он будет «разбужен» системой, переведен в ранг «готовых к выполнению» и, если был откачан будет возвращен с диска в память (но, может быть, на другое место в памяти!). Эта процедура носит название «своппинг» (swapping).

Можно запустить несколько процессов, выполняющих программу из одного и того же файла; при этом все они будут (если только специально не было предусмотрено иначе) независимыми друг от друга. Так, у каждого пользователя, работающего в системе, имеется свой собственный процесс-интерпретатор команд (своя копия), выполняющий программу из файла /bin/csh (или /bin/sh).

Процесс представляет собой изолированный «мир», общающийся с другими «мирами» во Вселенной при помощи:

a) Аргументов функции main:

void main(int argc, char *argv[], char *envp[]);

Если мы наберем команду

$ a.out a1 a2 a3

то функция main программы из файла a.out вызовется с

argc = 4 /* количество аргументов */ argv[0] = "a.out" argv[1] = "a1" argv[2] = "a2" argv[3] = "a3" argv[4] = NULL 

По соглашению argv[0] содержит имя выполняемого файла из которого загружена эта программа * .

b) Так называемого «окружения» (или «среды») char *envp[], продублированного также в предопределенной переменной

extern char **environ;

Окружение состоит из строк вида

"ИМЯПЕРЕМЕННОЙ=значение"

Массив этих строк завершается NULL (как и argv). Для получения значения переменной с именем ИМЯ существует стандартная функция

char *getenv( char *ИМЯ );

Она выдает либо значение, либо NULL если переменной с таким именем нет.

c) Открытых файлов. По умолчанию (неявно) всегда открыты 3 канала:

ВВОД В Ы В О Д FILE * stdin stdout stderr соответствует fd 0 1 2 связан с клавиатурой дисплеем
#include main(ac, av) char **av; < execl("/bin/sleep", "Take it easy", "1000", NULL); >

Эти каналы достаются процессу «в наследство» от запускающего процесса и связаны с дисплеем и клавиатурой, если только не были перенаправлены. Кроме того, программа может сама явно открывать файлы (при помощи open, creat, pipe, fopen). Всего программа может одновременно открыть до 20 файлов (считая стандартные каналы), а в некоторых системах и больше (например, 64). В MS DOS есть еще 2 предопределенных канала вывода: stdaux — в последовательный коммуникационный порт, stdprn — на принтер.

d) Процесс имеет уникальный номер, который он может узнать вызовом

int pid = getpid();

а также узнать номер «родителя» вызовом

int ppid = getppid();

Процессы могут по этому номеру посылать друг другу сигналы:

kill(pid /* кому */, sig /* номер сигнала */);

и реагировать на них

signal (sig /*по сигналу*/, f /*вызывать f(sig)*/);

e) Существуют и другие средства коммуникации процессов: семафоры, сообщения, общая память, сетевые коммуникации.

f) Существуют некоторые другие параметры (контекст) процесса: например, его текущий каталог, который достается в наследство от процесса-«родителя», и может быть затем изменен системным вызовом

chdir(char *имя_нового_каталога);

У каждого процесса есть свой собственный текущий рабочий каталог (в отличие от MS DOS, где текущий каталог одинаков для всех задач). К «прочим» характеристикам отнесем также: управляющий терминал; группу процессов (pgrp); идентификатор (номер) владельца процесса (uid), идентификатор группы владельца (gid), реакции и маски, заданные на различные сигналы; и.т.п.

g) Издания других запросов (системных вызовов) к операционной системе («богу») для выполнения различных «внешних» операций.

h) Все остальные действия происходят внутри процесса и никак не влияют на другие процессы и устройства («миры»). В частности, один процесс НИКАК не может получить доступ к памяти другого процесса, если тот не позволил ему это явно (механизм shared memory); адресные пространства процессов независимы и изолированы (равно и пространство ядра изолировано от памяти процессов).

Операционная система выступает в качестве коммуникационной среды, связывающей «миры»-процессы, «миры»-внешние устройства (включая терминал пользователя); а также в качестве распорядителя ресурсов «Вселенной», в частности — времени (по очереди выделяемого активным процессам) и пространства (в памяти компьютера и на дисках).

Мы уже неоднократно упоминали «системные вызовы». Что же это такое? С точки зрения Си-программиста — это обычные функции. В них передают аргументы, они возвращают значения. Внешне они ничем не отличаются от написанных нами или библиотечных функций и вызываются из программ одинаковым с ними способом.

С точки же зрения реализации — есть глубокое различие. Тело функции-сисвызова расположено не в нашей программе, а в резидентной (т.е. постоянно находящейся в памяти компьютера) управляющей программе, называемой ядром операционной системы * .

Поведение всех программ в системе вытекает из поведения системных вызовов, которыми они пользуются. Даже то, что UNIX является многозадачной системой, непосредственно вытекает из наличия системных вызовов fork, exec, wait и спецификации их функционирования! То же можно сказать про язык Си — мобильность программы зависит в основном от набора используемых в ней библиотечных функций (и, в меньшей степени, от диалекта самого языка, который должен удовлетворять стандарту на язык Си). Если две разные системы предоставляют все эти функции (которые могут быть по-разному реализованы, но должны делать одно и то же), то программа будет компилироваться и работать в обоих системах, более того, работать в них одинаково.

Сам термин «системный вызов» как раз означает «вызов системы для выполнения действия», т.е. вызов функции в ядре системы. Ядро работает в привелегированном режиме, в котором имеет доступ к некоторым системным таблицам * , регистрам и портам внешних устройств и диспетчера памяти, к которым обычным программам доступ аппаратно запрещен (в отличие от MS DOS, где все таблицы ядра доступны пользовательским программам, что создает раздолье для вирусов). Системный вызов происходит в 2 этапа: сначала в пользовательской программе вызывается библиотечная функция-«корешок», тело которой написано на ассемблере и содержит команду генерации программного прерывания. Это — главное отличие от нормальных Си-функций — вызов по прерыванию. Вторым этапом является реакция ядра на прерывание:

  1. переход в привелегированный режим;
  2. разбирательство, КТО обратился к ядру, и подключение u-area этого процесса к адресному пространству ядра (context switching);
  3. извлечение аргументов из памяти запросившего процесса;
  4. выяснение, ЧТО же хотят от ядра (один из аргументов, невидимый нам — это номер системного вызова);
  5. проверка корректности остальных аргументов;
  6. проверка прав процесса на допустимость выполнения такого запроса;
  7. вызов тела требуемого системного вызова — это обычная Си-функция в ядре;
  8. возврат ответа в память процесса;
  9. выключение привелегированного режима;
  10. возврат из прерывания.

Во время системного вызова (шаг 7) процесс может «заснуть», дожидаясь некоторого события (например, нажатия кнопки на клавиатуре). В это время ядро передаст управление другому процессу. Когда наш процесс будет «разбужен» (событие произошло) — он продолжит выполнение шагов системного вызова.

Большинство системных вызовов возвращают в программу в качестве своего значения признак успеха: 0 — все сделано, (-1) — сисвызов завершился неудачей; либо некоторое содержательное значение при успехе (вроде дескриптора файла в open(), и (-1) при неудаче. В случае неудачного завершения в предопределенную переменную errno заносится номер ошибки, описывающий причину неудачи (коды ошибок предопределены, описаны в include-файле errno.h> и имеют вид Eчтото). Заметим, что при УДАЧЕ эта переменная просто не изменяется и может содержать любой мусор, поэтому проверять ее имеет смысл лишь в случае, если ошибка действительно произошла:

#include errno.h> /* коды ошибок */ extern int errno; extern char *sys_errlist[]; int value; if((value = sys_call(. )) < 0 )< printf("Error:%s(%d)\n", sys_errlist[errno], errno ); exit(errno); /* принудительное завершение программы */ >

Предопределенный массив sys_errlist, хранящийся в стандартной библиотеке, содержит строки-расшифровку смысла ошибок (по-английски). Посмотрите описание функции per- ror().

6.1. Файлы и каталоги.

6.1.1. Используя системный вызов stat, напишите программу, определяющую тип файла: обычный файл, каталог, устройство, FIFO-файл. Ответ:

#include sys/types.h> #include sys/stat.h> typeOf( name ) char *name; < int type; struct stat st; if( stat( name, &st ) < 0 )< printf( "%s не существует\n", name ); return 0; > printf("Файл имеет %d имен\n", st.st_nlink); switch(type = (st.st_mode & S_IFMT))< case S_IFREG: printf( "Обычный файл размером %ld байт\n", st.st_size ); break; case S_IFDIR: printf( "Каталог\n" ); break; case S_IFCHR: /* байтоориентированное */ case S_IFBLK: /* блочноориентированное */ printf( "Устройство\n" ); break; case S_IFIFO: printf( "FIFO-файл\n" ); break; default: printf( "Другой тип\n" ); break; > return type; >

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

struct stat st; int used, fd; for(fd=0; fd < NOFILE; fd++ )< used = fstat(fd, &st)

Программа может использовать дескрипторы файлов с номерами 0..NOFILE-1 (обычно 0..19). Если fstat для какого-то fd вернул код ошибки (<0), это означает, что данный дескриптор не связан с открытым файлом (т.е. не используется). NOFILE определено в include-файле sys/param.h>, содержащем разнообразные параметры данной системы.

6.1.3. Напишите упрощенный аналог команды ls, распечатывающий содержимое текущего каталога (файла с именем «.») без сортировки имен по алфавиту. Предусмотрите чтение каталога, чье имя задается как аргумент программы. Имена «.» и «..» не выдавать.

Формат каталога описан в header-файле sys/dir.h> и в «канонической» версии выглядит так: каталог — это файл, состоящий из структур direct, каждая описывает одно имя файла, входящего в каталог:

struct direct < unsigned short d_ino; /* 2 байта: номер I-узла */ char d_name[DIRSIZ]; /* имя файла */ >;

В семействе BSD формат каталога несколько иной — там записи имеют разную длину, зависящую от длины имени файла, которое может иметь длину от 1 до 256 символов.

Имя файла может состоять из любых символов, кроме ‘\0’, служащего признаком конца имени и ‘/’, служащего разделителем. В имени допустимы пробелы, управляющие символы (но не рекомендуются!), любое число точек (в отличие от MS DOS, где допустима единственная точка, отделяющая собственно имя от суффикса (расширения)), разрешены даже непечатные (т.е. управляющие) символы! Если имя файла имеет длину 14 (DIRSIZ) символов, то оно не оканчивается байтом ‘\0’. В этом случае для печати имени файла возможны три подхода:

  1. Выводить символы при помощи putchar()-а в цикле. Цикл прерывать по индексу равному DIRSIZ, либо по достижению байта ‘\0‘.
  2. Скопировать поле d_name в другое место:

char buf[ DIRSIZ + 1 ]; strncpy(buf, d.d_name, DIRSIZ); buf[ DIRSIZ ] = '\0';

Этот способ лучший, если имя файла надо не просто напечатать, но и запомнить на будущее, чтобы использовать в своей программе.

#include sys/types.h> #include sys/dir.h> struct direct d; . printf( "%*.*s\n", DIRSIZ, DIRSIZ, d.d_name );

Если файл был стерт, то в поле d_ino записи каталога будет содержаться 0 (именно поэтому I-узлы нумеруются начиная с 1, а не с 0). При удалении файла содержимое его (блоки) уничтожается, I-узел освобождается, но имя в каталоге не затирается физически, а просто помечается как стертое: d_ino=0; Каталог при этом никак не уплотняется и не укорачивается! Поэтому имена с d_ino==0 выдавать не следует — это имена уже уничтоженных файлов.

При создании нового имени (creat, link, mknod) система просматривает каталог и переиспользует первый от начала свободный слот (ячейку каталога) где d_ino==0, записывая новое имя в него (только в этот момент старое имя-призрак окончательно исчезнет физически). Если пустых мест нет — каталог удлиняется.

Любой каталог всегда содержит два стандартных имени: «.» — ссылка на этот же каталог (на его собственный I-node), «..» — на вышележащий каталог. У корневого каталога «/» оба этих имени ссылаются на него же самого (т.е. содержат d_ino==2).

Имя каталога не содержится в нем самом. Оно содержится в «родительском» каталоге ...

Каталог в UNIX — это обычный дисковый файл. Вы можете читать его из своих программ. Однако никто (включая суперпользователя * ) не может записывать что-либо в каталог при помощи write. Изменения содержимого каталогов выполняет только ядро, отвечая на запросы в виде системных вызовов creat, unlink, link, mkdir, rmdir, rename, mknod. Коды доступа для каталога интерпретируются следующим образом:

w запись S_IWRITE. Означает право создавать и уничтожать в каталоге имена файлов при помощи этих вызовов. То есть: право создавать, удалять и переименовывать файлы в каталоге. Отметим, что для переименования или удаления файла вам не требуется иметь доступ по записи к самому файлу — достаточно иметь доступ по записи к каталогу, содержащему его имя! r чтение S_IREAD. Право читать каталог как обычный файл (право выполнять opendir, см. ниже): благодаря этому мы можем получить список имен файлов, содержащихся в каталоге. Однако, если мы ЗАРАНЕЕ знаем имена файлов в каталоге, мы МОЖЕМ работать с ними — если имеем право доступа «выполнение» для этого каталога! x выполнение S_IEXEC. Разрешает поиск в каталоге. Для открытия файла, создания/удаления файла, перехода в другой каталог (chdir), система выполняет следующие действия (осуществляемые функцией namei() в ядре): чтение каталога и поиск в нем указанного имени файла или каталога; найденному имени соответствует номер I-узла d_ino; по номеру узла система считывает с диска сам I-узел нужного файла и по нему добирается до содержимого файла. Код «выполнение» — это как раз разрешение такого просмотра каталога системой. Если каталог имеет доступ на чтение — мы можем получить список файлов (т.е. применить команду ls); но если он при этом не имеет кода доступа «выполнение» — мы не сможем получить доступа ни к одному из файлов каталога (ни открыть, ни удалить, ни создать, ни сделать stat, ни chdir). Т.е. «чтение» разрешает применение вызова read, а «выполнение» — функции ядра namei. Фактически «выполнение» означает «доступ к файлам в данном каталоге»; еще более точно — к I-nodам файлов этого каталога. t sticky bit S_ISVTX — для каталога он означает, что удалить или переименовать некий файл в данном каталоге могут только: владелец каталога, владелец данного файла, суперпользователь. И никто другой. Это исключает удаление файлов чужими.

Совет: для каталога полезно иметь такие коды доступа:

chmod o-w,+t каталог 

В системах BSD используется, как уже было упомянуто, формат каталога с переменной длиной записей. Чтобы иметь удобный доступ к именам в каталоге, возникли специальные функции чтения каталога: opendir, closedir, readdir. Покажем, как простейшая команда ls реализуется через эти функции.

#include #include #include int listdir(char *dirname)< register struct dirent *dirbuf; DIR *fddir; ino_t dot_ino = 0, dotdot_ino = 0; if((fddir = opendir (dirname)) == NULL)< fprintf(stderr, "Can't read %s\n", dirname); return 1; > /* Без сортировки по алфавиту */ while ((dirbuf = readdir (fddir)) != NULL ) < if (dirbuf->d_ino == 0) continue; if (strcmp (dirbuf->d_name, "." ) == 0)< dot_ino = dirbuf->d_ino; continue; > else if(strcmp (dirbuf->d_name, "..") == 0)< dotdot_ino = dirbuf->d_ino; continue; > else printf("%s\n", dirbuf->d_name); > closedir (fddir); if(dot_ino == 0) printf("Поврежденный каталог: нет имени \".\"\n"); if(dotdot_ino == 0) printf("Поврежденный каталог: нет имени \"..\"\n"); if(dot_ino && dot_ino == dotdot_ino) printf("Это корневой каталог диска\n"); return 0; > int main(int ac, char *av[])< int i; if(ac > 1) for(i=1; i < ac; i++) listdir(av[i]); else listdir("."); return 0; >

Обратите внимание, что тут не требуется добавление ‘\0’ в конец поля d_name, поскольку его предоставляет нам сама функция readdir().

6.1.4. Напишите программу удаления файлов и каталогов, заданных в argv. Делайте stat, чтобы определить тип файла (файл/каталог). Программа должна отказываться удалять файлы устройств. Для удаления пустого каталога (не содержащего иных имен, кроме «.» и «..») следует использовать сисвызов

rmdir(имя_каталога);

(если каталог не пуст — errno получит значение EEXIST); а для удаления обычных файлов (не каталогов)

unlink(имя_файла);

Программа должна запрашивать подтверждение на удаление каждого файла, выдавая его имя, тип, размер в килобайтах и вопрос «удалить ?».

* — Именно это имя показывает команда ps -ef

* — Собственно, операционная система характеризуется набором предоставляемых ею системных вызовов, поскольку все концепции, заложенные в системе, доступны нам только через них. Если мы имеем две реализации системы с разным внутренним устройством ядер, но предоставляющие одинаковый интерфейс системных вызовов (их набор, смысл и поведение), то это все-таки одна и та же система! Ядра могут не просто отличаться, но и быть построенными на совершенно различных принципах: так обстоит дело с UNIX-ами на однопроцессорных и многопроцессорных машинах. Но для нас ядро — это «черный ящик», полностью определяемый его поведением, т.е. своим интерфейсом с программами, но не внутренним устройством. Вторым параметром, характеризующим ОС, являются форматы данных, используемые системой: форматы данных для сисвызовов и формат информации в различных файлах, в том числе формат оформления выполняемых файлов (формат данных в физической памяти машины в этот список не входит — он зависим от реализации и от процессора). Как правило, программа пишется так, чтобы использовать соглашения, принятые в данной системе, для чего она просто включает ряд стандартных include-файлов с описанием этих форматов. Имена этих файлов также можно отнести к интерфейсу системы.

* — Таким как таблица процессов, таблица открытых файлов (всех вместе и для каждого процесса), и.т.п.

* — Суперпользователь (superuser) имеет uid==0. Это «привелегированный» пользователь, который имеет право делать ВСЕ. Ему доступны любые сисвызовы и файлы, несмотря на коды доступа и.т.п.

© Copyright А. Богатырев, 1992-95
Си в UNIX

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *