§ 9. Системные вызовы
Введение
01 Несмотря на то что ядро операционной системы и приложение располагаются в одной и той же физической памяти, в программах используются логические адреса, а значит прямого доступа к памяти ядра приложение получить не может. Для того чтобы осуществить взаимодействие между ядром и приложением используются так называемые системные вызовы — специальные функции, предназначенные для выполнения действий, требующих особых привилегий. Раньше для осуществления системных вызовов использовались прерывания, в современных же процессорах из соображений эффективности для этого используются специальные инструкции. Выполняя такую инструкцию, процессор переходит в привилегированный режим и отдает управление ядру, которое выполняет инструкцию, считывая ее номер и аргументы из регистров.
Как программы взаимодействуют с ядром?
02 Современные приложения не делают системные вызовы напрямую, вместо этого используются функции-обертки на языке Си. Есть и функция-обертка, которая подходит для любого системного вызова, она называется syscall
. Первым аргументом этой функции является номер системного вызова, а остальные аргументы у каждого системного вызова свои. Полученные аргументы передаются во вспомогательную функцию, написанную на ассемблере, в которой осуществляется системный вызов. Справа на картинке показана такая функция для архитектуры x86. Для других архитектур функция будет отличаться. В данном случае все сводится к манипуляциям с регистрами и вызову одноименной инструкции syscall
. После завершения системного вызова, функция syscall
проверяет выходное значение, и если оно меньше нуля, то сохраняет это значение в глобальной переменной errno
и возвращает -1, иначе возвращает 0 (то есть завершается успешно).
03 Процессоры разных архитектур имеют разные способы осуществления системных вызовов, а также разные способы передачи аргументов для этих системных вызовов, из-за чего для каждого системного вызова пишутся функции-обертки. Библиотека с такими функциями называется libc
. Помимо оберток она полностью включает в себя стандартную библиотеку языка Си, а также некоторые дополнительные функции, которые упрощают работу с системой. Эта библиотека подключается ко всем программам, собираемым на Linux, независимо от языка, на котором они написаны.
04 Существует несколько реализаций библиотеки libc
, которые представлены на картинке ниже. Основной реализацией считается glibc
— это библиотека, которая поддерживается разработчиками ядра и которая всегда первой получает обновления. Поскольку glibc
довольно старая и поскольку разработчики ядра очень трепетно относятся к обратной совместимости, она использует много памяти. Альтернативные библиотеки лишены данного недостатка. Наиболее популярной из альтернатив является библиотека musl
, она часто используется в образах для Docker-контейнеров из-за своих скромных размеров и малого потребления памяти. Библиотеки uclibc
и dietlibc
используются во встраиваемых системах, они потребляют еще меньше памяти, но используют более простые и более медленные алгоритмы (например, алгоритм сортировки).
Как ядро взаимодействует с программами?
05 Мы с вами поговорили о том, как приложения могут взаимодействовать с ядром. Перефразируя, приложения смотрят на ядро через призму системных вызовов. А как ядро смотрит на приложения? Для ядра все приложения, будь то системная утилита, вроде команды ls
, или Docker-контейнер, являются процессами. Процесс — это единица планирования ресурсов в Linux.
06 Процессы объединяются в строгую иерархию, всегда имеющую топологию дерева. Первый процесс в иерархии всегда имеет номер 1 и создается ядром после завершения инициализации. Все остальные процессы создаются путем копирования из первого процесса и его дочерних процессов. Задачей первого процесса является дальнейшая инициализация системы: запуск резидентных процессов, монтирование сетевых каталогов и прочее. Также, дочерние процессы, родители которых завершились раньше (не подождав завершения потомков), становятся дочерними процессами первого процесса. Это делается для предотвращения утечек памяти и поддержания иерархии в связном виде.
07 Процессы создаются с помощью функции-обертки fork
. Эта функция в каком-то смысле уникальна: у нее нет аргументов, зато она возвращается два раза. Первый раз в дочерний процесс, и в данном случае возвращаемое значение равно нулю. Второй раз — в родительский процесс, и в данном случае возвращаемое значение равно номеру дочернего процесса. Как же ей удается вернуться два раза? Дело в том что системный вызов, осуществляемый данной функцией, копирует текущий процесс вместе со всеми структурами ядра и всеми его страницами памяти, включая стек, глобальные переменные и код программы, загруженный в память. Изменяются только номера процессов и некоторые другие поля, которые должны быть уникальными. Это приводит к тому, что в памяти оказываются два идентичных процесса, различить которые можно лишь, проверив выходное значение данного системного вызова.
17 Какой можно подвести итог? Потоки, контейнеры и обычные процессы с точки зрения ядра Linux различаются лишь набором флагов, или набором системных структур и ресурсов, которые выделяются процессу и его потомкам. В случае потока большинство структур и ресурсов используются совместно с родительским процессом, в случае контейнера почти все структуры и ресурсы копируются. Если расположить эти сущности на спектре, то получится примерно следующая картина. Потоки — это процессы-иждивенцы, полностью полагающиеся на ресурсы родителей, а контейнеры — это самостоятельные процессы, полностью изолированные от родителей.
Запуск программ в дочерних процессах
18 В примерах, использованных ранее все дочерние процессы выполняли тот же самый код, что и родительский процесс: в их памяти хранились те же самые глобальные переменные, функции и те же самые библиотеки. В реальности же мы часто хотим запустить совершенно другую программу в дочернем процессе. Для этого используется функция-обертка execvp
, в названии которой v
обозначает вектор, а p
— путь. В функцию передается вектор аргументов, представляющий собой массив строк, заканчивающийся нулевым элементом. По соглашению нулевой аргумент является названием программы, которая была запущено. Для соблюдения этого соглашения мы передаем нулевой элемент массива в качестве первого аргументы функции execvp
.
19 Эта функция заменяет текущий бинарный код, загруженный в память, на код из указанного файла. Суффикс p
означает, что система должна сама найти исполняемый файл с указанным именем в стандартных директориях. После загрузки в память все потоки останавливаются и запускается функция main
из только что загруженной программы. Функция execvp
уникальна тем, что при успешном вызове не возвращается. Если же произошла ошибка, то функция возвращает -1. В данном примере в этом случае дочерний процесс завершается с помощью системного вызова exit
.
20 Как видите, в Linux создание нового процесса и загрузка кода разделены. Это удобно, поскольку после создания дочернего процесса, родительский может его настроить (ограничить ресурсы, изменить рабочую директорию и т.п.), используя те же самые системные вызовы, что и для себя самого. После настройки вызывается функция exec
, заменяющая код на новый. При этом настройки сохраняются (наследуются).
21 В таблице показаны некоторые основные системные вызовы. Здесь есть вызовы для получения номеров текущего и родительского процессов, номеров пользователя и группы, переменных среды и рабочей директории. Для ядра все пользователи и группы являются номерами. Даже если этим номерам не соответствуют зарегистрированные имена и пароли пользователей, ядро все равно может запустить процесс, работающий под номером такого пользователя или группы. Таким образом, пользователь и группа — это всего лишь поля структуры, описывающей процесс (для ядра все является процессом!). Права на ту или иную операцию даются процессу, а не пользователю. А сейчас перейдем к обсуждению переменных среды.
Переменные среды
22 Переменные среды — это вектор (аналогичный вектору аргументов), каждый элемент которого является парой ключ-значение, разделенными знаком =
. Стандартных переменных довольно много, однако вы не можете быть уверены в наличии ни одной из них. Ниже показаны две переменные, которые чаще всего встречаются. Это переменная HOME
, содержащая путь к домашней директории текущего пользователя, и переменная LANG
, обозначающая текущий язык и кодировку. Эта переменная используется при выборе переводов строк из программ на языки мира.
Задания
Системные вызовы1 балл
23 Сделайте системный вызов getpid
с помощью одноименной функции-обертки из заголовочного файла unistd.h. Сделайте тот же самый вызов с помощью универсальной функции-обертки syscall
. Удостоверьтесь, что они возвращают одинаковые значения.
Дочерние процессы1 балл
24 Создайте дочерний процесс с помощью функции-обертки fork
и запустите в нем команду expr 2 + 2 * 2
с помощью системного вызова execlp
. Дождитесь завершения дочернего процесса с помощью функции wait
.
Переменные среды1 балл
25 Выведите переменные среды, имя которых содержит символ L
. Для этого воспользуйтесь глобальной переменной environ
из заголовочного файла environ.h.
Аргументы и пространства имен2 балла
26 Создайте дочерний процесс, в котором вы измените имя компьютера на имя, переданное родительскому процессу в качестве первого аргумента. Для этого воспользуйтесь системным вызовом clone
и подходящими флагами этого вызова, а также системным вызовом sethostname
. Проверьте с помощью вызова gethostname
, что имя компьютера разное в дочернем и родительском процессе.