Материалы к спецкурсу ОС (Unix)

История создания Unix. Ссылки на документацию по Unix

POSIX.1-2008. The Open Group Base Specifications Issue 7

Oracle. Solaris. Programming Interfaces Guide

SCO. Unixware 7. UnixWare 7 Documentation

Illustrated UNIX System V/BSD - книга 1992 года. Интересна тем, что в ней комментируются различия в семантике некоторых операций в различных реализациях Unix.

А.П. Полищук, С.А. Семериков СИСТЕМНОЕ ПРОГРАММИРОВАНИЕ В UNIX средствами Free Pascal Книга основана на UNIX System Programming: A programmer’s guide to software development by Keith Haviland, Dina Gray, Ben Salama. Кроме качественного перевода (пересказа) оригинала, книга расширена ссылками на особенности реализации некоторых системных вызовов в Linux и FreeBSD. Выбор нетипичного для Unix языка программирования не сильно мешает, но требует самостоятельного поиска имён стандартных функций языка C, которые подменены в примерах обёртками на Паскале.

Диаграмма версий Unix в Википедии

год версия
1969 First Edition
1973 Fifth Edition
1978 Seventh Edition
1979 3BSD
1982 SystemIII
1983 System V, SunOS
1989 System VR4
1990 OSF/1
1992 Solaris

Слабые стороны Unix

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

Ниже приведен конспект книги The UNIX-HATERS Handbook под редакцией Simson Garfinkel, Daniel Weise и Steven Strassmann, опубликованной IDG Books в 1994 году. За последние двадцать лет несколько из упомянутых в книге ошибок были сглажены, но в целом ситуация в мире Unix/Linux осталась прежней, поскольку слабость Unix'а заложена в самых базовых его концепциях.


"Два самых знаменитых продукта, вышедших из стен университета в Беркли, это LSD и Unix. И похоже что это не случайное совпадение" (Anonymous)

Unix это вирус - он маленький, переносимый, жрёт ресурсы хозяина, быстро мутирует.

Unix это наркотик. Как опытный наркоделец AT&T раздавала первые версии бесплатно.

Что означают названия языков C и C++? Это оценки. (В США оценки обозначаются буквами А - отлично, В - хорошо, С - так себе).


Мифы о Unix'е

  1. Он стандартен
  2. Он быстр и эффективен
  3. Он пригоден для любых приложений
  4. Он маленький простой и элегантный
  5. Шелловские программы и пайпы позволяют создавать сложные системы
  6. Он имеет электронную документацию
  7. Он имеет документацию
  8. Он написан на языке высокого уровня
  9. X-Window и Motif (Gnome, KDE) делают его дружественным к пользователю как Mac (Windows)
  10. Процессы не добавляют накладных расходов
  11. Он ввёл в обиход:
    • иерархическую файловую систему
    • электронную почту
    • сетевые и интернетовские протоколы
    • удалённый доступ к файлам
    • секретность/пароли/права доступа к файлам
    • программу finger
    • единообразный подход к устройствам ввода/вывода
  12. Он предоставляет удобную среду программирования
  13. Он - современная ОС
  14. Он, то что нужно людям
  15. Исходные коды:
    • доступны
    • понятны
    • соответствуют двоичному коду, который вы запускаете

Мистические имена

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


Случайная порча данных в Unix'е

  1. Unix не поддерживает версии файлов. Случайное изменение нельзя откатить.
  2. Многие Unix программы (на момент написания) не проверяют коды ответов системных вызовов. Такая программа может получить ошибку при создании копии файла, но всё равно уничтожить первоисточник.
  3. Шелл выполняет подстановку метасимволов, таких как "*", не сохраняя для программы исходные параметры. В отличие от DOS, где команда del *.* выдаёт предупреждение пользователю, в Unix невозможно отличить rm * от rm file1 file2 file3...
  4. Файлы в Unix'е удаляются мгновенно без возможности последующего восстановления.

Примеры:

Опечатка rm *>o вместо rm *.o уничтожит все файлы в каталоге и создаст пустой файл "o" . Лишний пробел в rm * .o также приведёт к печальным последствиям.

Удаление администратором подкаталога, совпадающего по имени со стандартным - опасно. Вместо rm -r ./etc легко напечатать rm -r /etc, что убьёт систему. Unix не предусматривает особой защиты для системных каталогов.

Замена rm на альяс rm -i или на что-то совсем другое (например mv $@ ~/.Deleted) не является панацеей, т.к. не влияет на команды удаления файлов, встроенные в оконную систему, среду разработки и т.п. Кроме того использование альяса может нарушить работу скриптов (скрипт начнёт запрашивать подтверждения) и сбить с толку сисадмина, который будет пытаться понять, почему у пользователя неверно работает программа.

Команда rm *, выполненная в одном каталоге, сохраняется в истории команд и может быть случайно вызвана в другом подстановкой !r (последняя команда в истории на букву r).

Удаление файла с именем "*" - отдельное искусство.


Отсутствие стиля как стиль

Программы в Unix не имеют общего стиля. Каждый волен придумать свой набор опций, свой конфигурационный файл и свою систему оповещения об ошибках. Не существует требования по использованию определённых библиотек. Так ed, sed, grep и shell имеют схожие, но различные форматы регулярных выражений.

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

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

Уже упоминалось, что подстановка "*" при обработке шеллом (вместо использования стандартной функции в самой программе) приводит к потере части информации о командной строке. В сочетании с тем, что Unix не отличает в командной строке имена файлов от опций, это приводит к катастрофическим последствиям. Имена файлов, начинающиеся с "-" нельзя отличить от опций. Например, команда rm * в каталоге, содержащем файл "-r" приведёт к рекурсивному удалению подкаталогов, но сохранит сам файл "-r".

Обратная ситуация. Некоторые утилиты могут воспринимать имена файлов, начинающиеся с "-" как неверные опции и не смогут обработать такие файлы:

$ mv -file file
mv: invalid option -- l
$  rm -file
usage: rm [-rif] file ...
$ rm ?file
usage: rm [-rif] file ...
$ rm ?????
usage: rm [-rif] file ...
$ rm *file
usage: rm [-rif] file ..

(В современном Linux'е выдаётся подсказка Try 'rm ./-file' to remove the file '-file', но само поведение команды не изменилось).

MAN-страница по rm в Linux'е предлагает использовать rm -- -foo для удаления -foo, но это не является частью стандарта. Авторам оригинальной книге в MANе предложили использовать rm - -foo.

Шутка с ls. Готовим каталог и файл

% mkdir foo
% touch foo/foo~

Теперь зовём ничего не подозревающего соседа и просим объяснить результат выполнения команд

% ls foo*
foo~
% rm foo~
rm: foo~ nonexistent
% rm foo*
rm: foo directory
% ls foo*
foo~
%

Попробуйте объяснить, что делает команда cat - - - (подсказка: тройное нажатие ^D завершит её работу).


Электронная документация

Основой электронной документации в Unix являются man-страницы. К сожалению, часть команд являются исполняемыми файлами (wc,ls,rm), а часть встроенными командами шелла (fg,job,alias). man-страницы описывают внешние команды и шелл в целом. Если новичок не знает какой у него шелл, он не сможет добраться до описания встроенных команд.


Предупреждения и сообщения об ошибках в Unix - ИХ НЕТ!

Ошибка в порядке написания имён файлов cc -o prog.c prog вместо cc -o prog prog.с при запуске компилятора молча уничтожит исходные тексты. Ошибка в опциях архиватора tar cf bigarchive.tar вместо tar xf bigarchive.tarмолча уничтожит архив.


Управление терминалом

То, что ранние версии Unix'а разрабатывались на компьютере с примитивным телетайпом в качестве терминала, привело к тому, что в ядре Unix'а вообще отсутствуют средства для работы с интеллектуальными средствами взаимодействия с пользователями.

Телетайпы умели построчно печатать текст и (по приходу специального символа) переходить на следующую строку (NL \n), возвращать каретку в начало строки (CR \r) и звенеть звонком (BELL \b). После телетайпов на рынок вышли текстовые видео терминалы, которые выводили текст существенно быстрее и позволяли (с помощью управляющих последовательностей символов) проделывать разные трюки с текстом на экране. К сожалению, у разных производителей управляющие последовательности были разными.

Бил Джой во время разработки редактора vi создал базу управляющих последовательностей - termcap, которая позволяла извлекать последовательности, выполнявшие определённые функции на конкретном терминале. К сожалению termcap отражала не те функции, которые были придуманы производителями, а те которые были нужны для работы редактора vi. (В последующем набор функций был несколько расширен, но сути дела это не меняет). Кроме того, код работавший с termcap в vi так и не был оформлен в отдельную библиотеку, что заставляло программистов самостоятельно изобретать собственные API.

В конце концов Кен Арнольд написал библиотеку для управления текстовым терминалом под названием curses. К сожалению, библиотека ориентировалась на ту же урезанную базу терминальных функций termcap и к тому же была не очень профессионально написана. В результате curses стала полустандартом в мире Unix. В книге есть фраза: ...и сейчас в 1994 году стандарта управления терминалом по прежнему нет. Нет его и двадцать лет спустя.

Вместо того, чтобы включить в ядро вызовы для манипулирования с абстрактным терминалом, разработчики Unix'а вынесли всю логику в относительно стандартные библиотеки или вообще зашили работу с терминалом в код программ. В первую очередь такой подход лишил Unix-программы совместно использовать один экран. Кроме того, в Unix (отчасти из из за идеи, что всё есть последовательный файл, а отчасти из за ограничений termcap и curses) никогда не была реализована работа с "умными" терминалами, которые позволяют создавать экранные формы, рисовать изображения и т.п.

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

Inode и каталоги

В традиционной файловой системе (ФС) Unix доступный объём физического носителя делится между блоками данных и областью хранения метаданных - Inode. Чило Inode определяет максимальное число объектов (файлов, каталогов, сокетов и т.п) которые может хранить ФС.При форматировании ФС. Необходимо соблюдать баланс между данными и метаданными. При заполнении ФС мелкими файлами возможна ситуация, когда при свободной области данных исчерпывается запас Inode и создание новых файлов становится невозможным.

Inode

C каждым файлом в ОС Unix связана особая структура данных - индексный дескриптор (inode), хранящий метаинформацию файла (владелец, права доступа и т.п.).

Индексные дескрипторы в оригинальной UnixFS объединялись в последовательно нумерованный массив, что и дало название самой структуре (индексированный node). Для хранения массива индексных дескрипторов в традиционной ФС на диске выделялся непрерывный участок логических блоков. Размер этого участка определял максимальное число файлов, которые могли быть созданы в ФС. В современных ФС эта структура может иметь разные размеры и набор полей или отсутствовать вовсе. Соответственно, классические утилиты мониторинга ФС могут выдавать неверные данные о количестве занятых и свободных inode ФС.

Просмотр числа Inode

df -i

Индексный дескриптор можно рассматривать в двух ипостасях:

В реализации API доступа к ФС – это стандартизованная структура данных для обобщённого представления атрибутов файла. В оперативной памяти индексный дескриптор может быть представлен в виртуальном виде – vnode. Для ФС , не хранящих индексные дескрипторы, vnode создаётся на основе других подходящих источников данных.

Номер индексного дескриптора уникален в рамках одной ФС, однако, при монтировании нескольких ФС в одно дерево номера индексных дескрипторов будут повторяться. Поэтому vnode хранит номер индексного дескриптора плюc идентификатор ФС, в которой он находится. Для диcковых ФС в Linux номером ФС является число, составленное из мажора и минора блочного устройства, на котором ФС расположена. Для NFS, похоже, номер ФС определяется порядком монтирования и последовательно возрастает начиная с 1Ah.

Каталоги

Древовидную структуру файловой системы в Unix обеспечивают каталоги, которые хранят таблицу соответствий ИМЯ->inode. В этой таблице требуется уникальность имен, но не уникальность номеров Inode. Благодаря этому, каждый объект ФС может иметь несколько имён. Счётчик имён хранится в inode объекта.

У каталогов есть одно "нормальное" имя в каталоге верхнего уровня, имя '.' в самом каталоге, и имя '..' в каждом из подкаталогов. В Linux другие имена для каталога создать нельзя. Нарушение этого правила привело к тому, что в структуре файловой системы могли бы образоваться циклы, а это бы нарушило работу алгоритмов обхода дерева каталогов.

У других типов объектов (файлов, FIFO, файлов устройств, сокетов, символических ссылок) может быть много имен в одном или в нескольких каталогах. Такие имена называют "жёсткими ссылками" (hard links), поскольку они гарантированно ссылаются на существующий inode. В противоположность этому, "мягкие ссылки" (soft links) - это особые объекты файловой системы, которые хранят, вообще говоря, произвольные текстовые строки, которые интерпретируются, как пути к файлам. Мягкие ссылки могут ссылаться на несуществующие объекты и не отражаются в счетчике Inode.

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

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

Метки времени

С каждым объектом ФС Unix связаны три метки времени:

  • ctime – время модификации атрибутов файла (inode). Изменяется при смене прав доступа, владельца или группы файла, а также при изменении содержимого файла.
  • mtime – время последней записи в файл.
  • atime – время последнего доступа к файлу. В зависимости от опций ФС может оставаться неизменным (оптимизация), изменяться при открытии файла, изменяться каждые n-секунд пока файл остаётся открытым. Вызов stat не меняет atime.

Пользователь может произвольно изменить mtime и atime, в том числе на прошлое или будущее время (например, touch -t 200012311800 time.txt). При этом ctime изменится на момент выполнения операции.

Метка времени создания файлов в классическом Unix отсутствует, что довольно неудобно для администраторов.

В конкретных реализациях ФС могут быть и другие метки, но они недоступны через стандартные функции API. Например, в ex2fs есть поле для хранения времени удаления файла dtime, а в Sun StorEdge QFS хранится время создания файла creation time.

Права доступа на файлы и каталоги

В Unix права доступа к объекту ФС хранятся в битовом поле индексного дескриптора (inode). Шестнадцатиразрядное битовое поле, называемое mode, включает в себя четыре бита, определяющие тип объекта, три бита особых признаков (suid, sgid, sticky) и девять бит прав доступа. Правила интерпретации флагов mode (особенно suid, sgid, sticky) и правила манипуляции ими могут отличаться в разных ОС. В данном тексте описываются правила Linux.

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

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

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

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

Права доступа включают право на чтение (R__ead), запись (__W__rite) и исполнение (e__X__ecute). Существует три набора прав __rwx для владельца файла (U__ser), группы файла (__G__roup) и остальных (__O__ther). Традиционно права записываются в виде строки из трёх троек __rwx. Тройки расположены слева направо в порядке ugo. Отсутствующее право помечается прочерком. Набор прав также может быть представлен в виде трёхзначного восьмеричного числа, в котором 1 соответствует наличию права, а 0 отсутствию. Например, rwxr-xr-- эквивалентно 7548. Утилита stat позволяет выдать права доступа к файлу в восьмеричном виде путём задания формата %a. Например, для каталога /tmp установлены все права для всех и sticky bit:

$> stat --format=%a /tmp
1777

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

Права доступа к файлам

Для файла права rw проверяются в момент выполнения вызова ядра open(). При этом права доступа сверяются с флагами доступа, передаваемыми в open. Право x проверяется в момент выполнения вызова exec(). Для выполнения двоичных файлов право на чтение не обязательно. Защищенные от чтения исполняемые файлы нельзя запустить под отладчиком. Для скриптов запуск означает запуск программы интерпретатора, которая получает в качестве первого параметра имя файла скрипта. В этом случае интерпретатор должен открыть файл скрипта на чтение и право r необходимо.

Флаги suid и sgid в сочетании с правом на исполнение изменяют эффективные права процесса в момент выполнения программы из этого файла вызовом exec. Эффективные права соответствуют владельцу (группе владельцев) файла. При наличии права на исполнение suid и sgid отображаются буквой s в позиции флага права на исполнение. Флаг suid (sgid) может быть назначен файлу, не имеющему флага права на выполнение для владельца (группы). При отсутствии права на исполнение suid и sgid отображаются буквой S в позиции флага права на исполнение. Флаг suid без права на выполнение владельцем ни на что не влияет. Флаг sgid без права на выполнение группой используется как признак принудительной блокировки файла при выполнении системного вызова fcntl(fd, F_SETLK,...).

Флаг sticky bit не оказывает в Linux влияния на работу с файлом. Более того, системный вызов chmod() молча игнорирует попытки установить sticky bit, не выдавая ошибки, но и не выполняя действия. В старых версиях Unix sticky bit в сочетании с флагом прав на исполнение указывал, что после завершения программы её код должен быть сохранён в области свопа для быстрого повторного запуска.

Права доступа к каталогам

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

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

sticky бит используется для каталогов, запись в которые разрешена группе или остальным. Данный бит указывает на то, что создавать записи в каталоге может любой, имеющий право на запись, а удалять только владелец объекта, на который указывает запись или владелец каталога. sTicky бит обозначается буквой t в позиции права на исполнение для остальных, если само право есть, и буквой T, если такого права нет.

Флаг setgid, установленный на каталог, приводит к тому, что все объекты, создаваемые в этом каталоге, наследуют группу каталога. Создаваемые подкаталоги дополнительно наследуют сам бит setgid.

Флаг setuid, установленный на каталог в System V и Linux, игнорируется. В BSD системах setuid, установленный на каталог, действует аналогично setgid.

ACL

Традиционные права доступа, хранящиеся в поле mode, не позволяют задать права доступа с точностью до пользователя или до группы. Скажем, нельзя распределить права доступа так, чтобы пользователь user1 имел право только на чтение, user2 - только на запись, а user3 - только на исполнение.

Для преодоления подобных ограничений современные реализации Unix поддерживают списки доступа (Access Lists) - ACL. Для хранения списков доступа может резервироваться отдельный inode, что позволяет выделять для них место в области данных ФС, не создавая при этом отдельного видимого файла.

Списки доступа состоят из записей, содержащих тип записи (пользователь, группа, остальные, маска), идентификатор пользователя или группы, флаги прав на чтение, запись и исполнение. Права доступа, содержащиеся в inode обязательно дублируются тремя записями в ACL - владелец, группа, остальные. Маска определяет максимальные права, которые будут доступны через ACL. Если дать кому-либо права rwx, а маска равна r--, то результатом будет право r--. Действие маски не распространяется на владельца файла и на остальных.

Для индикации наличия ACL информационные утилиты добавляют символ + после стандартного списка прав доступа

Команда просмотра ACL

$ getfacl /etc
getfacl: Removing leading '/' from absolute path names
# file: etc
# owner: root
# group: root
user::rwx
group::r-x
other::r-x

Команда изменения acl

setfacl -m u:lisa:r file

устанавливает право на чтение file для пользователя lisa

setfacl -x u:lisa:r file

отбирает право на чтение file для пользователя lisa

Capabilities

В Linux кроме повышения прав программы через флаг setuid, возможно повышений прав на отдельные привилегированные функции - capabilities.

Команда просмотра capabilities

$ getcap /bin/ping
/bin/ping = cap_net_admin,cap_net_raw+p

Команда установки capabilities

setcap capabilities filename

Формат capabilities описан в man cap_from_text.

Макросы для выделения отдельных флагов

identifier value comment
S_IFMT F000 format mask
S_IFSOCK A000 socket
S_IFLNK C000 symbolic link
S_IFREG 8000 regular file
S_IFBLK 6000 block device
S_IFDIR 4000 directory
S_IFCHR 2000 character device
S_IFIFO 1000 fifo
S_ISUID 0800 SUID
S_ISGID 0400 SGID
S_ISVTX 0200 sticky bit
S_IRWXU 01C0 user mask
S_IRUSR 0100 read
S_IWUSR 0080 write
S_IXUSR 0040 execute
S_IRWXG 0038 group mask
S_IRGRP 0020 read
S_IWGRP 0010 write
S_IXGRP 0008 execute
S_IRWXO 0007 other mask
S_IROTH 0004 read
S_IWOTH 0002 write
S_IXOTH 0001 execute

Ссылки:

http://www.softpanorama.org/Access_control/Permissions/suid_attribute.shtml

http://www.het.brown.edu/guide/chmod.html

Структура Inode

Атрибуты файла, хранящиеся в vnode, могут быть получены вызовом stat/fstat/lstat, который возвращает структуру данных

struct stat {
    dev_t         st_dev;      /* устройство */
    ino_t         st_ino;      /* индексный дескриптор */
    mode_t        st_mode;     /* режим доступа */
    nlink_t       st_nlink;    /* количество жестких ссылок */
    uid_t         st_uid;      /* идентификатор пользователя-владельца */
    gid_t         st_gid;      /* идентификатор группы-владельца */
    dev_t         st_rdev;     /* тип устройства */
                               /* (если это устройство) */
    off_t         st_size;     /* общий размер в байтах */
    blksize_t     st_blksize;  /* размер блока ввода-вывода */
                               /* в файловой системе */
    blkcnt_t      st_blocks;   /* количество выделенных блоков */
    time_t        st_atime;    /* время последнего доступа */
    time_t        st_mtime;    /* время последней модификации */
    time_t        st_ctime;    /* время последнего изменения */
};

Из командной строки просмотреть атрибуты можно командой stat

$ stat /bin/ping
  File: `/bin/ping'
  Size: 40760           Blocks: 88         IO Block: 4096   regular file
Device: fd00h/64768d    inode: 113377318   Links: 1
Access: (4755/-rwsr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2014-03-17 08:38:31.000000000 +0600
Modify: 2011-05-21 03:08:42.000000000 +0600
Change: 2012-02-02 11:17:13.000000000 +0600

Структуры данных ext2fs

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

Дисковое пространство в ext2fs разбивается на логические блоки размером 1, 2 или 4 КБ. Блоки используются под хранение нескольких служебных структур, массива индексных дескрипторов и, собственно, под хранение содержимого файлов. Для оптимизации времени доступа блоки поделены на группы. По возможности индексный дескриптор файла и его данные размещаются в пределах одной группы, что снижает время на перемещение головок по диску.

В одном из начальных блоков (со смещением 1024 байта от начала раздела) размещается Суперблок – структура данных размером 1024 байта, описывающая основные настраиваемые параметры ФС. В этих параметрах задаётся размер блока, количество индексных дескрипторов, количество блоков, отведенных под хранение данных и т.п. Размер блока влияет на потери дискового пространства в "хвостах" файлов (больше блок – больше потери) и на максимальную длину файла (больше блок – больше максимальная длина). Количество индексных дескрипторов определяет максимальное число объектов, которые могут быть размещены в данной ФС.

Для повышения надёжности Суперблок дублируется в начале каждой группы. За ним следует массив дескрипторов групп, который также дублируется во всех группах. Далее идут битовые карты свободных индексных дескрипторов и свободных блоков данных группы. Эти битовые карты нужны для быстрого создания файлов и быстрого выделения блоков хранения данных. Далее находятся область хранения индексных дескрипторов и область хранения данных.

Структура группы блоков в ext2fs

Суперблок Массив дескрипторов групп Карта свободных блоков Карта свободных индексных дескрипторов Массив индексных дескрипторов Блоки данных
Дублируются во всех группах блоков для надёжности Данные, индивидуальные для каждой группы

Суперблок

поле описание
s_inodes_count Число индексных дескрипторов во всей ФС
s_blocks_count Число блоков, отведённых под ФС
s_r_blocks_count Число зарезервированных блоков данных
s_free_blocks_count Число свободных блоков данных
s_free_inodes_count Число свободных индексных дескрипторов
s_first_data_block Адрес первого блока данных
s_log_block_size Размер блока
s_log_frag_size
s_blocks_per_group Число блоков в группе
s_frags_per_group
s_inodes_per_group Число индексных дескрипторов в группе
s_mtime Время последнего монтирования
s_wtime Время последней записи
s_mnt_count Количество монтирований
s_max_mnt_count Количество монтирований без проверки на ошибки
s_magic Магическое число ex2fs
s_state Флаг "чистого" выключения
....
s_reserved[235] дополнение до 1024 байтов

Дескриптор группы

поле описание
bg_block_bitmap Адрес битовой карты свободных блоков
bg_inode_bitmap Адрес битовой карты свободных индексных дескрипторов
bg_inode_table Адрес таблицы индексных дескрипторов
bg_free_blocks_count Количество свободных блоков в группе
bg_free_inodes_count Количество свободных индексных дескрипторов в группе
bg_used_dirs_count Количество каталогов группе (для fsck, например)
bg_pad выравнивание до удобного размера

Поля индексного дескриптора

поле описание
i_mode Тип, suid, sgid, sticky, права доступа
i_uid Владелец
i_size Размер
i_atime Access time
i_ctime Creation time
i_mtime Modification time
i_dtime Deletion Time
i_gid Группа
i_links_count Число имён
i_blocks Число занимаемых блоков
i_flags Флаги
i_reserved1
i_block[15] Указатели на блоки данных
i_version Версия (для NFS)
i_file_acl File ACL
i_dir_acl Directory ACL
.... прочее, дополненное до удобного размера

Указатели на блоки данных

номер описание
1 адрес блока или 0
... ...
12 адрес блока или 0
13 адрес блока косвенной адресации или 0
14 адрес блока двойной косвенной адресации или 0
15 адрес блока тройной косвенной адресации или 0

Блоки адресуются с единицы. Ноль в указателе означает, что блок не выделялся.

Зарезервированные номера индексных дескрипторов

идентификатор номер Описание
EXT2_BAD_INO 1 Сбойные блоки
EXT2_ROOT_INO 2 Корневой каталог
EXT2_ACL_IDX_INO 3 ACL (списки доступа)
EXT2_ACL_DATA_INO 4 ACL (списки доступа)
EXT2_BOOT_LOADER_INO 5 Загрузчик
EXT2_UNDEL_DIR_INO 6 Каталог для восстановления стёртых файлов
EXT2_FIRST_INO 11 Первый нормальный inode. Часто занят каталогом lost+found

Запись в каталоге

поле описание
inode Номер индексного дескриптора
rec_len Длина записи
name_len Длина имени файла
name Имя файла (переменной длины до 255 символов)

Простая программа для проверки максимальной длины имени

 F=""
 for I in {1..1024};do
      F=${F}Z
      if touch $F; then
          rm $F
      else
          echo "Maximum name length="$((I-1))
          break
      fi
  done  2>/dev/null

Ссылки

http://uranus.chrysocome.net/explore2fs/es2fs.htm

API файловой системы

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

Файлы в файловой системе

В файловой системе UNIX хранятся различные объекты: файлы, каталоги, символические ссылки, файлы устройств, FIFO, сокеты. Объекты в ФС адресуются именами и характеризуются правами доступа. Один объект может иметь несколько имен. Права доступа (R)ead, (W)rite, e(X)ecute определены по отдельности для трех категорий пользователей (U)ser, (G)roup, (O)ther. Дополнительно к правам доступа есть флаг смены владельца на время выполнения файла, смены группы на время выполнения файла и признак «липкости», что бы он не означал. Права доступа и флаги для разных типов объектов интерпретируются немного по-разному.

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

Файл оставшийся без имени и не используемый для вода/вывода или в качестве источника данных программы – уничтожается.

int chown(const char *path, uid_t owner, gid_t group); //смена владельца и группы
int chmod(const char *path, mode_t mode); //смена прав доступа

int mkdir(const char *pathname, mode_t mode); //создание каталога
int rmdir(const char *pathname); //удаление каталога

int symlink(const char *oldpath, const char *newpath); //создание файла ссылки newpath ссылающегося на oldpath
int link(const char *oldpath, const char *newpath); //создание нового имени
int unlink(const char *pathname); //удаление старого имени
int rename(const char *oldpath, const char *newpath); //атомарная операция удаляющая имя oldpath и создающая newpath
int stat(const char *file_name, struct stat *buf); //получение всех атрибутов файла

Открытые файлы = файловые дескрипторы

В качестве источника данных файлы представлены в программе в виде файловых дескрипторов – целых чисел являющимися индексами в таблице открытых файлов. За файловыми дескрипторами могут скрываться файлы, FIFO и файлы устройств в ФС, неименованые каналы - pipe или сокеты. Для операций чтения и записи все эти источники данных равноценны.

По соглашениям UNIX вновь запущенная программа может рассчитывать на три открытых файла с индексами 0,1,2, соответствующие stdin, stdout, stderr. Ответственность за это возлагается на программу вызвавшую exec().

Работа с метаданными файла может происходить и по имени и по файловому дескриптору.

int fchmod(int fd, mode_t mode);
int fchown(int fd, uid_t owner, gid_t group);
int fstat(int fd, struct stat *buf);

Некоторые примеры использования API файловой системы

//Перенаправление стандартного файла ошибок
int newfd;
char *fname="file";
if( (newd = creat(fname, S_IRUSR|S_IWUSR) >=0 ) ){
   dup2(newfd,2);
   close(newfd);
}else{
   perror("Cannot open new stderr file:");
   exit(1);
}

// Создание большого "дырявого" файла, который имеет длину 10000000 байт а реально занимает 1 блок на диске
newd = open(fname, O_WRONLY|O_TRUNC);
lseek(fd,10000000,SEEK_SET);
write(fd," ",1);

//узнаем текущую позицию чтения/записи
pos=lseek(newfd,0,SEEK_CUR);

//делаем "невидимым" временный  файл 
int tmpfd;
char tmpname[]="/tmp/qqqXXXXXX"
mktemp(tmpname);
int tmpfs = open(tmpfname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR)
unlink(tmpfname);
//теперь файл не имеет имени и будет уничтожен по завершению нашей программы
//читать и писать в него можно без проблем
write(tmpfd,tmpname,1);
lseek(tmpfd,0,SEEK_SET);
read(tmpfd,tmpname,1);

Доступ к файлам

Вызовы ядра Unix, связанные с файловой системой можно условно поделить на несколько групп:

  • работа с содержимым файла или каталога
  • работа с именами объектов
  • работа с символическими ссылками
  • работа с FIFO и файлами устройств
  • работа с метаданными

Работа с содержимым файла или каталога

Открытие файла

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

При открытии файла в вызове ядра open() проверяются соответствие флагов и прав доступа к файлу.

//почти псевдокод
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int fd;
int flags;
mode_t mode;
// открытие (+создание) файла
fd=open("pathname", flags, mode);
//или
fd=creat("pathname",  mode);
// при открытии существующего файла можно опустить параметр mode
fd=open("pathname", flags);

mode - права доступа к файлу, назначаемые в момент его создания. Чтобы нельзя было случайно создать файл со слишком свободным доступом, при создании файла производится побитовое умножение mode на битовую маску umask (mode & ~umask). mode и _umask__ удобно задавать в восьмеричном виде считая, что классические права доступа rwx соответствуют одной восьмеричной цифре. Например, права доступа rwxr-xr-- запишутся в восьмеричном виде как 0754. Типичная маска выглядит так ---w--w- или 022в восьмеричной записи. Такая маска отбирает права на запись у группы и остальных.

Системный вызов umask(mask) устанавливает новую маску и возвращает старую.

#include <sys/types.h>
#include <sys/stat.h>

mode_t old_mask=umask(new_mask);

Для удобства записи прав доступа существуют мнемонические макросы:

S_IRWXU 00700 User Read, Write,eXecute; S_IRUSR 00400 User Read; S_IWUSR 00200 User Write и т.д. S_IXUSR, S_IRWXG, S_IRGRP, S_IWGRP, S_IXGRP, S_IRWXO, S_IROTH, S_IWOTH, S_IXOTH.

flags - флаги уточняющие режим открытия файла. Флаги делятся на несколько групп:

  • режим доступа: O_RDONLY, O_WRONLY, O_RDWR, O_WRONLY+O_APPEND(чтение,запись, чтение+запись, запись всегда в конец файла);
  • режим создания - O_CREAT, O_CREAT+O_EXCL, O_TRUNC (создавать файл, создавать только если не существует, обрезать существующий до нулевой длины);
  • прочие O_NOFOLLOW, O_CLOEXEC, O_NOCTTY (не открывать символические ссылки, закрывать при вызове exec, не рассматривать открытый файл как управляющий терминал CTTY=Control TeleTYpe).

При ошибке открытия файла возвращается -1 и в переменную errno заносится код ошибки. Возможные значения ошибки (не все):

  • EACCES - нет прав на сам файл или на поиск в каталоге в котором он находится;
  • ENOENT - файл не существует и не указан флаг O_CREAT;
  • EEXIST - файл существует и указаны флаги O_CREAT+O_EXCL;
  • EFAULT - плохой указатель на имя файла (например NULL);
  • EISDIR - попытка открыть каталог;
  • ELOOP - символические ссылки создали кольцо в структуре каталогов.

Манипулирование файловыми дескрипторами

Возможно создание ссылки на файловый дескриптор.

#include <unistd.h>
int fd1=dup(oldfd);
int fd2=dup2(oldfd, newfd);

dup() - выбирает в таблице открытых файлов первую свободную строку и записывает ссылку на oldfd в неё, dup2() - закрывает файл, связанный с дескриптором newfd (если он был открыт) и записывает ссылку oldfd в newfd. В случае успеха возвращается файловый дескриптор, в случае ошибки -1.

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

Типичное применение dup2() - это подмена стандартных дескрипторов 0,1,2 (stdin,stdout,stderr). oldfd в этом случае закрывается после создания ссылки. dup2() предпочтительнее чем dup(), т.к. выполняется атомарно, что может быть важно в многопоточной среде.

int newfd=open("file",O_RDONLY);
dup2(newfd,0);
close(newfd);

вариант с dup()

int newfd=open("file",O_RDONLY);
close(0);
dup(newfd);
close(newfd);

Чтение/запись файла

#include <unistd.h>
// чтение/запись определенного числа байт
int fd
char buf[SIZE];
size_t count=SIZE;
ssize_t res;
res=read  (fd, buf, count);
res=write(fd, buf, count);

Чтение и запись возвращают количество прочитанных/записанных байтов или -1. -1 не всегда означает ошибку.

Возможные варианты ответа при записи:

  • число от 0 до count - число реально записанных байтов;
  • -1 - ошибка. Если errno при ошибке выставлено в EAGAIN, EWOULDBLOCK или EINTR , то операцию можно повторить см. ниже.

Возможные варианты ответа при чтении:

  • число от 1 до count - число реально записанных байтов;
  • 0 - признак конца файла
  • -1 - ошибка. Если errno при ошибке выставлено в EAGAIN, EWOULDBLOCK или EINTR , то операцию можно повторить см. ниже.

Ошибки чтения/записи:

  • EAGAIN или EWOULDBLOCK (только для сокетов) - не удалось провести неблокирующее чтение/запись для файла (сокета), открытого в режиме O_NONBLOCK;
  • EINTR - операция чтения/записи была прервана доставкой сигнала до того, как удалось прочитать/записать хотя бы один байт;
  • EBADF - плохой дескриптор файла (файл закрыт);
  • EINVAL - неверный режим доступа к файлу (чтение вместо записи или наоборот);
  • EFAULT - неверный указатель на буфер (например NULL).

Чтение/запись сложных объектов за один системный вызов

Чтение/запись из/в фрагментированной памяти

struct iovec {
     void  *iov_base;    /* Starting address */
     size_t iov_len;     /* Number of bytes to transfer */
} iov[SIZE];

res=readv (fd, iov, SIZE);
res=writev(fd, iov, SIZE);

Чтение/запись в определенную позицию. offset - смещение в байтах относительно начала файла.

res=pread (fd, buf, count, offset);
res=pwrite(fd, buf, count, offset);
res=preadv (fd, iov, int iovcnt, offset);
res=pwritev(fd, iov, int iovcnt, offset);

Закрытие файла

// закрытие файла
int retval=close(fd);

Ошибки при закрытии файла встречаются редко, но встречаются.

  • EBADF - попытка закрыть файловый дескриптор, не связанный с открытым файлом;
  • EINTR - операция прервана доставкой сигнала. Может встретиться на медленных (например сетевых) ФС;
  • EIO - ошибка нижележащей системы ввода/вывода. Например обрыв соединения с файловым сервером.

Установка смещения в файле

Для установки позиции/чтения записи в файле используются два параметра offset - смещение в байтах и whence - место от которого отсчитывается смещение. Возможные значения whence:

  • SEEK_SET - от начала файла;
  • SEEK_CUR - от текущей позиции;
  • SEEK_END - от конца файла.

Пример:

#include <sys/types.h>
#include <unistd.h>

// установка позиции чтения/записи
off_t offset=100;
int whence=SEEK_END;
off_t pos=lseek(fd, offset, whence);

Возвращается установленная позиция или -1 в случае ошибки.

Сочетание offset=0 и whence=SEEK_CUR позволяет узнать текущую позицию чтения/записи.

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

int fd=open("/tmp/sparse-file", O_WRONLY|O_CREAT|O_TRUNC, 0700);
off_t pos=lseek(fd, 1000000000, SEEK_SET);
int res=write(fd,"c",1);

В данном примере создаётся файл длиной примерно 1 ГБ, занимающий на диске один блок данных (например 512 Б).

В 32-х разрядных системах могут быть проблемы с большими файлами. В этом случае надо использовать вызов lseek64 и некоторые дополнительные трюки.

Связь со стандартной библиотекой языка Си

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

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

FILE *fp=fdopen(int fd, const char *mode); //mode как в fopen вида “rb” или “w” и т.п.

Совместный доступ к файлам

include <sys/file.h>

int flock(int fd, int operation);

  • LOCK_SH Place a shared lock. More than one process may hold a shared lock for a given file at a given time.
  • LOCK_EX Place an exclusive lock. Only one process may hold an exclusive lock for a given file at a given time.
  • LOCK_UN Remove an existing lock held by this process.

include <unistd.h>

int lockf(int fd, int cmd, off_t len);

  • F_LOCK Set an exclusive lock on the specified section of the file. If (part of) this section is already locked, the call blocks until the previous lock is released. If this section overlaps an earlier locked section, both are merged. File locks are released as soon as the process holding the locks closes some file descriptor for the file. A child process does not inherit these locks.
  • F_TLOCK Same as F_LOCK but the call never blocks and returns an error instead if the file is already locked.
  • F_ULOCK Unlock the indicated section of the file. This may cause a locked section to be split into two locked sections.
  • F_TEST Test the lock: return 0 if the specified section is unlocked or locked by this process; return -1, set errno to EACCES, if another process holds a lock.

Доступ к каталогам

Создание каталога

Для создания каталога в вызов mkdir() передаётся путь к создаваемому каталогу. Вышележащий каталог должен существовать (??) и пользователь должен иметь право на запись в него.

#include <sys/stat.h>
int mkdir(const char *path, mode_t mode);

Создание имени файла

При создании файла вызовом open() с флагом O_CREAT в указанном в пути к файлу каталоге создаётся запись с именем файла. Новое имя для файла можно создать вызовом link().

#include <unistd.h>
 int link("oldpath", "newpath");

Поскольку жёсткие ссылки возможны только в рамках одной ФС, то и имена "oldpath" и "newpath" должны находиться внутри одной ФС.

Удаление имени файла

В Unix нет операции удаление файла. Есть лишь операция удаление из каталога жёсткой ссылки (имени) на объект. Каждый раз после удаления имени уменьшается счётчик имён в Inode файла. Когда счётчик имён становится равным нулю, файл становится недоступным по имени. Однако, если в этот момент файл был открыт одним или несколькими процессами, то он не удаляется из ФС. Только тогда, когда у файла ноль имён и он не открыт ни в одном процессе, его Inode и его блоки данных помечаются как свободные, т.е. происходит уничтожение файла.

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

Вызов unlink() неприменим к каталогам. unlink() символической ссылки удаляет имя символической ссылки, никак не влияя на объект, на который указывает символическая ссылка.

#include <unistd.h>
 int unlink("file");

Переименование файла

Вызов rename("old", "new") эквивалентен паре вызовов link("old", "new"); unlink("old");. В отличие от этой пары rename() можно применять к каталогам, а так же он не удалит объект при переименовании его в самого себя - rename("x", "x").

Чтение каталога

Каталог в Linux можно открыть как файл с помощью open с флагом O_DIRECTORY:

struct old_linux_dirent  {
     long d_ino;                 /* inode number */
     off_t d_off;                /* offset to this dirent */
     unsigned short d_reclen;    /* length of this d_name */
     char d_name [NAME_MAX+1];   /* file name (null-terminated) */
} olddirp[SIZE];

// в Linux каталог можно открыть с помощью open
int fd=open("dirname", O_DIRECTORY); 
// и прочитать его внутреннюю структуру
retval=readdir(fd, olddirp, SIZE);

но так делать не надо.

Для работы с каталогами надо использовать библиотечные функции opendir(3), readdir(3) и т.д.

#include <dirent.h>
struct dirent {
       ino_t          d_ino;       /* номер inode */
       off_t          d_off;         /* заглушка */
       unsigned short d_reclen;    /* заглушка */
       unsigned char  d_type;      /* тип файла; поле не стандартизовано */
       char           d_name[256]; /* имя файла */
};

// открыть каталог
DIR *dirp;
dirp=opendir("dirname");
// или
dirp=fopendir(fd);

// прочитать запись за записью
struct dirent *rec;
do{
    rec=readdir(dirp);
}while(rec)

// начать сначала
rewinddir(dirp);

// закрыть каталог
closedir(dirp);

Удаление каталога

Удалять можно только пустые каталоги.

#include <unistd.h>
int rmdir("pathname");

lseek

С каждым открытым файлом в UNIX связано понятие позиции чтения-записи. Это понятие не применимо к сокетам и каналам, но обычном файле у нас есть внутренняя нумерация байт, начинающаяся с нуля, и некая условная головка чтения-записи. Позиция головки одна для чтения и записи. При открытии файла на чтение или на запись головка выставляется на начало файла. При каждой операции чтения-записи головка устанавливается на позиции за последним считанным-записанным байтом. Особый случай - это открытие файла на дозапись с флагом O_APPEND. В этом случае каждая операция записи предварительно перемещает головку в конец файла. Конец файла - это позиция равная числу байт в файле. Если мы начнем запись в конец файла, то будем писать после последнего существующего байта. Если начнем читать, то прочитаем 0 байт, что является признаком конца файла.

Иногда появляется желание переместиться по файлу вперед или назад и начать читать или писать с некой определенной позиции. Для того чтобы управлять положением головки используется вызов lseek, который изменяет положение головки чтения-записи. lseek получает файловый дескриптор и два значения: целочисленное значение смещения и макрос whence, который описывает, откуда это смещение отсчитывается. В man странице написано, что использование слова whence нарушает правила английского языка, но сохраняется по историческим причинам.

off_t pos=lseek(int fd, off_t offset, int whence); 

Параметр whence может принимать три значения: + SEEK_CUR - смещение относительно текущей позиции. Если смещение положительное, то головка смещается к концу файла, если отрицательное - то ближе к началу файла. + SEEK_END - смещение вычисляется относительно текущего размера файла. + SEEK_SET - смещение от начала файла. Отрицательное смещение не имеет смысла.

В качестве результата lseek возвращает текущую позицию относительно начала файла.

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

Если один файловый дескриптор создан из другого с помощью dup() или dup2(), то положение их головок всегда совпадает. Если применить lseek() к таким файловым дескрипторам из разных потоков, то это может привести к состоянию гонок и неожиданному поведению.

У вызова lseek есть проблема, связанная с разрядностью чисел.

В 32 разрядных системах максимальное значение целочисленного смещения четыре гигабайта, в то время как все современные файловые системы поддерживают файлы большего размера. Проблема в том, что мы просто не можем записать значение смещение больше 4ГБ. Тем не менее выполняя последовательные вызовы lseek мы можем перемещаться неограниченно далеко.

В таком случае lseek возвращает -1 и выставляет переменную errno в значение EOVERFLOW.

Чтобы работать со сверхбольшими файлами в 32 разрядной системе компилятор gcc и библиотека gnulibc предоставляют специальную функцию lseek64, у которой в качестве параметра offset передается структура, состоящая из двух целочисленных значений. Для ее использования необходимо открывать файл с флагом O_LARGEFILE и указывать при компиляции особые макросы -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS.

Структура file в Linux

Сокращенный вариант из Linux 2.6

struct path { //Ссылка на запись dentry в каталоге, расположенном на ФС mnt
	struct vfsmount *mnt;
	struct dentry *dentry;
};

struct file_operations { //Реализация операций с файлом в драйвере ФС
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
...
}

struct file {
	struct path		f_path;

	const struct file_operations	*f_op; 

	atomic_long_t		f_count;

	unsigned int 		f_flags;
	fmode_t			f_mode;

	loff_t			f_pos;

	struct fown_struct	f_owner; //uid,gid файла
	const struct cred	*f_cred; //uid,gid,euid,egid процесса

	/* needed for tty driver, and maybe others */
	void			*private_data;
};

Каналы (pipe,fifo)

Каналы - неименованные (pipe) и именованные (fifo) - это средство передачи данных между процессами.

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

При использовании канала в программировании на языке shell

ls | grep \.sh

блокировки чтения/записи обеспечивают синхронизацию скорости выполнения двух программ и их одновременное завершение.

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

Для архитектуры i386 размер буфера, связанного с каналом устанавливают кратным размеру страницы (4096 байт). В Linux в версиях до 2.6.11 использовалась одна страница (4 КБ), после - 16 страниц (65 КБ), с возможностью изменения через fcntl. POSIX определяет значение PIPE_BUF, задающего максимальный размер атомарной записи. В Linux PIPE_BUF равен 4096 байт.

Неименованные каналы

Неименованный канал создается вызовом pipe, который заносит в массив int [2] два дескриптора открытых файлов. fd[0] – открыт на чтение, fd[1] – на запись (вспомните STDIN == 0, STDOUT == 1). Канал уничтожается, когда будут закрыты все файловые дескрипторы ссылающиеся на него.

В рамках одного процесса pipe смысла не имеет, передать информацию о нем в произвольный процесс нельзя (имени нет, а номера файловых дескрипторов в каждом процессе свои). Единственный способ использовать pipe – унаследовать дескрипторы при вызове fork (и последующем exec). После вызова fork канал окажется открытым на чтение и запись в родительском и дочернем процессе. Т.е. теперь на него будут ссылаться 4 дескриптора. Теперь надо определиться с направлением передачи данных – если надо передавать данные от родителя к потомку, то родитель закрывает дескриптор на чтение, а потомок - дескриптор на запись.

int fd[2];
char c;
pipe(fd);
if( fork() ) { //родитель
    close(fd[0]);
    c=0;
    while(write(fd[1],&c,1) >0)  {
         c++;
     }
} else { //дочерний процесс
    dup2(fd[0],0); //подменили STDIN
    close(fd[0]);
    close(fd[1]);
    execl("prog","prog",NULL); //запустили новую программу для которой STDIN = pipe
}

Оставлять оба дескриптора незакрытыми плохо по двум причинам:

  1. Родитель после записи не может узнать считал ли дочерний процесс данные, а если считал то сколько. Соответственно, если родитель попробует читать из pipe, то, вполне вероятно, он считает часть собственных данных, которые станут недоступными для потомка.

  2. Если один из процессов завершился или закрыл свои дескрипторы, то второй этого не заметит, так как pipe на его стороне по-прежнему открыт на чтение и на запись.

Если надо организовать двунаправленную передачу данных, то можно создать два pipe.

Именованные каналы

Именованный канал FIFO доступен как объект в файловой системе. При этом, до открытия объекта FIFO на чтение, собственно коммуникационного объекта не создаётся. После открытия открытия объекта FIFO в одном процессе на чтение, а в другом на запись, возникает ситуация полностью эквивалентная использованию неименованного канала. FIFO создаётся вызовом функции int mkfifo(const char *pathname, mode_t mode);,

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

Правила обмена через канал

Чтение:

  1. При чтении числа байт, меньшего чем находится в канале, возвращается требуемое число байтов, остаток сохраняется для последующих чтений.
  2. При чтении числа байт, большего чем находится в канале, возвращается доступное число байт.
  3. При чтении из пустого канала, открытого каким либо процессом на на запись при сброшенном флаге O_NONBLOCK произойдёт блокировка процесса, а при установленном флаге O_NONBLOCK будет возвращено -1 и установлено значение errno равное EAGAIN.
  4. Если канал пуст и ни один процесс не открыл его на запись, то при чтении из канала будет получено 0 байтов - т.е конец файла.

Запись:

  1. Если процесс пытается записать данные в канал, не открытый ни одним процессом на чтение, то процессу отправляется сигнал SIGPIPE. Если не установлена обработка сигнала, то процесс завершается, в противном случае вызов write() возвращает -1 с установкой ошибки EPIPE.
  2. Запись числа байт меньше чем PIPE_BUF выполняется атомарно. При записи из нескольких процессов данные не перемешиваются.
  3. При записи числа байт больше чем PIPE_BUF атомарность операции не гарантируется.
  4. Если флаг O_NONBLOCK не установлен, то запись может быть заблокирована, но в конце концов будет возвращено значение, указывающее, что все байты записаны.
  5. Если флаг O_NONBLOCK установлен и записывается меньше чем PIPE_BUF, то возможны два варианта: если есть достаточно свободного места в буфере, то производится атомарная запись, если нет, то возвращается -1, а errno выставляется в EAGAIN.
  6. Если флаг O_NONBLOCK установлен и записывается больше чем PIPE_BUF то возможны два варианта: если в буфере есть хотя бы один свободный байт, то производится запись доступного числа байт, если нет, то возвращается -1, а errno выставляется в EAGAIN.

Процессы

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

UID

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

Процесс может поменять своего владельца и группу в двух случаях:

  1. текущий UID равен 0 (соответствует пользователю root) и процесс обратился к системному вызову setuid(newuid). В этом случае процесс полностью меняет владельца.
  2. процесс обратился к вызову exec(file) загрузив в свою память программу из файла в атрибутах которого выставлен флаг suid. В этом случае владелец процесса сохраняется, но права доступа будут вычисляться на основе UID владельца файла.

PID

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

Значения PID 0 и 1 зарезервированы. Процесс с PID==0 не используется, PID==1 - принадлежит программе init.

Максимальное значение PID в Linux можно посмотреть командой:

cat /proc/sys/kernel/pid_max

По умолчанию это 2^16 (32768) однако его можно увеличить до 2^22:

echo 4194303 > /proc/sys/kernel/pid_max

PID назначаются последовательно. При создании нового процесса вызовом fork ищется PID, больший по значению, чем тот, который был возвращён предыдущим вызовом fork. Если при поиске достигнуто значение pid_max, то поиск продолжается с PID=2

Прикрепленный файлРазмер
Иконка изображения fork-exex-wait.png25.8 КБ

Жизненный цикл процесса

Создание процесса

Вызов newpid=fork() создает новый процесс, являющейся точной копией текущего и отличающийся лишь возвращаемым значением newpid. В родительском процессе newpid равно PID дочернего процесса, в дочернем процессе newpid равно 0. Свой PID можно узнать вызовом mypid=getpid(), родительский – вызовом parentpid=getppid().

Типичный пример

int pid, cpid;
int 
int status;
pid=fork()
if( pid > 0 ){
    cpid=waitpid(&status);
    if( cpid > 0 ){
        printf("Я старый процесс (pid=%i) создал новый (pid=%i) завершившийся с кодом %i\n",
                     getpid(),pid,WEXITSTATUS(status) );
    }
}else if( pid == 0 ){
    printf("Я новый процесс (pid=%i) создан старым (pid=%i)\n",getpid(),getppid());
}else{
    perror("Страшная ошибка:");
}

Запуск программы

В оперативной памяти процесса находятся код и данные, загруженные из файла. При запуске программы из командной строки, обычно создается новый процесс и в его память загружается файл с программой. Загрузка файла делается вызовом одной из функций семейства exec (см. man 3 exec). Функции отличаются способом передачи параметров, а также тем, используется ли переменная окружения PATH для поиска исполняемого файла. Например execl в качестве первого параметра принимает имя исполняемого файла, вторым и последующими – строки аргументы, передаваемые в argv[], и, наконец, последний параметр должен быть NULL, он дает процедуре возможность определить, что параметров больше нет.

int pid=fork();
if( pid > 0 ){
     waitpid(NULL);
}else if( pid == 0 ) {
     if(-1 == execl("/bin/ls","ls","-l",NULL) ) {
          exit(1);
      }
}

Пример exec с двумя ошибками:

if( 0 == execl("/bin/ls","-l",NULL) ){
    printf("Программа ls запущена успешно\n");
}else{
    printf("Программа ls не запущена\n");
}

Ошибка 1: Первый аргумент передаваемый программе это имя самой программы. В данном примере в списке процессов будет видна программа с именем -l, запущенная без параметров.

Ошибка 2: Поскольку код из файла /bin/ls будет загружен в текущий процесс, то старый код и данные, в том числе printf("Программа ls запущена успешно\n"), будет затерты. printf не сработает никогда.

Завершение процесса

Процесс может завершиться, получив сигнал или через системный вызов _exit(int status). status может принимать значения от 0 до 255. По соглашению, status==0 означает успешное завершение программы, а ненулевое значение - означает ошибку. Некоторые программы (например kaspersky для Linux) используют статус для возврата некоторой информации о результатах работы программы.

_exit() может быть вызван несколькими путями.

  • return status; в функции main(). В этом случае _exit() выполнит некая служебная функция, вызывающая main()
  • через библиотечную функцию exit(status), которая завершает работу библиотеки libc и вызывает _exit()
  • явным вызовом _exit()

Удаление завершенного процесса из таблицы процессов

После завершения процесса его pid остается занят - это состояние процесса называется "зомби". Чтобы освободить pid родительский процесс должен дождаться завершения дочернего и очистить таблицу процессов. Это достигается вызовом:

pid_t cpid=waitpid(pid_t pid, int *status, int options)
//или
pid_t cpid=wait(int *status)

Вызов wait(&status); эквивалентен waitpid(-1, &status, 0);

waitpid ждет завершения дочернего процесса и возвращает его PID. Код завершения и обстоятельства завершения заносятся в переменную status. Дополнительно, поведением waitpid можно управлять через параметр options.

  • pid < -1 - ожидание завершения дочернего процесса из группы с pgid==-pid
  • pid == -1 - ожидание завершения любого дочернего процесса
  • pid == 0 - ожидание завершения дочернего процесса из группы, pgid которой совпадает с pgid текущего процесса
  • pid > 0 - ожидание завершения любого дочернего процесса с указанным pid

Опция WNOHANG - означает неблокирующую проверку завершившихся дочерних процессов.

Статус завершения проверяется макросами:

  • WIFEXITED(status) - истина если дочерний процесс завершился вызовом _exit(st)
  • WEXITSTATUS(status) - код завершения st переданный в _exit(st)
  • WIFSIGNALED(status) - истина если дочерний процесс завершился по сигналу
  • WTERMSIG(status) - номер завершившего сигнала
  • WCOREDUMP(status)истина если дочерний процесс завершился с дампом памяти
  • WIFSTOPPED(status) истина если дочерний процесс остановлен
  • WSTOPSIG(status) - номер остановившего сигнала
  • WIFCONTINUED(status) истина если дочерний процесс перезапущен

Основы планирования процессов

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

Прерывания по таймеру происходят в соответствии с квантом времени, выделенному процессу. В Linux квант времени по умолчанию (DEF_TIMESLICE) равен 0,1 секунды, но может быть пересчитан планировщиком процессов (sheduler).

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

В момент возврата в пользовательскую программу происходит доставка сигналов - т.е. вызов процедуры обработчика сигнала, остановка, перезапуск или завершение процесса. Некоторые сигналы (SIGSTOP) - приводят к тому, что процесс включается в список остановленных процессов, которые не поступают в очередь процессов на исполнение. Сигнал SIGCONT возвращает остановленный процесс в очередь процессов на исполнение, сигнал SIGKILL завершает остановленный процесс.

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

На диаграмме показаны различные состояния процесса

В Linux команда ps использует следующие обозначения состояния процесса:

  • R выполняется (в том числе в обработчике сигнала) или стоит в очереди на выполнение
  • S системный вызов ожидает ресурс, но может быть прерван
  • D системный вызов ожидает ресурс, и не может быть прерван (обычно это ввод/вывод)
  • T остановлен сигналом
  • t остановлен отладчиком
  • Z "Зомби" - завершён, но не удалён из списка процессов родительским процессом.

Планировщик процессов

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

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

  1. Было определено 140 приоритетов (100 реального времени + 40 назначаемых динамически), каждый из которых получил свою очередь FIFO. На запуск выбирается первый процесс в самой приоритетной очереди.
  2. В многопроцессорных системах для каждого ядра был сформирован свой набор из 140 очередей. Раз в 0,2 секунды просматриваются размеры очередей процессоров и, при необходимости балансировки, часть процессов переносится с загруженных ядер на менее загруженные
  3. Динамический приоритет назначается процессу в зависимости от отношении времени ожидания ресурсов к времени пребывания в состоянии выполнения. Чем дольше процесс ожидал ресурс, тем выше его приоритет. Таким образом, диалоговые задачи, которые 99% времени ожидают пользовательского ввода, всегда имеют наивысший приоритет.

Ссылка: Планировщик задач Linux

Прикладной программист может дополнительно понизить приоритет процесса функцией int nice(int inc); (в Linux nice() - интерфейс к вызову setpriority()). Большее значение nice означает меньший приоритет. В командной строке используется "запускалка" с таким же именем:

nice -n 50 command

Процесс Idle

Если нет процессов готовых для выполнения, то планировщик вызывает нить (процесс) Idle. В Linux 2.2 однопроцессорная кроссплатформенная версия Idle выглядела так:

int cpu_idle(void *unused) {
     for(;;)
      idle();
}

В аппаратно-зависимую реализацию idle() может быть вынесено управление энергосбережением.

В ранних версиях Linux процесс Idle имел PID=0, но, вообще говоря, Idle как самостоятельный процесс не существует.

Вычисление средней загрузки

Средняя загрузка (Load Average, LA) - усредненная мера использования ресурсов компьютера запущенными процессами. Величина LA пропорциональна числу процессоров в системе и на ненагруженной системе колеблется от нуля до значения, равного числу процессоров. Высокие значения LA (10*число ядер и более) говорят о чрезмерной нагрузке на систему и потенциальных проблемах с производительностью.

В классическом Unix LA имеет смысл среднего количества процессов в очереди на исполнение + количества выполняемых процессов за единицу времени. Т.е. LA == 1 означает, что в системе считается один процесс, LA > 1 определяет сколько процессов не смогли стартовать, поскольку им не хватило кванта времени, а LA < 1 означает, что в системе есть незагруженные ядра.

В Linux к к количеству процессов добавили ещё и процессы, ожидающих ресурсы. Теперь на рост LA значительно влияют проблемы ввода/вывода, такие как недостаточная пропускная способность сети или медленные диски.

LA усредняется по следующей формуле LAt+1=(LAcur+LAt)/2. Где LAt+1 - отображаемое значение в момент t+1, LAcur - текущее измеренное значение, LAt - значение отображавшееся в момент t. Таким образом сглаживаются пики и после резкого падения нагрузки значение LA будет медленно снижаться, а кратковременный пик нагрузки будет отображен половинной величиной LA.

Ссылка: Как считается Load Average

Выдача команды top

Выдача команды top в Linux на компьютере с 36 ядрами:

top - 19:43:53 up 4 days,  5:54,  1 user,  load average: 34.07, 33.75, 33.80
Tasks: 550 total,  12 running, 538 sleeping,   0 stopped,   0 zombie
%Cpu(s): 93.9 us,  0.5 sy,  0.0 ni,  5.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

На компьютере запущена многопоточная счётная задача, которая занимает почти все ядра и не использует ввод/вывод. LA немного меньше 36, что согласуется с распределением времени процессора: 93.9 us - пользователь, 0.5 sy - ядро, 5.5 id - Idle, 0.0 wa - ожидание устройств.

Прикрепленный файлРазмер
Иконка изображения sheduler.png58.86 КБ

Эффективные права процесса

С каждым процессом Unix связаны два атрибута uid и gid - пользователь и основная группа. В принципе они могли бы определять права доступа процесса к ФС и другим процессам, однако существует несколько ситуаций, когда права процесса отличаются от прав его владельца. Поэтому кроме uid/gid (иногда называемых реальными ruid/rgid) с процессом связаны атрибуты прав доступа - эффективные uid/gid - euid/egid, чаще всего совпадающие с ruid/rgid. Кроме того, с uid связан список вспомогательных групп, описанных в файле /etc/group . euid/egid и список групп определяют права доступа процесса к ФС. Вновь создаваемые файлы наследуют атрибуты uid/gid от euid/egid процесса. Кроме того euid определяет права доступа к другим процессам (отладка, отправка сигналов и т.п.).

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

Примечание: в современных ОС особые привилегии процесса определяются через набор особых флагов - capabilities и не обязательно привязаны к euid=0.

(re)uid/(re)gid, а также вспомогательные группы, наследуются от родительского процесса при вызове fork(). При вызове exec() ruid/rgid сохраняются, а euid/egid могут быть изменены если у исполняемого файла выставлен флаг смены владельца. Для скриптов флаг смены владельца игнорируется т.к. фактически запускается интерпретатор, а скрипт передаётся ему в качестве параметра. В момент входа пользователя в систему программа login считывает из файлов /etc/passwd и /etc/group необходимые величины и устанавливает их перед загрузкой командного интерпретатора.

Список вспомогательных групп можно считать в массив функцией int getgroups(int size, gid_t list[]). Будет ли при этом в списке основная группа неизвестно - это зависит от реализации конкретной ОС. Максимальное число вспомогательных групп можно получить так: long ngroups_max = sysconf(_SC_NGROUPS_MAX); или из командной строки getconf NGROUPS_MAX. В моём Linux'е максимальное число групп - 65536.

Для инициализации вспомогательных групп в Linux можно воспользоваться функцией int initgroups(const char *user, gid_t group); эта функция разбирает файл /etc/group, а за тем обращается к системному вызову int setgroups(size_t size, const gid_t *list);.

Существуют несколько функций для управление атрибутами uid/gid. Для экономии места далее перечисляются только функции для работы с uid. Получить значения атрибутов можно с помощью функций getuid(), geteuid() Установить значения можно с помощью
setuid(id); - установить ruid и euid в id
seteuid(id); - установить euid в id
setreuid(rid,eid); - установить ruid и euid в rid и eid. -1 в качестве параметра означает, что значение не меняется

В Linux, HP-UX и некоторых других ОС дополнительно поддерживаются атрибут сохраненных прав процесса suid/sgid (не путать с одноименными атрибутами файла). Соответственно есть функция для установки всех трёх атрибутов setresuid(rid,eid,sid);

Если euid=0 или ruid=0 то ruid и euid могут меняться произвольно. Т.е. можно сделать euid<>0 или ruid<>0, а затем вернуться в состояние euid=ruid=0. Если оба атрибута не равны нулю, то возможно лишь изменение euid в ruid (отказ от дополнительных прав). Программа su получает euid=0 благодаря соответствующему атрибуту файла и использует возможности привилегированного процесса для запуска программ от имени произвольного пользователя (в том числе root). Веб-сервер apache, наоборот, стартует с ruid=euid=0, но затем отбирает у себя лишние права меняя ruid и euid на непривилегированные значения.

Сигналы, группы, сеансы

Сигналы в Unix указывают ядру, что надо прервать нормальное планирование процесса и завершить/остановить его, или, вместо продолжения выполнения процесса с места остановки, выполнить функцию - обработчик сигнала, и лишь затем продолжить выполнение основного кода.

Сигналы, предназначенные процессу, создаются (отправляются) в нескольких ситуациях: при аппаратных сбоях, при срабатывании особого таймера, при обработке спецсимволов (Ctrl C, Ctrl Z) драйвером управляющего терминала, с помощью системного вызова kill(). В зависимости от причины, отправляются сигналы разных типов. Тип сигнала обозначается целым числом (номером). В Linux сигналы нумеруются от 1 до 64. Сигнал может быть отправлен отдельной нити процесса, процессу в целом или группе процессов.

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

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

Сигналы SIGKILL и SIGSTOP не могут быть заблокированы или проигнорированы и на них нельзя установить свой обработчик.

Действия по умолчанию:

  • SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU - остановка процесса
  • SIGCONT - запуск остановленного процесса
  • SIGCHLD - игнорируется
  • SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGFPE, SIGSEGV - сохранение дампа памяти и завершение (Linux)
  • остальные - завершение процесса

Ссылка: ПРАВИЛА ИГРЫ В СИГНАЛЫ UNIX

Сигнал, маска, обработчик

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

Номер Есть Сигнал? Маска Обработчик
1 1 1 SIG_DFL
2 0 0 SIG_IGN
3 0 0 sig_func()
... ... ... ...
  • Есть сигнал? - битовое поле, указывающее наличие недоставленного сигнала
  • Маска - битовое поле, указывающее временный запрет на доставку сигнала
  • Обработчик - указатель на действие, выполняемое при доставке сигнала. Может принимать значения: SIG_DFL - действие по умолчанию, SIG_IGN - игнорирование сигнала или указатель на функцию - обработчика сигнала.

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

Примеры использования сигналов

SIGKILL, SIGTERM, SIGINT, SIGHUP - завершение процесса. SIGKILL - не может быть проигнорирован, остальные могут. SIGTERM - оповещение служб о завершении работы ОС, SIGINT - завершение программы по нажатию Ctrl C, SIGHUP - оповещение программ, запущенных через модемное соединение, об обрыве связи (в настоящее время практически не используется).

SIGILL, SIGFPE, SIGBUS, SIGSEGV - аппаратный сбой. SIGILL - недопустимая инструкция CPU, SIGFPE - ошибка вычислений с плавающей точкой (деление на ноль), SIGBUS - физический сбой памяти, SIGSEGV - попытка доступа к несуществующим (защищенным) адресам памяти.

SIGSTOP, SIGCONT - приостановка и продолжение выполнения процесса

SIGPIPE - попытка записи в канал или сокет, у которого нет читателя

SIGCHLD - оповещение о завершении дочернего процесса.

Список сигналов

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

В Linux используется 64 сигнала. Список можно посмотреть в терминале командой kill -l

Posix.1-1990

Источник - man 7 signal

   Signal     Value     Action   Comment
   -------------------------------------------------------------------------
   SIGHUP        1       Term    Hangup detected on controlling terminal
                                 or death of controlling process
   SIGINT        2       Term    Interrupt from keyboard
   SIGQUIT       3       Core    Quit from keyboard
   SIGILL        4       Core    Illegal Instruction
   SIGABRT       6       Core    Abort signal from abort(3)
   SIGFPE        8       Core    Floating point exception
   SIGKILL       9       Term    Kill signal
   SIGSEGV      11       Core    Invalid memory reference
   SIGPIPE      13       Term    Broken pipe: write to pipe with no readers
   SIGALRM      14       Term    Timer signal from alarm(2)
   SIGTERM      15       Term    Termination signal
   SIGUSR1   30,10,16    Term    User-defined signal 1
   SIGUSR2   31,12,17    Term    User-defined signal 2
   SIGCHLD   20,17,18    Ign     Child stopped or terminated
   SIGCONT   19,18,25    Cont    Continue if stopped
   SIGSTOP   17,19,23    Stop    Stop process
   SIGTSTP   18,20,24    Stop    Stop typed at tty
   SIGTTIN   21,21,26    Stop    tty input for background process
   SIGTTOU   22,22,27    Stop    tty output for background process

Отправка сигнала

int kill(pid_t pid, int signum);

Для отправка сигнала необходимо, чтобы uid или euid текущего процесса был равен 0 или совпадал с uid процесса получателя.

Получатель сигнала зависит от величины и знака параметра pid:

  • pid > 0 - сигнал отправляется конкретному процессу
  • pid == 0 - сигнал отправляется всем членам группы
  • pid == -1 - сигнал отправляется всем процессам кроме init (в Linux'е еще кроме себя)
  • pid < -1 - сигнал отправляется группе с номером -pid

Если signum == 0 - сигнал не посылается, но делается проверка прав на посылку сигнала и формируются код ответа и errno.

int sigqueue(pid_t pid, int sig, const union sigval value);

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

rice(int signum);

Отправка сигнала текущему процессу. Эквивалент kill(getpid(),signum);

abort();

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

alarm(time);

Отправка сигнала SIGALRM себе через time секунд. Возвращает 0 если ранее alarm не был установлен или число секунд остававшихся до срабатывания предыдущего alarma. Таймер управляющий alarm'ами один. Соответственно установка нового alarm'а отменяет старый. Параметр time==0 позволяет получить оставшееся до alarm'а время без установки нового.

Исторический способ обработки сигнала - signal

Установка реакции на сигнал через функцию signal() не до конца стандартизована и сохраняется для совместимости с историческими версиями Unix. Не рекомендуется к использованию. В стандарте POSIX signal() заменен на вызов sigaction(), сохраняющий для совместимости эмуляцию поведения signal().

signal(SIGINT,sighandler);

sighandler - адрес функции обработчика void sighandler(int) или один из двух макросов: SIG_DFL (обработчик по умолчанию) или SIG_IGN (игнорирование сигнала).

signal(...) возвращает предыдущее значение обработчика или SIG_ERR в случае ошибки.

В Linux и SysV при вызове sighandler обработчик сбрасывается в SIG_DFL и возможна доставка нового сигнала во время работы sighandler. Такое поведение заставляет первой строкой в sighandler восстанавливать себя в качестве обработчика сигнала, и, даже в этом случае, не гарантирует от вызова обработчика по умолчанию. В BSD системах сброс не происходит, доставка новых сигнала блокируется до выхода из sighandler.

Пример кода:

#include <signal.h>
void sighandler(int signum) {
   signal(signum,sighandler);
   ...
}

main() {
      signal(SIGINT,sighandler);
      signal(SIUSR1,SIG_IGN);
      signal(SIUSR2,SIG_DFL);
}

Реакция на сигнал - sigaction

Параметры для установки обработчика сигнала через sigaction()

struct sigaction {
   void (*sa_handler)(int);                        // Обработчик сигнала старого стиля
   void (*sa_sigaction)(int, siginfo_t *, void *); // Обработчик сигнала нового стиля
                                                   // Обработчик выбирается на основе флага SA_SIGINFO
                                                   // в поле sa_flags
   sigset_t sa_mask; // Маска блокируемых сигналов 
   int sa_flags; // Набор флагов
        //  SA_RESETHAND - сброс обработчика на SIG_DFL после выхода из назначенного обработчика.
        //  SA_RESTART - восстановление прерванных системных вызовов после выхода из обработчика.
        //  SA_SIGINFO - вызов sa_sigaction вместо sa_handler

  void (*sa_restorer)(void); // Устаревшее поле
}

Данные, передаваемые в обработчик сигнала sa_sigaction()

siginfo_t {
  int      si_signo;  // Номер сигнала
  int      si_code;   // Способ отправки сигнала или уточняющее значение
                      // SI_USER сигнал отправлен через вызов kill()
                      // SI_QUEUE сигнал отправлен через вызов sigqueue()
                      // FPE_FLTDIV - уточнение для сигнала SIGFPE - деление на ноль
                      // ILL_ILLOPC - уточнение для сигнала SIGILL - недопустимый опкод
                      // ...                         
  pid_t    si_pid;    // PID процесса отправителя (дочернего процесса при SIGCHLD)
  uid_t    si_uid;    // UID процесса отправителя
  int      si_status; // Статус завершения дочернего процесса при SIGCHLD
  sigval_t si_value;  // Значение, переданое через параметр value при вызове sigqueue()
  void *   si_addr;   // Адрес в памяти
                      // SIGILL, SIGFPE - адрес сбойной инструкции
                      // SIGSEGV, SIGBUS - адрес сбойной памяти
}

В Linux структура siginfo_t содержит больше полей, но они служат для совместимости и не заполняются.

Код

#include <signal.h>
void myhandler(int sig) {
   ...
}
void myaction(int signum, siginfo_t * siginfo, void *code)  {
...
}

main() {
struct sigaction act,oldact;
    act.sa_sigaction=myaction;
    act.sa_flags=SA_SIGINFO;
    sigaction(signum, &act, &oldact);
}

Блокирование сигналов

Все функции блокирования сигналов работают с маской сигналов типа sigset_t. В Linux это 64 бита, т.е. два слова в 32х разрядной архитектуре или одно в 64х разрядной. Выставленный бит в маске сигналов означает, что доставка сигнала с соответствующим номером будет заблокирована. Сигналы SIGKILL и SIGSTOP заблокировать нельзя.

Манипулирование маской

#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); Изменить маску сигналов

Параметр how определяет операцию над текущей маской. Значения SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK.

Проверка наличия заблокированных сигналов

int sigpending(sigset_t *set); // Получить список недоставленных из-за блокировки сигналов.
int sigsuspend(const sigset_t *mask);  //Установить новую маску и "уснуть" до получения и обработки разрешенного в ней сигнала.

Очистка заблокированных сигналов

sigwait ожидает блокировки какого-либо из сигналов, указанных в маске, удаляет этот сигнал и возвращает его номер в параметр sig. Если реализация сигналов предусматривает очередь сигналов, то удаляется только один элемент из очереди.

int sigwait(const sigset_t *set, int *sig);

Управляющий терминал, сеанс, группы

Управляющий терминал, сеанс, группы

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

С точки зрения ядра - терминальная сессия - это группа процессов, имеющих один идентификатор сеанса sid. С идентификатором sid связан драйвер управляющего терминала, доступный всем членам сеанса как файл символьного устройства /dev/tty. Для каждого сеанса существует свой /dev/tty. Управляющий терминал взаимодействует с процессами сеанса с помощью отправки сигналов.

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

Группа процессов

Группа процессов - инструмент для доставки сигнала нескольким процессам, а также способ арбитража при доступе к терминалу. Идентификатор группы pgid равен pid создавшего её процесса - лидера группы. Процесс может переходить из группы в группу внутри одного сеанса.

#include <unistd.h>

int setpgid(pid_t pid, pid_t pgid); // включить процесс pid в группу pgid.
                                             // pid=0 означает текущий процесс,
                                             // pgid=0 означает pgid=pid текущего процесса
                                             // pid=pgid=0 - создание новой группы с pgid=pid текущего процесса
                                             //                      и переход в эту группу
pid_t getpgid(pid_t pid); // получить номер группы процесса pid.
                       // pid=0 - текущий процесс
int setpgrp(void); // создание группы, эквивалент setpgid(0.0);
pid_t getpgrp(void); // запрос текущей группы, эквивалент getpgid(0);

Сеанс

Сеанс - средство для контроля путем посылки сигналов над несколькими группами процессов со стороны терминального драйвера. Как правило соответствует диалоговой пользовательской сессии. Идентификатор сеанса sid равняется идентификатору pid, создавшего его процесса - лидера сеанса. Одновременно с сеансом создаётся новая группа с pgid равным pid лидера сеанса. Поскольку переход группы из сеанса в сеанс невозможен, то создающий сеанс процесс не может быть лидером группы.

#include <unistd.h>
pid_t setsid(void); //Создание  новой группы и нового сеанса. Текущий процесс не должен быть лидером группы.
pid_t getsid(pid_t pid); //Возвращает номер сеанса для указанного процесса

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

if( fork() ) exit(0);
setsid();

Фоновая группа сеанса

Процессы в фоновой группе выполняются до тех пор, пока не попытаются осуществить чтение или запись через файловый дескриптор управляющего терминала. В этот момент они получают сигнал SIGTTIN или SIGTTOU соответственно. Действие по умолчанию для данного сигнала - приостановка выполнения процесса.

Назначение фоновой группы:

#include <unistd.h>

 pid_t tcgetpgrp(int fd); // получить pgid фоновой группы, связанной с управляющим терминалом, 
                     // на который ссылается файловый дескриптор fd
 int tcsetpgrp(int fd, pid_t pgrp); // назначить pgid фоновой группы терминалу,
                    // на который ссылается файловый дескриптор fd

Управляющий терминал

Некоторые сочетания клавиш позволяют посылать сигналы процессам сеанса:

  • ^C - SIGINT - завершение работы
  • ^Z - SIGTSTP - приостановка выполнения. bash отслеживает остановку дочерних процессов и вносит их в списки своих фоновых процессов. Остановленный процесс может быть продолжен командой fg n, где n - порядковый нрмер процесса в списке фоновых процессов

Открыть управляющий терминал сеанса

#include <stdio.h>

char name[L_ctermid];
int fd;
ctermid(name); // если name=NULL, то используется внутренний буфер
           // ctermid возвращает указатель на буфер
           // L_ctermid - библиотечная константа
fd = open(name, O_RDWR, 0);

Управление памятью

Адресация в архитектуре i386

Ссылка: Исследование модели памяти Linux

Распределение адресов в памяти процесса Linux i386

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

Изменение сегмента данных

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

 #include <unistd.h>
 int brk(void *addr); //явное задание адреса
 void *sbrk(intptr_t increment); //задание адреса относительно текущего значения
                                 // возвращает предыдущее значение адреса границы 

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

Вызов sbrk(0) позволяет узнать текущую границу сегмента памяти.

Файлы, отображаемые в память

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

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
int msync(void *addr, size_t length, int flags);

int munmap(void *addr, size_t length);

Функция mmap() отображает length байтов, начиная со смещения offset файла, заданного файловым дескриптором fd, в память, начиная с адреса addr. Параметр addr является рекомендательным, и обычно бывает выставляется в NULL. mmap() возвращает фактический адрес отображения или значение MAP_FAILED (равное (void *) -1) в случае ошибки.

Аргумент prot описывает режим доступа к памяти (не должен конфликтовать с режимом открытия файла).

  • PROT_EXEC данные в отображаемой памяти могут исполняться
  • PROT_READ данные можно читать
  • PROT_WRITE в эту память можно писать
  • PROT_NONE доступ к этой области памяти запрещен.

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

  • MAP_SHARED запись в эту область памяти будет эквивалентна записи в файл и изменения доступны другим процессам. Файл может не обновляться до вызова функций msync() или munmap().
  • MAP_PRIVATE неразделяемое отображение с механизмом copy-on-write. Запись в эту область памяти не влияет на файл.
  • MAP_ANONYMOUS память не отображается ни в какой файл, аргументы fd и offset игнорируются. Совместно с MAP_SHARED может использоваться для создания общей памяти, разделяемой с дочерними процессами
  • MAP_FIXED память обязательно отображается с адреса addr или возвращается ошибка. Не рекомендуется к использованию, так как сильно зависит от архитектуры процессора и конкретной ОС.

msync() сбрасывает изменения сделанные в памяти в отображенный файл. Параметры addr и length позволяют синхронизировать только часть отображенной памяти.

munmap() отменяет отображение файла в память и сохраняет сделанные в памяти изменения в файл.

Прикрепленный файлРазмер
Иконка изображения memory-maps.png33.08 КБ

Тестовая программа

Тестовая программа

#include <unistd.h>
#include <malloc.h>
#include <stdio.h>
#include <sys/mman.h>

int bss, data=10;
int main(int argc, char *argv[])
{
  int stack;
  void *heap, *brk, *mmp;
  heap=malloc(1);
  brk=sbrk(0);
  mmp=mmap(0,1,PROT_READ,MAP_SHARED|MAP_ANONYMOUS,-1,0);
  printf("Text=%p\nData=%p\nBSS=%p\nHeap=%p\nBrk=%p\nlibc.so=%p\nMmap=%p\nStack=%p\nArgv=%p\n ",main,&data,&bss,heap,brk,printf,mmp,&stack,argv);
  sleep(100);
  return 0;
}

Вывод тестовой программы

$ ./testmap
Text   =0x0045c690
Data   =0x0045e02c
BSS    =0x0045e034
Heap   =0x01435008
Brk    =0x01456000
libc.so=0xb75bb940
Mmap   =0xb773a000
Stack  =0xbfe8f850
Argv   =0xbfe8f924

Распределение памяти тестовой программы

$ ./testmap >/dev/null &
[1] 6662

$ cat /proc/6662/maps 
0042a000-0042b000 r-xp 00000000 08:01 305009     /home/student/testmap
0042b000-0042c000 r--p 00000000 08:01 305009     /home/student/testmap
0042c000-0042d000 rw-p 00001000 08:01 305009     /home/student/testmap
00ece000-00eef000 rw-p 00000000 00:00 0          [heap]
b7603000-b77b4000 r-xp 00000000 08:01 151995     /lib/i386-linux-gnu/libc-2.24.so
b77b4000-b77b5000 ---p 001b1000 08:01 151995     /lib/i386-linux-gnu/libc-2.24.so
b77b5000-b77b7000 r--p 001b1000 08:01 151995     /lib/i386-linux-gnu/libc-2.24.so
b77b7000-b77b8000 rw-p 001b3000 08:01 151995     /lib/i386-linux-gnu/libc-2.24.so
b77b8000-b77bb000 rw-p 00000000 00:00 0 
b77cb000-b77cc000 r--s 00000000 00:05 100324     /dev/zero (deleted)
b77cc000-b77ce000 rw-p 00000000 00:00 0 
b77ce000-b77d0000 r--p 00000000 00:00 0          [vvar]
b77d0000-b77d2000 r-xp 00000000 00:00 0          [vdso]
b77d2000-b77f5000 r-xp 00000000 08:01 138298     /lib/i386-linux-gnu/ld-2.24.so
b77f5000-b77f6000 r--p 00022000 08:01 138298     /lib/i386-linux-gnu/ld-2.24.so
b77f6000-b77f7000 rw-p 00023000 08:01 138298     /lib/i386-linux-gnu/ld-2.24.so
bff5d000-bff7e000 rw-p 00000000 00:00 0          [stack]


$ uname -a
Linux antix-1 4.9.87-antix.1-486-smp #1 SMP Tue Mar 13 12:29:54 EDT 2018 i686 GNU/Linux

Формат исполняемых файлов

##Тестовая программа##

Программа size выдает размер секций исполняемого файла. По умолчанию выдаётся суммарный размер по типам в формате Berkley:

$ size /bin/ls
text data bss dec hex filename
103119 4768 3360 111247 1b28f /bin/ls

Опция -A выдает все секции в формате SystemV:

$ size -A /bin/ls
/bin/ls :
section size addr
.interp 28 4194872
.note.ABI-tag 32 4194900
...
.init 26 4202832
.plt 1808 4202864
.plt.got 24 4204672
.text 65866 4204704
.fini 9 4270572
...
.data 576 6402976
.bss 3360 6403552
.gnu_debuglink 16 0
.gnu_debugdata 3296 0
Total 114559

##Заголовок ELF##
$ objdump -f ./mmp

./mmp: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080483e0

##Заголовки секций##

$ objdump -h ./mmp

./mmp: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 00000013 08048154 08048154 00000154 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 08048168 08048168 00000168 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 08048188 08048188 00000188 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .gnu.hash 00000024 080481ac 080481ac 000001ac 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .dynsym 00000090 080481d0 080481d0 000001d0 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynstr 00000063 08048260 08048260 00000260 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .gnu.version 00000012 080482c4 080482c4 000002c4 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version_r 00000020 080482d8 080482d8 000002d8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .rel.dyn 00000008 080482f8 080482f8 000002f8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rel.plt 00000030 08048300 08048300 00000300 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .init 00000023 08048330 08048330 00000330 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .plt 00000070 08048360 08048360 00000360 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt.got 00000008 080483d0 080483d0 000003d0 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .text 00000242 080483e0 080483e0 000003e0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .fini 00000014 08048624 08048624 00000624 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .rodata 00000058 08048638 08048638 00000638 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame_hdr 0000002c 08048690 08048690 00000690 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .eh_frame 000000b0 080486bc 080486bc 000006bc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .init_array 00000004 08049f08 08049f08 00000f08 2**2
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000004 08049f0c 08049f0c 00000f0c 2**2
CONTENTS, ALLOC, LOAD, DATA
20 .jcr 00000004 08049f10 08049f10 00000f10 2**2
CONTENTS, ALLOC, LOAD, DATA
21 .dynamic 000000e8 08049f14 08049f14 00000f14 2**2
CONTENTS, ALLOC, LOAD, DATA
22 .got 00000004 08049ffc 08049ffc 00000ffc 2**2
CONTENTS, ALLOC, LOAD, DATA
23 .got.plt 00000024 0804a000 0804a000 00001000 2**2
CONTENTS, ALLOC, LOAD, DATA
24 .data 00000008 0804a024 0804a024 00001024 2**2
CONTENTS, ALLOC, LOAD, DATA
25 .bss 00000008 0804a02c 0804a02c 0000102c 2**2
ALLOC
26 .comment 0000002d 00000000 00000000 0000102c 2**0
CONTENTS, READONLY

SystemV IPC

Утилиты командной строки

Просмотр информации о подсистеме SysV IPC

lsipc

Создание объекта

ipcmk -Q  # -Q queue, -M size - shmem -S number - semaphore

Просмотр объектов

ipcs

Удаление объектов

ipcrm

Пространство имён

Для формирование используются две составляющие: файл в файловой системе (точнее номер его Inode) и целое число. При выборе файла, для формирования имени, необходимо обеспечить достаточную длительность его жизни. Если файл удалён, то новые процессы не смогут получить доступ к объектам IPC , в имени которых использовался данный файл. Удаление файла и создание нового с тем же именем так же нарушит доступ к объектам IPC поскольку новый файл будет иметь отличный номер Inode.

   #include <sys/types.h>
   #include <sys/ipc.h>

   key_t ftok(const char *pathname, int proj_id);

Права доступа и операции

При создании используются флаги

   IPC_CREAT     Create entry if key doesn't exist.
   IPC_EXCL      Fail if key exists.
   IPC_PRIVATE   Private key.

и права доступа. Права доступа аналогичны файлам (mode - rwxrwxrwx)

Управление объектами SysV IPC (в том числе их уничтожение) ведется c помощью функций msgctl, semctl, shmctl. Параметр cmd этих функций может принимать значения:

   IPC_RMID      Remove resource.
   IPC_SET       Set resource options.
   IPC_STAT      Get resource options.

Дополнительный флаг

   IPC_NOWAIT    Error if request must wait.

MSG

Очередь сообщений

   #include <sys/types.h>
   #include <sys/ipc.h>
   #include <sys/msg.h>

   int msgget(key_t key, int msgflg);
   int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
   ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
                  int msgflg);

   struct msgbuf {
           long mtype;       /* message type, must be > 0 */
           char mtext[1];    /* message data */
   };

   int msgctl(int msqid, int cmd, struct msqid_ds *buf);

SEM

Семафоры

   #include <sys/types.h>
   #include <sys/ipc.h>
   #include <sys/sem.h>

   int semget(key_t key, int nsems, int semflg);
   int semop(int semid, struct sembuf *sops, unsigned nsops);

struct sembuf {
      unsigned short sem_num;  /* semaphore number */
       short          sem_op;   /* semaphore operation */
       short          sem_flg;  /* operation flags */
}

int semctl(int semid, int semnum, int cmd, ...);

SHMEM

Общая память

   #include <sys/ipc.h>
   #include <sys/shm.h>

   int shmget(key_t key, size_t size, int shmflg);
   void *shmat(int shmid, const void *shmaddr, int shmflg);
   int shmdt(const void *shmaddr);
   int shmctl(int shmid, int cmd, struct shmid_ds *buf);

Сокеты

Сокет - универсальный интерфейс для создания каналов (часто один ко многим) для межпроцессного взаимодействия.

Классификация сокетов

Stream

  • Непрерывный поток байтов
  • Упорядоченная передача данных
  • Надёжная доставка данных

Datagram

  • Деление потока данных на отдельные записи
  • Упорядоченная передача записей
  • Возможна потеря записей

Sequential packets

Использовался в Sequence Packet Protocol для Xerox Network Systems. Не реализован в TCP/IP, но может быть имитирован в TCP через Urgent Pointer (http://urchin.earth.li/~twic/Sequenced_Packets_Over_Ordinary_TCP.html).

  • Деление потока данных на отдельные записи
  • Упорядоченная передача данных
  • Надёжная доставка данных

Raw

  • Управление нижележащим сетевым драйвером

Имена сокетов

  • Inet - сокеты именуются с помощью IP адресов и номеров портов
  • Unix - сокетам даются имена в ФС
  • IPX - имена на основе MAC-адресов сетевых карт
  • ... - возможны и другие варианты

TCP/IP

  • TCP = Stream
  • UDP = Datagram
  • ICMP = RAW
  • Sequential packets - были экспериментальные реализации в 1990-х

API Сокетов

Создание сокета

#include <sys/types.h>
#include <sys/socket.h>

int s = socket(int domain, int type, int protocol);

domain

  • PF_UNIX - внутреннее межпроцессное взаимодействие
  • PF_INET - стек TCP/IP

type

  • SOCK_DGRAM - ненадежная передача данных с сохранением границ сообщений (соответствует протоколу UDP),
  • SOCK_STREAM - надежная передача данных без сохранения границ сообщений (соответствует протоколу TCP),
  • SOCK_SEQ - надежная передача данных с сохранением границ сообщений (в стеке TCP/IP не поддерживается),
  • SOCK_RAW - низкоуровневый доступ к протоколу (уровень IP, ICMP).

protocol == 0 - автовыбор

Назначение имени

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *localaddr, int addrlen);

struct sockaddr {
  u_short   sa_family;
  char      sa_data[14];
};


struct sockaddr_un {
  short sun_family; /* AF_UNIX */
  char  sun_path[108];
};

struct sockaddr_in {
  short         sin_family; /* AF_INET */
  u_short       sin_port;       /* Port Number */
  struct in_addr    sin_addr;/* Internet address */
  char          sin_zero[8];    /*Not used*/
}

struct in_addr {
  unsigned long int s_addr;
}

Соединение с сервером

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *servaddr, int addrlen);

Прослушивание сокетов сервером

#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

Обработка запроса клиента

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *clntaddr, int *addrlen);

Чтение/запись данных

#include <sys/types.h>
#include <sys/socket.h>

int send(int sockfd, const char *msg, int len, int flags);
int sendto(int sockfd, const char *msg, int len, int flags,const struct sockaddr *toaddr, int tolen) ;
int recv(int sockfd, char *buf, int len, int flags);
int recvfrom(int sockfd, char *buf, int len, int flags, struct sockaddr *fromaddr, int *fromlen);

Управление окончанием соединения

Вызов close() закрывает сокет. Для контроля над закрытием потоковых сокетов используется вызов shutdown (), который позволяет управлять окончанием соединения.

int shutdown () (int sock, int cntl);

Аргумент cntl может принимать следующие значения:

  • 0: больше нельзя получать данные из сокета;
  • 1: больше нельзя посылать данные в сокет;
  • 2: больше нельзя ни посылать, ни принимать данные на сокет.

Асинхронные операции - select

/* According to earlier standards */
#include
#include
#include

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

#include

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};