С какого системного вызова начинается выполнение программы в linux

Обновлено: 01.07.2024

[Системное программирование Linux] Обзор системных вызовов Linux

Системный вызов , Как следует из названия, относится к набору «специальных» интерфейсов, которые операционная система предоставляет пользовательской программе для вызова. Пользовательские программы могут получать услуги, предоставляемые ядром операционной системы, через этот набор «специальных» интерфейсов. Например, пользователи могут запрашивать у системы открытие файлов, закрытие файлов или чтение и запись файлов с помощью вызовов, связанных с файловой системой, и могут получать системное время с помощью системных вызовов, связанных с часами. Или установить таймер и т. Д.


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


Причина, по которой системные службы необходимо предоставлять пользовательскому пространству с помощью системных вызовов. основная причина Это для «защиты» системы, потому что мы знаем, что операционное пространство Linux делится на Пространство ядра против Пользовательское пространство , Они работают на разных уровнях и логически изолированы друг от друга. и другие Пользовательским процессам обычно не разрешен доступ к данным ядра, и они не могут использовать функции ядра. Они могут только манипулировать данными пользователя в пользовательском пространстве и вызывать функции пользовательского пространства. . Например, знакомая программа «hello world» (при выполнении) является стандартным процессом в пространстве пользователя. Используемая ею функция печати printf принадлежит функции пространства пользователя, а напечатанные символы «привет, слово» - Строки также относятся к данным пользовательского пространства.

Однако во многих случаях пользовательский процесс должен получать системные службы (вызывать системные программы). В это время необходимо использовать "специальный интерфейс", предоставляемый системой пользователю - системный вызов, его особый Производительность в основном заключается в указании конкретного места, где пользовательский процесс входит в ядро; другими словами, путь для доступа пользователя к ядру заранее определен, Может входить в ядро ​​только с указанной позиции, и ему не разрешено произвольно прыгать в ядро . Только с таким ограничением единого пути доступа, заложенным в ядре, ядро ​​может быть защищено. Мы можем ярко описать этот механизм : Как турист, вы можете купить билет для входа в сафари-парк, но вы должны честно сесть в экскурсионную машину и следовать предписанному маршруту для осмотра достопримечательностей. Конечно, вам не разрешают выходить из машины, потому что это слишком опасно - потерять жизнь или напугать диких животных.

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

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

Прерывание имеет два важных атрибута: номер прерывания и обработчик прерывания. Номер прерывания используется для идентификации разных прерываний, и разные прерывания имеют разные обработчики прерываний. В ядре операционной системы поддерживается таблица векторов прерываний, в этом массиве хранятся адреса всех обработчиков прерываний, а номер прерывания представляет собой смещение соответствующего прерывания в таблице векторов прерываний.Более подробные инструкции см. В разделе «Принцип реализации системных вызовов».。

В Linux есть два способа манипулирования файлами: системный вызов и библиотечные функции.

Библиотечные функции предоставляются Два типа функций сочинение:

1) Нет необходимости вызывать системные вызовы

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

2) Необходимо вызвать системный вызов

При необходимости переключиться в пространство ядра, этот вид функций реализует соответствующие функции путем инкапсуляции системных вызовов, таких как printf, fread и т. Д.



Системные вызовы требуют времени.Частое использование системных вызовов в программе снижает эффективность программы. При запуске кода ядра ЦП работает в режиме ядра.Прежде чем произойдет системный вызов, ему необходимо сохранить стек пользовательского режима и среду памяти, а затем перейти в режим ядра для работы. После завершения системного вызова вернитесь в пользовательский режим. Такое переключение среды потребует много времени.


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

Этот процесс похож на Курьер доставляет курьера в определенную область (пространство ядра), у курьера есть два способа доставки:

1) Одно место экспресс-доставки будет доставлено в пункт назначения немедленно, одно - целиком, что приводит к частым поездкам туда и обратно ( Системный вызов )

2) После того, как курьер почти накопил (буфер), он будет доставлен в пункт назначения за один раз ( Вызов функции библиотеки )


Вызов библиотеки функций VS системный вызов

Некоторые функции имеют одинаковый эффект, но параметры отличаются. (Возможно, многие друзья, знакомые с C ++, сразу же подумают о перегрузке функций, но не забывайте, что ядро ​​Linux написано на языке C, поэтому вы можете использовать только разные имена функций). Есть также некоторые функции, которые устарели и заменены новыми и лучшими функциями (gcc выдаст предупреждение при связывании этих функций), но они по-прежнему зарезервированы по соображениям совместимости. Я отмечу эти функции знаком "*" перед Чтобы показать разницу.



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

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

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

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

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

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


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


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


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


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

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


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


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

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


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


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


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

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

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


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

  • Управление процессами
  • Управление файлами
  • Управление каталогами и файловой системой
  • Прочие


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


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

  • В начале строки находится имя выполняемого системного вызова — это execve.
  • Текст в круглых скобках — это аргументы, передаваемые системному вызову.
  • Число после знака = (в данном случае 0) — это значение, возвращаемое системным вызовом.

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


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


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


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

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


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


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


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


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

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


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


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

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


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

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

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

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

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

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


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


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


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


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


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

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

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


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


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


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


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


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

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


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

Заключение

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

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

Процессы. Системные вызовы fork() и exec(). Нити.

Процесс в Linux (как и в UNIX) - это программа, которая выполняется в отдельном виртуальном адресном пространстве. Когда пользователь регистрируется в системе, автоматически создается процесс, в котором выполняется оболочка (shell), например, /bin/bash.

В Linux поддерживается классическая схема мультипрограммирования. Linux поддерживает параллельное (или квазипараллельного при наличии только одного процессора) выполнение процессов пользователя. Каждый процесс выполняется в собственном виртуальном адресном пространстве, т.е. процессы защищены друг от друга и крах одного процесса никак не повлияет на другие выполняющиеся процессы и на всю систему в целом. Один процесс не может прочитать что-либо из памяти (или записать в нее) другого процесса без "разрешения" на то другого процесса. Санкционированные взаимодействия между процессами допускаются системой.

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

В связи с развитием SMP (Symmetric Multiprocessor Architectures) в ядро Linux был внедрен механизм нитей или потоков управления (threads). Нить - это процесс, который выполняется в виртуальной памяти, используемой вместе с другими нитями процесса, который обладает отдельной виртуальной памятью.

Если интерпретатору (shell) встречается команда, соответствующая выполняемому файлу, интерпретатор выполняет ее, начиная с точки входа (entry point). Для С-программ entry point - это функция main. Запущенная программа тоже может создать процесс, т.е. запустить какую-то программу и ее выполнение тоже начнется с функции main.

Для создания процессов используются два системных вызова: fork() и exec. fork() создает новое адресное пространство, которое полностью идентично адресному пространству основного процесса. После выполнения этого системного вызова мы получаем два абсолютно одинаковых процесса - основной и порожденный. Функция fork() возвращает 0 в порожденном процессе и PID (Process ID - идентификатор порожденного процесса) - в основном. PID - это целое число.
Теперь, когда мы уже создали процесс, мы можем запустить программу с помощью вызова exec. Параметрами функции exec является имя выполняемого файла и, если нужно, параметры, которые будут переданы этой программе. В адресное пространство порожденного с помощью fork() процесса будет загружена новая программа и ее выполнение начнется с точки входа (адрес функции main).

В качестве примера рассмотрим этот фрагмент программы

if (fork()==0) wait(0);
else execl("ls", "ls", 0); /* порожденный процесс */

  1. Выделяется память для описателя нового процесса в таблице процессов
  2. Назначается идентификатор процесса PID
  3. Создается логическая копия процесса, который выполняет fork() - полное копирование содержимого виртуальной памяти родительского процесса, копирование составляющих ядерного статического и динамического контекстов процесса-предка
  4. Увеличиваются счетчики открытия файлов (порожденный процесс наследует все открытые файлы родительского процесса).
  5. Возвращается PID в точку возврата из системного вызова в родительском процессе и 0 - в процессе-потомке.

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

  1. окончание порожденного процесса (например, из-за системного вызова exit (см. ниже))
  2. возникновение исключительной ситуации
  3. сигналы, поступающие от пользователя при нажатии определенных клавиш.

Установить реакцию на поступление сигнала можно с помощью системного вызова signal
func = signal(snum, function);

snum - номер сигнала, а function - адрес функции, которая должна быть выполнена при поступлении указанного сигнала. Возвращаемое значение - адрес функции, которая будет реагировать на поступление сигнала. Вместо function можно указать ноль или единицу. Если был указан ноль, то при поступлении сигнала snum выполнение процесса будет прервано аналогично вызову exit. Если указать единицу, данный сигнал будет проигнорирован, но это возможно не для всех процессов.

С помощью системного вызова kill можно сгенерировать сигналы и передать их другим процессам.
kill(pid, snum);
где pid - идентификатор процесса, а snum - номер сигнала, который будет передан процессу. Обычно kill используется для того, чтобы принудительно завершить ("убить") процесс.
Pid состоит из идентификатора группы процессов и идентификатора процесса в группе. Если вместо pid указать нуль, то сигнал snum будет направлен всем процессам, относящимся к данной группе (понятие группы процессов аналогично группе пользователей). В одну группу включаются процессы, имеющие общего предка, идентификатор группы процесса можно изменить с помощью системного вызова setpgrp. Если вместо pid указать -1, ядро передаст сигнал всем процессам, идентификатор пользователя которых равен идентификатору текущего выполнения процесса, который посылает сигнал.

Сигналы (точнее их номера) описаны в файле singnal.h

Для нормального завершение процесса используется вызов
exit(status);
где status - это целое число, возвращаемое процессу-предку для его информирования о причинах завершения процесса-потомка.
Вызов exit может задаваться в любой точке программы, но может быть и неявным, например при выходе из функции main (при программировании на C) оператор return 0 будет воспринят как системный вызов exit(0);

Перенаправление ввода/вывода

Практически все операционные системы обладают механизмом перенаправления ввода/вывода. Linux не является исключением из этого правила. Обычно программы вводят текстовые данные с консоли (терминала) и выводят данные на консоль. При вводе под консолью подразумевается клавиатура, а при выводе - дисплей терминала. Клавиатура и дисплей - это, соответственно, стандартный ввод и вывод (stdin и stdout). Любой ввод/вывод можно интерпретировать как ввод из некоторого файла и вывод в файл. Работа с файлами производится через их дескрипторы. Для организации ввода/вывода в UNIX используются три файла: stdin (дескриптор 1), stdout (2) и stderr(3).

Символ > используется для перенаправления стандартного вывода в файл.
Пример:
$ cat > newfile.txt Стандартный ввод команды cat будет перенаправлен в файл newfile.txt, который будет создан после выполнения этой команды. Если файл с этим именем уже существует, то он будет перезаписан. Нажатие Ctrl + D остановит перенаправление и прерывает выполнение команды cat.

Символ < используется для переназначения стандартного ввода команды. Например, при выполнении команды cat > используется для присоединения данных в конец файла (append) стандартного вывода команды. Например, в отличие от случая с символом >, выполнение команды cat >> newfile.txt не перезапишет файл в случае его существования, а добавит данные в его конец.

Команды для управления процессами

Предназначена для вывода информации о выполняемых процессах. Данная команда имеет много параметров, о которых вы можете прочитать в руководстве (man ps). Здесь я опишу лишь наиболее часто используемые мной:

Параметр Описание
-a отобразить все процессы, связанных с терминалом (отображаются процессы всех пользователей)
-e отобразить все процессы
-t список терминалов отобразить процессы, связанные с терминалами
-u идентификаторы пользователей отобразить процессы, связанные с данными идентификаторыми
-g идентификаторы групп отобразить процессы, связанные с данными идентификаторыми групп
-x отобразить все процессы, не связанные с терминалом

Например, после ввода команды ps -a вы увидите примерно следующее:

Для вывода информации о конкретном процессе мы можем воспользоваться командой:

Программа top

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

Изменение приоритета процесса - команда nice

nice [-коэффициент понижения] команда [аргумент]

Команда nice выполняет указанную команду с пониженным приоритетом, коэффициент понижения указывается в диапазоне 1..19 (по умолчанию он равен 10). Суперпользователь может повышать приоритет команды, для этого нужно указать отрицательный коэффициент, например --10. Если указать коэффициент больше 19, то он будет рассматриваться как 19.

nohup - игнорирование сигналов прерывания

nohup команда [аргумент]

nohup выполняет запуск команды в режиме игнорирования сигналов. Не игнорируются только сигналы SIGHUP и SIGQUIT.

kill - принудительное завершение процесса

kill [-номер сигнала] PID

где PID - идентификатор процесса, который можно узнать с помощью команды ps.

Команды выполнения процессов в фоновом режиме - jobs, fg, bg

Команда jobs выводит список процессов, которые выполняются в фоновом режиме, fg - переводит процесс в нормальные режим ("на передний план" - foreground), а bg - в фоновый. Запустить программу в фоновом режиме можно с помощью конструкции &

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

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

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

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

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

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

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

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

AppInVat.jpg

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

Syscall.jpg

Обычное пользовательское приложение работает в непривилегированном режиме в своём виртуальном адресном пространстве. В обычном случае для выполнения вычислительной работы, для доступа к памяти и пр. не требуются системные вызовы. Например, функции библиотеки 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.jpg

  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):

Инструкция 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):

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

Системные вызовы требуют переключения контекста и перехода процессора в режим с высоким уровнем привилегий. Поэтому системный вызов выполняется относительно медленно по сравнению с вызовом обычной 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-страниц:

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

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

Читайте также: