Перевод статьи подготовлен специально для студентов базового и продвинутого курсов Administrator Linux.
Системный вызов — это механизм взаимодействия пользовательских программ с ядром Linux, а strace — мощный инструмент, для их отслеживания. Для лучшего понимания работы операционной системы полезно разобраться с тем, как они работают.
В операционной системе можно выделить два режима работы:
Пользователи при повседневной работе обычно используют утилиты командной строки и графический интерфейс (GUI). При этом в фоне незаметно работают системные вызовы, обращаясь к ядру для выполнения работы.
Системные вызовы очень похожи на вызовы функций, в том смысле, что в них передаются аргументы и они возвращают значения. Единственное отличие состоит в том, что системные вызовы работают на уровне ядра, а функции нет. Переключение из пользовательского режима в режим ядра осуществляется с помощью специального механизма прерываний.
Большая часть этих деталей скрыта от пользователя в системных библиотеках (glibc в Linux-системах). Системные вызовы по своей природе являются универсальными, но несмотря на это, механика их выполнения во многом аппаратно-зависима.
В этой статье рассматривается несколько практических примеров анализа системных вызовов с помощью
Для начала убедитесь, что в вашей системе установлены необходимые инструменты. Проверить установлен ли
Если
Для примера создайте тестовый каталог в
(Я использую каталог
С помощью команды
Вероятно, вы используете команду
Команда
Если вы хотите узнать, какие функции вызывались из библиотеки glibc, то используйте команду
Если
На экране будет много информации, но не беспокойтесь — мы это рассмотрим далее. Вот некоторые из важных библиотечных функций из вывода
Изучив этот вывод, вы, вероятно, поймете, что происходит. Каталог с именем
Как вы видите, можно легко посмотреть вызываемые библиотечные функции, но в этой статье мы сфокусируемся на системных вызовах, которые вызываются функциями системных библиотек.
Для просмотра системных вызовов используйте
В результате выполнения
Есть удобный способ анализа полученной информации — записать вывод в файл с помощью опции
На этот раз на экране не будет никаких данных — команда
Взгляните на первую строку в файле
Теперь результат не кажется слишком пугающим, не так ли? И вы можете применить ту же логику и для других строк.
Обратите внимание на ту единственную команду, которую вы вызвали —
Возвращаясь к приведенному выше анализу
Не нужно запоминать все системные вызовы и то, что они делают: все есть в документации. Man-страницы спешат на помощь! Перед запуском команды man убедитесь, что установлен пакет
Помните, что вам нужно добавить «2» между командой
Ниже приведены номера разделов
Для просмотра документации по системному вызову запустите man с именем этого системного вызова.
В соответствии с документацией системный вызов
В следующий системный вызов
Для просмотра документации используйте
Далее системный вызов
Теперь откройте файл
В документации (
Теперь, когда получено содержимое каталога, нужен способ отобразить информацию в терминале. Итак, делаем
В аргументах вы можете видеть имена файлов, которые будут выводится:
Таким образом, системный вызов
Теперь вы знаете, какие системные вызовы сделали большую часть работы для команды
Операционная система выполняет много вспомогательных действий для запуска процесса, поэтому многое из того, что вы видите в файле
Теперь вы можете анализировать системные вызовы для любых программ. Утилита strace так же предоставляет множество полезных параметров командной строки, некоторые из которых описаны ниже.
По умолчанию
Хорошая практика использовать параметр
А если вам нужны только имена системных вызовов, количество их запусков и процент времени, затраченного на выполнение? Вы можете использовать опцию
Если вы хотите отследить определенный системный вызов, например,
А что, если нужно отфильтровать по нескольким системным вызовам? Не волнуйтесь, можно использовать ту же опцию
До сих пор мы отслеживали только явный запуск команд. Но как насчет команд, которые были запущены ранее? Что, если вы хотите отслеживать демонов? Для этого у
Мы не будем запускать демона, а используем команду
Запустите команду
На другом терминале найдите идентификатор процесса (PID) с помощью команды
Теперь запустите
Теперь вернитесь к терминалу, где вы оставили запущенную команду
Вернитесь к терминалу, где
Представляете, какую пользу может принести вам запуск
Для просмотра отметок времени системных вызовов используйте опцию
А если вы хотите узнать время, проведенное между системными вызовами? Есть удобная опция
Утилита
Системный вызов — это механизм взаимодействия пользовательских программ с ядром Linux, а strace — мощный инструмент, для их отслеживания. Для лучшего понимания работы операционной системы полезно разобраться с тем, как они работают.
В операционной системе можно выделить два режима работы:
- Режим ядра (kernel mode) — привилегированный режим, используемый ядром операционной системы.
- Пользовательский режим (user mode) — режим, в котором выполняется большинство пользовательских приложений.
Пользователи при повседневной работе обычно используют утилиты командной строки и графический интерфейс (GUI). При этом в фоне незаметно работают системные вызовы, обращаясь к ядру для выполнения работы.
Системные вызовы очень похожи на вызовы функций, в том смысле, что в них передаются аргументы и они возвращают значения. Единственное отличие состоит в том, что системные вызовы работают на уровне ядра, а функции нет. Переключение из пользовательского режима в режим ядра осуществляется с помощью специального механизма прерываний.
Большая часть этих деталей скрыта от пользователя в системных библиотеках (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
<<< truncated strace output >>>
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
. Все системные вызовы можно разделить на следующие категории:- Управление процессами
- Управление файлами
- Управление каталогами и файловой системой
- Прочие
Есть удобный способ анализа полученной информации — записать вывод в файл с помощью опции
-o
.[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/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 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/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 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 — стандартный поток ошибок
Таким образом, системный вызов
write
выводит file1
и file2
на стандартный вывод, которым является терминал, обозначаемый числом 1. Теперь вы знаете, какие системные вызовы сделали большую часть работы для команды
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
для демонов: вы можете увидеть все, что делается в фоне. Завершите команду cat
, нажав 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 и онлайн-документации.