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


Содержание

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

Окружение: Windows NT, 2K, XP, 2003

Большинство материалов, которые описывают системные вызовы Windows NT, оставлют многие важные детали за кадром. Это приводит к затруднениям при попытке понять, что же именно происходит, когда код пользовательского режима вызывает код ядра. Данная статья призвана пролить свет на действительный механизм, который Windows NT использует для переключения в режим ядра для исполнения системных операций. Описание приводится для x86-совместимого процессора в защищенном режиме. Другие платформы, поддерживаемые Windows NT имеют схожий механизм для переключения в режим ядра.

Что такое режим ядра?

Вопреки мнению многих программистов (и в том числе системных) у x86-процессора не существует так называемого «режима ядра». Другие процессоры, такие, как Motorola 68000, имеют два процессорных режима «встроенных» в процессор. Т.е. в статусном регистре содержится флаг, который определяет происходит ли сейчас исполнение кода в пользовательском режиме или в режиме супервизора. У процессоров Intel x86 такого флага нет. Вместо этого, уровень привелегий исполняемой программы определяется уровнем привилегий текущего сегмента кода. Каждый сегмент кода в приложении пользовательского режима на процессоре x86 описывается 8-байтой структурой данных, называемой сегментым дескриптором. Сегментный дескриптор (среди прочего) содержит начальный адресс описываемого сегмента кода, его длину и уровень привилегий, на котором будет исполняться код, содержащийся в этом сегменте. Код исполняемый с уровнем привилегий равным 3 считается кодом пользовательского режима, а код с уровнем привилегий 0 – кодом режима ядра. Другими словами, режим ядна (уровень привилегий 0) и пользовательский режим (уровень привилегий 3) являеются атрибутами кода, а не процессора. В терминологии Intel уровень привилегий 0 называется «Ring 0», а уровень привилегий 3 – «Ring 3». У x86-процессора есть еще два уровня привилегий, которые не используются Windows NT («Ring 1» и «Ring 2»). Причина этого в том, что Windows NT спроектирована с учетом возможности работы и на других аппаратных платформах, которые могут иметь, а могут и не иметь четырех уровней привилегий подобно Intel x86.

Процессор не позволит коду с более низким (численно большим) уровнем привилегий непосредственно сделать вызов в код с более высоким (численно меньшим) уровнем привилегий. Если такая попытка будет предпринята, то процессором автоматически будет сгенерировано исключение общей защиты (general protection, GP). Будет вызван обработчик этого исключения, предоставленный операционной системой, и будут предприняты соответсвующие действия (выдача сообщения пользователю, принудительное завершение приложения и др.). Следует отметить что весь вышеописанный механизм защиты является свойством x86-процессора, а не операционной системы. Без надлежащей поддержки со стороны процессора, реализовать данный механизм средствами ОС было бы невозможно.

Где располагаются сегментные дескрипторы?

Поскольку каждый сегмент кода, который существует в системе, описывается сегментным дескриптором, а их количество может быть достаточно велико (каждая программа может иметь несколько), то сегментные дескрипторы должны где-то храниться, чтобы процессор мог прочитать их и разрешить либо запретить программе доступ к сегменту. Разработчики Intel приняли решение хранить всю эту информацию не в самом чипе процессора, а в основной памяти. Есть два таблицы в основной памяти, которые хранят сегментные дескрипторы: глобальная таблица дескрипторов (Global Descriptor Table, GDT) и локальная таблица дескрипторов (Local Descriptor Table, LDT). Адреса и размеры этих таблиц хранятся в соответсвующих регистрах процесоора. Этими регистрами являются Global Descriptor Table Register (GDTR) и Local Descriptor Table Register (LDTR). Заполнение таблиц и установка значений этих регистров является обязанностью операционной системы. И это должно быть сделано на самых ранних этапах процесса загрузки, еще до переключения в защищенный режим, потому, что без таблиц дескрипторов в защищенном режиме невозможно обратиться ни к одному сегменту памяти. Рисунок 1 иллюстрирует отношения между GDTR, LDTR, GDT и LDT.

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

Как можно видеть из рисунка 2, селектор сегмента также содерижт 2-битное поле, которое называется Requestor Privilege Level (RPL). Эти биты используются для определения того, может ли определенный кусок кода обращаться к дескриптору, заданному данным селектором. К примеру, если код с уровнем привилегий 3 (режим пользователя) пытается сделать переход или вызвать код из другого сегмента, через селектор с RPL=0, то произойдет исключение общей защиты. Таким образом x86-процессор гарантирует что никакой код режима пользователя не получит доступ к коду уровня ядра. На самом деле, истинное положение вещей куда сложнее, чем изложено здесь. Получить детальную информацию по этой Рекомендуется For the information-eager please see the further reading list, «Protected Mode Software Architecture» for the details of the RPL field. For our purposes it is enough to know that the RPL field is used for privilege checks of the code trying to use the segment selector to read a segment descriptor.

Шлюзы прерываний

Итак, если приложение, исполняющееся в пользовательском режиме (с уровнем привилегий 3), не может вызвать кода ядра (с уровнем привилегий 0), то как же тогда работаю системные вызовы в Windows NT? Ответ опять лежит в использовании возможностей процессора. Для того чтобы контролировать переходы между кодом с разным уровнем привилегий, Windows NT использует возмножность x86-процессора, называемую шлюз прерывания. Для того, чтобы понять суть шлюза прерывания, необходимо сначала разобраться с тем, как используются прерывания в x86-процессоре в защищенном режиме.

Подобно многим другим процессорам, x86-процессор имеет таблицу векторов прерывания, которая содержит информацию о том, как следует обрабатывать каждое прерывание. В реальном режиме, таблица векторов прерываний x86-процессора, просто содержит указатели (4 байта каждый) на процедуры обслуживания прерываний (Interrupt Service Routines, ISR). А в защищенном режиме таблица векторов прерываний содержит декскрипторы шлюзов прерываний, которые являются 8-байтовыми структурами, которые описывают как данное прерывание должно быть обработано. Дескриптор шлюза прерывания содержит информацию о том, какой сегмент кода содержит процедуру обслуживания прерываний и ее адресс внутри сегмента. Причина, по которой в таблице используются декскрипторы шлюзов прерываний вместо простых указателей, заключается в требовании того, что код пользовательского режима не может вызвать произвольный код ядра, в том числе и путем генерации программных прерываний. Проверяя уровень привилегий в дескрипторе шлюза прерывания процессор гарантирует, что вызывающему приложению разрешено вызывать код ядра только в специально отведенных для этого точках (в этом и заключается суть термина «шлюз прерывания», т.е. это четко определенный шлюз, который может передать управление от кода уровня пользователя к коду уровня ядра).

Дескриптор шлюза прерырвания содержит селектор сегмента, который однозначно определяет дексриптор сегмента кода, который описывает сегмент кода, в котором располагается процедура обслуживания прерывания. В случае системных вызовов Windows NT, селектор сегмента указывает на дескриптор в GDT. В GDT хранятся декскрипторы всех сегментов, которые являются общими для все системы, и не связаны с конкретным процессом (т.е.это сегменты кода и данных ядра самой операционной системы). На рисунке 3 изображены связи между записью в IDT, закрепленной за командой ‘int 2e’, записью в GDT и процедурой обслуживания прерывания в целевом сегменте кода.

Возвращаясь к системным вызовам Windows NT

Теперь, после рассмотрения базового материала, можно описать как же именно системный вызов Windows NT находит свой путь из режима пользователя в режим ядра. Системные вызовы в Windows NT инициируются исполнением инструкции «int 2e». Комаднда ‘int’ заставляет процессор сгенерировать программное прерывание, т.е. проследовать в IDT по индексу 2e и считать находящийся там дескриптор шлюза прерывания. Использую селектор из декскриптора шлюза, процессор загрузит дескриптор сегмента кода и установит значение EIP на смещение точки входа в процедуру обслуживания прерывания, взятое из дескриптора шлюза. На этом шаге процессор уже почти готов начать исполнение кода ISR в сегменте режима ядра.

Процессор автоматически переключается к стеку режима ядра

Прежде чем процессор начнет исполнять ISR в кодовом сегмента режима ядра, он должен переключиться на стек ядра. Причина этого заключается в том, что код режима ядра не может полагаться на то что в стеке пользовательского режима достаточно свободного места для работы в режиме ядра. К примеру, вредоносный код пользовательского режима может модифицировать собственный указатель стека и установить его на несуществующий адрес, выполнить инструкцию ‘int 2e’ и таким образом добиться краха системы когда код ядра обратится по недействительному указателю стека. Каждый уровень привилегий в защищенном режиме x86-процессора имеет свой собственный стрек. Когда происходит вызов функции более привелигированного уровня через шлюз прерывания, как описано выше, процессор автоматически сохраняет значения регистров SS, ESP, EFLAGS, CS и EIP из пользовательского режима в стеке режима ядра. В нашем случай системного вызова Windows NT функции диспетчера сервисов (KiSystemService) требуются доступ к параметрам системного вызова, которые код пользовательского режима поместил в стек перед выполнением ‘int 2e’. По соглашению, код пользовательского режима должен поместить в региср EBX указатель на блок параметров в стеке пользовательского режима. Тогда функция KiSystemService может просто скопировать требуемое число байтов из стека пользовательского режима в стек режима ядра перед вызовом системной функции. Рисунок 4 иллюстрирует это.

Как системный вызов вызывается?

Поскольку все системные вызовы Windows NT используют одно и тоже программное прерывание ‘int 2e’ для переключения в режим ядра, как код пользовательского режима информирует код ядра какую именно системную функцию нужно выполнить? Ответ заключается в том, что индекс помещается в регистр EAX перед вызовом инструкции int 2e. В режиме ядра ISR считывает значение регистра EAX и вызывает указанную системную функцию, если все переданные параметры проходят предварительную проверку. Параметры системного вызова (к примеру, те что были переданы в функцию OpenFile) передаются функции ядра в теле ISR.

Возврат из системного вызова

Когда работа системного вызова завершается, исполняется инструкция iret. Исполняя эту инструкцию, процессор восстанавливает из стека значения сохраненные значения регистров пользовательского режима и продолжает исполнение с инструкции следующей за инструкцией ‘int 2e’.

Эксперимент

Изучая дескриптор шлюза прерывания 2e, можно убедиться что процессор действительно находит процедуру диспетчера системных функций как описано в этой статье. Приложенный пример кода, содержит расширение для отладчика WinDbg, которое в режиме отладки ядра выводит содержимое дескрипторов в GDT, LDT или IDT.

Скачать пример кода: ProtMode.zip

Приведенное расширение отладчика является DLL с именем ‘protmode.dll’ (Protected Mode). Она загружается в WinDbg командной «.load protmode.dll», предварительно скопировав DLL в каталог, содержащий файл kdextx86.dll для целевой платформы. Остановиnt выполнение в отладчике WinDbg (CTRL-C) подключившись к целевой платформе. Синтаксис для вывода дескриптора в IDT для прерывания ‘int 2e’ следующий: «!descriptor IDT 2e». Это выводит следующую информацию:

Комманда ‘descriptor’ показывает следующее:

  • Дескриптор с индексом 2e в IDT находится по адресу 0x80036570.
  • Двоичный данные дескриптора C0 62 08 00 00 EE 46 80.
  • Что означает:
    • Сегмент, который содержит дескриптор кодового сегмента, загружен в память.
    • Для получения доступа к шлюзу нужен уровень привилегий не ниже 3 (т.е. любой).
    • Сегмент, содержащий обработчик прерывания для системного вызова (2e) описывается дексриптором с индексом 1 в GDT.
    • Точка входа в функцию KiSystemService находится по смещению 0x804552c0 внутри целевого сегмента.

Комманда «!descriptor IDT 2e» также выводит выводит дескриптор целевого сегмента кода по индексу 1 в GDT. Вот объяснение данным извлеченным из дескриптора в GDT:

  • Дескриптор сегмента кода с индексом 1 в GDT находится по адресу 0x80036008.
  • Двоичный данные дескриптора FF FF 00 00 00 9B CF 00.
  • Что означает:
    • Размер указан в 4KB страницах. Это значит, что поле размера (0x000fffff) следует умножить на размер страницы виртуальной памяти (4096 байтов) для получения действительного размера полного адресного пространства описанного дескриптором. Полученный размер доходит до 4GB, т.е. данный дескриптор описывает все доступное адресное пространство в 4GB. Благодаря этому код из режима ядра может получить доступ к любому адресу как в режиме ядра, так и в пользовательском режиме.
    • Сегмент является сегментом режима ядра (DPL=0).
    • Сегмент не является подчиненным. См. «Protected Mode Software Architecture» для детального объяснения этого поля.
    • Сегмент доступен для чтения. См. «Protected Mode Software Architecture» для детального объяснения этого поля.
    • К сегменту были обращениям. См. «Protected Mode Software Architecture» для детального объяснения этого поля.

Для сброки файла ProtMode.dll, откройте проект в Visual Studio 6.0 и нажмите кнопку «build». Для вводной информации по созданию расширений отладчика вроде ProtMode.dll, смотрите сопутствующее SDK для «Debugging Tools for Windows», которое можно бесплатно скачать с сайта Microsoft.

Дальнейшее чтение


Вот два отличных источника по защищенному режиму процессоров Intel x86:

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

Пока что все программы, которые мы сделали должны были использовать хорошо определенные механизмы ядра, чтобы регистрировать /proc файлы и драйверы устройства. Это прекрасно, если Вы хотите делать что-то уже предусмотренное программистами ядра, например писать драйвер устройства. Но что, если Вы хотите сделать что-то необычное, изменить поведение системы некоторым способом?

Это как раз то место, где программирование ядра становится опасным. При написании примера ниже, я уничтожил системный вызов open . Это подразумевало, что я не могу открывать любые файлы, я не могу выполнять любые программы, и я не могу закрыть систему командой shutdown . Я должен выключить питание, чтобы ее остановить. К счастью, никакие файлы не были уничтожены. Чтобы гарантировать, что Вы также не будете терять файлы, пожалуйста выполните sync прежде чем Вы отдадите команды insmod и rmmod .

Забудьте про /proc файлы и файлы устройств. Они только малые детали. Реальный процесс связи с ядром, используемый всеми процессами, это системные вызовы. Когда процесс запрашивает обслуживание из ядра (типа открытия файла, запуска нового процесса или запроса большего количества памяти), используется этот механизм. Если Вы хотите изменить поведение ядра интересными способами, это как раз подходящее место. Между прочим, если Вы хотите видеть какие системные вызовы использованы программой, выполните: strace .

Вообще, процесс не способен обратиться к ядру. Он не может обращаться к памяти ядра и не может вызывать функции ядра. Аппаратные средства CPU предписывают такое положение дел (недаром это называется `protected mode’ (защищенный режим)). Системные вызовы исключение из этого общего правила. Процесс заполняет регистры соответствующими значениями и затем вызывает специальную команду, которая переходит к предварительно определенному месту в ядре (конечно, оно читается процессами пользователя, но не перезаписывается ими). Под Intel CPUs, это выполнено посредством прерывания 0x80. Аппаратные средства знают, что, как только Вы переходите к этому месту, Вы больше не работаете в ограниченном режиме пользователя. Вместо этого Вы работаете как ядро операционной системы, и следовательно вам позволено делать все, что Вы хотите сделать.

Место в ядре, к которому процесс может переходить, названо system_call . Процедура, которая там находится, проверяет номер системного вызова, который сообщает ядру чего именно хочет процесс. Затем, она просматривает таблицу системных вызовов ( sys_call_table ), чтобы найти адрес функции ядра, которую надо вызвать. Затем вызывается нужная функция, и после того, как она возвращает значение, делается несколько проверок системы. Затем результат возвращается обратно процессу (или другому процессу, если процесс завершился). Если Вы хотите посмотреть код, который все это делает, он находится в исходном файле arch/ architecture > /kernel/entry.S , после строки ENTRY(system_call) .

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

Исходный текст, приводимый здесь, является примером такого модуля. Мы хотим «шпионить» за некоторым пользователем, и посылать через printk сообщение всякий раз, когда данный пользователь открывает файл. Мы заменяем системный вызов, открытия файла нашей собственной функцией, названной our_sys_open . Эта функция проверяет uid (user id) текущего процесса, и если он равен uid, за которым мы шпионим, вызывает printk , чтобы отобразить имя файла, который будет открыт. Затем вызывает оригинал функции open с теми же самыми параметрами, фактически открывает файл.

Функция init_module меняет соответствующее место в sys_call_table и сохраняет первоначальный указатель в переменной. Функция cleanup_module использует эту переменную, чтобы восстановить все назад к норме. Этот подход опасен, из-за возможности существования двух модулей, меняющих один и тот же системный вызов. Вообразите, что мы имеем два модуля, А и B. Системный вызов open модуля А назовем A_open и такой же вызов модуля B назовем B_open. Теперь, когда вставленный в ядро системный вызов заменен на A_open, который вызовет оригинал sys_open, когда сделает все, что ему нужно. Затем, B будет вставлен в ядро, и заменит системный вызов на B_open, который вызовет то, что как он думает, является первоначальным системным вызовом, а на самом деле является A_open.

Теперь, если B удален первым, все будет хорошо: это просто восстановит системный вызов на A_open, который вызывает оригинал. Однако, если удален А, и затем удален B, система разрушится. Удаление А восстановит системный вызов к оригиналу, sys_open, вырезая B из цикла. Затем, когда B удален, он восстановит системный вызов к тому, что он считает оригиналом, На самом деле вызов будет направлен на A_open, который больше не в памяти. На первый взгляд кажется, что мы могли бы решать эту специфическую проблему, проверяя, если системный вызов равен нашей функции open и если так, не менять значение этого вызова (так, чтобы B не изменил системный вызов, когда удаляется), но это вызовет еще худшую проблему. Когда А удаляется, он видит, что системный вызов был изменен на B_open так, чтобы он больше не указывал на A_open, так что он не будет восстанавливать указатель на sys_open прежде, чем будет удалено из памяти. К сожалению, B_open будет все еще пробовать вызывать A_open, который больше не в памяти, так что даже без удаления B система все равно рухнет.

Я вижу два способа предотвратить эту проблему. Первое: восстановить обращение к первоначальному значению sys_open. К сожалению, sys_open не является частью таблицы ядра системы в /proc/ksyms , , так что мы не можем обращаться к нему. Другое решение состоит в том, чтобы использовать счетчик ссылки, чтобы предотвратить выгрузку модуля. Это хорошо для обычных модулей, но плохо для «образовательных» модулей.

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

1.1. Ядро LINUX
1.1.1. Ядро является базой LINUX-а. Оно включает в себя драйвера устройств, механизм распределения памяти, управление процессами и связями. Разработчики ядра следуют рекомендациям POSIX.

1.2. Библиотека libc
1.2.1. Библиотека libc включает:
— YP функции;
— Функции кодирования;
— базовые теневые программы;
— старые программы для совместимости с libcompact;
— сообщения об ошибках;
— bsd4.4lite-совместимые программы работы с экраном в libcourses;
— bsd-совместимые программы в libbsd;
— программы работы с экраном в libtermcap;
— поддержку баз данных в libdbm;
— математику в libm,
— профилирование пространства пользователя в libgmon.
Большая часть библиотеки libc находится под лицензией GNU (Library GNU Public License)

1.3. Системные вызовы
1.3.1. Системный вызов — это требование к операционной системе (ОС) произвести аппаратно/системно специфическую или привилегированную операцию (см. приложение). Такие вызовы, как close() реализованы в Linux libc. Эта реализация часто включает в себя макрос, который вызывает syscall(). Параметры, передаваемые syscall-y — это номер системного вызова, перед которым ставятся требуемые аргументы. Если появились новые системные вызовы, но их нет в libc, можно использовать syscall(). Ниже приведен пример закрытия файла при помощи syscall-а:

extern int syscall(int. )

int my_close(int filedescriptor)
<
return syscall(SYS_close, filedescriptor);
>

На архитектуре i386 системные вызовы ограничены 5-ю аргументами кроме номера вызова из-за аппаратного числа регистров. На другой архитектуре можно найти макрос _syscall и посмотреть, сколько аргументов поддерживается. Макросами _syscall можно пользоваться вместо syscall(), но это не рекомендуется, поскольку макрос может развернуться в функцию, которая уже существует в библиотеке. Ниже приведен пример close(), использующий макрос _syscall.

_syscall1 (int, close, int, filedescriptor);
Макрос _syscall1 раскрывается в функцию close(), и в результате появились один close() в libc и один в программе. Возвращаемое syscall()-ом (или _syscall-ом) значение есть -1, если вызов неудачен, и 0 или больше — в случае успеха. В случае неудачи ошибку можно определить по глобальной переменной errno.
Ниже приведены системные вызовы, возможные в BSD и SYS V, но недопустимые в LINUX:
— audit(),
— audition(),
— fchroot(),
— getauid(),
— getdents(),
— getmsg(),
— mincore()
— poll(),
— putmsg(),
— setaudit(),
— setauid().

2. ВХОДНЫЕ И ВЫХОДНЫЕ ДАННЫЕ

2.1. Межпроцессовые коммуникации LINUX
2.1.1. Система Linux IPC (Inter-process communication) предоставляет средства для взаимодействия процессов между собой.

В распоряжении программиста есть несколько методов IPC:
— полудуплексные каналы UNIX;
— FIFO (именованные каналы);
— очереди сообщений в стиле SYSV;
— множества семафоров в стиле SYSV;
— разделяемые сегменты памяти в стиле SYSV;
— сетевые сокеты;
— полнодуплексные каналы (каналы потоков).
Если эти возможности эффективно используются, то они обеспечивают солидную базу для поддержания идеологии клиент/сервер в любой UNIX-системе, включая Linux.

2.1.2. Полудуплексные каналы UNIX
2.1.2.1. Канал — это средство связи стандартного вывода одного процесса со стандартным вводом другого. Каналы — это старейший из инструментов IPC. Они предоставляют метод односторонних коммуникаций между процессами.
Эта особенность широко используется даже в командной строке UNIX (в shell-е).

Приведенный выше канал принимает вывод ls как ввод sort, и вывод sort за ввод lp. Данные проходят через полудуплексный канал, перемещаясь (визуально) слева направо.
Когда процесс создает канал, ядро устанавливает два файловых дескриптора для пользования этим каналом. Один такой дескриптор используется, чтобы открыть путь ввода в канал (запись), в то время как другой применяется для получения данных из канала (чтение). В этом смысле канал мало применим практически, так как создающий его процесс может использовать канал только для взаимодействия с самим собой.

Из рисунка видно, как файловые дескрипторы связаны друг с другом. Если процесс посылает данные через канал (fd0), он имеет возможность получить эту информацию из fd1. Хотя канал первоначально связывает процесс с самим собой, данные, идущие через канал, проходят через ядро. В частности, в Linux-е каналы внутренне представлены корректным inode-ом. Этот inode существует в пределах самого ядра, а не в какой-либо физической файловой системе.
Процесс, создающий канал, обычно порождает дочерний процесс. Как только дочерний процесс унаследует какой-нибудь открытый файловый дескриптор от родителя, создается база для мультипроцессовой коммуникации (между родителем и потомком).

Оба процесса имеют доступ к файловым дескрипторам, которые основывают канал. Два процесса взаимно согласовываются и «закрывают» неиспользуемый конец канала.
Чтобы получить прямой доступ к каналу, можно применять системные вызовы, подобные тем, которые нужны для ввода/вывода в файл или из файла на низком уровне.
Чтобы послать данные в канал, используется системный вызов write(), а чтобы получить данные из канала — системный вызов read(). Системные вызовы ввода/вывода в файл или из файла работают с файловыми дескрипторами (но некоторые системные вызовы, как, например, lseek(), не работают с дескрипторами.)

2.1.2.2. Создание каналов на Си
Для создагния простого канала на Си, используется системный вызов pipe(). Для него требуется единственный аргумент, который является массивом из двух целых (integer), и, в случае успеха, массив будет содержать два новых файловых дескриптора, которые будут использованы для канала. После создания канала процесс обычно порождает новый процесс (процесс-потомок наследует открытые файловые дескрипторы).

SYSTEM CALL: pipe();

PROTOTYPE: int pipe( int fd[2] );
RETURNS: 0 в случае успеха
-1 в случае ошибки:
errno = EMFILE (нет свободных дескрипторов)
EMFILE (системная файловая таблица переполнена)
EFAULT (массив fd некорректен)

NOTES: fd[0] устанавливается для чтения, fd[1] — для записи
Первое целое в массиве (элемент 0) установлено и открыто для чтения, в то время как второе целое (элемент 1) установлено и открыто для записи. Вывод fd1 становится вводом для fd0. Все данные, проходящие через канал, перемещаются через ядро.

#include
#include
#include

После установления канала, можно ответвить нового потомка:

#include
#include
#include

main()
<
int fd[2];
pid_t childpid;

Если родитель хочет получить данные от потомка, то он должен закрыть fd1, а потомок должен закрыть fd0. Если родитель хочет послать данные потомку, то он должен закрыть fd0, а потомок — fd1. С тех пор как родитель и потомок делят между собой дескрипторы, необходимо быть уверенным, что не используемый в данный момент конец канала закрыт; EOF никогда не будет возвращен, если ненужные концы канала не закрыты.


#include
#include
#include
main()
<
int fd[2];
pid_t childpid;
if((childp > <
perror(«fork»);
exit(1);
>

if(childp > <
/* Потомок закрывает вход */
close(fd[0]);
>
else
<
/* Родитель закрывает выход */
close(fd[1]);
>
.
.
>

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

#include
#include
#include

int main(void)
<
int fd[2], nbytes;
pid_t childpid;
char string[] = «Hello, world!\n»;
char readbuffer[80];

if(childp > <
/* Потомок закрывает вход */
close(fd[0]);

/* Посылаем «string» через выход канала */
write(fd[1], string, strlen(string));
exit(0);
>
else
<
/* Родитель закрывает выход */
close(fd[1]);

/* Чтение строки из канала */
nbytes = read(fd[0], readbuffer, sizeof(readbuffer));
printf(«Received string: %s», readbuffer);
>

Часто дескрипторы потомка раздваиваются на стандартный ввод или вывод. Ниже приведен системный вызов dup():

SYSTEM CALL: dup();

PROTOTYPE: int dup( int oldfd );
RETURNS: new descriptor on success
-1 on error: errno = EBADF (oldfd некорректен)
EBADF ($newfd is out of range$)
EMFILE (слишком много дескрипторов для процесса)

Несмотря на то, что старый и новосозданный дескрипторы взаимозаменяемы, сначала закрывается один из стандартных потоков.

.
.
childp >
if(childp > <
/* Закрываем стандартный ввод потомка */
close(0);

/* Дублируем вход канала на stdin */
dup(fd[0]);
execlp(«sort», «sort», NULL);
.
>

Поскольку файловый дескриптор 0 (stdin) был закрыт, вызов dup() дублировал дескриптор ввода канала (fd0) на его стандартный ввод. Затем мы сделали вызов execlp(), чтобы покрыть код потомка кодом программы sort. Поскольку стандартные потоки exec()-нутой программы наследуются от родителей, это означает, что вход канала становится для потомка стандартным вводом. Теперь все, что первоначальный процесс-родитель посылает в канал, идет в sort. C
Cуществует другой системный вызов dup2(), который также может использоваться.

SYSTEM CALL: dup2();

PROTOTYPE: int dup2( int oldfd, int newfd );
RETURNS: новый дескриптор в случае успеха
-1 в случае ошибки: errno = EBADF (oldfd некорректен)
EBADF ($newfd is out of range$)
EMFILE (слишком много дескрипторов для процесса)

NOTES: старый дескриптор закрыл dup2()!

Благодаря этому особенному вызову имеется закрытая операция и действующая копия за один системный вызов. Он гарантированно неделим, что означает, что он никогда не будет прерван поступающим сигналом. С первым системным вызовом dup() программисты были вынуждены предварительно выполнять операцию close(). Это приводило к наличию двух системных вызовов с малой степенью защищенности в краткий промежуток времени между ними. Если бы сигнал поступил в течение этого интервала времени, копия дескриптора не состоялась бы. Вызов dup2() разрешает эту проблему.

.
.
childp >
if(childp > <
/* Закрываем стандартный ввод, дублируем вход канала на
стандартный ввод */
dup2(0, fd[0]);

2.1.2.3. Функция popen()
Cтандартная библиотечная функция popen() создает полудуплексный канал посредством вызывания pipe() внутренне. Затем она порождает дочерний процесс, запускает Bourne shell и исполняет аргумент command внутри shell-а. Управление потоком данных определяется вторым аргументом «type». Он может быть «r» или «w» — для чтения или записи, но не может быть и то, и другое. Под Linux-ом канал будет открыт в виде, определенном первой литерой аргумента «type». Поэтому, если ввести «rw», канал будет открыт только в виде «read».

LIBRARY FUNCTION: popen();

PROTOTYPE: FILE *popen ( char *command, char *type );
RETURNS: новый файловый поток в случае успеха
NULL при неудачном fork() или pipe()

Каналы, созданные popen(), должны быть закрыты pclose().

PROTOTYPE: int pclose( FILE *stream )
RETURNS: выход из статуса системного вызова wait4()
-1, если «stream» некорректен или облом с wait4()

NOTES: ожидает окончания связанного каналом процесса, затем закрывает поток.

Функция pclose() выполняет wait4() над процессом, порожденным popen()-ом. Когда она возвращается, то уничтожает канал и файловый поток. Этот эффект аналогичен эффекту, вызываемому функцией fclose() для нормального, основанного на потоке файлового ввода/вывода.
Ниже приведен пример, который открывает канал для команды сортировки
и начинает сортировать массив строк:

#define MAXSTRS 5

/* Создаем односторонний канал вызовом popen() */
if (( pipe_fp = popen(«sort», «w»)) == NULL)
<
perror(«popen»);
exit(1);
>

/* Цикл */
for(cntr=0; cntr /tmp/foo», «w»)
popen(«sort | uniq | more», «w»);

В качестве другого примера popen()-а, приведена небольшая программа, открывающая два канала (один — для команды ls, другой — для сортировки):

int main(void)
<
FILE *pipein_fp, *pipeout_fp;
char readbuf[80];

/* Создаем односторонний канал вызовом popen() */
if (( pipein_fp = popen(«ls», «r»)) == NULL)
<
perror(«popen»);
exit(1);
>

/* Создаем односторонний канал вызовом popen() */
if (( pipeout_fp = popen(«sort», «w»)) == NULL)
<
perror(«popen»);
exit(1);
>

/* Цикл */
while(fgets(readbuf, 80, pipein_fp))
fputs(readbuf, pipeout_fp);

/* Закрываем каналы */
pclose(pipein_fp);
pclose(pipeout_fp);

2.1.2.4. Атомарные (неделимые) операции с каналами
Для того чтобы операция рассматривалась как «атомарная», она не должна прерываться ни по какой причине. Неделимая операция выполняется сразу. Согласно POSIX стандарту в /usr/include/posix_lim.h максимальные размеры буфера для атомарной операции в канале таковы:


#define _POSIX_PIPE_BUF 512

Атомарно по каналу может быть п олучено или записано до 512 байт. Все, что выходит за эти пределы, будет разбито и не будет выполняться атомарно. Под Linux-ом этот атомарный операционный лимит определен в «linux/limits.h» следующим образом:

#define PIPE_BUF 4096

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

2.1.2.5. Примечания к полудуплексным каналам
Двусторонние каналы могут быть созданы посредством открывания двух каналов и правильным переопределением файловых дескрипторов в процессе-потомке.
Вызов pipe() должен быть произведен перед вызовом fork(), иначе дескрипторы не будут унаследованы процессом-потомком (то же и для popen()).
С полудуплексными каналами любые связанные процессы должны разделять происхождение. Поскольку канал находится в пределах ядра, любой процесс, не состоящий в родстве с создателем канала, не имеет способа адресовать его. Это не относится к случаю с именованными каналами (FIFOS).

2.1.3. Именованные каналы (FIFOs — First In First Out)
Именованные каналы во многом работают так же, как и обычные каналы, но все же имеют несколько заметных отличий.
Именованные каналы существуют в виде специального файла устройства в файловой системе.
Процессы различного происхождения могут разделять данные через такой канал.
Именованный канал остается в файловой системе для дальнейшего использования и после того, как весь ввод/вывод сделан.

2.1.3.1. Создание FIFO
Есть несколько способов создания именованного канала. Первые два могут быть осуществлены непосредственно из shell-а.

mknod MYFIFO p
mkfifo a=rw MYFIFO

Эти две команды выполняют идентичные операции, за одним исключением. Команда mkfifo предоставляет возможность для изменения прав доступа к файлу FIFO непосредственно после создания. При использовании mknod будет необходим вызов команды chmod.
Файлы FIFO могут быть быстро идентифицированы в физической файловой системе посредством индикатора «p», представленного здесь в длинном листинге директории.

$ ls -1 MYFIFO
^prw-r—r— 1 root root 0 Dec 14 22:15 MYFIFO| .

Чтобы создать FIFO на Си, можно прибегнуть к использованию системного вызова mknod():

LIBRARY FUNCTION: mknod();

PROTOTYPE: int mknod( char *pathname, mode_t mode, dev_t dev );
RETURNS: 0 в случае успеха,
-1 в случае ошибки:
errno = EFAULT (ошибочно указан путь)
EACCESS (нет прав)
ENAMETOOLONG (слишком длинный путь)
ENOENT (ошибочно указан путь)
ENOTDIR (ошибочно указан путь)
(остальные смотрите в man page для mknod)

NOTES: Создает узел файловой системы (файл, файл устройства или FIFO)

Ниже приведен простой пример создания FIFO на Си:

mknod(«/tmp/MYFIFO», S_IFIFO|0666, 0);

В данном случае файл «/tmp/MYFIFO» создан как FIFO-файл. Требуемые права — это «0666», хотя они находятся под влиянием установки umask, как например:

Общая хитрость — использовать системный вызов umask() для того, чтобы временно устранить значение umask-а:

umask(0);
mknod(«/tmp/MYFIFO», S_IFIFO|0666, 0);

Кроме того, третий аргумент mknod()-а игнорируется, в противном случае создается файл устройства. В этом случае он должен отметить верхнее и нижнее числа файла устройства.

2.1.3.2. Операции FIFO
Операции ввода/вывода FIFO, по существу, такие же, как для обычных каналов, за одним исключением. Чтобы физически открыть проход к каналу, должен быть использован системный вызов «open» или библиотечная функция. С полудуплексными каналами это невозможно, поскольку канал находится в ядре, а не в физической файловой системе. В приведенном ниже примере сервер-процесса канал трактуется как поток, открывается он с помощью fopen()-а и закрывается — fclose()-ом.

#include
#include
#include
#include

#define FIFO_FILE «MYFIFO»

int main(void)
<
FILE *fp;
char readbuf[80];

/* Создаем FIFO, если он еще не существует */
umask(0);
mknod(FIFO_FILE, S_IFIFO|0666, 0);

while(1)
<
fp = fopen(FIFO_FILE, «r»);
fgets(readbuf, 80, fp);
printf(«Received string: %s\n», readbuf);
fclose(fp);
>

Поскольку FIFO блокирует по умолчанию, сервер запускается в фоновом режиме после того, как его откомпилировали:

Ниже приведен текст простого клиента для сервера:

#define FIFO_FILE «MYFIFO»

int main(int argc, char *argv[])
<
FILE *fp;

if ( argc != 2 ) <
printf(«USAGE: fifoclient [string]\n»);
exit(1);
>

2.1.3.3. Действие блокирования над FIFO
Если FIFO открыт для чтения, процесс его блокирует до тех пор, пока какой-нибудь другой процесс не откроет FIFO для записи. Аналогично для обратной ситуации. Если такое поведение нежелательно, то может быть использован флаг O_NONBLOCK в системном вызове open(), чтобы отменить действие блокирования.

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

2.1.4. System V IPC
2.1.4.1. Идентификаторы IPC
Каждый объект IPC имеет уникальный IPC идентификатор. (под «объектом IPC», подразумевается очередь единичных сообщений, множество семафоров или разделяемый сегмент памяти.) Этот идентификатор требуется ядру для однозначного определения объекта IPC. Например, чтобы сослаться на определенный разделяемый сегмент, единственное, что потребуется,
это уникальное значение ID, которое привязано к этому сегменту.
Идентификатор IPC уникален только для своего типа объектов. Например, возможна только одна очередь сообщений с идентификатором «12345», так же как номер «12345» может иметь какое-нибудь одно множество семафоров или (и) какой-то разделяемый сегмент.

2.1.4.2. Ключи IPC
Чтобы получить уникальный ID, нужен ключ. Ключ должен быть взаимно согласован процессом-клиентом и процессом-сервером. Для приложения это согласование должно быть первым шагом в построении среды. Например, чтобы позвонить кому-либо по телефону, необходимо знать номер. Кроме того, телефонная компания должна знать, как провести вызов к адресату. И только, когда этот адресат ответит, связь состоится.)
В случае System V IPC «телефон» соединяет объекты IPC одного типа. Под «телефонной компанией», или методом маршрутизации, следует понимать ключ IPC.
Ключ, генерируемый приложением самостоятельно, может быть каждый раз один и тот же. Это неудобно, полученный ключ может уже использоваться в настоящий момент. Функцию ftok() используют для генерации ключа и для клиента, и для сервера:

LIBRARY FUNCTION: ftok();

PROTOTYPE: key_t ftok( char *pathname, char proj );
RETURNS: новый IPC ключ в случае успеха
-1 в случае неудачи,
errno устанавливается как значение вызова stat()


Возвращаемый ftok()-ом ключ инициируется от значения inode и нижним числом устройства файла — первого аргумента, и от литеры — второго аргумента. Это не гарантирует уникальности, но приложение может проверить наличие коллизий и, если понадобится, сгенерировать новый ключ.

key_t mykey;
mykey = ftok («/tmp/myapp», ‘a’);

В приведенном выше тексте директория /tmp/myapp смешивается с однолитерным идентификатором ‘a’. Другой распространенный пример — использовать текущую директорию.

key_t mykey;
mykey = ftok(«.», ‘a’);

Выбор алгоритма генерации ключа полностью отдается на усмотрение прикладного программиста. Так же как и меры по предотвращению ситуации гонок, дедлоков и т.п., любой метод имеет право на жизнь. Если условиться, что каждый процесс-клиент запускается со своей уникальной «домашней» директории, то генерируемые ключи будут всегда удовлетворительны.
Значение ключа, когда оно получено, используется в последующих системных вызовах IPC для создания или улучшения доступа к объектам IPC.
Команда ipcs выдает статус всех объектов System V IPC:

ipcs -q: показать только очереди сообщений
ipcs -s: показать только семафоры
ipcs -m: показать только разделяемую память
ipcs —help: помощь

Команда ipcrm удаляет объект IPC из ядра. Однако, поскольку объекты IPC можно удалить через системные вызовы в программе пользователя, часто нужды удалять их «вручную» нет. Особенно это касается всяких программных оболочек.
Требуется сказать, является ли удаляемый объект очередью сообщений (msg), набором семафоров (sem), или сегментом разделяемой памяти (shm). IPC ID может быть получен через команду ipcs. ID уникален в пределах одного из трех типов объектов IPC, поэтому необходимо назвать этот тип.

2.1.4.3. Очереди сообщений
Очереди сообщений представляют собой связный список в адресном пространстве ядра. Сообщения могут посылаться в очередь по порядку и выбираться из очереди несколькими разными путями. Каждая очередь сообщений однозначно определена идентификатором IPC.
1) Внутренние и пользовательские структуры данных
Ключом к полному осознанию такой сложной системы, как Sy stem V IPC, является более тесное знакомство с различными структурами данных, которые лежат внутри самого ядра. Даже для большинства примитивных операций необходим прямой доступ к некоторым из этих структур, хотя другие используются только на гораздо более низком уровне.
2) Буфер сообщения
Структуру msgbuf. можно понимать как шаблон для данных сообщения. Поскольку данные в сообщении программист определяет сам, он обязан понимать, что на самом деле они являются структурой msgbuf. Его описание находится в linux/msg.h:

/* буфер сообщения для вызовов msgsnd и msgrcv*/
struct msgbuf <
long mtype; /* тип сообщения */
char mtext[1]; /* текст сообщения */
>;

Возможность приписывать тип конкретному сообщению позволяет держать в одной очереди разнородные сообщения. Это может понадобиться, например, когда сообщения процесса-клиента помечаются одним магическим числом, а сообщения процесса-сервера — другим; или приложение ставит в очередь сообщения об ошибках с типом 1, сообщения-запросы — с типом 2 и т.д.

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

2.1.4.5. Разделяемая память
Разделяемая память — это отображение участка (сегмента) памяти, которая будет разделена между более чем одним процессом. Это гораздо более быстрая форма IPC, потому что здесь нет никакого посредничества (т.е. каналов, очередей сообщений и т.п.). Вместо этого, информация отображается непосредственно из сегмента памяти в адресное пространство вызывающего процесса. Сегмент может быть создан одним процессом и впоследствии использован для чтения/записи любым количеством процессов.

2.2. Программирование звука
2.2.1. Программирование встроенного динамика
Встроенный динамик — это часть консоли Linux и, поэтому является символьным устройством. Как следствие, существуют запросы ioctl для манипуляций с ним. Для встроенного микрофона это такие два запроса:
1) KDMKTONE
Генерирует сигнал beep заданной длительности, используя таймер ядра. Например, ioctl(fd, KDMKTONE, (long) argument).
2) KIOCSOUND
Генерирует бесконечный beep или прерывает звучащий в настоящий момент. Например, ioctl(fd, KIOCSOUND, (int) tone).
Третий аргумент первого примера содержит значение тона в нижнем слове и сдвиг в верхнем. Тон — это не частота. Таймер 8254 материнской платы ПК заведен на 1.19 МГц и поэтому тон — это 1190000/частота. Сдвиг измеряется в шагах таймера. Оба вызова срабатывают немедленно, поэтому можно порождать звуковые сигналы, не блокируя программу.
KDMKTONE можно использовать для предупреждающих сигналов, поскольку не надо заботиться о его прекращении. При помощи KIOCSOUND можно проигрывать мелодии, как это демонстрируется в примере программы splay. Для остановки сигнала значение тона устанавливается в 0.

2.2.2. Программирование звуковой карты
Современные Linux-системы имеет встроенную звуковую карту. Один путь проверки — испытать /dev/sndstat. Если открытие /dev/sndstat закончилось неудачно и errno = ENODEV, то это означает, что ни один звуковой драйвер не активирован. Тот же результат может иметь попытка открыть /dev/dsp, если он не связан с драйвером pcsnd.
Комбинация вызовов outb() и inb() определит искомую звуковую карту.
Программы, использующие звуковой драйвер, будут хорошо переноситься на другие i386 системы. Звуковая карта — это не часть консоли Linux, но специальное устройство. Звуковая карта предлагает три основных возможности:
— цифровой образец ввода/вывода;
— вывод модуляции частоты;
— midi интерфейс.
Каждая из возможностей имеет свой драйвер устройства. Для цифровых образцов — это /dev/dsp, для модуляции частоты — /dev/sequencer, для midi интерфейса — /dev/midi. Звуковые установки (volume, balance, bass) контролируются через /dev/mixer интерфейс.

2.3. Символьная графика
2.3.1. Функции ввода/вывода в libc
2.3.1.1. Форматированный вывод
Функции printf(. ) в libc обеспечивают форматированный вывод и позволяют трансформировать аргументы.
Функция int fprintf(FILE *stream, const char *format, . ) преобразует выводимые аргументы в соответствии с шаблоном и записывает его в stream. Формат определяется аргументом format. Функция возвращает число записанных символов или отрицательное число в случае ошибки.
Аргумент format содержит два типа объектов: обычные символы и информацию, как трансформировать или форматировать аргументы.
Форматная информация должна начинаться с %, за которым следуют значения для формата (см. таблицу 1), дальше идет символ для трансляции (чтобы напечатать знак %, используется %%).

2.3.1.2.Форматированный ввод
Точно так же, как printf(. ) для форматированного вывода, можно использовать функцию scanf(. ) для форматированного ввода.
int fscanf(FILE *stream, const char *format, . )
Функция fscanf(. ) читает из stream и преобразует ввод по правилам, определяемым в format. Результаты помещаются в аргументы, заданные в «. » (эти аргументы должны быть указателями). Чтение заканчивается, когда в format исчерпаны правила форматирования.
Таблица 1 libc — трансформации printf

Форматируется в

Родительский процесс Ядро Дочерний процесс
d,i int signed, десятиричный
o int unsigned, восьмеричный, без предваряющего 0
x,X int unsigned, шестнадцатиричный, без предваряющего 0x
u int unsigned, десятиричный
c int (unsigned) одиночный символ
s char * до \0
f double как [-]mmm.ddd
e, E double как [-]m.dddddde+xx, [-]m.dddddde-xx
g, G double использует %e или %f когда нужно
p void *
n int *
% %

Функция fscanf вернет EOF, при первом достижении конца файла или при возникшей ошибке. Если этого не случится, будет возвращено количество трансформированных аргументов.
Аргумент format может содержать правила форматирования аргументов (см. таблицу 2). Он может также включать:
— пропуски или табуляции, которые игнорируются;
— любой нормальный символ, кроме %;
— правила преобразования, заданные с %.
Таблица 2: libc — трансформации sсanf

Примечание: перед d,i,n,o,u,x может стоять h, если указатель — short
то же для l, если указатель — long
l также может быть перед e,f,g, если указатель — double
L может стоять перед e,f,g, если указатель — long double

Символ Вход — тип аргумента
d десятичный integer — int*
i integer — int* (вход может быть восьми- или шестнадцатиричным)
o восьмеричный integer — int* (с или без предваряющего 0)
u десятичный unsigned — unsigned int*
x шестнадцатиричный integer — int* (с или без предваряющего 0x)
c одна или более литер — char* (без завершающего /0)
e,f,gf float — float* (такой как [-]m.dddddde+xx, [-]m.dddddde-xx)
p указатель — void*
n число трансформированных аргументов — int*
[. ] непустое множество литер на входе — char*
[^. ] исключая такие литеры — c`har*
% %

2.4. Программирование портов ввода/вывода
Обычно персональный компьютер (ПК) имеет как минимум 2 последовательных и 1 параллельный интерфейс. Они являются специальными устройствами и отображаются следующим образом:

— /dev/ttyS0 — /dev/ttySn
RS232 последовательные устройства 0 — n, где n зависит от аппаратного обеспечения.

— /dev/cua0 — /dev/cuan
RS232 последовательные устройства 0 — n, где n зависит от аппаратного обеспечения.

— /dev/lp0 — /dev/lpn
параллельные устройства 0 — n, где n зависит от аппаратного обеспечения.
— /dev/js0 — /dev/jsn
джойстики 0 — n, где 0 =0 курсор выезжает с противоположной стороны, если он зашел за пределы на slack пикселов.
9) int maxx
Разрешение текущего терминала по x. Символы шрифта по умолчанию имеют ширину 10 пикселов, поэтому полный режим по x равен 10*80-1.
10) int maxy
Шрифт по умолчанию имеет символы высотой 12 пикселов, поэтому полное разрешение экрана по y 12*25-1 пиксел.
Функция get_ms_event() нуждается только в указателе на структуру ms_event. Если get_ms_event() возвращает -1, то произошла ошибка. В случае успеха возвращается 0, а ms_event содержит текущее состояние мыши.

3. ПЕРЕНОС ПРИКЛАДНЫХ ПРОГРАММ В LINUX

Перенос UNIX-приложений под Linux несложен. Linux и его GNU Си библиотека разработана для приложений, переносимых по замыслу; это означает, что многие программы компилируются просто через make. Речь идет обо всех программах, не обращающихся к каким-то возможностям частной реализации, или сильно завязанных на недокументированном или неопределенном поведении, или особенном системном вызове.
Linux часто не согласуется со стандартом IEEE Std 1003.1-1988 (POSIX.1), но это никак не сертифицировано. Linux позаимствовал много хорошего от SVID и BSD ветвей UNIX, но опять же не подражал им во всех возможных случаях. Проще говоря, Linux разработан, чтобы быть совместимым с другими реализациями UNIX, чтобы сделать прикладные программы легко переносимыми. Например, аргумент timeout, посылаемый системному вызову select, на самом деле уменьшается Linux-ом во время опроса. Другие реализации не изменяют это значение вовсе, и программа, скомпилированная под Linux-ом, может «сломаться».
Цель этого раздела — сделать обзор основных вещей, связанных с переносом приложений в Linux, освещая различия между Linux, POSIX.1, SVID и BSD в следующих областях: обработка сигналов, ввод/вывод с терминала, управление процессами и сбор информации и переносимая условная компиляция.

3.1. Обработка сигналов
С годами определение и семантика сигналов изменялись и совершенствовались различными реализациями UNIX. В настоящее время существует 2 класса сигналов: ненадежные (unreliable) и надежные (reliable). Ненадежные сигналы — это те, для которых вызванный однажды обработчик сигнала не остается. Такие «сигналы-выстрелы» должны перезапускать обработчик внутри самого обработчика, если есть желание сохранить сигнал действующим. Из-за этого возможна ситуация гонок, в которой сигнал может прийти до перезапуска обработчика — и тогда он будет потерян, или придет вовремя — и тогда сработает в соответствии с заданным поведением (например, убьет процесс). Такие сигналы ненадежны, поскольку отлов сигнала и переинсталяция обработчика не являются атомарными операциями.
В семантике ненадежных процессов системные вызовы не повторяются, автоматически будучи прерванными поступившим сигналом. Поэтому для обеспечения отработки всех системных вызовов программа должна проверять значение errno после каждого из них и повторять вызовы, если это значение равно EINTR.
По тем же причинам семантика ненадежных сигналов не предоставляет легкого пути реализации атомарных пауз для усыпления процесса до получения сигнала. Ненадежное поведение постоянно перезапускающегося обработчика может привести к неготовности спящего в нужный момент принять сигнал. Напротив, семантика надежных сигналов оставляет обработчик проинсталированным и ситуация гонок при перезапуске избегается. В то же время определенные сигналы могут быть запущены заново, а атомарная операция паузы доступна через функцию POSIX sigsuspend.

3.1.1. Сигналы в SVR4, BSD и POSIX.1
SVR4-реализация сигналов заключается в функциях signal, sigset, sighold, sigrelse, sigignore и sigpause. Функция signal эквивалентна классическим сигналам UNIX V7, она предоставляет только ненадежные сигналы. Остальные функции автоматически перезапускают обработчик. Перезапуск системных вызовов не предусмотрен.
BSD предлагает функции signal, sigvec, sigblock, sigsetmask и sigpause. Все сигналы надежны, а все системные вызовы перезапускаемы. Программист имеет возможность это отключить.
В POSIX.1 предоставляются функции sigaction, sigprocmask, sigpending и sigsuspend. Указанные функции работают с надежными сигналами, но перезапуск системных вызовов не определен совсем. Если sigaction используется в BSD или SVR4, то перезапуск системных вызовов по умолчанию отключен. Но он может включаться поднятием флага SA_RESTART.
В итоге, лучший путь работы с сигналами — это функция sigaction, которая позволит точно определить поведение обработчиков сигналов. Однако signal до сих пор используется во многих приложениях и имеет различную семантику в BSD и SVR4.

3.1.2. Опции сигналов Linux
В Linux определены следующие значения члена sa_flags структуры sigaction:
— SA_NOCLDSTOP: не рекомендуется посылать SIGCHLD во время остановки процесса-потомка;
— SA_RESTART: осуществляет перезапуск определенных системных вызовов во время прерывания обработчиком сигналов;
— SA_NOMASK: обнуление маски сигнала (которое блокирует сигналы во время работы обработчика сигналов);
— SA_ONESHOT: очищает обработчик сигналов после исполнения (SVR4 использует SA_RESETHAND для тех же целей);
— SA_INTERRUPT: определен под Linux-ом, но не используется. Под SunOS системные вызовы автоматически перезапускались, а этот флаг отменял такое поведение;
— SA_STACK: предназначен для стеков сигналов.
3.1.3. Функция signal под Linux-ом
Функция signal в Linux-е эквивалентна применению sigaction с опциями SA_ONESHOT и SA_NOMASK, что соответствует классической ненадежной семантике сигналов подобно SVR4.
Если использовать signal с семантикой BSD, то большинство Linux-систем предоставляет совместимую с BSD библиотеку, которую можно прилинковать. Для подключения этой библиотеки можно добавить опции:

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

3.1.4. Сигналы, поддерживаемые Linux-ом
Linux поддерживает практически все сигналы, предоставляемые SVR4, BSD и POSIX, за несколькими исключениями:
— SIGEMT не поддерживается; он соответствует аппаратному сбою под SVR4 и BSD;
— SIGINFO не поддерживается; он используется для информационных запросов к клавиатуре под SVR4;
— SIGSYS не поддерживается; он относится к некорректному системному вызову в SVR4 и BSD. В сочетании с libbsd этот сигнал переопределяется в SIGUNUSED;
— SIGABRT и SIGIOT идентичны;
— SIGIO, SIGPOLL и SIGURG идентичны;
— SIGBUS определен как SIGUNUSED. Технически в среде Linux нет никаких «ошибок шины».

3.2. Ввод/вывод с терминала
Так же, как для сигналов, управление вводом/выводом имеет 3 различных реализации: под SVR4, BSD и POSIX.1. SVR4 работает со структурой termio и различными вызовами ioctl (такими, как TCSETA, TCGETA и т.д.) для получения и установки параметров терминала. Эта структура выглядит так:

struct termio <
unsigned short c_iflag; /* режимы ввода */
unsigned short c_oflag; /* режимы вывода */
unsigned short c_cflag; /* режимы управления */
unsigned short c_lflag; /* режимы упорядочения линий */
char c_line /* упорядочение линий */
unsigned char c_cc[NCC]; /* символы управления */

В BSD вызовы ioctl типа TIOCGETP, TIOCSETP и т.д. работают со структурой sgtty.
В POSIX-е используется структура termios вместе с различными функциями POSIX.1, такими как tcsetattr и tcgetattr. Структура termios соответствует структуре termio в SVR4, но типы переименованы (например, tcflag_t вместо unsigned short), и для размера массива c_cc употребляется NCCS.
Под Linux-ом ядром поддерживается и termios POSIX.1, и termio SVR4. Это означает, что, если программа использует оба метода доступа к вводу/выводу на терминал, то ее следует компилировать прямо под Linux-ом.
Следует обратить внимание на то, пытается ли программа использовать поле c_line структуры termio. Практически для всех приложений оно должно быть равно N_TTY, и если программа предполагает возможность другого упорядочения линий, то может возникнуть ошибка.
Если программа использует реализацию BSD sgtty, можно прилинковать libbsd, как описывалось выше. Это обеспечит перекройку ioctl, означающую пересмотр запросов ввода/вывода на терминал в термины структуры termios POSIX-а, поддерживаемые ядром. При компиляции такой программы, если символы вроде TIOCGETP не определены, вам придется прилинковать libbsd.

3.3. Управление процессами
Такие программы, как ps, top и free, должны иметь способ получения информации от ядра о процессах и ресурсах системы. Аналогично, отладчикам и другим подобным средствам требуется управлять и инспектировать работающий процесс. Такие возможности предоставляет ряд интерфейсов различных версий UNIX, и практически все они либо машинно-зависимы, либо привязаны к конкретной реализации ядра, поэтому не существует универсально доступного интерфейса для такого взаимодействия ядра и процесса.

3.3.1. Подпрограммы kvm
Для прямого доступа к структурам ядра многие системы используют устройство /dev/kmem и подпрограммы kvm_open, kvm_nlist и kvm_read. Программа открывает /dev/kmem, читает символьную таблицу ядра, определяет при помощи этой таблицы расположение данных в работающем ядре и читает соответствующие адреса адресного пространства ядра, используя названные подпрограммы. Поскольку это требует согласования между программой пользователя и ядром, размера и формата структур данных, подобные программы перестраиваются для каждой новой версии ядра, типа процессора и т.д.

3.3.2. ptrace и файловая система /proc
Системный вызов ptrace используется в 4.3BSD и SVID для управления процессом и считывания из него информации. Классически он используется отладчиками для, например, trap-исполнения процесса (с условными точками останова) или исследования его состояния. Под SVR4 ptrace заменен файловой системой /proc, которая появляется как директория, содержащая единственную точку входа в файл для каждого работающего процесса, называемую ID процесса. Пользовательская программа может открыть файл интересующего ее процесса и совершить над ним различные вызовы ioctl для управления выполнением процесса или получения информации о процессе от ядра. Аналогично, программа может читать или записывать данные напрямую в адресное пространство процесса через файловый дескриптор в файловую систему /proc.


3.3.3. Управление процессами под Linux
Под Linux-ом для управления процессом поддерживается системный вызов ptrace, работающий так же, как 4.3BSD. Для получения информации о процессе или системе Linux также предоставляет файловую систему /proc, но с совершенно другой семантикой. Под Linux-ом /proc состоит из ряда файлов с общесистемной информацией, такой как использование памяти, средняя загруженность, статистика загружаемых модулей и сетевая статистика. Эти файлы общедоступны для read и write; их содержимое можно разбирать, используя scanf. Файловая система /proc под Linux-ом также предоставляет точку входа в директорию для каждого работающего процесса, называемую ID процесса. Она содержит файловые точки входа для информации типа командной линии, связей с текущей директорией и исполняемым файлом, открытых файловых дескрипторов и т.д. Ядро предоставляет всю эту информацию в ответ на запрос read. Такая реализация имеет некоторые недостатки. Например, для ps, чтобы просмотреть таблицу с информацией о всех работающих процессах, нужно пересечь многие директории, открыть и прочитать многие файлы. Для сравнения: подпрограммы kvm в других UNIX-системах считывают структуры ядра напрямую, потратив лишь несколько системных вызовов.
Все реализации настолько различны, что перенос приложений, их использующих, может стать серьезной задачей. Следует особо отметить, что файловая система /proc в SVR4 намного грубее, чем в Linux-е, и их нельзя использовать в одно и том же контексте. На самом деле каждая программа, которая использует kvm или файловую систему /proc SVR4, просто непереносима, и такие фрагменты кода должны быть переписаны.
Вызовы ptrace Linux-а и BSD похожи, но все же имеют несколько отличий:
— Запросы PTRACE_PEEKUSER и PTRACE_POKEUSER под BSD названы соответственно PTRACE_PEEKUSR и PTRACE_POKEUSR в Linux-е;
— Регистры процесса могут быть установлены с использованием запроса PTRACE_POKEUSR со смещениями, находящимися в /usr/include/linux/ptrace.h;
— Запросы SunOS-а PTRACE_ не поддерживаются, как не поддерживаются ни PTRACE_SETACBKPT, ни PTRACE_SETWRBKPT, ни PTRACE_CLRBKPT, ни PTRACE_DUMPCORE. Отсутствие этих запросов влияет лишь на малое количество существующих программ.
Linux не имеет подпрограмм kvm для чтения адресного пространства ядра из пользовательской программы, но в нем есть средства, такие как kmem_ps, в действительности являющиеся версией подобных подпрограмм. Вообще говоря, они непереносимы, и каждый код, использующий kvm, вероятнее всего, зависит от определенных обозначений или от типов данных ядра, поэтому такой код следует признать машинно-зависимым.

3.4. Переносимая условная компиляция
Для исправления существующего кода для достижения совместимости с Linux-ом потребуется использовать ifdef. endif для того, чтобы окружить необходимые для этого участки. Не существует стандарта выделения кода, зависящего от операционной системы, но многие программы используют соглашение, принятое в SVR4 для кода System V, в BSD для BSD-кода и для linux — в Linux-зависимом коде:
— __STRICT_ANSI__: только для ANSI C;
— _POSIX_SOURCE: для POSIX.1;
— _POSIX_C_SOURCE: если определено как 1, то используется POSIX.1, если 2 — то POSIX.2;
— _BSD_SOURCE: ANSI, POSIX и BSD;
— _SVID_SOURCE: ANSI, POSIX и System V;
— _GNU_SOURCE: ANSI, POSIX, BSD, SVID и GNU расширения. Это значение по умолчанию, если ничто из вышеперечисленного не определено.
Если определено _BSD_SOURSE, то для библиотеки определится _FAVOR_BSD. Тогда некоторые вещи POSIX-а и SVR4 будут вести себя, как в BSD. Например, если определено _FAVOR_BSD, setgmp и longgmp будут сохранять и запоминать маску сигнала, а getpgrp будет допускать аргумент PID. Необходимо собирать программу с libbsd, чтобы добиться BSD-поведения.
Компилятор gcc Linux-а автоматически определяет набор макросов, которые можно использовать в своей программе:
— __GNUC__ (major GNU C версия, e.g., 2);
— __GNUC_MINOR__ (minor GNU C версия, e.g., 2);
— unix;
— i386;
— linux;
— __unix__;
— __i386__;
— __linux__;
— __unix;
— __i386;
— __linux.
Многие программы используют #ifdef linux для окружения Linux-зависимого кода.

ПРИЛОЖЕНИЕ

СПРАВОЧНИК СИСТЕМНЫХ ВЫЗОВОВ

_exit — как exit, только с меньшими возможностями (m+c)
accept — установка связи на сокете (m+c!)
access — проверка прав доступа пользователя к файлу (m+c)
acct — пока не реализован (mc)
adjtimex — установка/получение переменных времени ядра (-c)
afs_syscall — зарезервированный системный вызов файловой системы andrew (-)
alarm — посылает SIGALARM в назначенное время (m+c)
bdflush — сливает грязные буфера на диск (-c)
bind — назначает сокет для межпроцессовой коммуникации (m!c)
break — пока не реализован (-)
brk — изменяет размеры сегмента данных (mc)
chdir — изменяет рабочую директорию (m+c)
chmod — изменяет атрибуты файла (m+c)
chown — изменяет владение файлом (m+c)
chroot — устанавливает новую корневую директорию (mc)
clone — см. fork (m-)
close — закрывает файл по ссылке (m+c)
connect — связывает 2 сокета (m!c)
creat — создание файла (m+c)
creat_module — захватывает память для загружаемого модуля ядра (-)
delete_module — выгружает модуль ядра (-)
dup — дублирует файловый дескриптор (m+c)
dup2 — дублирует файловый дескриптор (m+c)
execl, execlp, execle, . — см. execve (m+!c)
execve — исполняет файл (m+c)
exit — завершает программу (m+c)
fchdir — изменяет рабочую директорию по ссылке ()
fchmod — см. chmode (mc)
fchown — изменяет владение файлом (mc)
fclose — закрывает файл по ссылке (m+!c)
fcntl — управление файлом/файловым дескриптором (m+c) flock — изменение запирания файла (m!c)
fork — порождение потомка (m+c)
fpathconf — получение информации о файле по ссылке (m+!c)
fread — чтение массива двоичных данных из потока (m+!c)
fstat — получение статуса файла (m+c)
fstatus — получение статуса файловой системы по ссылке (mc)
fsync — запись кэша файла на диск (mc)
ftime — интервал времени + секунды с 1.1.1970 (m!c)
ftruncate — изменение размеров файла (mc)
fwrite — запись массива двоичных данных в поток (m+!c)
get_kernel_syms — получение символьной таблицы ядра или ее размеры (-)
getdomainname — получение имени системной области (m!c)
getdtablesize — получение размеров таблицы файлового дескриптора (m!c)
getegid — получение эффективного id группы (m+c)
geteuid — получение эффективного id пользователя (m+c)
getgid — получение id группы (m+c)
getgroups — получение дополнительных групп (m+c)
gethostid — получение уникального идентификатора основной системы (m!c)
gethostname — получение имени основной системы (m!c)
getitimer — получение значения интервального таймера (mc)
getpagesize — получение размеров страницы в системе (m-!c)
getpeername — получение имени присоединенного равного сокета (m!c)
getpgid — получение id группы родительского процесса (+c)
getpgrp — получение id группы родителя текущего процесса (m+c)
getpid — получение id текущего процесса (m+c)
getppid — получение id родительского процесса (m+c)
getpriority — получение приоритета (процесса, группы, пользователя) (mc)
getrlimit — получение лимита ресурсов (mc)
getrusage — сводка ресурсов (m)
getsockname — получение адреса сокета (m!c)
getsockopt — получение установок опций сокета (m!c)
gettimeofday — получение времени дня с 1.1.1970 (mc)
getuid — получение действительного id пользователя (m+c)
gtty — пока не реализован ()
idle — делает процесс кандидатом на свопинг (mc)
init_module — вставка загружаемого модуля ядра (-)
ioctl — работа с символьным устройством (mc)
ioperm — установка некоторых прав на ввод/вывод из порта (m-c)
iopl — установка всех прав на ввод/вывод из порта (m-c)
ipc — межпроцессовая коммуникация (-c)
kill — посылает сигнал процессу (m+c)
killpg — посылает сигнал группе процесса (mc!)
klog — см. syslog (-!)
link — создание жесткой ссылки на существующий файл (m+c)
listen — прослушивание связей сокета (m!c)
llseek — lseek для больших файлов
lock — пока не реализован ()
lseek — изменение позиции ptr файлового дескриптора (m+c)
lstat — получение статуса файла (mc)
mkdir — создание директории (m+c)
mknod — создание устройства (mc)
mmap — отображение файла в память (mc)
modify_ldt — чтение или запись локальной таблицы дескриптора (-)
mount — монтирование файловой системы (mc)
mprotect — чтение, запись или исполнение для защищенной памяти (-)
mpx — пока не реализован ()
msgctl — управление сообщением ipc (m!c)
msgget — получение id очереди сообщений ipc (m!c)
msgrcv — получение сообщения ipc (m!c)
msgsnd — посылка сообщение ipc (m!c)
munmap — удаление отображения файла из памяти (mc)
nice — изменение приоритета процесса (mc)
oldfstat — больше не существует
oldlstat — больше не существует
oldolduname — больше не существует
oldstat — больше не существует
olduname — больше не существует
open — открытие файла (m+c)
pathconf — получение информации о файле (m+!c)
pause — ждет до сигнала (m+c)
personality — получение текущей области исполнения для ibcs (-)
phys — пока не реализован (m)
pipe — создание канал (m+c)
prof — пока не реализован ()
profil — исполнение временн’ого профиля (m!c)
ptrace — трассировка потомка (mc)
quotactl — пока не реализован ()
read — чтение данных из файла (m+c)
readv — чтение блоков данных с файла (m!c)
readdir — чтение директории (m+c)
readlink — получение содержимого символической связи (mc)
reboot — перезапуск или завтрак в кратере действующего вулкана (-mc)
recv — получение сообщения из присоединенного сокета (m!c)
recvfrom — получение сообщения из сокета (m!c)
rename — перемещение/переименование файла (m+c)
rmdir — удаление пустой директории (m+c)
sbrk — см. brk (mc!)
select — усыпление до действия над файловым дескриптором (mc)
semctl — управление семафором ipc (m!c)
semget — ipc выдает идентификатор множества семафоров (m!c)
semop — операция ipc над членами множества семафоров (m!c)
send — посылка сообщения в присоединенный сокет (m!c)
sendto — посылка сообщения в сокет (m!c)
setdomainname — установка имени системной области (mc)
setfsgid — установка id группы файловой системы ()
setfsuid — установка id группы пользователя файловой системы ()
setgid — установка действительного id группы (m+c)
setgroups — установка дополнительных групп (mc)
sethostid — установка уникального идентификатора основной системы (mc)
sethostname — установка имени основной системы (mc)
setitimer — установка интервального таймера (mc)
setpgid — установка идентификатора группы процесса (m+c)
setpgrp — не имеет никакого эффекта (mc!)
setpriority — установка приоритета (процесса, группы, пользователя) (mc)
setregid — установка действительного и эффективного идентификатора группы (mc)
setreuid — установка действительного и эффективного идентификатора пользователя (mc)
setrlimit — установка лимита ресурса (mc)
setsid — создание сессии (+c)
setsockopt — изменение опций сокета (mc)
settimeofday — установка времени дня (с 1.1.1970) (mc)
setuid — установка действительного идентификатора пользователя (m+c)
setup — инициализация устройств и монтирование корня (-)
sgetmask — см. siggetmask (m)
shmat — привязка разделяемой памяти к сегменту данных (m!c)
shmctl — манипуляции с разделяемой памятью (m!c)
shmdt — отвязка разделяемой памяти от сегмента данных (m!c)
shmget — получение/создание разделяемого сегмента памяти (m!c)
shutdown — закрытие сокета (m!c)
sigaction — установка/получение обработчика сигнала (m+c)
sigblock — блокировка сигналов (m!c)
siggetmask — получение сигнала, блокирующего текущий процесс (!c)
signal — установка обработчика сигнала (mc)
sigpause — использование новой маски сигнала, пока не signal (mc)
sigpending — получение ожидающих, но заблокированных сигналов (m+c)
sigprocmask — установка/получение сигнала, блокирующего текущий процесс (+c)
sigreturn — пока не используется ()
sigsetmask — установка сигнала, блокирующего текущий процесс (c!)
sigsuspend — переустановка для sigpause (m+c)
sigvec — см. sigaction (m!)
socket — создание точки коммуникации сокета (m!c)
socketcall — сокет вызывает мультиплексор (-)
socketpair — создание 2 связанных сокетов (m!c)
ssetmask — см. sigsetmask (m)
stat — получение статуса файла (m+c)
statfs — получение статуса файловой системы (mc)
stime — установка секунд с 1.1.1970 (mc)
stty — пока не реализован ()
swapoff — окончание свопинга в файл/устройство (m-c)
swapon — начало свопинга в файл/устройство (m-c)
symlink — создание символической связи с файлом (m+c)
sync — синхронизация буферов памяти и диска (mc)
syscall — исполнение системного вызова по номеру (-!c)
sysconf — получение значения системной переменной (m+!c)
sysfs — получение информации о конфигурированных файловых системах ()
sysinfo — получение системной информации Linux-а (m-)
syslog — работа с системной регистрацией (m-c)
system — исполнение команды shell-а (m!c)
time — получение секунд с 1.1.1970 (m+c)
times — получение временн’ых характеристик процесса (m+c)
truncate — изменение размера файла (mc)
ulimit — установка/получение границ файла (c!)
umask — установка маски создания файла (m+c)
umount — размонтирование файловых системы (mc)
uname — получение системной информации (m+c)
unlink — удаление незанятого файла (m+c)
uselib — использование разделяемой библиотеки (m-c)
ustat — пока не реализован (c)
utime — модификация временн’ых элементов inode (m+c)
utimes — см. utime (m!c)
vfork — см. fork (m!c)
vhangup — виртуально подвешивает текущий tty (m-c)
vm86 — войти в виртуальный режим 8086 (m-c)
wait — ожидание завершения процесса (m+!c)
wait3 — bsd ждет указанный процесс (m!c)
wait4 — bsd ждет указанный процесс (mc)
waitpid — ожидание указанного процесса (m+c)
write — запись данных в файл (m+c)
writev — запись блоков данных в файл (m!c)

(m) существует manual page.
(+) поддерживается POSIX-ом.
(-) специфично для Linux-а.
(c) в libc.
(!) не одиночный системный вызов, использует другой системный вызов.

Системные вызовы и функции стандартных библиотек

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

  1. Cовременные взгляды на атопические болезни как на системные заболевания. Алергические заболевания, класификация, клинические примеры.
  2. DOS Fn 5eH: Разные сетевые функции
  3. Functio laesa (нарушение функции).
  4. I. 3. Функции минеральных веществ плазмы крови
  5. I. 4. Функции белков плазмы крови
  6. I. Сущность и основные функции перестрахования.
  7. II-4.6 Функции причастия в предложении и их перевод
  8. II. Основные задачи и функции
  9. II. Роль, функции, отграничение трудового права от смежных отраслей права.
  10. II. Тригонометрические функции и функции работающие с углами.
  11. III. Функции и участники рынка ценных бумаг.
  12. SCADA-система: назначение и функции

ИНТЕРФЕЙС С ФАЙЛОВОЙ СИСТЕМОЙ

Все версии UNIX предоставляют строго определенный ограниченный набор входов в ядро ОС, через который прикладные программы имеют возможность воспользоваться базовыми услугами операционной системы. Эти точки входа получили название системных вызовов (system calls). Системный вызов определяет функцию, выполняемую ядром операционной системы от имени процесса, выполнившего вызов и является интерфейсом самого низкого уровня взаимодействия прикладных процессов с ядром. (в нашей версии Linux их 164, просмотреть их можно с помощью команды man syscalls).

Системные вызовы обычно документированы во втором разделе справочной системы man. В среде программирования Си они определяются как функции, независимо от того, что они, по сути, являются обращением к ядру ОС. В UNIX используется подход, при котором каждый системный вызов имеет соответствующую функцию (или функции) с тем же именем, хранящуюся в стандартной библиотеке языка Си. Эти функции выполняют необходимые преобразования аргументов и вызывают процедуру ядра, используя различные приемы. Библиотечный код, по сути, играет роль оболочки (обертки), в то время как фактические инструкции расположены в ядре операционной системы.

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

printf(3) использует системный вызов write(2)

strcpy(3) не использует

atoi (3) не использует

Функции, о которых идет речь, хранятся в стандартных библиотеках Си и наряду с системными вызовами составляют основу среды программирования в UNIX. Библиотека стандартных функций языка Си (стандарт 1990 г) включает следующие разделы:

— Функции обработки строк и символов

— Функции ввода/вывода времени и даты

— Функции динамического распределения памяти

— Вспомогательные (служебные) функции

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

Библиотечные функции являются «надстройкой» над системными вызовами и обеспечивают более удобный способ получения системных услуг. Например, системный вызов time(2) возвращает время в секундах с момента Epoch: 1 января 1970 года. А функции ctime(3), localtime(3) и т.п. преобразуют это значение в вид, удобный для человеческого восприятия (дата, время с учетом временной зоны и т.п.). К таким функциям можно отнести и функции ввода/вывода, распределения памяти, часть функций управления процессами и т.д.

Системные вызовы (2) и библиотечные функции (3) также различаются по способу передачи процессу информации об ошибке, произошедшей во время системного вызова или функции библиотеки. Системные вызовы в случае ошибки (невозможности своего выполнения) обычно возвращают –1 и устанавливают значение системной переменной errno, указывающее причину возникновения ошибки. Так, например, существует более десятка причин, по которым функция open(2) не сможет открыть файл. Файл заголовков содержит коды ошибок, значения которых может принимать переменная errno, с краткими комментариями.

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

Библиотечные функции, как правило, не устанавливают значение errno, а возвращают код ошибки (типа int ), который по смыслуразличен для разных функций. Для уточнения возвращаемого значения библиотечной функции следует обращаться к справочной системе man.

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

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

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

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

1. переход в привилегированный режим;

2. разбирательство, КТО обратился к ядру, и подключение u-area этого процесса к адресному пространству ядра (context switching);

3. извлечение аргументов из памяти запросившего процесса;

4. выяснение, ЧТО же хотят от ядра (один из аргументов, невидимый нам — это номер системного вызова);

5. проверка корректности остальных аргументов;

6. проверка прав процесса на допустимость выполнения такого запроса;

7. вызов тела требуемого системного вызова — это обычная Си-функция в ядре;


8. возврат ответа в память процесса;

9. выключение привилегированного режима;

10. возврат из прерывания.

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

| следующая лекция ==>
При переломі плеча фіксують плечовий і ліктьовий суглоб (мал. №2, №3) | Основные системные вызовы файловой системы

Дата добавления: 2014-01-11 ; Просмотров: 2048 ; Нарушение авторских прав? ;

Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет

1.Интерфейс системных вызовов

2.Стандартная библиотека ввода/вывода

1.2. Системные вызовы и их особенности при работе с файлами.

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

указатель позиции чтения/записи, который в дальнейшем мы будем обозначать как RWptr. Это long-число, равное расстоянию в байтах от начала файла до позиции чтения/записи;

режимы открытия файла: чтение, запись, чтение и запись, некоторые дополнительные флаги;

расположение файла на диске.

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

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

Дескрипторы являются локальными для каждой программы. Т.е. если две программы открыли один и тот же файл — дескрипторы этого файла в каждой из них не обязательно совпадут (хотя и могут). Обратно: одинаковые дескрипторы (номера) в разных программах не обязательно обозначают один и тот же файл.

1.2.1. Открытие файла. Системный вызов open().

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

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

int open(char *path, int flags);

int open(char *path, int flags, int mode);

Описание системного вызова

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

Параметр path является указателем на строку, содержащую полное или относительное имя файла.

Параметр flags может принимать одно из следующих трех значений:

O_RDONLY – если над файлом в дальнейшем будут совершаться только операции чтения;

O_WRONLY – если над файлом в дальнейшем будут осуществляться только операции записи;

O_RDWR – если над файлом будут осуществляться и операции чтения, и операции записи.

Каждое из этих значений может быть скомбинировано посредством операции «побитовое или ( | )» с одним или несколькими флагами:

O_CREAT – если файла с указанным именем не существует, он должен быть создан;

O_EXCL – применяется совместно с флагом O_CREAT. При совместном их использовании и существовании файла с указанным именем, открытие файла не производится и констатируется ошибочная ситуация;

O_NDELAY – запрещает перевод процесса в состояние ожидание при выполнении операции открытия и любых последующих операциях над этим файлом;

O_APPEND – при открытии файла и перед выполнением каждой операции записи (если она, конечно, разрешена) указатель текущей позиции в файле устанавливается на конец файла;

O_TRUNC – если файл существует, уменьшить его размер до 0, с сохранением существующих атрибутов файла, кроме, быть может, времен последнего доступа к файлу и его последней модификации.

O_SYNC – любая операция записи в файл будет блокироваться (т. е. процесс будет переведен в состояние ожидание) до тех пор, пока записанная информация не будет физически помещена на соответствующий нижележащий уровень hardware;

O_NOCTTY – если имя файла относится к терминальному устройству, оно не становится управляющим терминалом процесса, даже если до этого процесс не имел управляющего терминала.


Параметр mode устанавливает атрибуты прав доступа различных категорий пользователей к новому файлу при его создании. Он обязателен, если среди заданных флагов присутствует флаг O_CREAT, и может быть опущен в противном случае. Этот параметр задается как сумма следующих восьмеричных значений:

0400 – разрешено чтение для пользователя, создавшего файл;

При создании файла реально устанавливаемые права доступа получаются из стандартной комбинации параметра mode и маски создания файлов текущего процесса umask, а именно – они равны mode &

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

Часть 5. Системные вызовы

Серия контента:

Этот контент является частью # из серии # статей: Разработка модулей ядра Linux

Этот контент является частью серии: Разработка модулей ядра Linux

Следите за выходом новых статей этой серии.

Чтобы полностью разобраться, чем является модуль для ядра, необходимо вернуться назад и рассмотреть следующие вопросы:

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

Траектория системного вызова

Для полного описания взаимодействия между всеми вышеперечисленными участниками недостаёт ещё одного фрагмента: механизма взаимодействия пользовательских процессов с ядром и модулем. В конечном итоге, главная задача операционной системы — это обслуживание потребностей прикладных пользовательских процессов. И обеспечивается это обслуживание через механизм системных вызовов. Если сказать, что операционная система 99.999% своего рабочего времени занимается обслуживанием системных вызовов, и только на оставшуюся часть приходится все остальные активности, как обработка прерываний, обслуживание таймеров, диспетчеризация потоков и другие подобные «мелочи», то это не будет слишком большим преувеличением! Для данного утверждения есть стандартное возражение, что «для серверов это не верно». Возможно, но если учесть, что вся деятельность серверов связана с обслуживанием всё тех же системных вызовов, но запрошенных с других узлов сети, то это полностью соответствует представленному утверждению. Поэтому изучение вопросов взаимосвязей и взаимодействий в операционной системе всегда нужно начинать с рассмотрения цепочки, по которой проходит системный вызов.

В любой (в том числе и микроядерной) операционной системе системный вызов выполняется некоторой выделенной процессорной инструкцией, прерывающей последовательное выполнение команд и передающий управление коду режима супервизора. Это обычно некая команда программного прерывания, в зависимости от архитектуры процессора в разные времена это были команды с мнемониками вида: svc , emt , trap , int и им подобными. Если обратиться только к архитектуре Intel x86, то в ней для этого традиционно используется команда программного прерывания с различным вектором. В таблице 1 приведены команды прерываний для различных операционных систем.

Таблица 1. Команды прерывания для ОС, построенных на архитектуре Intel x86
Операционная система Дескриптор прерывания для системного вызова
MS-DOS 21h
Windows 2Eh
Linux 80h
QNX 21h
MINIX 3 21h

Примечание: Начиная с определенного момента (примерно с начала 2008 года или момента выхода Windows XP Service Pack 2) многие операционные системы (Windows, Linux) отказались от использования программного прерывания int , и перешли к реализации системного вызова и возврата из него через новые команды процессора sysenter ( sysexit ). Это было связано с заметным падением производительности Pentium IV при классическом способе системного вызова и желанием восстановить эту производительность любой ценой. Но ничего принципиально нового в данном случае не появилось, так как ключевые параметры перехода ( CS , SP , IP ) просто стали загружаться не из памяти, а из специальных внутренних регистров MSR (Model Specific Registers) с предопределёнными (0х174, 0х175, 0х176) номерами (из большого их общего числа), куда предварительно эти значения записываются специальной новой командой процессора wmsr . По описанию можно заметить, как это громоздко, но производительность выросла, а по сути: «вектор прерывания теперь записывается в аппаратном обеспечении, а процессор помогает быстрее перейти с одного уровня привилегий на другой».

Библиотечный и системный вызов из процесса

Теперь можно переходить к детальному рассмотрению трассировки системного вызова в Linux (рассмотрение основано на классической реализации через команды int 80h / iret , так как реализация через sysenter / sysexit ничего принципиально нового не вносит). Прикладной процесс вызывает требуемые ему службы посредством библиотечного вызова к множеству библиотек либо вида *.so (динамическое связывание), либо прикомпоновывая к себе фрагмент из библиотеки вида *.a (статическое связывание). Самые известные примеры — это стандартная библиотека языка С libc.so или libpthread.so — библиотека POSIX потоков. Значительная часть вызовов обслуживается непосредственно внутри библиотеки, не требуя никакого вмешательства со стороны ядра, например, вызов sprintf() или все строковые POSIX-функции вида str*() . Другая часть требует дальнейшего обслуживания со стороны ядра, например, вызов printf() (синтаксически близкий к sprintf() ). Тем не менее, все подобные вызовы API классифицируются как библиотечные вызовы. В Linux чётко регламентируются группы вызовов, при этом библиотечные API относятся к секции 3 в руководствах man . Хорошим примером может служить целая группа функций для запуска дочернего процесса execl() , execlp() , execle() , execv() , execvp() :

Хотя ни один из этих библиотечных вызовов не запускает непосредственно дочерний процесс, а только ретранслируют вызов единственному системному вызову execve() :

Описания системных вызовов (в отличие от библиотечных) отнесены к секции 2 в руководствах man . Все системные вызовы далее преобразуются в вызов ядра функцией syscall() , 1-м параметром которого будет идентификатор выполняемого системного вызова, например __NR_execve . Таким образом, справочную информацию по библиотечным функциям необходимо искать в секции 3, а по системным вызовам — в секции 2:

Ещё один простой пример, поясняющий данный аспект:

  • библиотечный вызов printf( «%s», string ) всегда вызывает библиотечную функцию sprintf() для формирования выводимой строки, которая в данном простом случае будет совпадать со string ;
  • далее из printf() выполняется системный вызов write( 1, string, strlen( string ) ) ;
  • который трансформируется в вызов sys_call( __NR_write, . ) ;
  • который затем выполнит команду int 0x80 (полный код такого примера будет рассмотрен ниже);
  • и в конце управление будет передано обработчику системного вызова в пространстве ядра.

Информацию о вызове syscall() можно посмотреть в справочнике man:

Образцы численных идентификаторов системных вызовов из верхней части таблицы уже рассматривались ранее.

Листинг 1. Идентификаторы системных вызовов

Кроме syscall() в Linux поддерживается и другой механизм системного вызова — lcall7() , в котором устанавливается шлюз системного вызова для поддержки стандарта iBCS2 (Intel Binary Compatibility Specification), благодаря чему на x86 Linux может выполняться бинарный код, подготовленный для других операционных систем: FreeBSD, Solaris/86, SCO Unix. Но этот механизм в рамках данного цикла статей рассматриваться не будет.

Системные вызовы syscall() в Linux на процессоре x86 выполняются через прерывание int 0x80 . Соглашение о системных вызовах в Linux отличается от общепринятого в UNIX и соответствует типу «fastcall». Согласно ему, программа помещает в регистр eax идентификатор системного вызова, входные аргументы размещаются в других регистрах процессора (таким образом, системному вызову может быть передано до 6 аргументов через регистры в такой последовательности: ebx, ecx, edx, esi, edi и ebp ), после чего вызывается инструкция int 0x80 . В тех редких случаях, когда системному вызову необходимо передать большее количество аргументов, то они размещаются в структуре, адрес на которую передается в качестве первого аргумента ( ebx ). Результат возвращается в регистре eax , а стек вообще не используется. Системный вызов syscall() , попав в ядро, всегда попадает в таблицу sys_call_table , и далее переадресовывается по индексу (смещению) в этой таблице на величину 1-го параметра вызова syscall() — идентификатора требуемого системного вызова.

Заключение

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

Системные вызовы… brk vs sbrk в частности

Изучаю программинг под Linux, в частности системные вызовы :)

Многочасовые поиски нормального справочника по системным вызовам завершились, по сути, тремя ссылками:

Всё остальное — примерно то же самое, только с разным дизайном. Ну или учебник какой-нибудь о Unix в целом (хоть и на русском).

Так вот, на практике придётся использовать все 3 справочника.


  1. В первом находить нужную функцию и читать её описание (неплохо, что там всё по-русски).
  2. Во втором находить номер функции по названию.
  3. Третий использовать, когда информации из 1-го недостаточно (там расписано подробнее и кол-во функций значительно больше. и даже исходники есть).

У кого есть, киньте, плиз, ссылкой на удобный, полный, структурированный справочник, в котором будут указаны (важно!) номера функций и значения констант (типа EBADF и т.д., т.к. нужно для ассемблера). Что дескриптор стандартного ввода = 0, а вывода = 1 я уже понял, но сдаётся мне, есть там ещё куча подобных штук, искать которые опять же придётся в example’ах на просторах всея интернета, что не очень удобно. А если там ещё и примеры использования будут, то будет вообще супер (хотя не критично).

В частности, вот такой вопрос у меня возник: brk и sbrk вроде разные функции. А номер функции (eax) как будто один и тот же. Как это может быть? В чём подвох? Или система по значению передаваемого параметра определяет (типа > 0x8000000, значит brk, иначе sbrk)?

Национальная библиотека им. Н. Э. Баумана
Bauman National Library

Персональные инструменты

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

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

Сюда могут входить услуги, связанные с аппаратным обеспечением (например, доступ к жесткому диску), создание и выполнение новых процессов, связь с интегральными службами ядра, такими как планирование процессов. Системные вызовы обеспечивают необходимый интерфейс между процессом и операционной системой. В основном, системные вызовы могут быть сделаны только самим пользователем, однако в таких системах как OS/360 привилегированный код системы также может вызвать системный вызов. [Источник 1]

Содержание

Привилегии

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

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

Промежуточная библиотека

Обычно, системы предоставляют библиотеку или API , которые находятся среди обычных программ и операционной системой. В Unix-подобных системах этот API обычно является частью реализации библиотеки С (libc), такой как glibc, которая обеспечивает функции –оболочки для системных вызовов, которые, в свою очередь, часто называются также, как и системные вызовы, которые они вызывают. В Windows NT этот API является частью Native API, в библиотеке ntdll.dll; Это недокументированный API, используемый реализациями обычного Windows API и непосредственно используется некоторыми системными программами в Windows. Функции-оболочки библиотеки предоставляют обычное соглашение о вызове функций (вызов подпрограммы на уровне сборки) для использования системного вызова, а также делают системный вызов более модульным. Здесь основной функцией-оболочки является помещение всех аргументов, которые должны быть переданы системному вызову в соответствующие регистры процессора (возможно, и в стек вызовов), а также установка уникального номера системного вызова для вызова ядра. Таким образом, библиотека, которая существует между ОС и приложением, увеличивает мобильность.

Вызов самой функции библиотеки не приводит к переключению в режим ядра (если исполнение уже не было в режиме ядра) и обычно является обычным вызовом подпрограммы. Фактический системный вызов передает управление ядру (и более зависит от конкретной реализации и платформы, чем библиотека вызова). Например, в Unix-подобных системах функции fork и execve являются функциями библиотеки С, которые, в свою очередь, выполняют инструкции, вызывающие системные вызовы fork и exec. Создание системного вызова непосредственно в коде приложения сложнее и, к тому же, может потребовать использования встроенного ассемблерного кода (на С и С++), а также знание низкоуровневого двоичного интерфейса для операции системного вызова, который может меняться с течением времени и, следовательно, не быть частью бинарного интерфейса приложения. В системах, основанных на exokernels, библиотека особенно важна как посредник. На exokernels библиотеки защищают пользовательские приложения от API ядра с очень низким уровнем и обеспечивают управление ресурсами.

Операционные системы IBM, происходящие от OS / 360 и DOS / 360, включая z / OS и z / VSE, реализуют системные вызовы через библиотеку макросов языка ассемблера. Это связано с их происхождение, поскольку в то время программирование на языке ассемблера было более распространено, чем использование языков высокого уровня. Таким образом, системные вызовы IBM не могут напрямую исполняться языковыми программами высокого уровня, но требуют подпрограмму вызываемую ассемблером.

Примеры и инструменты

В UNIX-подобных и других совместимых с POSIX операционных системах популярными системными вызовами являются open, read, write, close, wait, exec, fork, exit, and kill. Многие современные операционные системы имеют сотни системных вызовов. Например, в Linux и OpenBSD каждый из них имеет более 300 различных вызовов. У NetBSD около 500, у FreeBSD более 500, Microsoft Windows 7 имеет почти 700, пока Plan 9 имеет 51. [Источник 2]

Такие инструменты, как strace и truss , позволяют процессу запускаться с самого начала и сообщать обо всех системных вызовах, которые процесс вызывает, или могут присоединяться к уже запущенному процессу и перехватывать любой системный вызов, сделанный упомянутым процессом, если эта операция не нарушает права пользователя. Эта специальная способность программы, как правило, также реализуется с помощью системного вызова. Наприме, strace реализуется с помощью ptrace или системных вызовов файлов в procfs .

Типичные реализации

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

Это единственный метод, предусмотренный для многих RISC-процессоров, но архитектуры CISC, такие как x86, поддерживают дополнительные методы. Например, набор команд x86 содержит инструкции SYSCALL / SYSRET и SYSENTER / SYSEXIT (эти два механизма были независимо созданы AMD и Intel, соответственно, но по сути они делают то же самое). Это «быстрые» инструкции, которые предназначены для быстрой передачи управления ядру для системного вызова без накладных расходов прерывания. Linux 2.5 начал использовать это на x86; Ранее он использовал инструкцию INT, где номер системного вызова был помещен в регистр EAX до того, как было выполнено прерывание 0x80. Старые x86-механизмы – это некие ворота вызова. Он позволяет программе вызывать функцию ядра напрямую, используя безопасный механизм передачи управления, который операционная система настраивает заранее. Такой подход был непопулярен, по-видимому, из-за требования удаленного вызова (вызов процедуры, расположенной в другом сегменте, чем текущий сегмент кода), который использует сегментацию памяти x86 и, как следствие, отсутствие переносимости, которую он вызывает, и Существование более быстрых инструкций, упомянутых выше. [Источник 3]

Для архитектуры IA-64 используется инструкция EPC (ввод привилегированного кода). Первые восемь аргументов системного вызова передаются в регистрах, а остальные передаются в стек. В семействе мэйнфреймов IBM System / 360 команда Supervisor Call реализует системный вызов устаревших объектов; Инструкция Program Call (PC) используется для новых объектов. В частности, ПК используется, когда вызывающий абонент может находиться в режиме SRB.

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

Управление процессами
  • load
  • execute
  • end (exit), abort
  • создание процесса (fork в UNIX-подобных, NtCreateProcess в Windows NT Native API)
  • завершение процесса
  • get/set process attributes
  • wait время, события, signal события
  • allocate, free memory
Работа с файлами


  • create file, delete file
  • open, close
  • read, write, reposition
  • get/set file attributes
Управление устройствами
  • request device, release device
  • read, write, reposition
  • get/set device attributes
  • logically attach or detach devices
Работа с информацией
  • get/set time or date
  • get/set system data
  • get/set process, file, or device attributes
Связь, коммуникация
  • create, delete communication connection
  • send, receive messages
  • transfer status information
  • attach or detach remote devices

Режим процессора и переключение контекста

Системные вызовы в большинстве Unix-подобных систем обрабатываются в режиме ядра, что достигается путем изменения режима выполнения процессора на более привилегированный, но не требуется переключения контекста процесса, хотя переключение контекста имеет место . Аппаратные средства рассматривают мир с точки зрения режима выполнения в соответствии с регистром статуса процессора, а процессы являются абстракциями, предоставляемыми операционной системой. Системный вызов обычно не требует переключения контекста на другой процесс; Вместо этого он обрабатывается в контексте того процесса, который его вызывал. [Источник 4]

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

  • Модель «многие-к-одному»: все системные вызовы из любого пользовательского потока в процессе обрабатываются одним потоком уровня ядра. Эта модель имеет серьезный недостаток — любой системный вызов блокировки (например, ожидание ввода от пользователя) может заморозить все остальные потоки. Кроме того, поскольку только один поток может одновременно обращаться к ядру, эта модель не может использовать несколько ядер процессора.
  • Модель один-к-одному: каждый поток пользователя во время системного вызова присоединяется к своему собственному потоку. Эта модель решает проблему блокирования системных вызовов. она применяется в большинстве дистрибутивов Linux, Windows,Solaris последних версий.
  • Модель многие-к-многим: в этой модели во время системного вызова множество пользовательских потоков увязываются с множеством потоков уровня ядра.
  • Гибридная модель: в этой модели реализованы модели «многие-к-многим» и «один-к-одному» в зависимости от выбора ядра ОС.

1.Интерфейс системных вызовов

2.Стандартная библиотека ввода/вывода

1.2. Системные вызовы и их особенности при работе с файлами.

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

указатель позиции чтения/записи, который в дальнейшем мы будем обозначать как RWptr. Это long-число, равное расстоянию в байтах от начала файла до позиции чтения/записи;

режимы открытия файла: чтение, запись, чтение и запись, некоторые дополнительные флаги;

расположение файла на диске.

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

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

Дескрипторы являются локальными для каждой программы. Т.е. если две программы открыли один и тот же файл — дескрипторы этого файла в каждой из них не обязательно совпадут (хотя и могут). Обратно: одинаковые дескрипторы (номера) в разных программах не обязательно обозначают один и тот же файл.

1.2.1. Открытие файла. Системный вызов open().

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

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

int open(char *path, int flags);

int open(char *path, int flags, int mode);

Описание системного вызова

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

Параметр path является указателем на строку, содержащую полное или относительное имя файла.

Параметр flags может принимать одно из следующих трех значений:

O_RDONLY – если над файлом в дальнейшем будут совершаться только операции чтения;

O_WRONLY – если над файлом в дальнейшем будут осуществляться только операции записи;

O_RDWR – если над файлом будут осуществляться и операции чтения, и операции записи.

Каждое из этих значений может быть скомбинировано посредством операции «побитовое или ( | )» с одним или несколькими флагами:

O_CREAT – если файла с указанным именем не существует, он должен быть создан;

O_EXCL – применяется совместно с флагом O_CREAT. При совместном их использовании и существовании файла с указанным именем, открытие файла не производится и констатируется ошибочная ситуация;

O_NDELAY – запрещает перевод процесса в состояние ожидание при выполнении операции открытия и любых последующих операциях над этим файлом;

O_APPEND – при открытии файла и перед выполнением каждой операции записи (если она, конечно, разрешена) указатель текущей позиции в файле устанавливается на конец файла;

O_TRUNC – если файл существует, уменьшить его размер до 0, с сохранением существующих атрибутов файла, кроме, быть может, времен последнего доступа к файлу и его последней модификации.

O_SYNC – любая операция записи в файл будет блокироваться (т. е. процесс будет переведен в состояние ожидание) до тех пор, пока записанная информация не будет физически помещена на соответствующий нижележащий уровень hardware;

O_NOCTTY – если имя файла относится к терминальному устройству, оно не становится управляющим терминалом процесса, даже если до этого процесс не имел управляющего терминала.

Параметр mode устанавливает атрибуты прав доступа различных категорий пользователей к новому файлу при его создании. Он обязателен, если среди заданных флагов присутствует флаг O_CREAT, и может быть опущен в противном случае. Этот параметр задается как сумма следующих восьмеричных значений:

0400 – разрешено чтение для пользователя, создавшего файл;

При создании файла реально устанавливаемые права доступа получаются из стандартной комбинации параметра mode и маски создания файлов текущего процесса umask, а именно – они равны mode &

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

Илон Маск рекомендует:  Свободная память
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL