PR_СП_лекции_укр

Міністерство освіти і науки України
ОДЕСЬКА ДЕРЖАВНА АКАДЕМІЯ ХОЛОДУ



Жуковецька С.Л.

CИСТЕМНЕ
ПРОГРАМУВАННЯ



Навчальний посібник









Одеса 2008
Жуковецька С.Л. Системне програмування: навчальний посібник. Одеська державна академія холоду, 2008. – 97 с.
Посібник призначений для студентів, що навчаються за спеціальностями “Компьютерні системи та мережі” та «Спеціалізовані комп’ютерні системи».
Посібник містить теоретичні основи системного програмування в ОС UNIX. Розглянути питання маніпулювання файловою системою, процесами, взаємодія процесів, використання командного інтерпретатора.


Рецензент: Шпінарьова І.М., к. т. н., доцент кафедри МОКС ОНУ


























(ОДАХ, 2008
Анотація
Курс “Системне програмування” орієнтований на вивчення основних принципів створення системних програм. Завданням курсу є одержання теоретичних знань і практичних навичок у створенні й використанні компонент системного програмного забезпечення. Результатом вивчення курсу повинне бути вміння оперувати із простими й складними структурами даних, користуватися стандартними системними засобами введення й висновку, працювати з дисковими файлами, розробляти драйвери, управляти розподілом пам'яті, файлами, процесами, здійснювати захист інформації від несанкціонованого доступу.
Одна із самих мобільних і масштабованих систем - операційна система UNIX, застосовується на сьогоднішній момент практично у всіх сферах інформаційних додатків: від настільних додатків до найбільших обчислювальних центрів. На базі цієї операційної системи були створені найвідоміші інтерфейси прикладного програмування для стерпного обчислювального середовища POSIX, які по праву можуть вважатися стандартами де-факто для створення сучасного стерпного програмного забезпечення.
У рамках даного курсу даються базові знання за найвідомішими стандартами POSIX, стандартам мови C, а також інтерфейсам прикладного програмування (API) UNIX-подібних систем. Приділяється увага прикладним інструментам розроблювача й базових методів керування програмним проектом. Окреме місце приділяється прийомам і методам міжпроцесної взаємодії, у тому числі й у розподілених структурах - мережах ЕОМ, а так само методам створення розподіленого програмного забезпечення.
Зміст
13 TOC \o "1-3" \h \z \u 1413 LINK \l "_Toc214296771" 141 Середовище програмування ОС UNIX 13 PAGEREF _Toc214296771 \h 1481515
13 LINK \l "_Toc214296772" 141.1 Структура ОС UNIX 13 PAGEREF _Toc214296772 \h 1481515
13 LINK \l "_Toc214296773" 141.2 Створення програми 13 PAGEREF _Toc214296773 \h 14101515
13 LINK \l "_Toc214296774" 141.3 Формати об'єктних файлів 13 PAGEREF _Toc214296774 \h 14131515
13 LINK \l "_Toc214296775" 142 Використання файлової системи UNIX 13 PAGEREF _Toc214296775 \h 14211515
13 LINK \l "_Toc214296776" 142.1 Основні поняття 13 PAGEREF _Toc214296776 \h 14211515
13 LINK \l "_Toc214296777" 142.1.1 Типи та іменування файлів 13 PAGEREF _Toc214296777 \h 14211515
13 LINK \l "_Toc214296778" 142.1.2 Права доступу до файлів 13 PAGEREF _Toc214296778 \h 14221515
13 LINK \l "_Toc214296779" 142.1.3 Cистемные структури керування файлами 13 PAGEREF _Toc214296779 \h 14241515
13 LINK \l "_Toc214296780" 142.1.4 Системні виклики роботи з файлами 13 PAGEREF _Toc214296780 \h 14291515
13 LINK \l "_Toc214296781" 142.2 Створення та відкриття файлів 13 PAGEREF _Toc214296781 \h 14301515
13 LINK \l "_Toc214296782" 142.2.1 Системний виклик open 13 PAGEREF _Toc214296782 \h 14301515
13 LINK \l "_Toc214296783" 142.2.2 Системний виклик create 13 PAGEREF _Toc214296783 \h 14361515
13 LINK \l "_Toc214296784" 142.2.3 Системний виклик close 13 PAGEREF _Toc214296784 \h 14361515
13 LINK \l "_Toc214296785" 142.3 Читання та зипис в файл 13 PAGEREF _Toc214296785 \h 14371515
13 LINK \l "_Toc214296786" 142.3.1 Системний виклик read 13 PAGEREF _Toc214296786 \h 14371515
13 LINK \l "_Toc214296787" 142.3.2 Системний виклик write 13 PAGEREF _Toc214296787 \h 14381515
13 LINK \l "_Toc214296788" 142.4 Позіціонування у файлі 13 PAGEREF _Toc214296788 \h 14391515
13 LINK \l "_Toc214296789" 142.4.1 Системний виклик lseek 13 PAGEREF _Toc214296789 \h 14391515
13 LINK \l "_Toc214296790" 142.4.2 Системні виклики pread і pwrite 13 PAGEREF _Toc214296790 \h 14421515
13 LINK \l "_Toc214296791" 142.4.3 Системні виклики readv і writev 13 PAGEREF _Toc214296791 \h 14441515
13 LINK \l "_Toc214296792" 142.5 Контрольні операції над файлами 13 PAGEREF _Toc214296792 \h 14451515
13 LINK \l "_Toc214296793" 142.5.1 Системні виклики stat, fstat і lstat 13 PAGEREF _Toc214296793 \h 14451515
13 LINK \l "_Toc214296794" 142.5.2 Контроль прав доступу до файлу 13 PAGEREF _Toc214296794 \h 14511515
13 LINK \l "_Toc214296795" 142.5.3 Функція fcntl 13 PAGEREF _Toc214296795 \h 14531515
13 LINK \l "_Toc214296796" 142.5.4 Системні виклики truncate і ftruncate 13 PAGEREF _Toc214296796 \h 14571515
13 LINK \l "_Toc214296797" 142.6 Управління каталогами 13 PAGEREF _Toc214296797 \h 14581515
13 LINK \l "_Toc214296798" 142.6.1 Створення і видалення 13 PAGEREF _Toc214296798 \h 14581515
13 LINK \l "_Toc214296799" 142.6.2 Читання каталога 13 PAGEREF _Toc214296799 \h 14591515
13 LINK \l "_Toc214296800" 142.6.3 Зміна каталога 13 PAGEREF _Toc214296800 \h 14621515
13 LINK \l "_Toc214296801" 142.7 Управління посиланнями 13 PAGEREF _Toc214296801 \h 14631515
13 LINK \l "_Toc214296802" 142.7.1 Управління жорсткими посиланнями 13 PAGEREF _Toc214296802 \h 14631515
13 LINK \l "_Toc214296803" 142.7.2 Управління символічними посиланнями 13 PAGEREF _Toc214296803 \h 14641515
13 LINK \l "_Toc214296804" 142.7.3 Перейменування файлів і каталогів 13 PAGEREF _Toc214296804 \h 14651515
13 LINK \l "_Toc214296805" 143 Керування процесами ОС UNIX 13 PAGEREF _Toc214296805 \h 14681515
13 LINK \l "_Toc214296806" 143.1 Процеси ОС UNIX 13 PAGEREF _Toc214296806 \h 14681515
13 LINK \l "_Toc214296807" 143.2 Сисвизови створення процесів 13 PAGEREF _Toc214296807 \h 14681515
13 LINK \l "_Toc214296808" 143.2.1 fork(2) 13 PAGEREF _Toc214296808 \h 14691515
13 LINK \l "_Toc214296809" 143.2.2 Еxec(2) 13 PAGEREF _Toc214296809 \h 14711515
13 LINK \l "_Toc214296810" 143.3 Завершення процесу 13 PAGEREF _Toc214296810 \h 14741515
13 LINK \l "_Toc214296811" 143.4 Взаємодія процесів в ОС UNIX 13 PAGEREF _Toc214296811 \h 14751515
13 LINK \l "_Toc214296812" 143.5 Сигнали 13 PAGEREF _Toc214296812 \h 14771515
13 LINK \l "_Toc214296813" 143.5.1 Поняття сигналу 13 PAGEREF _Toc214296813 \h 14771515
13 LINK \l "_Toc214296814" 143.5.2 Розробка оброблювачів сигналів 13 PAGEREF _Toc214296814 \h 14791515
13 LINK \l "_Toc214296815" 143.5.3 Генерація сигналів 13 PAGEREF _Toc214296815 \h 14821515
13 LINK \l "_Toc214296816" 143.5.4 Посилка сигналів іншим процесам 13 PAGEREF _Toc214296816 \h 14821515
13 LINK \l "_Toc214296817" 143.5.5 Блокування сигналів 13 PAGEREF _Toc214296817 \h 14841515
13 LINK \l "_Toc214296818" 143.5.6 Очікування сигналу 13 PAGEREF _Toc214296818 \h 14871515
13 LINK \l "_Toc214296819" 143.6 Трасування процесу. 13 PAGEREF _Toc214296819 \h 14881515
13 LINK \l "_Toc214296820" 143.7 Канали й FIFO-Файли 13 PAGEREF _Toc214296820 \h 14891515
13 LINK \l "_Toc214296821" 143.7.1 Поняття 13 PAGEREF _Toc214296821 \h 14891515
13 LINK \l "_Toc214296822" 143.7.2 Створення каналу 13 PAGEREF _Toc214296822 \h 14891515
13 LINK \l "_Toc214296823" 143.7.3 Створення FIFO-Файлів 13 PAGEREF _Toc214296823 \h 14911515
13 LINK \l "_Toc214296824" 143.8 Семафори 13 PAGEREF _Toc214296824 \h 14921515
13 LINK \l "_Toc214296825" 143.9 Повідомлення. 13 PAGEREF _Toc214296825 \h 14931515
13 LINK \l "_Toc214296826" 143.10 Поділювана пам'ять. 13 PAGEREF _Toc214296826 \h 14941515
13 LINK \l "_Toc214296827" 14Література 13 PAGEREF _Toc214296827 \h 14961515
15
Вступ

Системне програмування (або програмування систем) - рід діяльності, що полягає в роботі над системним програмним забезпеченням.
Основна відмітна риса системного програмування в порівнянні із прикладним програмуванням полягає в тому, що результатом останнього є випуск програмного забезпечення, що пропонує певні послуги користувачам (наприклад, текстовий процесор), у той час як результатом системного програмування є випуск програмного забезпечення, що пропонує сервіси по взаємодії з апаратним забезпеченням (наприклад, дефрагментация жорсткого диска), що має на увазі сильну залежність таких програм від апаратної частини.

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

Робота системного програміста має такі характерні риси:
програміст повинен ураховувати специфіку апаратної частини й інші властивості системи, у якій функціонує програма,
звичайно використовується низького рівня мова програмування або такий діалект мови програмування, що
дозволяє функціонування в оточенні з обмеженим набором системних ресурсів
працює максимально ефективно й має мінімальне запізнювання за часом завершення
має маленьку бібліотеку часу виконання (RTL) або не має взагалі її
дозволяє пряме керування (прямій доступ) до пам'яті й керуючої логіки
дозволяє робити ассемблерні вставки в код
налагодження програми може бути утруднена при неможливості запустити її в отладчике через обмеження на ресурси, тому може застосовуватися комп'ютерне моделювання для рішення цієї проблеми.
для системного програмування доступний обмежений набір засобів. Використання автоматичного складання сміття досить рідке явище. Бібліотека часу виконання, при її наявності, часто менш здатна й робить меншу кількість перевірок.
1 Середовище програмування ОС UNIX
1.1 Структура ОС UNIX

Обчислювальна система (ВС) це деяке об'єднання апаратних засобів, засобів управління апаратурою (фізичними ресурсами), засобів управління логічними ресурсами, системи програмування і прикладне програмне забезпечення.

Прикладні програми

Системи програмування

Управління логічними пристроями

Управління фізичними пристроями

Апаратні засоби


Малюнок 1.1 – Структура Обчислювальної системи

Нижній рівень - це чисто апаратура.
Наступний рівень це програми, але програми, орієнтовані на якість і властивості апаратури – асемблери. Нижня межа між фізичним рівнем і апаратурою - це інтерфейс цього управління – драйвер - набори команд управління, зрозумілих конкретному пристрою.
Наступний рівень - це рівень, який орієнтований на згладжування апаратних особливостей. Він призначений для створення комфортніших умов в роботі користувача. Тут оперують поняттями файл, процес, системний виклик.
Операційною системою вважають два рівні - логічний і фізичний. Деякі автори в ОС включають і системи програмування.
Тепер давайте подивимося на конструкцію ОС.








Малюнок 1.2 – Структура Операційної системи

1. Ядро. UNIX, як практично будь-яка ОС має поняття ядра. Ядром ОС є її резидентна частина, тобто та частина ОС, яка завжди присутня в оперативній пам'яті.
У ядро входять базові засоби управління основною суттю (файл і процес), а також деякий набір програм, що забезпечують управління деякими фізичними пристроями, – драйверів (у ядро ОС повинен входити драйвер оперативного пристрою, що запам'ятовує). У функції ядра входить обробка переривань.

2. Оболонка. Оболонку складають додатки як системні, такі, що забезпечують функціональність системи, так і прикладні, такі, що забезпечують призначений для користувача інтерфейс Unix. Оболонка багатошарова. Навколо ядра нарощуються програми управління ресурсами обчислювальної системи. Перший рівень в основному складається з драйверів фізичних пристроїв. Наступний рівень - управління логічними пристроями. І так далі. Таких рівнів може бути досить багато. Чим далі від ядра, тим більша абстрактність має місце бути.
Взаємодія оболонки з ядром відбувається за допомогою системних викликів. Системний виклик - це вимога до ОС (до ядра) провести аппаратно/системно специфічну або привілейовану операцію.
Системний виклик є функцією, написаною на мові З і є точкою входу в ядро операційної системи, через які прикладні програми мають можливість користуватися базовими послугами, UNIX, що надаються.
Системні виклики забезпечують:
зіставлення дій користувача із запитами драйверів пристроїв;
створення і припинення процесів;
реалізацію операцій введення-виводу;
доступ до файлів і дисків;
підтримку функцій терміналу.

Unix має обмежений набір системних викликів, які документовані в розділі 2 електронні довідники. (man 2 kill).
У UNIX використовується підхід, при якому кожному системному виклику відповідає функція (функції) з тим же ім'ям, що зберігається в бібліотеці мови C. Ці функції виконують необхідне перетворення аргументів і викликають необхідну процедуру ядра. В цьому випадку бібліотечний код виконує тільки роль оболонки, тоді як фактичні команди, розташовані в ядрі ОС (написані на З або Assemblere).

Крім системних викликів програмістові пропонується великий набір функцій загального призначення. Вони не є точками входу в ядро ОС, хоча в ході виконання багато хто з них (але не все) виконує системні виклики. Ці функції зберігаються в системній бібліотеці З і разом з системними викликами складають середовище програмування ОС UNIX (документовано в розділі 2 електронні довідники).
Використання системних функцій зазвичай вимагає включення в текст програми за допомогою директиви #include файлів заголовків, що містять визначення функцій. При цьому якщо ім'я файлу поміщене в уголковые дужки, це означає, що пошук файлу проводитиметься в загальноприйнятих каталогах зберігання файлів заголовків. Якщо ж ім'я файлу заголовка поміщене в лапки, то використовується явно вказане або відносне ім'я файлу.

1.2 Створення програми

Розглянемо процес створення програми на (ANSI) С для ОС UNIX.
Розробка програми починається із створення початкових файлів, що містять текст на мові Сі. Текстові файли проводяться за допомогою одного з текстових редакторів, підтримуваних в середовищі UNIX.
Хоча UNIX-файлы не мають розширень, задаємо створеному файлу розширення для того, щоб його міг розпізнати компілятор.
.c для мови С і .сс (або .С) для С++.

При розробці програм, а тим більше - складних, використовується принцип модульності, розбиття складної програми на складові частини, кожна з яких може зберігатися в окремому файлі і готуватися окремо.
Після того, як текстовий файл створений, його потрібно відкомпілювати для отримання об'єктного файлу.

На малюнку показані етапи, які проходить програма від свого написання до виконання


Малюнок 1.3 – Процес підготовки програми до виконання

Процес підготовки виконуваного файлу роздільний на чотири фази:
1. Препроцесор
Здійснює вставку початкових текстів з інших файлів (#include)
Замінює входження імен, для яких визначена заміна (#define)
Здійснює умовну обробку початкового файлу (#ifdef)
Знищує коментарі

Вихідні дані препроцесора - це ще ASCII текст (Сі-оператори). Все, що робить препроцесор, це вставляє і замінює текст. На виході виходить нова редакція тексту (на мал. - НИМ'). У більшості систем програмування Макропроцесор суміщений з транслятором, і для програміста його робота і проміжний ЇМ' "не видні".
З одного боку макропроцесор "не розуміє" операторів мови програмування і "не знає" змінних програми, з іншою, всі оператори і змінні Макромови (тих виразів в програмі, які адресовані Макропроцесору) в проміжному ЇМ' вже відсутні і для подальших етапів обробки "не видні".

2. Транслятор
Трансляція - перетворення програми, представленої на одній мові програмування, в програму на іншій мові програмування, в певному значенні рівносильну першою.
Транслятори підрозділяються на Асемблери і Компілятори - залежно від початкової мови програми, яку вони обробляють. Асемблери працюють з Автокодами або мовами Асемблера, Компілятори - з мовами високого рівня.

2.1 Компілятор
Перетворить конструкції мови Сі в конструкції асемблера
Здійснює перевірку наявності синтаксичних помилок
Ця фаза займає більше всього часу
Мова Асемблера - мова програмування, яка є символьною формою машинної мови з поряд можливостей, характерних для мови високого рівня

2.2 Асемблер
Перетворить конструкції мови асемблера в машинні команди
Машинна мова - мова програмування, призначена для представлення програми у формі, що дозволяє виконувати її безпосередньо технічними засобами обробки інформації.
Фаза асемблера конвертує початкові тексти на мові асемблера в об'єктний код.
Об'єктний модуль - програмний модуль, що отримується в результаті трансляції початкового модуля.

3. Редактор зв'язків(Компонувальником)
Здійснює збірку main і інших об'єктних файлів в завантажуваний модуль
Проглядає бібліотеки для дозволу зовнішніх посилань
Здійснює підготовку виконуваної коди
Редактор Зв'язків сполучає разом всі об'єктні модулі, що входять в програму. Оскільки Редактор Зв'язків "бачить" вже всі компоненти програми, він має можливість обробити ті місця в об'єктних модулях, які містять зовнішні посилання. Результатом роботи Редактора Зв'язків є завантажувальний модуль.
Завантажувальний модуль - програмний модуль, представлений у формі, придатній для завантаження в оперативну пам'ять для виконання.
Завантажувальний модуль зберігається у вигляді файлу на зовнішній пам'яті.

4. Завантажувач.
Для виконання програма повинна бути завантажена в оперативну пам'ять. Іноді при цьому потрібна деяка додаткова обробка (наприклад, настройка адрес в програмі на ту область оперативної пам'яті, в яку програма завантажилася). Ця функція виконується Завантажувачем, який зазвичай входить до складу операційної системи.

Можливий також варіант, в якому редагування зв'язків виконується при кожному запуску програми на виконання і поєднується із завантаженням. Це робить Зв'язуючий Завантажувач. Варіант скріплення при запуску більш витратний, оскільки витрати на скріплення тиражуються при кожному запуску. Але він забезпечує:
велику гнучкість в супроводі, оскільки дозволяє міняти окремі об'єктні модулі програми, не міняючи решти модулів;
економію зовнішньої пам'яті, оскільки об'єктні модулі, використовувані в багатьох програмах не копіюються в кожен завантажувальний модуль, а зберігаються в одному екземплярі.
Варіант інтерпретації має на увазі прямого виконання початкового модуля. Інтерпретація - реалізація сенсу деякого синтаксично закінченого тексту, представленого на конкретній мові. Інтерпретатор читає з початкового модуля чергову пропозицію програми, переводить його в машинну мову і виконує. Всі витрати на підготовку тиражуються при кожному виконанні, отже, интепретируемая програма принципово менш ефективна, чим трансльована. Проте, інтерпретація забезпечує зручність розробки, гнучкість в супроводі і переносимість.

1.3 Формати об'єктних файлів

Об'єктні файли мають особливу конструкцію і є планом роботи, спец, що містить. символи із зіставленими ним адресами і значеннями.
Об'єктні файли розташовані на диску і складаються з декількох секцій:
заголовки (HEADER) - містять інформацію для завантажувача
текстові сегмент (TEXT SEGMENT) - містить захищені коди програм
сегменти даних (DATA SEGMENT) - містить ініціалізовані дані. Ці дані можуть бути модифіковані програмою, але простір, що відводиться під них, завжди залишається незмінним.
символьна таблиця (SYMBOL TABLE) - містить посилання на всі використовувані в програмі символи (літерали). Тут поміщається інформація про всю форматовану інформацію.
налагоджувальна таблиця (DEBAG TABLE), якщо при запуску програми використовуються налагоджувальні опції. Ця таблиця містить інформацію по відладці програм при застосуванні стандартних відладчиків і копія її є на диску.

Завантажувач завантажує виконуваний файл з диска у відведену йому пам'ять і створює образ процесу. Між виконуваним файлом і образом однозначної відповідності немає.
Образ містить три зони:
сегмент коди – текст машинних команд. Він не змінюється ні в розмірах, ні в змісті і дозволений тільки для читання.
сегмент даних - містить змінні, рядки, масиви і інші дані програми. Він складається з двох частин
дані, що ініціалізували, - змінні і константи, значення яких відомі при запуску програми
BSS(Bulk Storage System) – пристрій великої ємкості, що запам'ятовує) може збільшуватися під час виконання процесу, містить неініціалізовані дані застосування, включаючи всі змінні, що декларують як статичні у всіх функціях або модулях (більшість глобальних змінних не ініціалізувалися).
може модифікуватися. Більш того, програма може зажадати виділення додатковій пам'яті динамічно, під час виконання.
стік - містить змінні оточення (оболонки), а також командний рядок, введений при виклику цієї програми

Розмір, вміст і розташування сегментів в пам'яті визначається як самою програмою (наприклад, використанням бібліотек, розміром коди і даних), так і форматом виконуваного файлу цієї програми (COFF, ELF)
У UNIX існує три домінуючі формати виконуваних файлів:
а.out
COFF
ELF

а.out
Assembler Output
Старий і ``классический'' об'єктний формат UNIX. Він використовує короткий і компактний заголовок з магічним числом на початку, яке часто використовується для опису формату. Він містить три завантажувані сегменти: .text, .data і .bss плюс таблицю символів і таблицю рядків.
Образ підтримує тільки три стандартні секції .text, .data і .bss.

COFF
Загальний формат об'єктних файлів
Об'єктний файл формату COFF складається із заголовка файлу, заголовка системи UNIX, таблиці заголовків секцій, секцій даних, настроювальної інформації, номерів рядків (необов'язково), і таблиці символів.

Заголовок файлу

заголовок системи UNIX

заголовок секції (1..n)

дані секції (1..n)

настройка секції (1..n)

номери рядків секції (1..n)

таблиця символів

таблиця рядків


Малюнок 1.4 - Структура файлу формату COFF

Заголовок файлу містить кількість секцій, тимчасові відмітки, покажчик на таблицю символів і іншу інформацію.
Заголовок системи UNIX містить системний код файлу (за умовчанням вісімкове 410 для тексту, що розділяється, або 407 для суміщених тексту і даних), розмір тексту, розмір даних, що ініціалізували, розмір неініціалізованих даних, точку входу, базові адреси тексту і даних.
Таблиця заголовків секцій. Для кожної секції визначений елемент, який містить ім'я секції, фізичну і віртуальну адреси, розмір секції, покажчик на початкові дані секції, покажчик на точку переміщення, покажчик на номери рядків і іншу інформацію.
Зауваження: Секція .bss має розмір і символи, які посилаються на неї. Але в ній немає ні покажчиків на точку переміщення, ні номерів рядків, ні даних. Тому секція .bss має запис в таблиці заголовків секцій, але більше не займає місця ніде в COFF-файле.
Далі слідують секції початкових даних, що складаються з відповідної кількості байт тексту або даних. Файли, створені cc і as завжди містять три секції: .text (виконуваний код), .data (змінні, що ініціалізували) і .bss (неініціалізовані змінні).
Останні чотири складові формату COFF (настройка, номери рядків, таблиця символів і таблиця рядків) можуть бути відсутніми, якщо програма була зібрана з опцією -s редактора зв'язку або якщо таблиця символів і біти настройки були видалені за допомогою команди strip(1). Якщо не залишилося недозволених посилань після компоновки програми, буде відсутня настроювальна інформація.
Секція номерів рядків додається до об'єктного файлу тільки при компіляції з опцією -g, для подальшого використання sdb(1).
Кожен вхід в таблиці символів містить або ім'я символу, якщо воно не більше 8 знаків, або зсув в таблиці рядків, де зберігається рядок імені, а також значення символу, тип, клас пам'яті і іншу інформацію. Команда nm(1) показує таблицю символів об'єктних файлів.
Таблиця рядків створюється тільки при необхідності. Тут суцільним масивом зберігаються імена символів довше за вісім знаків, що кожне завершується нульовим байтом.

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



Малюнок 1.5 - Базова структура пам'яті для процесів, що завантажуються з файлу формату COFF

Весь файл від початку до кінця завантажується в єдиний сегмент, званий "code" або "text".
Heap область - це область пам'яті, що динамічно розподіляється додатком, причому само застосування має можливість управляти зростанням сегменту даних, виділяючи додаткову пам'ять з цієї ж heap області.
Якщо частини (стік і BSS), що ростуть, зійшлися, відбувається апаратне переривання, при якому додається одна сторінка.

Elf (Executable and Linkable Format)
формат виконуваних і бібліотечних файлів
Найбільш популярний зараз формат. Крім банального створення виконуваних файлів програм і бібліотек, ELF полегшує формування і функціонування складних файлових комплексів, відладку програм.
Спадкоємець формату COFF, що підтримує множинні сегменти і 32-бітові або 64-бітові значення.

Об'єктні файли мають таблицю заголовків секцій, виконувані - таблицю програмних заголовків, а бібліотеки - і те і інше.
Форма ELF'a описана у файлі /usr/include/elf.h.
У загальному випадку, формат файлу виглядає таким чином:

ELF заголовок
<- настройки для завантажувача

Програмний заголовок (1..n)


Секційний заголовок (1..n)


дані
<- виконуваний код і дані

Дані


............... n


...............


Дані


Таблиця заголовків сегментів



Малюнок 1.6 - Структура файлу формату ELF (це сам файл)

ELF заголовок потрібний для того, щоб дати зрозуміти завантажувачу для якої версії Unix призначена програма, для якого процесора і якої архітектури. У заголовку заданий розмір програми, скільки виділити для програми пам'яті, точка входу в програму і інша супутня інформація.

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

Секційні заголовки - це вказівки для завантажувача з інформацією про
типі секції і дії ОС з даною секцією
розташуванні секції у файлі
стартова адреса секції у віртуальній пам'яті процесу
розмір секції у файлі
розмір секції в пам'яті
прапори доступу

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

Приблизно так виглядає образ запущеної програми ELF в пам'яті.


Малюнок 1.7 - Базова структура пам'яті для процесів, що завантажуються з файлу формату ELF (це виділена під процес пам'ять)

Динамічний сегмент це сегмент службових даних про підвантажувані бібліотеки під час запуску програми.
[це коли частина процесу динамічною линковки бібліотек відбувається не у момент запуску виконуваного файлу, а проводиться один раз до самого запуску. Кожній бібліотеці, що бере участь в процесі, відводиться фіксований віртуальний адресний простір, виходячи з цих адрес розраховуються relocations, інформація зберігається в додаткових секціях ELF. Має обмеження на кількість бібліотек (зважаючи на обмеження адрес)]

Якщо компіляція пройшла вдало і вийшов об'єктний файл, ми маємо можливість вивчити його. Найбільш популярними при вивченні об'єктних файлів, є наступні:
nm: Виводить перелік символів об'єктного файлу.
objdump: Виводить докладну інформацію, що міститься в об'єктних файлах.
readelf: Виводить інформацію про об'єктні файли ELF.

nm
Виводить перелік символів об'єктного файлу, а також їх типів і значень. Якщо ви введете команду nm, ви відмітите, що за умовчанням вона шукає файл з назвою а.out Якщо такого файлу немає, утиліта виразить свою незадоволеність. якщо інструмент знаходить файл а.out, створений компілятором, він виводить лістинг, подібний
08049594 A __bss_start
080482e4 t call_gmon_start
08049594 b completed.4463
08049498 d __CTOR_END__
08049494 d __CTOR_LIST__
08049588 D __data_start
08049588 W data_start
0804842c t __do_global_ctors_aux
0804830c t __do_global_dtors_aux
0804958c D __dso_handle
080494a0 d __DTOR_END__
0804949c d __DTOR_LIST__
Перед кожним символом, виведеним командою nm, указується відповідне йому значення в шістнадцятиричному (за умовчанням) форматі, а також символьний код, відповідний типу символу. Деякі найбільш поширені коди: A (абсолютне значення) - значення, які не змінюватимуться при подальшому линковании; B – символи з розділу BSS; C – загальні символи, вказуючі на неініціалізовані дані.

Objdump (objdump -d а.out)
Для докладнішого вивчення вмісту об'єктного файлу (зокрема тип), для чого видає асемблерний лістинг кожного з розділів об'єктного файлу, що містить виконуваний код.
Для програми "Hello World"

а.out: file format elf32-i386

Disassembly of section .init:

08048278 <_init>:
8048278: 55 push %ebp
8048279: 89 e5 mov %esp,%ebp
804827b: 83 ec 08 sub $0x8,%esp
804827e:
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·
·І так далі

Readelf (readelf -all а.out)
також може вивести вміст об'єктного файлу з подібною ж прозорістю. Має іншу структуру. Наприклад, в заголовку ELF представлена коротка інформація про всі розділи файлу. Ця інформація може бути корисна при вивченні великих об'єктних файлів.


2 Використання файлової системи UNIX
2.1 Основні поняття
2.1.1 Типи та іменування файлів

Файлами є області пам'яті на зовнішньому носієві (як правило магнітному диску), призначені для зберігання даних.
Файли відрізняються від звичайних масивів тим, що
вони можуть змінювати свій розмір;
звернення до елементів цих масивів проводиться не за допомогою операції індексації [], а за допомогою спеціальних системних викликів і функцій;
доступ до елементів файлу відбувається в так званій "позиції читання/запису", яка автоматично просувається при операціях читання/запису, тобто файл є видимим послідовно. Є, правда, функції для довільної зміни цій позиції.

Файли мають імена і організовані в ієрархічну деревовидну структуру з каталогів і простих файлів.

Усередині UNIX'а розрізняються разные типу файлів. Така відмінність пов'язана із способами управління структурою файлів і контролем за передачею в нього даних.
Звичайні файли, тобто початкові тексти і двійкові коди програм або тексти документів. У загальному випадку регулярні файли визначаються і контролюються користувачами, тобто запис, читання і управління повноваженнями в них проводяться користувачем.
Директорії є маршрутним деревом для виявлення конкретного файлу у файловій системі. Структура директорії контролюється ОС і користувач її модифікувати не може. Користувач може тільки читати файли директорії, але записувати в них може тільки ОС.
Спеціальні файли пристроїв - це файли, що забезпечують передачу даних відповідним пристроям. Типів таких пристроїв в UNIX чотири: познаковой передачі, передачі по блоках, raw blocks (що не буферизують) і терміналів введення/виводу. По спеціальному дозволу користувач може дістати доступ на запис або читання в спеціальні файли за допомогою інструкцій оболонки або через системні виклики з програми.
Канали – це файли, що забезпечують напівдуплексну межпроцессное взаємодію. PIPE може містити будь-які типи даних, що поміщаються в них користувачем або ОС. Єдине обмеження в них - вони не можуть взяти більше 10 блоків даних. Якщо дані в ці файли поміщаються користувачем, структура такого файлу підвладна контролю з боку користувача. Проте попереднє таке право треба отримати, використовуючи макроси ініціалізації.
Сокети - це файли, що забезпечують повнодуплексну межпроцессное взаємодію.
Символічне посилання (symbolic link) - це спеціальний файл, який містить шлях до іншого файлу.

У ОС UNIX підтримується три способи вказівки імен файлів:
Коротке ім'я. Ім'я, що не містить спеціальних метасимволів коса межа (/), є коротким ім'ям файлу. По короткому імені можна послатися на файли поточного каталога.
Відносне ім'я. Ім'я, що не починається з символу косої межі (/), але що включає такі символи. Воно посилається на файл щодо поточного каталога.
Абсолютне (повне) ім'я. Ім'я, що починається з символу косої межі (/). Воно посилається на файл щодо кореневого каталога.

Інші символи, окрім косої межі, не мають в іменах файлів UNIX особливого значення (це не метасимволи). Зокрема, немає системного поняття розширення файлу. Імена файлів чутливі до регістра.
У ОС UNIX немає теоретичних обмежень на кількість вкладених каталогів. Проте, в кожній реалізації є практичні обмеження на максимальну довжину імені файлу, яке указується в командах (як і на довжину командного рядка в цілому).

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

2.1.2 Права доступу до файлів

Кожен файл в ОС UNIX містить набір прав доступу, по якому визначається, як користувач взаємодіє з даним файлом. Цей набір зберігається в індексному дескрипторі даного файлу у вигляді цілого значення, з якого зазвичай використовується 12 бітів. Причому кожен біт використовується як перемикач, вирішуючи (значення 1) або забороняючи (значення 0) той або інший доступ.
0644
Три перших біта встановлюють різні види поведінки при виконанні. Що залишилися дев'ять діляться на три групи по три, визначаючи права доступу для власника, групи і решти користувачів. Кожна група задає права на читання, запис і виконання.

Права доступу можна дізнатися за допомогою команди

ls – l

r - Біт читання для всіх типів файлів має одне і те ж значення: він дозволяє читати вміст файлу (отримувати лістинг каталога командою [ Cкачайте файл, чтобы посмотреть ссылку ]).
w - Біт запису також має одне і те ж значення: він дозволяє редагувати цей файл. Для каталога – це можливість міняти його вміст, тобто створювати і видаляти файли.
x - Якщо для деякого файлу встановлений біт виконання, то файл може виконуватися як команда. У разі установки цього біта для каталога, цей каталог можна зробити поточним (перейти в нього командою [ Cкачайте файл, чтобы посмотреть ссылку ]).
s - Встановлений біт зміни ідентифікатора користувача SUID означає, що доступний користувачеві на виконання файл виконуватиметься з правами (з ефективним ідентифікатором) власника, а не користувача, що викликав файл (як це зазвичай відбувається).
S - Встановлений біт зміни ідентифікатора групи SGID означає, що доступний користувачеві на виконання файл виконуватиметься з правами (з ефективним ідентифікатором) групи-власника, а не користувача, що викликав файл (як це зазвичай відбувається).
Прикладом може бути утиліта passwd, що дозволяє користувачеві міняти свій пароль. Для зміни пароля потрібно редагувати вміст файлу /etc/passwd і /etc/shadow. Надати права всім змінювати вміст цих файлів неможливо. Установка SUID для утиліти passwd дозволяє вирішити цю проблему. Власником файлу /usr/bin/passwd, в якому зберігається утиліта є суперкористувач. Хто б не запустив утиліту на виконання, на час роботи даної програми отримує права суперкористувача, а значить, може проводити зміни в системних файлах.

Якщо битий SGID встановлений для файлу, не доступного для виконання, він означає обов'язкове блокування, тобто незмінність прав доступу на читання і запис поки файл відкритий певною програмою.
t - Встановлений клейкий біт має сенс для каталога. Файл в цьому каталозі може бути видалений або перейменований тільки в наступних випадках:
користувачем-власником файлу;
користувачем-власником каталога;
якщо файл доступний користувачеві на запис;
користувачем root.
Прикладом може служити каталог /tmp, який відкритий на запис для всіх користувачів, але, в якому небажано видаляти чужі тимчасові файли.

2.1.3 Cистемные структури керування файлами

Індексні дескриптори

Загальним елементом у всіх 4-х типах файлів є те, що вони управляються через INODE, що є інформаційним ядром для файлів будь-якого типу, містить всі необхідні інформаційні дані й використовується системою.
Файли і їхні імена жорстко пов'язані з INODE. З одним INODE може бути зв'язано кілька імен, але кожн активний INODE зв'язується тільки з одним файлом. Це забезпечує доступ до одного фізичного файлу через кілька імен, пов'язаних із загальним INODE.
Атрибути файлу, такі як дозвіл на доступ і інші адміністративні дані, містяться в INODE. Крім того тут же втримуються адреси фактичних блоків даних.
В UNIX мінімальною величиною фізичного зберігання даних є блок, розмір якого фіксується в кожній конкретній системі залежно від обсягу використовуваних дисків. Характерними є блоки в 512 або 1024 байта. Можливе використання логічних блоків, які як правило містять у собі два або більше фізичних блоки.
INODE's завжди резидентны на диску, а при роботі з конкретними файлами їх INODE's переміщаються в ОЗУ.
Зміст скільки-небудь на диску INODE:

di_mode унікальний номер
file mode прапори прав доступу
di_count ідентифікатор користувача
di_uid ідентифікатор власника
di_gid ідентифікатор групи
di_size розмір файлу
di_addr[0]. адреса блоку даних

di_addr[n] адреса блоку даних
di_atime час останнього звертання до файлу
di_mtime час останньої модифікації файлу
di_ctime час останньої модифікації індексного
дескриптора

У кодах ядра посилання на INODE завжди починаються з посилання "di_". Вся інформація в INODE має тип "integer".
file mode - 2-х байтовый прапор, де перших 9 байтів задають право доступу (read/write) або дозвіл на виконання (файл типу exe). Ця інформація представляється у вигляді 3-х бітових полів, що задають зазначені права, відповідно для власника файлу, для груп директорій або для всіх директорій файлової системи. Наступний байт містить 4 біти, що визначають вид здійсненного (exe) файлу й тип файлу ( щовипливають 4 біти).
link count - поле, що вказує скільки директорій посилається на даний файл. Якщо в цьому полі коштує 0, то INODE не приєднаний ні до якого файлу, тобто файлу немає. Для директорій ця величина завжди => 2 (для батьківської директорії й для себе принаймні). Ця величина инкрементируется, якщо INODE одержує ім'я-синонім і декрементируется, якщо такоеимя вбирається.
group id - групи ідентифікаторів директорій, пов'язаних з файлом.
file size - розмір файлу в байтах, або 0 для спеціальних файлів.
Інші параметри ясні по своєму змісті.

З одним INODE може бути зв'язане декілька імен, але кожен активний INODE зв'язується тільки з одним файлом. Це забезпечує доступ до одного фізичного файлу через декілька імен, пов'язаних із загальним INODE.
Ядро UNIX містить таблицю i_list – список всіх активних INODE's. Таблиця i_list розташована на жорсткому диску і зберігається після виключення комп'ютера.

Для управління відкритими файлами ОС UNIX надає два базисні механізми: дескриптори (описувачі) файлу і потоки (файлові покажчики).

Дескриптори
Дескриптор файлу - це ціле число без знаку, за допомогою якого процес звертається до відкритого файлу.

FILE
Для кожного відкритого файлу в системі створюється структура fiIe, яка описує як відкритий файл, так і операції, які процес збирається проводити з файлом
Структура file містить такі поля, як:
ознака режиму відкриття;
покажчик на структуру vnode;
поточний зсув у файлі;
лічильник посилань на дану структуру;
покажчик на структуру, що містить права процесу, що відкрив файл (ця структура знаходиться в дескрипторі процесу);
покажчики на попередню і подальшу структури file, що зв'язують всі такі структури в подвійний список.
Для потокових файлів (наприклад, каналів і сокетів) поняття зсуву не підтримується, оскільки довільний доступ до цих файлів неможливий.

Обидва об'єкти відбиваються в спеціальних системних таблицях після відкриття файлу. Таблиця відкритих файлів створюється в оперативній пам'яті під час завантаження системи. Таблиця файлових дескрипторів створюється для кожного процесу. Зв'язок між таблицями:


Малюнок 2.1 – Системні структури керування файлами

Таблиця файлових дескрипторів

Дескриптори файлів виконують роль індексів таблиці дескрипторів, яка створюється ядром для кожного процесу. При відкритті файлу в цій таблиці шукається вільний осередок, в неї заноситься посилання на структуру file.
Кожен запис такої таблиці містить два поля: поле прапорів і покажчик на структуру файл. Найчастіше процес отримує дескриптори за допомогою операцій відкриття і створення файлів, а також шляхом спадкоємства від батьківського процесу.
Програма може використовувати дескриптори файлів з номерами 0..N. N визначене в include-файле .
[Щоб набути фактичного значення числа N, можна викликати функцію sysconf з аргументом _SC_OPEN_MAX, наприклад, так:

printf(“_SC_OPEN_MAX = %ld\n”, sysconf(_SC_OPEN_MAX));
У операційній системі Linux 2.4 ми отримаємо число 1024, в FREEBSD 957, в Solaris 256.]
Дескриптори є локальними для кожної програми. Тобто якщо дві програми відкрили один і той же файл - дескриптори цього файлу в кожній з них не обов'язково співпадуть (хоча і можуть). Назад: однакові дескриптори (номери) в різних програмах не обов'язково позначають один і той же файл. Декілька або один процесів можуть відкрити один і той же файл одночасно кілька разів. При цьому буде створено декілька структур, що "пов'язують" (по одній для кожного відкриття); кожна з них матиме СВІЙ покажчик читання/запису. Можлива і ситуація, коли декілька дескрипторів посилаються до однієї структури.


Таблиця відкритих файлів

Система має таблицю відкритих файлів. Об'єкти цієї таблиці є покажчиками на структуру file.
При закритті файлу зв'язна структура в ядрі знищується, осередок в таблиці вважається вільним, тобто зв'язок програми і файлу розривається.
При створенні процесу йому автоматично надаються три зумовлені потоки. Ці потоки оголошені в заголовному файлі "stdio.h".

FILE * stdin стандартний вхідний потік, який є нормальним джерелом введення для програми.
FILE * stdout потік стандартного виводу, який використовується для нормального виведення програми.
FILE * stderr стандартний потік помилки, який використовується для повідомлень про помилки і діагностики, виданою програмою.

Стандартним потокам відповідають стандартні дескриптори.
0 вхідному потоку або STDIN_FILENO
1 вихідному потоку або STDOUT_FILENO
2 потоку помилок або STDERR_FILENO

У нормальному інтерактивному режимі роботи стандартний потік введення зв'язує процес з клавіатурою, а стандартні потоки виводу і виведення помилок – з поточним терміналом.

2.1.4 Системні виклики роботи з файлами

Системні функції роботи з файлами класифікуються на декілька категорій
уведення-виведення (open, read, write, close)
створення файлу
зміна поточній директорії
монтаж файлової системи
виконання файлу
отримання статусу файлу
зміна статусу файлу
модифікація імені файлу (link/unlink).

Системних викликів, безпосередньо доступних користувачеві, 19 і їх можна розбити на 7 категорій:
виклики, що повертають файлові дескриптори (покажчики на INODE) з можливою їх модифікацією:
OPEN, CLOSE, CREAT, PIPE, DUP.
виклики, що привласнюють або звільняючі INODE's:
CREAT, LINK, UNLINK.
виклики введення/виводу в процес:
READ, WRITE, LSEEK.
виклики для установки або модифікації атрибутів
файлів:
CHOWN, CHMOD, STAT, FSTAT.
виклики для зміни файлової системи:
MOUNT, UNMOUNT.
виклики, що дозволяють процесу модифікувати шлях у
файловій системі:
CHDIR, CHROOT.
виклики, що використовують алгоритм namei(), щоб аналізувати маршрут:
OPEN, MKNOD, CHROOT, CHMOD, MOUNT, CREAT, CHDIR, CHOWN, STAT, UMOUNT.

Системні функції забезпечуються системними викликами.
Функції для роботи з потоками починаються на букву f, що стоїть перед ім'ям аналогічною функцією для дескрипторів:
fopen - open
fclose - close
fseek - lseek і т.п.
2.2 Створення та відкриття файлів
2.2.1 Системний виклик open

Функція відкриття файлу створює новий потік і встановлює з'єднання між потоком і файлом, можливо включаючи створення нового файлу.
Функції creat і open визначені в заголовному файлі , а функція close – у файлі .

open відкриває або створює файл

Виклик системної функції open (відкрити файл) - це перший крок, який повинен зробити процес, щоб звернутися до даним у файлі. Синтаксис виклику функції open:

fd = open(pathname, flags, modes);

де pathname - ім'я файлу
flags указує режим відкриття (наприклад, для читання або запису)
а modes містить права доступу до файлу у випадку, якщо файл створюється.

open повертає ціле число, відповідне дескриптору файлу і використовуване іншими операціями над файлами.
Виклик open може завершитися з помилкою з багатьох причин. В цьому випадку open() повертає -1 (в цьому випадку errno встановлюється належним чином). Найчастіше зустрічаються наступні: файл не існує (errno==ENOENT), або не дозволений відповідний доступ до цього файлу (errno==EACCES);

Значення параметра flags

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

O_RDONLY – якщо над файлом надалі здійснюватимуться тільки операції читання;
O_WRONLY – якщо над файлом надалі здійснюватимуться тільки операції запису;
O_RDWR – якщо над файлом здійснюватимуться і операції читання, і операції запису.

Кожне з цих значень може бути скомбіноване за допомогою операції "побітове або ( | )" з одним або декількома прапорами:
O_CREAT – якщо файлу з вказаним ім'ям не існує, він повинен бути створений;
O_EXCL – застосовується спільно з прапором O_CREAT. У разі, коли ми вимагаємо, щоб файл на диску був відсутній і був створений у момент відкриття, При існуванні файлу з вказаним ім'ям, відкриття файлу не проводиться і констатується помилкова ситуація. Використовується для запобігання ненавмисному знищенню вже існуючого файлу.
O_NDELAY – забороняє переклад процесу в стан очікування при виконанні операції відкриття і будь-яких подальших операціях над цим файлом;
O_APPEND – при відкритті файлу і перед виконанням кожної операції запису (якщо вона, звичайно, дозволена) покажчик поточної позиції у файлі встановлюється на кінець файлу;
O_TRUNC – якщо файл існує, зменшити його розмір 0, із збереженням існуючих атрибутів файлу, окрім, мабуть, часів останнього доступу до файлу і його останньої модифікації.

Використання прапора O_TRUNC приводить до знищення всіх даних у файлі, тому його допустимо указувати тільки при відкритті існуючого файлу і за умови, що процес має право на запис у файл, що відкривається. З тієї ж причини, прапор O_TRUNC не може використовуватися спільно з прапором O_RDONLY. Щоб створити новий файл (з прапором O_CREAT), процес повинен мати право на запис в каталог, оскільки посилання на файл буде записано у файл каталога. При відкритті існуючого файлу права доступу до каталога не мають значення. В цьому випадку в розрахунок приймаються тільки права доступу до файлу.
Слід також відмітити, що процес повинен володіти правом на пошук (виконання) в проміжних каталогах. Прапор O_TRUNC не повинен використовуватися без O_CREAT, якщо передбачається створення нового файлу. Сам по собі він означає наступне: «якщо файл існує видалити з нього всі дані, якщо файл не існує завершитися з ознакою помилки».

Крім того, в деяких версіях операційної системи UNIX можуть застосовуватися додаткові значення прапорів:
O_SYNC – будь-яка операція запису у файл блокуватиметься (тобто процес буде переведений в стан очікування) до тих пір, поки записана інформація не буде фізично поміщена на соответсвующий рівень hardware, що пролягає нижче;
O_NOCTTY – якщо ім'я файлу відноситься до термінального пристрою, воно не стає терміналом процесу, що управляє, навіть якщо до цього процес не мав терміналу, що управляє.

У разі відкриття існуючого файлу, аргумент MODE ігнорується і в текстах програм, як правило, взагалі опускається. Таким чином, функція open викликається всього з двома аргументами, наприклад:

Якщо файл із заданим ім'ям існує на диску, і права доступу до нього для користувача, від імені якого працює поточний процес, не противоречат запитаному набору операцій, то операційна система сканує таблицю відкритих файлів від її початку до кінця у пошуках першого вільного елементу, заповнює його і повертає індекс цього елементу як файловий дескриптор відкритого файлу. Якщо файлу на диску немає, не вистачає прав або відсутнє вільне місце в таблиці відкритих файлів, то констатується виникнення помилки.

Створення файлу за допомогою сисвызова open()

У разі, коли ми допускаємо, що файл на диску може бути відсутнім, і хочемо, щоб він був створений, прапор для набору операцій повинен використовуватися в комбінації з прапором O_CREAT. Якщо файл існує, то все відбувається за розглянутим вище сценарієм. Якщо файл з вказаним ім'ям не існує, виклик open створить його. Крім того, необхідно визначити параметр mode.
Параметр mode встановлює атрибути прав доступу різних категорій користувачів до нового файлу при його створенні. Він обов'язковий, якщо серед заданих прапорів присутній прапор O_CREAT, і може бути опущений інакше. Цей параметр задається як сума наступних вісімкових значень:
0400 – дозволено читання для користувача, що створив файл;
0200 – дозволений запис для користувача, що створив файл;
0100 – дозволено виконання для користувача, що створив файл;
0040 – дозволено читання для групи користувача, що створив
файл;
0020 – дозволений запис для групи користувача, що створив
файл;
0010 – дозволено виконання для групи користувача, що створив
файл;
0004 – дозволено читання для решти всіх користувачів;
0002 – дозволений запис для решти всіх користувачів;
0001 – дозволено виконання для решти всіх користувачів.

У стандарті POSIX Ці коди мають ще і мнемонічні імена (використовувані у виклику stat):
Принцип їх іменування слідує шаблону S_Ipwww, де p визначає режим доступу (R, w або X), а www кому видається право на цей режим доступу (USR, GRP або OTH).
Наприклад, для вісімкового числа 755 можна записати:
S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH
Існують окремі ідентифікатори для USR, GRP і OTH, які описують повні права доступу. Іменування цих ідентифікаторів слідує формі S_IRWXw. Тут символ w визначає, кому видається повне право доступу до файлу (від англ, «whom» «кому») U, G або О. Таким образом, попередній приклад може бути записаний в наступному вигляді:
S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH
Використання ідентифікаторів дає розробникам ОС свободу вибору порядку проходження битий, що описують права доступу до файлу.

Параметр mode завжди повинен бути вказаний при використанні O_CREAT, в решті всіх випадків цей параметр ігнорується.
При створенні файлу реально встановлювані права доступу виходять із стандартної комбінації параметра mode і маски створення файлів поточного процесу umask, а саме – вони дорівнюють
mode & umask.

Маска зазвичай встановлюється під час реєстрації користувача в системі (командою umask) або системним викликом umask. Фраза «логічне множення на логічне доповнення» означає: якщо якийсь біт маски встановлений, то відповідний йому битий в остаточному наборі прав доступу скидається.
Так, маска 002 приведе до того, що битий S_IWOTH (право на запис для всіх останніх) буде скинутий, навіть якщо в системний виклик open буде переданий прапор S_IWOTH.
Що відбудеться, якщо ви створити файл з прапором O_WRONLY або O_RDWR, але при цьому не дати йому права на запис? Оскільки файл був тільки що створений, він поки що доступний для запису. Проте, коли наступного разу ви спробуєте відкрити його, всі накладені вами обмеження набудуть чинності.

Приклади
#include
#define TMPFILE "/tmp/tmpfile"
main (int argc, char ** argv)
{
char account[] = "account";
int logfd, acctfd, fd, fdin, fdout;
char *file;

/* Файл account, що знаходиться в поточному каталозі, відкривається для читання. */
acctfd = open(account, O_RDONLY);

/* Файл, на ймення який указує file, відкривається для запису.
Якщо файл не існує, він створюється з маскою прав доступу 0600 (не рахуючи umask-установок). Якщо файл існує, він буде усічений до нульового розміру.*/
file = TMPFILE;
fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0600);

/* Файл з абсолютним путнім ім'ям ("/sys/log") відкривається для записи. Всі записи проводяться в кінець файлу. Якщо файл не існує, він створюється з маскою прав доступу 0600.*/
logfd = open("/sys/log", O_WRONLY | O_APPEND | O_CREAT, 0600);

/* Файл, ім'я якого було передане main як перший аргумент відкривається на читання/запис. */
fdin = open(argv[1], O_RDWR);

/* Файл відкривається на запис. Якщо він не існує, він створюється. Інакше виклик буде завершений неуспіх. Відмітьте, що виклик open(2) перевіряється в тілі оператора if. Програма винна зробити якісь дії у разі неуспіху. */
if ((fdout = open(TMPFILE, O_WRONLY | O_CREAT | O_EXCL
0666)) == -1)
perror(TMPFILE);
}

Відповідність аргументів fopen і open:
rwmode irwmode
------------------------
"r" O_RDONLY
"w" O_WRONLY|O_CREAT |O_TRUNC
"r+" O_RDWR
"w+" O_RDWR |O_CREAT |O_TRUNC
"a" O_WRONLY|O_CREAT |O_APPEND
"a+" O_RDWR |O_CREAT |O_APPEND


Приклад програми

#include
#include
#include

int main()
{
extern int errno;
int fd;
if( fd = open( "nosuchfile", O_RDONLY )== -1 )
{
fprintf( stderr, "Error %d\n", errno );
perror( "hello" );
}
}/

Вихідні дані:
Error 2
hello: No such file or directory


2.2.2 Системний виклик create

Виклик creat - це просто різновид виклику open у формі

fd = open(имя_файла, O_WRONLY|O_TRUNC|O_CREAT,
коды_доступа);

creat створює новий або очищає існуючий файл і відкриває його на запис

#include
#include
#include
int creat(const char *path, mode_t perms)

path - повне ім'я файлу
perms - права доступу
Повертає дескриптор файлу або -1 у разі помилки
(код помилки знаходиться в глобальній змінній errno)

При зверненні до системного виклику open для відкриття існуючого файлу, можна указувати тільки перший і другий аргументи (path і flags). Системний виклик creat використовує тільки перший і третій аргументи.
Знов створеному файлу призначаються ідентифікатор користувача-власника і ідентифікатор групи-власника. Правила призначення власника і групи:
власник файлу рівний ефективному ідентифікатору користувача процесу.
група рівна або ідентифікатор групи каталога, в якому створюється файл, або ефективному ідентифікатору групи процесу.
У додатку немає можливості вибрати, яка з двох груп буде привласнена файлу, але воно може дізнатися результат за допомогою системного виклику stat. Щоб примусово призначити файлу потрібну групу, можна скористатися системним викликом chgrp.

2.2.3 Системний виклик close

close закриває дескриптор файлу #include

#include
int close(int fd)

fd - дескриптор файлу
Повертає 0 у разі успіху -1 у разі помилки
(код помилки -в змінної errno)

Найважливіше, що потрібно знати про системний виклик close, це те, що він практично нічого не робить. Він не виштовхує на диск вміст буферів ядра - він просто звільняє дескриптор файлу для повторного використання. Коли закривається останній дескриптор, що посилається на запис в таблиці файлів, цей запис також може бути видалена. У свою чергу, коли віддаляється останній запис в таблиці файлів, що посилається на копію індексного вузла в пам'яті, вона також може бути видалена. І ще один момент: якщо лічильник посилань даного індексного вузла виявився рівним 0, то з диска віддаляється індексний вузол і всі пов'язані з ним дані.
Фактично, закривати дескриптори файлів зовсім необов'язково, оскільки вони автоматично утилізують після закінчення процесу. Проте всі структури ядра краще звільняти своєчасно, а в текст програми додавати коментар, який повідомлятиме про закінчення роботи з файлом. Це допоможе вам під час відладки і запобіжить використанню дескриптора файлу помилково.

2.3 Читання та зипис в файл
2.3.1 Системний виклик read

read виконує читання з файлового дескриптора

#include
#include
ssize_t read(int fd, void *buf, size_t nbytes)

fd- дескриптор файлу
*buf - адреса буфера для даних, що приймаються
nbytes - об'єм даних, що приймаються

Повертає кількість прочитаних байт або -1 у разі помилки(код помилки - в змінній errno)

Системний виклик read прочитує nbytes байт з файлу, представленого дескриптором fd, і розміщує їх в буфері buf. Читання починається з поточної позиції у файлі, після закінчення читання поточна позиція зміщується на кількість прочитаних байт. У зухвалу програму повертається число прочитаних байт, 0 (після досягнення кінця файлу), або -1, як ознака помилки.
На відміну від write, виклик read не має таких широких можливостей в «обмані» Він не може спочатку передати дані зухвалому процесу, а потім прочитати їх з диска. Якщо в кеші немає необхідних даних (що можливо потрапили туди в результаті інших операцій введення-виводу), то процес вимушений чекати, поки ядро прочитає їх з диска. Іноді ядро прочитує з диска дані, що не тільки зажадалися, а відразу декілька сусідніх блоків, намагаючись у такий спосіб передбачити потреби програми і підвищити швидкість читання даних з файлу. Якщо система працює з невеликим навантаженням, дані можуть зберігатися в буферах достатньо довго і часто виконується читання сусідніх блоків даних, то такий прийом випереджаючого читання дає істотний виграш.

2.3.2 Системний виклик write
write виконує запис у файловий дескриптор

#include
#include
ssize_t write(int fd, void *buf, size_t nbytes)

fd - дескриптор
*buf - адреса буфера із записуваними даними
nbytes - кількість даних для запису

Повертає кількість записаних байт або -1 у разі помилки (код помилки - в змінній errno)

Системний виклик write записує nbytes байт з буфера buf у відкритий файл, який представлений дескриптором fd. Запис починається з поточної позиції у файлі, а після її закінчення поточна позиція зміщується на число записаних байт. У зухвалу програму повертається число байт, яке було записано або -1,если виникла помилка. Якщо файл був відкритий з прапором O_APPEND, то безпосередньо перед записом поточна позиція автоматично переміщатиметься в кінець файлу.
Виклик write застосовує техніку відкладеного запису. Суть її полягає в наступному. Насправді, виклик write не виконує запис на диск. Він просто переписує дані в буферний кеш ядра і закінчує свою роботу. Якщо виклик write закінчив свою роботу без помилки це означає, що з дескриптором файлу все гаразд, дані скопійовані і виконана перевірка, чи достатньо місця на диску. Пізніше, в більш відповідний для ядра момент, дані записуються на диск. Якщо який-небудь інший процес спробуєте прочитати ці дані до того, як вони будуть записані на диск, узяти їх можна з кеша. Таким чином, не відомо, в який момент часу дані фізично будуть записані у файл.
Техніка відкладеного запису має три проблеми:
невизначеності з часом фактичному запису даних на диск.
процес, що ініціював запис, не може бути проінформований про помилки, що виникли під час запису на диск. Насправді, буфери файлової системи не належать жодному з процесів якщо декілька процесів намагаються записувати дані в одну і ту ж ділянку одного і того ж файлу, то їх дані потраплять в один і той же буфер.
фізичний порядок запису буферів не піддається регулюванню. Навіть якщо серія системних викликів write слідує в певному порядку, це не гарантує той же порядок запису буферів на диск.

Сучасні ОС уміють справлятися з цими проблемами і переважно мати виграш за часом, який дає відкладений запис.
Виклик write може також використовуватися для запису в канали, в спеціальні файли або в сокети, але в цих випадках він має деякі особливості. Найважливіша відмінність: у подібних застосуваннях виклик write може блокуватися, це означає, що запис припиняється до настання деякої події, наприклад звільнення місця для запису чергової порції даних. Якщо виклик write заблокований, він може бути перерваний сигналом, що поступив. В цьому випадку зухвалому процесу повертається -1 і код помилки EINTR в змінній errno.

2.4 Позіціонування у файлі
2.4.1 Системний виклик lseek

Системний виклик lseek використовується для переустановлення поточної позиції у файлі. Він не виконує ніяких операцій введення-виводу і не віддає команд контроллеру диска.

lseek встановлює і повертає поточну позицію у файлі

#include
#include
off_t lseek( int fd, off_t offset, int whence );

fd - дескриптор файлу
pos - позиція у файлі
whence - інтерпретація аргументу pos
Повертає нову позицію у файлі або -1 у разі помилки (код помилки в змінній errno)

Аргумент whence може приймати одне з наступних значень:
SEEK_SET Аргумент pos містить зсув від початку файлу (абсолютна позиція у файлі).
SEEK_CUR Аргумент pos містить зсув від поточної позиції у файлі. Може бути позитивним числом, нулем і негативним числом. Вказавши в аргументі pos значення 0 ми отримаємо поточну позицію у файлі.
SEEK_ENO Аргумент pos містить зсув від кінця файлу. Може бути позитивним числом, нулем і негативним числом. Вказавши в аргументі pos значення 0 ми встановимо поточну позицію в кінець файлу.

Результатом роботи виклику може бути будь-яке ненегативне число, що навіть перевищує розмір файлу. Якщо нова поточна позиція опинилася за межами файлу, то найближчий виклик write вставить «бракуючий» шматок в кінець файлу і заповнить його байтами із значенням 0. Виклик read, з поточною позицією встановленою в кінець файлу або за його межами, поверне ознаку кінця файлу 0. Спроба читання з інтервалу, який заповнив виклик write при вставці «бракуючого» шматка, увінчається успіхом і в зухвалу програму буде повернений буфер, заповнений нулями, як того і слід було чекати.
Коли проводиться запис за межами файлу, більшість реалізацій ОС UNIX не зберігають вставлені таким чином порожні блоки. Це дає можливість, наприклад, на диску об'ємом в 3 000 000 блоків зберігати файли з сумарною довжиною більше 3 000 000 блоків. Така особливість може породити серйозні проблеми при відновленні файлів з резервних копій, тому що при створенні резервної копії на пристрій зберігання буде переданий повний об'єм файлів, тобто більше 3000 000 блоків!

Приклад використання
1) Перейти до байта № 10
newpos = lseek( fd, 10, SEEK_SET );
0 #

1 !

2 /

3 b

4 i

5 n

6 /

7 b

8 а

9 s

10 h

11 \n

12

.

100


2) Рух вперед на 4 байти
newpos = lseek( fd, 4, SEEK_CUR );
0 #

1 !

2 /

3 b

4 i

5 n

6 /

7 b

8 а

9 s

10 h

11 \n

12

13

14

15

.

100








lseek(fd -5, SEEK_CUR)
0 #

1 !

2 /

3 b

4 i

5 n

6 /

7 b

8 а

9 s

10 h

11 \n

12

13

14

15

.

100


3) Зрушити на 8 байт від кінця
newpos = lseek( fd -3, SEEK_END );

0 #

1 !

2 /

3 b

4 i

5 n

6 /

7 b

8 а

9 s

10 h

11 \n

..

96

97

98

99

100


2.4.2 Системні виклики pread і pwrite

pread виконує читання з файлу, починаючи із заданої позиції


#include
#include
ssize_t pread( int fd, void *buf, size_t nbytes,
off_t offset)

fd - дескриптор файлу
*buf адреса буфера для даних, що приймаються
nbytes - об'єм даних, що приймаються
offset - позиція у файлі;
Повертає кількість прочитаних байт або -1 у разі помилки (код помилки - в змінній errno)

pwrite виконує запис у файл, в задану позицію

#include
#include
ssize_t pwrite(int fd, void *buf, size_t nbytes, off_t offset)

fd - дескриптор файлу
*buf - адреса буфера з даними для запису
nbytes - кількість даних для запису
offset - позиція у файлі
Повертає кількість записаних байт або -1 у разі помилки (код помилки - в змінній errno)

Системні виклики pread і pwrite по своїй функціональності практично повністю співпадають з комбінацією виклику lseek і подальшим викликом read або write відповідно, за виключенням:
вони не користуються поточною позицією у файлі, оскільки позиція для запису або читання передається явно, через аргумент offset;
вони не змінюють поточну позицію у файлі. Фактично, поточна позиція повністю ігнорується цими викликами.

Для файлів, відкритих з прапором O_APPEND (див. розділ 2.8), поведінка pwrite повністю співпадає з поведінкою виклику write (аргумент offset ігнорується).
Зручність застосування одного виклику замість двох незаперечно, але, що важливіше, викликам pread і pwrite невластиві проблеми, пов'язані із зміною поточній позиції у файлі між lseek і подальшими викликами read або write з іншого процесу або потоку. Це може відбутися при використанні одного дескриптора декількома потоками додатку або при використанні дублікатів дескрипторів декількома процесами. То ж відноситься і до файлів, відкритих з прапором O_APPEND. Оскільки ні pread, ні pwrite не використовують покажчик поточної позиції у файлі, їм невластиві помилки пов'язані з невірним позиціонуванням.

2.4.3 Системні виклики readv і writev

readv читання урозкид

#include
#include
ssize_t readv(int fd, const struct iovec *iov,int iovcnt );

fd - дескриптор файлу
*iov - масив буферів для даних, що приймаються
iovcnt - кількість буферів
Повертає кількість прочитаних байт або -1 у разі помилки (код помилки - в змінній errno)

writev запис із злиттям
#include
#include
ssize_t writev(int fd, const struct iovec *iov, int iovcnt)

fd - дескриптор файлу
*iov - масив буферів з даними
iovcnt - кількість буферів
Повертає кількість записаних байт або -1 у разі помилки (код помилки - в змінній errno)

Виклики readv і writev дуже схожі на виклики read і write, але на відміну від останніх мають справу не з одним, а з декількома буферами. Іноді їх називають «читання урозкид» і «запис із злиттям». У файлі дані як і раніше розташовуються підряд, один за одним, але буфери можуть бути розкидані в пам'яті процесу.
Іншими словами, якщо вам необхідно записати у файл три структури, то замість трьох викликів write це можна зробити одним викликом writev, проте, поглянувши па другий аргумент, ви без зусиль зрозумієте щоб використовувати ці виклики, доведеться трохи потрудитися, але чи коштують вони наших зусиль?
Перед викликом будь-якій з цих функцій, ви повинні створити масив iov (елементів iovcnt), в якому кожен елемент містить покажчик на буфер з даними і розмір буфера, по суті другий і третій аргументи викликів read і write.

struct iovec структура, використовувана в системних викликах rcadv і writev*
struct iovec {
void *iov_base; /* адреса буфера з даними */
size_t iov_len; /* розмір буфера */
}

У своїх програмах ви можете оголосити статичний масив з struct iovec а також розмістити його в динамічній пам'яті викликом malloc або іншим способом. Максимальне число елементів масиву для різних OС може декілька відрізнятися, але воно ніколи не буває менше 16. У OСL, що задовольняють специфікації SUS, ідентифікатор IOV_MAX відповідає максимальному числу елементів масиву. Якщо цей ідентифікатор не визначений, можна скористатися функцією sysconf з аргументом _.SC_IOV_MAX Але в більшості: реальних застосувань 16 елементів більш ніж достатньо.

2.5 Контрольні операції над файлами
2.5.1 Системні виклики stat, fstat і lstat

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

Системні виклики stat, fstat і lstat
Індексний вузол містить повний набір метаданих: про файл все, окрім імені, яке насправді ніяк не пов'язано з індексним вузлом і даними, на які він указує. Процедура читання даних з індексного вузла прямо з файлу пристрою є дуже низькорівневою. На щастя, для отримання інформації з індексних вузлів, існують три стандартні системні виклики stat, lstat і fstat і одна дуже відома команда ls.

stat повертає відомості про файл по його імені
«include
int stat(const char *path, struct stat *buf )

path - повне ім'я файлу
struct stat *buf - структура, що зберігає значення індексного дескриптора файлу
Повертає 0 у разі успіху -1 у разі помилки (код помилки -в змінної errno)

lstat повертає відомості про файл по його імені без разыменования символічних посилань
«include
int lstat(const char*path, struct stat *buf);

path - повне ім'я файлу
struct stat *buf - повертана інформація
Повертає 0 у разі успіху -1 у разі помилки (код помилки - в змінній errno) */

fstat повертає відомості про файл по дескриптору
#include
int fstat(const int fd, struct stat *buf);

fd - дескриптор файлу
struct stat *buf - повертана інформація
Повертає 0 у разі успіху -1 у разі помилки (код помилки - в змінній errno)

Системний виклик stat приймає повне ім'я файлу і знаходить відповідний йому індексний вузол. Виклик fstat приймає дескриптор відкритого файлу і відшукує відповідний індексний вузол в таблиці активних вузлів ядра. Виклик lstat аналогічний виклику stat за одним виключенням якщо як ім'я файлу було передано символічне посилання, то буде повернена інформація про саме посилання, а не про об'єкт, який вона представляет*. Всі три виклики повертають одні і ті ж метадані, через структуру типу stat, в першому аргументі.
Нижче приводиться визначення структури stat. Проте в деяких реалізаціях поля структури можуть бути переставлені місцями, можуть бути додані нові поля
struct stat структура для прийому відомостей з 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; розмір в байтах
time_t st_atime; час останнього звернення
time_t st_mtime; час останньої зміни
time_t st_ctime; час останньої зміни
blksize_t st_blksize; оптимальний розмір блоку
(для операцій введення-виводу)
blkcnt_t st_blocks; (к-ть блоків по 512 байт)
}

Деякі зауваження по цій структурі.
Ідентифікатор пристрою (типу dev_t) це число, унікальне для кожної змонтованої файлової системи, навіть для NFS-томов. Таким чином, комбінації st_dev і st_ino однозначно ідентифікують будь-який індексний вузол в системі. власне пристрій. Як правило, молодший номер пристрою це самий молодший байт.
Поле st.dev це пристрій, на якому знаходиться індексний вузол. Поле st_rdev використовується тільки для спеціальних файлів пристроїв і є пристроєм, який спеціальний файл представляє.
Поле st_size інтерпретується залежно від типу файлу і реалізації. Для звичайних файлів, каталогів і символічних посилань це об'єм даних на диску (для символічних посилань довжина шляху). Для об'єктів в пам'яті, що розділяється, об'єм займаної пам'яті. Для каналів кількість даних в каналі.
Поле st_blksize призначене для зміни розміру блоку файлу. В більшості випадків, ймовірно, має те ж призначення, що і аналогічне поле в суперблоці.
Якщо файл має «дірки», отримані як результат установки поточної позиції у файлі за його межі і подальшому записі яких-небудь даних, значення st_blocks * 512 може опинитися менше значення st_size.
Системний виклик fstat особливо затребуваний в ситуаціях, коли потрібно отримати відомості про елемент, що не має шляху у файловій системі, наприклад, про неіменований канал або сокет. Для цих об'єктів поля st_mode, st_ino, st_dev, st_uid, st_gid, st_atime, st_ctime і st_mtime містять актуальні значення, але що стосується інших полів все залежить від реалізації. Проте, для іменованих і неіменованих каналів поле st_siz
·e, як правило, містить кількість непрочитаних байт в каналі.
Поле st_ctime Час останньої зміни індексного вузла (st_ctime) іноді називається як «час останньої зміни статуса».изменяется (встановлюється рівним поточному астрономічному часу) при застосуванні до файлу викликів creat, chmod, chown, link, unlink, mknod, utime*, write (оскільки змінюється довжина файлу); не оновлюється при читанні даних з файлу. Це поле слід розглядати як час модифікації прав доступу до файлу;
st_mtime - write, creat, mknod, utime; Це поле слід розглядати як час модифікації вмісту файлу (даних);
st_atime - read, creat, mknod, utime; Це поле слід розглядати як час читання вмісту файлу (даних).

Визначення типу файлу
Поле st_mode містить 12 битий, що визначають тип файлу і права доступу. Макрос S_IFMT це маска всіх бітів, які відносяться до типу файлу в полі st_mode.
Операція st_mode файлу & S_IFMT повертає значення типу:
S_IFREG - звичайний файл
S_IFDIR - каталог
S_IFBLK - спеціальний файл блокового пристрою
S_IFCHR - спеціальний файл символьного пристрою
S_IFIFO - іменований або неіменований канал
S_IFLNK - символічне посилання
S_IFSOCK - сокет

struct stat sbuf;
...
if(stat(file &sbuf) == 0)
if((sbuf.st_mode & S_IFMT) == S_IFDIR)
printf(“A directory\n”);

if ((buf.st_mode & S_IFSOCK) == S_IFSOCK) /*невірно*/
Треба так:
if ((buf.st_mode & S_IFMT) == S_IFSOCK)

UNIX містить ряд макросів для визначення типу файлу. Вони визначаються в and :
S_ISREG() regular file
S_ISDIR() directory file
S_ISCHR() char. special file
S_ISBLK() block special file
S_ISFIFO() pipe or FIFO
S_ISLNK() symbolic link
S_ISSOCK() socket

Перевірка чи є файл сокетом, за допомогою макросу може бути виконана так:
if (S_ISSOCK(buf.st_mode))

Визначення прав доступу до файлу

Для визначення прав доступу в полі st_mode є 9 битий. Імена цих макросів слідують формі запису: s_Ipwww, де символ p режим доступу (R, w або X), а www кому видано право на цей режим доступу (USR, GRP або OTH).

S_Ixwww де x = R|W|X, www = USR|GRP|OTH

приклади:
S_IRUSR
S_IWOTH
S_ISUID set-user-id bit is set
S_ISGID set-group-id bit is set
S_ISVTX sticky bit is set

Цими макросами є бітові маски.
1) Тоді перевірка прав на читання і на запис для групи буде такий:

if ((buf.st_mode &(S_IRGRP|S_IWGRP))= (S_IRGRP|S_IWGRP))
2) перевірка установки біта зміни ідентифікатора користувача

if((sbuf.st_mode & S_ISUID) != 0)
printf(“set-user-id bit is set\n”);

Приклад використання

Напишіть програму, що друкує інформацію про всі відкриті нею файли і канали. Для цієї мети використовуйте системний виклик

#include
#include
#include
#include
#include

main
{
struct stat st;
int fd;
open(“file”,O_RDONLY);

for(fd=0; fd < NOFILE; fd++ )
if (fstat(fd &st ==0)
{
printf("File stats\n");
printf(" File size: %ld\n", st.st_size);
printf(" Permission: %o\n", st.st_mode & 0x01ff);
/* шістнадцятиричне число = 0001 1111 1111
Можна так
stat( имяФайла &st);
коды Доступа = st.st_mode & 0777;
або S_IRWXU| S_IRWXG| S_IRWXO */
printf(" Owner uid: %d\n", st.st_uid);
printf(" Owner gid: %d\n", st.st_gid);
printf("\n");
}
}
}

Програма може використовувати дескриптори файлів з номерами 0..NOFILE-1 (зазвичай 0..19). Якщо fstat для якогось fd повернув код помилки (<0), це означає, що даний дескриптор не пов'язаний з відкритим файлом (тобто не використовується). NOFILE визначене в include-файле , що містить різноманітні параметри даної системи.
По відкритому файлу не існує простого способу дізнатися його ім'я. Для цього треба сисвызовом fstat дізнатися номер пристрою і номер індексного дескриптора, після чого знайти у файловій системі відповідний запис.

2.5.2 Контроль прав доступу до файлу

access(filename,mode)
char *filename;
int mode;

Функція access перевіряє, чи має процес дозвіл на читання, запис або виконання файлу Якщо pathname є символьним посиланням, то перевіряються права доступу до файлу, на який вона указує. mode - це маска, що складається з одного або більш за прапори R_OK, W_OK, X_OK і F_OK.
R_OK, W_OK і X_OK використовуються для перевірки, чи існує файл і чи можна його читати, записувати в нього або виконувати, F_OK просто перевіряє існування файлу.
Повертає 0 у разі успіху -1 у разі помилки (код помилки -в змінної errno)

Зміна власника і прав доступу до файлу

chown - зміна власника і групи файлу

int chown (path, owner, group)
char *path;
int owner, group;

Аргумент path є покажчиком на маршрутне ім'я файлу. Ідентифікатори власника і групи вказаного файлу встановлюються рівними числовим значенням, відповідно, аргументів owner і group.
Тільки суперкористувач може змінити власника файлу. Власник файлу може змінювати групу файлу на будь-яку групу, до якої він належить. Суперкористувач може довільно проводити цю заміну.
chmod - зміна режиму доступу до файлу

#include
#include

int chmod(const char *path, mode_t mode);
int fchmod(int fildes, mode_t mode);

Можна змінювати коди доступу, відповідні бітовій масці
0777 | S_ISUID | S_ISGID | S_ISVTX

Аргумент path є покажчиком на маршрутне ім'я файлу. Системний виклик chmod встановлює режим доступу до вказаного файлу відповідно до бітового шаблону, що міститься в аргументі mode.

Змінювати права доступу до файлу може тільки процес, що має ідентифікатор користувача, що діє, рівний ідентифікатору власника файлу або суперкористувача.

Якщо системні виклики chown і chmod виконуються не суперкористувачем, то біти переустановлення ідентифікаторів користувача і групи, 04000 і 02000 відповідно, обнуляються.

Системний виклик umask

Ми вже згадували про маску, що накладається на набір прав доступу при створенні файлу. Накладення маски виконує системний виклик umask, який рідко використовується де-небудь ще, окрім як в команді umask.
umask встановлює і повертає маску прав доступу для новостворюваних файлів.

#include
mode_t umask(mode_t cmask);

cmask - нова маска
Повертає попереднє значення маски повернення код помилок не передбачений

Оскільки кожен процес має маску і будь-яка комбінація з 9 біт вважається допустимим, umask не передбачає вихід помилково. Він завжди повертає колишнє значення маски. Щоб дізнатися поточне значення маски не змінюючи її, потрібно виконати два послідовні виклики umask. Перший щоб набути поточного значення маски (передавши у виклик будь-яке значення нової маски), другої, щоб відновити її в первинний стан.

2.5.3 Функція fcntl

Над дескрипторами файлів можна виконувати деякі операції, наприклад, відкривати файли, дублюючи дескриптори відкритих файлів, отримувати або встановлювати прапори, що описують стан дескриптора, захоплювати блоки файлу і так далі Всі ці операції виконуються функцією fcntl.

#include
int fcntl (int fd, int cmd, arg)

Функція виконує команду cmd над дескриптором файлу fd.
Деякі команди вимагають завдання додаткових аргументів.

Таблиця 1
Основні команди cmd
F_DUPFD
Дублювання дескриптора файлу. Повертає інший дескриптор, що асоціюється з тим же відкритим файлом;

F_GETFD
Отримує прапори, що асоціюються з дескриптором;

F_SETFD
Встановлює прапори, що асоціюються з дескриптором;

F_GETFL
Отримує прапори, що асоціюються з відкритим файлом;

F_SETFL
Встановлює прапори, що асоціюються з відкритим файлом;

F_GETLK
Отримує інформацію про захоплені області файлу;

F_SETLK
Встановлює або скидає захоплення файлу;

F_SETLKW
Ідентично F_SETLK, але чекає закінчення виконання;

F_GETOWN
Отримує процес або групу процесів, яким буде посланий сигнал SIGIO

F_SETOWN
Встановлює процес або групу процесів, яким буде посланий сигнал SIGIO


Діагностика
При успішному завершенні системного виклику залежно від операції cmd повертаються наступні значення:
F_DUPFD Новий дескриптор файлу.
F_GETFD Значення прапора (визначений тільки молодший біт).
F_SETFD Значення, відмінне від -1.
F_GETFL Значення прапорів статусу файлу.
F_SETFL Значення, відмінне від -1.
F_GETLK Значення, відмінне від -1.
F_SETLK Значення, відмінне від -1.
F_SETLKW Значення, відмінне від -1.
У разі помилки повертається -1, а змінною errno привласнюється код помилки.

Отримання і установка прапора стану файлу

1) Як по дескриптору файлу дізнатися, відкритий він на читання, запис, читання і запис одночасно?

int flags;
if((flags=fcntl (fd, F_GETFL, NULL)) < 0 )
return NULL; /* fd ймовірно не відкритий */
flags &= O_RDONLY | O_WRONLY | O_RDWR;
switch(flags){
case O_RDONLY: return "r";
case O_WRONLY: return "w";
case O_RDWR: return "r+w";
default: return NULL;
}

2) приклад зміни прапора дескриптора:
int flags = fcntl (fd, F_GETFL, 0);
fcntl (fd, F_SETFL, flags | O_NONBLOCK);

3) установка нового власника
fcntl(fd, F_SETOWN, getpid())

4) setflag (int desc, int value)
{
int oldflags = fcntl (desc, F_GETFL, 0);
/* якщо відбулася помилка, виходимо з програми */
if (oldflags == -1)
return -1;
/* встановлюємо тільки потрібний прапор */
if (value != 0)
oldflags |= O_NONBLOCK;
else
oldflags &= ~O_NONBLOCK;
/* зберігаємо прапор в дескрипторі файлу */
return fcntl (desc, F_SETFL, oldflags);
}

Передається два параметри: дескриптор і індикатор – 0, якщо треба зняти прапор і не 0, якщо встановити.
|= додає ?=~ знімає ~ інвертує
Зберігаємо в дескрипторі, повертаємо в зухвалу процедуру.

Блокування файлів з використанням fcntl

Окрім модифікації файлового дескриптора системний виклик fcntl(2) використовується для блокування файлу. Блокування областей файлу дозволяє декільком програмам спільно працювати з вмістом одного і того ж файлу, не заважаючи один одному.
Прикладом ситуації, в якій може бути корисний захоплення файлу служить бути програма, яка може бути запущена декількома користувачами незалежно, записує протокол роботи у файл. Декілька процесів, що одночасно записують інформацію у файл можуть зміщувати інформацію різних користувачів, що приведе до плутанини. У разі захоплення файлу цього не відбудеться.

В цьому випадку cmd - тип блокуючої операції, а в у arg указується адреса структури flock, в яку записується інформація про блокування. Структура і відповідні макроси описані в заголовному файлі .

Таблиця 2
Опис полий структури flock
Поле
Значення

short l_type
Тип блокування: записи – F_RDLCK, читання – F_WRLCK, скидання – F_UNLCK.

short l_whence
Адреса початку ділянки, що блокується; дається у вигляді зсуву відносно початку файлу (0), щодо поточної позиції покажчика (1), щодо кінця файлу (2)

long l_start
Зсув в байтах, що інтерпретується відповідно до значення l_whence

long l_len
Довжина ділянки, що блокується, в байтах. Якщо вказаний 0, блокується ділянка від l_start до кінця файлу

long l_pid
Ідентифікатор процесу, що встановив блокування (для GETLCK)

long l_sysid
Ссистемний ідентифікатор процесу, блокуючого файл


Якщо задана область вже заблокована, fcntl повертає -1.
За допомогою команди F_GETLK можна дізнатися, ідентифікатор процесу, що заблокував дану область.

Ядро автоматично знімає блокування, встановлені процесом, при закритті файлу.

За допомогою цієї функції можна маніпулювати дескрипторами файлів і встановлювати рекомендаційні (advisory) блокування (необов'язкові). В цьому випадку, якщо програма сама не використовує блокувань, блокування, встановлені іншими програмами, не матимуть для неї ніякого ефекту. Існує можливість додати рекомендаційним блокуванням fcntl() обов'язковий характер, але для цього відповідна файлова система повинна бути змонтована із спеціальним ключем.

Базові правила використання блокувань:
Програми, які хочуть читати з файлу, належний відкрити файл, а потім використовувати fcntl в режимі F_RDLCK
Програми, які хочуть читати з файлу і записувати у файл повинні відкрити файл, а потім використовувати flock в режимі F_WRLCK.
Виклик flock блокуватиме файл до тих пір, поки він доступний, в цей час необхідні операції можуть бути виконані з деякою мірою безпеки.
Після виконання операцій приберіть блокування шляхом закриття файлового дескриптора або за допомогою F_UNLCK

Приклад
1. Для установки блокування ми заповнюємо поля структури flock необхідними значеннями і викликаємо fcntl() з командою F_SETLK (встановити блокування):
..
struct flock fi;
fd=open(file, O_RDWR);
fi.l_type = F_WRLCK;
fi.l_whence = SEEK_SET;
fi.l_start = 0;
fi.l_len = 64;
off = 0;
while (fcntl(fd, F_SETLK &fi)== -1)
{ fcntl(fd, F_GETLK & fi);
...
printf("байти %i - %i заблоковані процесом %i\n", off, off+64
fi.l_pid);
}

Якщо задана область вже заблокована, fcntl повертає -1. За допомогою команди F_GETLK можна дізнатися, ідентифікатор процесу, що заблокував дану область.

2. Для того, щоб зняти блокування, ми викликаємо fctnl() з командою F_SETLK (дивно, чи не так?) і параметром l_type = F_UNLCK:
fi.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK &fi)== -1)
printf("Помилка разблокирования\n");

2.5.4 Системні виклики truncate і ftruncate

truncate змінює розмір файлу, заданого по імені
#include
#include
int truncate(const char *path, off_t length );

*path - повне ім'я файлу
Length - новий розмір файлу
Повертає 0 у разі успіху -1 у разі помилки (код помилки -в змінної errno)

ftruncate змінює розмір файлу, заданого дескриптором
#include
#include
int ftruncate(int fd, off_t length);

fd - дескриптор файлу
length - новий розмір файлу
Повертає 0 у разі успіху -1 у разі помилки (код помилки -в змінної errno)

Системні виклики truncate і ftruncate використовуються для зміни розмірів файлу як у велику, так і в меншу сторону. Як можна збільшити розмір файлу без фактичного запису даних в нього, вже розповідалося, при обговоренні системного виклику lseek. Хоча truncate і ftruncate можуть збільшувати розмір файлу, все ж таки частіше вони використовуються для зменшення (усікання) файлів. Раніше, до появи цих викликів в UNIX, усікання файлів було неможливим. (Доводилося створювати новий файл і потім перейменовувати його.)

2.6 Управління каталогами
2.6.1 Створення і видалення

mkdir() створює порожній каталог

#include
#include
int mkdir(const char *pathname, mode_t mode);
Return: 0 якщо OK -1 при помилці

Ця функція створює новий порожній каталог. Елементи крапка і крапка-крапка створюються автоматично. Вказані права доступу до файлів, mode, модифікується маскою створення файлів процесу. Загальна помилка - указувати такий же режим має рацію як і для файлу (тільки читання і запис). Але для директорія нам треба, по украй мірі, підняти один біт виконання, щоб дістати доступ до імен файлів усередині директорія.

rmdir() видаляє порожній каталог

#include
int rmdir(const char *pathname);
Return: 0, якщо все OK -1 при помилці

Якщо лічильник посилань на каталог стане рівним нулю разом з цим викликом і ніякій інший процес не відкрив каталог, тоді простір займане каталогом звільняється. Якщо один або більш за процеси мають каталог відкритим, коли лічильник посилань досягне 0, віддаляється останнє посилання, віддаляються елементи крапка і крапка-крапка перш ніж функція поверне управління. Проте, каталог не звільняється до тих пір, поки останній процес не закриє його.

2.6.2 Читання каталога

Каталог може бути прочитаний будь-яким користувачем у кого є права для читання каталога. Але здійснити запис в каталог може тільки ядро (для збереження працездатності файлової системи).

Стандартна бібліотека Linux надає два способи перерахування вмісту директорій:
за допомогою функції scandir() і функцій зворотного виклику

int scandir (const char *dir, struct dirent ***namelist,
int(*select)(const struct dirent *)
int(*compar)(const struct dirent **, const struct dirent **));

Як функція сортування запропоновані alphasort() і versionsort(), а ось фільтр, так розумію, потрібно писати свій.
Функція scandir() повертає кількість обраних записів або -1, якщо трапилася помилка.

другий – з використанням набору функцій opendir(), readdir(), closedir().

#include
#include
DIR * opendir(const char *pathname);
Return: покажчик, якщо все OK, NULL при помилці

struct dirent *readdir(DIR *dp);
Return: покажчик, якщо все OK, NULL якщо кінець каталога або помилка

void rewinddir(DIR *dp);
int closedir(DIR *dp);
Return: 0, якщо все OK -1 при помилці

Структура dirent визначена у файлі і є залежною від реалізації. Але вона містить по украй мірі два елементи.

struct dirent {
int_t d_ino; // номер I-node
char d_name[NAME_MAX+1]; // ім'я файлу
}

Константа NAME_MAX залежить від файлової системи в якій розміщується каталог і її значення береться з функції fpathconf(). Часте значення для BSD-систем складає 255.

Структура DIR це внутрішня структура, використовувана для обробки інформації про каталог, який був прочитаний. Покажчик на структуру DIR, який повертається функцією opendir(), використовується рештою функцій.

Розглянемо два варіанти програми printdir, що роздруковує вміст директорії, переданої їй як аргумент.

#include
#include

int sel(struct dirent * d)
{ return 1; // завжди підтверджуємо}

int main(int argc, char ** argv){
int i, n;
struct dirent ** entry;
if (argc != 2)
{
printf("Використання: %s <директория>\n", argv[0]);
return 0;
}
n = scandir(argv[1] &entry, sel, alphasort);
if (n < 0)
{
printf("Помилка читання директории\n");
return 1;
}
for (i = 0; i < n; i++)
printf("%s inode=%i\n", entry[i]->d_name, entry[i]->d_ino);
return 0;
}

Функція scandir() створює список елементів вказаної директорії. Їй необхідно передати покажчик на функцію-фільтр, яка, отримуючи дані про черговий елемент, ухвалює рішення, включати цей елемент в результуючий список. У нашому прикладі це функція sel(). Якщо при черговому виклику функція sel() поверне значення 0, відповідний елемент директорії не буде включений в кінцевий список. Останній параметр scandir - функція сортування елементів директорії. Ми використовуємо функцію alphasort(), що сортує елементи в лексикографічному порядку.

Дані про елементи директорії передаються в структурах dirent. Можна було б чекати, що структури типу dirent містять багато корисної інформації про елементи директорії, але це не так. Окрім імені файлу dirent містить номер inode для цього елементу (простим програмам зазвичай не навіщо знати номера inode, але, щоб наш приклад якось відрізнявся від стандартного, ми включаємо цю інформацію). У структури dirent є ще поле d_type типу char *, але воно, як правило, містить null.

Функція scandir() дозволяє нам отримати повний відсортований список елементів директорії за один виклик. У нас є можливість використовувати низькорівневі засоби, які можуть опинитися швидше в тому випадку, якщо сортування файлів нам не потрібне.

Розглянемо другий варіант програми:

#include
#include
int main(int argc, char ** argv)
{
DIR * d;
struct dirent * entry;
if (argc != 2)
{
printf("Використання: %s <директория>\n", argv[0]);
return 0;
}
d = opendir(argv[1]);
if (d == NULL)
{
printf("Помилка читання директории\n");
return 1;
}
while (entry = readdir(d))
printf("%s inode=%i\n", entry->d_name, entry->d_ino);
closedir(d);
return 0;
}

Цей варіант програми використовує функції opendir(), readdir() і closedir(), які працюють з директорією як з файлом. Функція readdir() повертає значення TRUE до тих пір, поки не будуть прочитані всі елементи директорії.

2.6.3 Зміна каталога

int chdir(char * pathname);

pathname - ім'я нового робочого каталога

Функція chdir змінює поточний робочий каталог на каталог, визначуваний по pathname; pathname повинен посилатися на існуючий каталог.

Функція chdir повертає значення 0, якщо робочий каталог успішно змінений. Повертане значення -1 указує на помилку; в цьому випадку errno встановлюється в ENOENT, указуючи, що задане path-имя не може бути знайдене. Помилки не виникають, якщо pathname визначає поточний робочий каталог.

Приклад:

наступного оператора змінює поточний робочий каталог на кореневий

chdir("/"); /* Зауваження: еквівалентно chdir("\")
chdir( ".." ); /* вийти вгору */

void lookdir(char *s, int ас, char **av)
chdir(s)

chdir("c:/temp");
c:\temp
Відмітимо, що ви повинні поміщати два зворотні слеші (\) в C-стрoку, щоб представити одиночний слэш (\). Зворотний слэш - це escape-символ для C-строк і тому вимагає спеціального звернення.

2.7 Управління посиланнями
2.7.1 Управління жорсткими посиланнями

Запис у файлі каталога, що містить ім'я і номер індексного вузла, називається жорстким посиланням. Існує ще один тип посилань символічні посилання

Жорстке посилання з'являється при створенні файлу будь-якого типу, включаючи каталоги. Можна створити додаткові жорсткі посилання на файли, що не є каталогами, за допомогою системного виклику link:

link створює жорстке посилання

#include
int link(const char *oldpath, const char *newpath)

oldpath, старе ім'я файлу
newpath нове ім'я файлу
Повертає 0 у разі успіху-1 у разі помилки (код помилки -в змінної errno)

Перший аргумент (oldpath) повинен бути ім'ям існуючого жорсткого посилання вона представляє номер використовуваного індексного вузла. Другий аргумент (newpath) задає ім'я нового жорсткого посилання. Старе і нове посилання абсолютно равноправны, оскільки UNIX не розрізняє первинні і вторинні посилання. Процес, що створює жорстке посилання, повинен мати право на запис в каталог, де вона розміщуватиметься. Імені посилання, представленого в другому аргументі, не повинно існувати системний виклик link не уміє змінювати існуючі посилання. У разі потреби, існуюче посилання повинне бути спочатку видалена викликом unlink.
2.7.2 Управління символічними посиланнями

На відміну від жорсткого посилання, символічне посилання просто маленький файл, який зберігає рядок повного імені об'єкту в текстовому вигляді. Символічні посилання можуть посилатися на інші символічні посилання. Зазвичай, коли системному виклику open передається символічне посилання такого роду, ядро разыменовывает їх по ланцюжку до тих пір, поки не зустріне об'єкт, який не є символічним посиланням. З жорсткими посиланнями цього не відбувається, тому що вони прямо посилаються на індексний вузол, який не може бути іншим жорстким посиланням (хоча він може бути каталогом, який містить жорстке посилання). Іншими словами, якщо шлях до об'єкту визначається жорстким посиланням, він може розглядатися буквально. Якщо символічним посиланням, то фактичний шлях до об'єкту залежить від вмісту проміжних символічних посилань.
Створюються символічні посилання системним викликом symlink:

symlink створює символічне посилання

#include
int symlink(const char *oldpath, const char *newpath)

oldpath, можливе старе повне ім'я
newpath нове повне ім'я
Повертає 0 у разі успіху -1 у разі помилки (код помилки -в змінної errno)

Головним чином, symlink працює аналогічно link. B обох випадках створюється нове жорстке посилання з ім'ям newpath. Проте у разі символічного посилання, це нове жорстке посилання указує на файл символічного посилання, який містить рядок oldpath.
У коментарях до системного виклику використано уточнення «можливе». Зроблено це тому, що symlink взагалі ніяк не перевіряє коректність аргументу oldpath.
Така особливість символічних посилань закладена в них навмисно вони можуть посилатися на неіснуючі об'єкти, до яких можна зарахувати об'єкти, що знаходяться в отмонтированных файлових системах.
З іншого боку, подібна незалежність від об'єкту посилання має свої недоліки ядро нічого не робить з символічним посиланням, коли віддаляється об'єкт посилання (жорсткою).

Видалити символічне посилання можна за допомогою системного виклику unlink.

readlink читає вміст файлу символічного посилання

#include
ssize_t readlink(const char *path, char *buf, size_t bufsize)

path - повне ім'я
buf повертаний текст
bufsize розмір буфера;
Повертає 0 у разі успіху -1 у разі помилки (код помилки -в змінної errno)
Системний виклик readlink повертає повне ім'я об'єкту посилання, але без завершуючого символу «0», який в UNIX зазвичай служить маркером кінця рядка. Тому як третій аргумент слід передавати число, на 1 менше фактичного розміру буфера, а після отримання заповненого буфера необхідно додати в нього символ «\0»:

2.7.3 Перейменування файлів і каталогів

Операція перейменування має на увазі зміну назви елементу каталога. Потрібно створити нове жорстке посилання викликом link і видалити старе посилання викликом unlink.
Але в цьому випадку виникає ряд проблем:
файл може бути каталогом, для якого не можна створити друге жорстке посилання;
перейменований об'єкт може опинитися в іншому каталозі. Якщо це каталог тієї ж файлової системи, то проблем не виникає, але якщо інший, то виникне конфлікт, оскільки link може створювати жорсткі посилання тільки в межах однієї файлової системи. Зрозуміло, можна створити символічне посилання в новому каталозі, але це не «перейменувати»;
якщо необхідно «перейменувати» (фактично «перемістити») каталог з однієї файлової системи в іншу, то доведеться перемістити ціле дерево підкаталогів зі всім їх вмістом. Перемістити лише порожні каталоги буде недостатньо;
в межах однієї файлової системи можна створити декілька жорстких посилань на один і той же файл - всі вони посилаються на один і той же індексний вузол, який не змінюється. Якщо буде необхідно перемістити файл в іншу файлову систему, то виникає наступна проблема. Створюється нове жорстке посилання, віддаляється стара, але при цьому «стара» копія файлу залишиться на місці, оскільки файл не можна видалити, поки на нього є хоч одне посилання. Таким чином, замість «перейменування» виходить копіювання.
якщо змінити повний шлях до файлу, що міститься в символічному посиланні, то в результаті отримаємо неправильне посилання.
І так далі

Тобто операція перейменування не така проста, як здається на перший погляд.

копіювати каталог і вкладені підкаталоги зі всім їх вмістом, а потім видаляти старий каталог, навіть якщо він просто перейменовується, залишаючись в тому ж самому батьківському каталозі. – Не підходить оскільки це дуже великий об'єм роботи;
використовувати системний виклик link для каталогів, нехай навіть викликати його може тільки суперкористувач. - знехтуваний POSIX і замінений на rename
використовувати новий системний виклик, який буде в змозі виконувати переміщення каталогів усередині файлової системи. - Це єдиний спосіб перейменувати каталог бсз повного копіювання його вмісту.

rename перейменовує файл

# include
int rename(const char *oldpath, const char *newpath)

oldpath, старе повне ім'я
newpath нове повне ім'я
Повертає 0 у разі успіху -1 у разі помилки (код помилки -в змінної errno)

Виклик rename виконує приблизно наступну послідовність дій:
Якщо newpath існує, то він віддаляється за допомогою unlink або rmdir.
Виконується link (oldpath, newpath), навіть якщо oldpath є каталогом.
Викликом unlink або rmdir віддаляється oldpath.

Крім того, він має свої особливості і обмеження.
Крок 2 працює і з каталогами, навіть якщо процес не володіє правами суперкористувача (процесу досить мати право на запис в батьківський каталог newpath).
Якщо newpath існує, тоді і newpath, і oldpath повинні бути одного типу, або файли, або каталоги.
Якщо файл переміщається в новий каталог, потрібно вказати ім'я файлу в новому каталозі явно
Якщо newpath існує і є каталогом, він повинен бути порожній (аналогічне правило існує і у rmdir). На кроці 3, якщо oldpath каталог, він віддаляється, навіть якщо це непорожній каталог, оскільки його вміст вже є в newpath.
якщо oldpath є символічним посиланням, то rename працює з нею, а не з тим, на що вона посилається.
Якщо rename терпить невдачу, то все залишається без змін.

На перший погляд здається, що кроки 1, 2 і 3 можна оформити у вигляді бібліотечної функції, але, на жаль, немає способу змусити її працювати подібно до системного виклику.
На основі системного виклику rename створена утиліта mv, яка може виконувати ширший діапазон дій.
переміщати окремі файли і каталоги між файловими системами.
переміщати групи файлів і каталогів.





3 Керування процесами ОС UNIX
3.1 Процеси ОС UNIX

В ОС UNIX щораз, коли Ви виконуєте команду, запускається процес, що ідентифікується та відслідковується операційною системою. Характерною рисою ОС UNIX є те, що одні процеси можуть породжуватися іншими. Інформацію про процеси можна одержати за допомогою команди командного інтерпретатора shell ps -f. На екрані з'явиться приблизно наступне:

UID PID PPID C STIME TTY TIME COMMAND
userow 94 1 0 10:15:56 tty6 0:02 -sh
userow 116 94 0 10:16:25 tty6 0:00 -sh
userow 125 116 0 10:16:27 tty6 9:38 /dss/rk/rk.20.02
-hrk=/dss/rk/d.hrk -nocore -in=stdin -out=stdout -display=D211
userow 285 125 1 13:11:10 tty6 0:00 sh
userow 290 285 10 13:11:58 tty6 0:00 ps -f

Таким чином, у користувача userow, що проробило описані вище дії, є 5 активних процесів. Цікаво простежити перемикання в колонках ідентифікатор процесу (PID) і ідентифікатор батьківського процесу (PPID). Shell, запущений при вході користувача userow у систему, має ідентифікатор процесу 94; його предком є процес ініціалізації (його ідентифікатор дорівнює 1). Процес із ідентифікатором 116 породжений для виконання shell-процедури rk; він є предком процесу з ідентифікатором 125 і т.д.
Точно так само нові процеси породжуються при виконанні програм. Уведення команди запуску програми означає запит до shell'у на породження ще одного процесу, побудованого з виконуваного файлу з усіма функціями, поміщеними в нього редактором зв'язків.

3.2 Сисвизови створення процесів
Для створення процесів є три основних системних виклики:

fork(2) - створити копію даного процесу.
system(3S) - запит до shell'у на виконання команди.
exec(2) - завершити один процес і приступитися до виконання іншого.

3.2.1 fork(2)

Новий процес створюється системним викликом fork(). Не створюються цим викликом в UNIX тільки три процеси 0, 1 і 2, вони створюються в момент ініціалізації системи. Процес 0 - батько процесів 1 і 2; процес 1 - батько всіх процесів по ініціалізації діалогу (LOGIN) і таким чином батько взагалі всіх процесів; процес 2 - свопер.
При створенні нового процесу насамперед використовується копія зухвалого процесу, що є процесом-батьком і породжує викликуваний процес, тобто процес-дитина. При цьому инициализируются деякі параметри в просторі процесу-дитини й, що найбільше важливо, сегменти тексту (кодів), даних і магазина процесу-дитини зберігаються ті ж самі, що й у процесу-батька.
fork() є причиною декількох подій, які відбуваються в момент створення нового процесу:
перевіряється чи досить простору в пам'яті для підкачування нового процесу,
потім викликається функція newproc(), щоб приєднати новий процес до таблиці входів,
після створення таблиці входів newproc() створює підкачується копию, що, процесу-батька,
далі ініціалізуються робочі параметри процесу-дитини у відведеній йому пам'яті.

У новому вході таблиці процесів ідентифікатор процесу-дитини встановлюється в p_pid. Ідентифікатор процесу-батька встановлюється в p_ppid. Ідентифікатор процесу - це число від 0 до MAXPID = 30000. Відразу ж підкреслимо, що ідентифікатор використовується тільки в межпроцессных взаємодіях. Наприкінці досліджується таблиця, щоб відзначити деякий певний процес за адресою або процес, що йде в цей момент, і вказати його значення в u_procp.
Звідси можна бачити, що newproc() є внутрішньою функцією fork() і робить роботи при дійсному створенні нового процесу:
одержує унікальний ідентифікатор процесу й знаходить вільний вхід у таблицю процесів,
заповнює вхід у таблицю процесу-дитини, копіюючи інформацію від процесу-батька й инициализируя потрібні поля,
приєднує пам'ять на пристроях покачки для свопингового зображення процесу-дитини,
нові копії процесу-батька й процеса-дитини виставляє в чергу активних завдань і робить виконуваними.

Системний виклик fork() створює новий процес - точну копію процесу, що викликав. У цьому випадку новий процес називається породженим процесом, а процес, що викликав, - батьківським.
Єдина істотна відмінність між цими двома процесами полягає в тім, що породжений процес має свій унікальний ідентифікатор. У випадку успішного завершення fork() повертає породженому процесу 0, а батьківському процесу - ідентифікатор породженого процесу. Ідея одержання двох однакових процесів може здатися трохи дивної, однак:
Оскільки повертаються значения, що, різні для породженого й батьківського процесів, процеси можуть виконуватися по-різному залежно від значення, що повертається.
Породжений процес може перетворитися в зовсім інший процес за допомогою виклику exec().
Батьківський процес може за допомогою виклику wait(2) чекати завершення породженого процесу.















Таблиця 3
Значення осередків у входах таблиці нового процесу після завершення ініціалізації


Входи таблиці процесів
Значення для
батька Значення для дитини


Статус процесу
Час роботи
Прапор статусу пам'яті
Ідентифікатор користувача
Груповий ідентифікатор
Значення "nice"
Ідентифікатор процесу
ідентифікатор процесу-
батька
Час у пам'яті
Пріоритет
p_stat
p_cpu
p_flag
p_uid
p_grp
p_nice
p_ppid
p_ppid
p_time
p_pri
SRUN
p_cpu
SLOAD
p_uid
p_grp
p_nice
новий унікальний ідентифікатор
p_pid
0 PUSER+p_nice-NZERO


Якщо створення процесу переривається з деяких причин, то в процесу-дитини переривання не відбудеться, тому що до повного завершення копіювання він не є виконуваним.

З таблиці можна помітити, що процес-дитина автоматично успадковує той же ідентифікатор користувача, що був у батька, те ж значення p_nice, але не використовує p_cpu для обчислення пріоритету.

3.2.2 Еxec(2)

Системний виклик exec(), що заміщає частини тексту й даних у свопинговом зображенні відповідними значеннями файлу, що запускається на виконання. Точні кроки, що випливають за викликом exec(), такі:
ім'я нового виконуваного файлу передається для того,щоб установити відповідний INODE. Це завдання виконується через системний виклик namei(). Потім виконується проверкатого, що запуск, що зажадав, цього файлу користувач має на це право. Перевіряється розмір програми, щоб переконатися в можливості її завантаження в цей момент;
аргументи нової програми збираються й зберігаються на свопинговом пристрої. Перепідкачується ОЗУ для нової програми; зчитується й завантажується у відповідне місце пам'яті нового процесу сегмент тексту нової виконуваної програми, що не є тривіальним завданням, тому що, наприклад, у моделях з поділюваним текстом він може вже виявитися завантаженим і тоді просто необхідно відзначити додатковий нестаток у ньому;
потім завантажується сегмент даних з нового виконуваного файлу. Аркуш аргументів копіюється на свопинговое пристрій. Нарешті всі сигнали приймають "неявні" значення й переустановлюються тегистры.

Тепер може початися виконання нової програми.

exec() - це найменування цілого сімейства функцій:
execv(), execl(), execle(), execve(), execlp() і execvp().
Всі вони перетворюють процес, що викликав, в іншій. Відмінності між функціями полягають у способі подання аргументів. Наприклад, функція execl() може бути викликана в такий спосіб:

execl ("/bin/prog", "prog", arg1, arg2, (char*) 0);

Аргументи функції execl() мають наступний сенс:
/bin/prog Маршрутне ім'я нового виконуваного файлу.
prog Ім'я, що новий процес одержить як argv[0].
arg1, ... Покажчики на ланцюжки символів, що містять
аргументи програми prog.
(char *) 0 Порожнє посилання, що відзначає кінець списку
аргументів.

Ключовою властивістю функцій сімейства exec є те, що у випадку їхнього успішного завершення керування не вертається, оскільки процес, що викликав, замінений новим. При цьому новий процес успадковує в старий його ідентифікатор і інші характеристики. Якщо звертання до exec() завершується невдачею, керування вертається в програму, що викликала, з результатом -1. Причина невдачі може бути встановлена за значенням змінної errno.

Щоб перекласти наведені міркування на мову C, можна включити в програму оператори, аналогічні наступної:

#include

int ch_stat, ch_pid, status;
char *progarg1;
char *progarg2;
void exit ();
extern int errno;

if ((ch_pid = fork ()) < 0)
{
/* Виклик fork завершився невдачею ... перевірка змінної errno */
}
else if (ch_pid == 0) {
/* Породжений процес */
(void) execl ("/bin/prog","prog",arg1,arg2,(char*)0);
exit (2);
/* execl() завершився невдачею */
}
else {
/* Батьківський процес */
while ((status = wait (&ch_stat)) != ch_pid) {
if (status < 0 && errno == ECHILD)
break;
errno = 0;
}
}

Оскільки при виклику exec() ідентифікатор породженого процесу успадковується новим процесом, батьківський процес знає цей ідентифікатор. Дія наведеного фрагмента програми зводиться до зупинки виконання однієї програми й запуску іншої, з наступним поверненням у ту крапку першої програми, де було зупинено її виконання. У точності т же саме відбувається при виклику функції system(3S). Дійсно, реалізація system() така, що при її виконанні викликаються fork(), exec() і wait().

Варто мати на увазі, що в даний приклад включена мінімальна кількість перевірок різних помилок. Так, необхідно проявляти обережність при спільній роботі з файлами. Крім іменованих файлів, новий процес, породжений викликом fork() або exec(), успадковує три відкритих файли: stdin, stdout і stderr.
Якщо вивід батьківського процесу є буферизованим і повинен з'явитися до того, як породжений процес почне свій вивід, буфери повинні бути виштовхнуті до виклику fork(). А якщо батьківський і породжений процеси читають із деякого потоку, то прочитане одним процесом уже не може бути прочитане іншим, оскільки при читанні просувається покажчик поточної позиції у файлі.

3.3 Завершення процесу

Завершення процесу є більше простим завданням, чим його запуск. Головним при цьому є від'єднання свопингового зображення процесу, що ускладнюється, коли текст або його частина є поділюваними й тому не можуть бути фізично отсоединены.
Можливий випадок, коли з ряду причин вхід у таблицю процесів для даного процесу може залишитися в пам'яті після від'єднання свопингового зображення. Це відбувається в тому випадку, коли процес-батько має потребу в інформації про процес-ребенке після його завершення.
Є дві можливості знищити процес. Перша полягає в тому, щоб послати до процесу сигнал про неявний (якому мається на увазі) дії, що й буде сигналом про смерть процесу. Друга полягає в тому, щоб використовувати системний виклик exit().
Якщо було викликане знищення процесу, то він сам переводить себе в те, що називається ZOMBIE PROCESS (знищуваний або пам'яті, що позбавляється, процес). Цей стан описує процес як "мертвий", тобто він позбавлений свопингового зображення, але ще існує в таблиці процесів. Такий стан необхідно тільки для того, щоб процес-батько або пов'язані з ним взаємодіючі процеси виводити зі стану wait(). Зомбі-Процес запустити знову не вдасться, тому що для нього вже не існує свопингового зображення.
Коли процес-дитина умирає, процес-батько сповіщається про це сигналом SIGCLD, після одержання якого він може зняти вхід процесу-дитини в таблиці процесів. Якщо процес-батько вмирає першим, процес-дитина йому успадковує через процес 1. Процес 1 є спадкоємцем процесу 0 і батьком всіх процесів, крім нульового. Якщо з якої-небудь причини процес-батько не знищив вхід померлого процесу-дитини, то це зробить процес 1.
Т.о. повторимо завдання, які виконує системний виклик exit():
придушує дії свех сигналів, які були заявлені для процесу, що завершується:
закриває файли, раніше відкриті процесом, що завершується;
від'єднує свопинговое зображення процесу, що від'єднується;
установлює статус процесу, що завершується, як p_stat = SZOMB;
повідомляє процес-батько про смерть процесу-дитині посилкою до нього сигналу SIGCLD;
указує процесу-ребенку, що він не є підлеглим процесу-батькові, установивши для процесу-батька ppid = 1;
якщо з'явився зомбі-дитина, то про це повідомляється процес 1;
передає ключ до нового процесу.

3.4 Взаємодія процесів в ОС UNIX

Дуже важливим фактором ОС UNIX є те, що процеси, що йдуть під його керуванням, повністю незалежні при виконанні й мають строго свої зони даних. Це спрощує керування виконанням кожного окремого процесу, але створює складності при передачі інформації між процесами, необхідність у якій періодично з'являється в процесі роботи. Такого роду передачі забезпечуються системними функціями UNIX, призначеними для комунікацій між процесами, що забезпечують:
обмін інформацією,
синхронізацію, що контролює розвиток пройесса в часі і його взаємозв'язок з іншими процесами, які можуть проходити одночасно з ним.

Є шість різних механізмів, що забезпечують межпроцессные комунікації:
messages - пакети даних, посыдаемых від одного процесу іншому;
shared memory - зони даних, використовувані більш ніж одним процесом;
pipes - спеціальний тип файлів даних, що використовує механізм доступу FIFO;
process tracing - трасування - обмін інформацією з контролем, виконуваний між процесом-батьком і процессом-ребенком (насамперед використовується отладчиком);
synchronization signals - системні прапори, що відзначають появу певних подій;
Death of Child - спеціальний сигнал, що повідомляє процесу-батькові, що процес-дитина закінчився.

Їм відповідають системні виклики, що дозволяють инициализировать відповідні дії із програми:

Таблиця 4
Системні викликі межпроцесних комунікацій

signal
signal()
kill()
pause()
визначає, що треба робити, якщо сигнал
отриманий.
посилає сигнал до процесу або групи
процесів.
припиняє прцессы до одержання
сигналу.


pipes
pipe()
mknode()
створює канал зв'язку між процесами.
створює файл із доступом FIFO.


Wait for Child
wait()
exit()
ожидаетостановки або завершення процесу-
дитини.
заершает процес.


process tracing
ptrace()
msgget()
msgsnd()
msgrcv()
msgctl()
трасування процесу.
одержання повідомлення із черги.
посилка повідомлення в чергу.
читання повідомлення із черги.
виконання операції по контролі за сооб-
щением.


semaphores
semget()
semop()
semctl()
одержати установку семафорів.
виконати операції над семафорами.
виконати операції по контролі за сема-
фором.


shared memory
shmget()
shmat()
shmdt()
shmctl()
одержати сегмент поділюваної пам'яті.
приєднати зазначений сегмент поділюваної
пам'яті.
отсоединить зазначений сегмент поділюваної
пам'яті.
виконати операції по контролі разделяе-
мий пам'яті.


3.5 Сигнали
3.5.1 Поняття сигналу

При межпроцессорном взаємодії найбільше часто розглядаються сигнали, які визначають спеціальні події, що відбуваються між процесами. сигнали встановлюються одним процесом, але одержати їх може інший процес або ж сам цей процес.
Сигналом називається програмне переривання, передане виконуваному процесу. Операційна система використовує сигнали для інформування виконуваних процесів про виняткові ситуації.
Всі події, що генерують сигнали можуть бути розбиті на три основні категорії: помилки, зовнішні події і явні виклики.
Помилка означає, що програма виконала некоректну операцію й не може бути продовжена. До помилок, які можуть генерувати сигнали ставляться помилки розподілу на нуль і помилки звертання до невірних адрес пам'яті. Зовнішня подія викликається як правило пристроями уведення-виводу або інших процесів. Безпосередній виклик означає використання бібліотечної функції (наприклад, kill), безпосереднім призначенням якої є генерація сигналу.
Сигнали можуть генеруватися синхронно й асинхронно. Синхронні сигнали властиві деяким діям, виконуваним програмою й виконуються безпосередньо під час цієї дії. До синхронних сигналів ставляться більшість сигналів, що повідомляють про помилки.
Асинхронні сигнали генеруються подіями, що відбуваються поза виконуваним процесом і надходять у невідомий заздалегідь момент часу. Коли сигнал генерується, він міститься операційною системою в чергу очікування й через якийсь час передається процесу для обробки.
Існує можливість блокування сигналів. У цьому випадку сигнал перебуває в стані очікування доти, поки він не буде разблокирован.
сигнал, Що Одержав, процес виконує деякі дії, пов'язані із цим сигналом. Для деяких сигналів виконувані дії заздалегідь визначені. Однак, при обробці більшості сигналів процес має альтернативу:
ігнорувати сигнал,
визначити функцію-оброблювач або
викликати оброблювач за замовчуванням.

Можуть бути послані наступні сигнали:
01 SIGHUP - останов програми через збій устаткування або
помилки в програмі.
02 SIGINT - переривання програми.
03 SIGQUIT - видалення програми.
04 SIGILL - невірна інструкція.
05 SIGTRAP - трасуюче відгалуження помилково.
06 SIGIOT - інструкція передачі на уведення/вивід.
07 SIGEMT - інструкція, що ідентифікує фізичну
границю використовуваної частини носія даних.
08 SIGFPT - помилка в операціях із плаваючою крапкою.
09 SIGKILL - знищення процеса-дитини.
10 SIGBUS - помилка на шині уведення/виводу (на лінії
мережі).
11 SIGSEGV - порушення сегментації.
12 SIGSYS - невірний аргумент системного виклику.
13 SIGPIPE - запис у трубопровід, з якого ніхто не зчитує.
14 SIGALARM - перевищення припустимого тимчасового
інтервалу.
15 SIGTERM - програмне завершення.
16 SIGUSR1 - обумовлений користувачем сигнал 1.
17 SIGUSR2 - обумовлений користувачем сигнал 2.
18 SIGCLD - смерть процеса-дитини.
19 SIGPWR - раптовий перебій в електроживленні.

Як відзначалося вище, у найбільш загальному випадку сигнали передаються лише між взаємно зв'язаними процесами й цим зв'язком може бути встановлена трьома способами:
процеси мають той самий ефективний користувальницький ідентифікатор;
процеси контролюються з одного термінала;
між процесами встановлений зв'язок батько-дитина.

3.5.2 Розробка оброблювачів сигналів

Існують дві базові стратегії, використовувані при розробці функцій-оброблювачів:
при надходженні сигналу функція виконує деяку дію й повертає керування в програму;
функція перериває виконання програми або виправляє ситуацію, що виникла в результаті помилки.
Варто звернути особливу увагу на те, що оброблювач може бути викликаний асинхронно, з будь-якої крапки програми. При дуже короткому інтервалі часу видачі сигналу два й більше процеси оброблювачів можуть виконуватися одночасно.
Оброблювачі першого типу використовуються для сигналів 'SIGALRM', сигналів, що надходять від пристроїв вводу-виводу. Оброблювач повинен змінювати деяку глобальну змінну в знак того, що оброблювач одержав керування. Тип даних цієї змінної повинен бути sig_atomic_t.
Приклад оброблювача сигналу:

#include
#include
#include
volatile sig_atomic_t keep_going = 1;

void catch_alarm (int sig)
{
keep_going = 0;
signal (sig, catch_alarm);
}

Void do_stuff (void)
{
puts ("Doing stuff while waiting for alarm....");
}

Int main (void)
{
signal (SIGALRM, catch_alarm);
alarm (2);
while (keep_going)
do_stuff ();
return EXIT_SUCCESS;
}

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

volatile sig_atomic_t fatal_error_in_progress = 0;
void fatal_error_signal (int sig)
{
if (fatal_error_in_progress)
raise (sig);
fatal_error_in_progress = 1;
...
signal (sig, SIG_DFL);
raise (sig);
}

Якщо запущено оброблювача для конкретного сигналу, то цей сигнал блокується доти, поки робота оброблювача не буде завершена. Проте, робота оброблювача може бути перервана приходом якого-небудь іншого сигналу. Для того, щоб уникнути цього потрібно використовувати поле sa_mask структури sigaction, щоб указати які сигнали будуть заблоковані в процесі виконання оброблювача.
При написанні оброблювачів сигналів варто прагнути до мінімізації його коду й скороченню часу виконання. Критичним місцем можуть стати структури даних, з якими працює оброблювач. Необхідно дотримуватися наступних принципів:
якщо оброблювачеві необхідно працювати із глобальної змінної, певної в програмі, то вона повинна бути визначена як volatile, що говорить компіляторові про можливість зміни її асинхронно;
якщо в оброблювачі викликається яка-небудь функція, переконаєтеся, що вона є повторно входимой або в тім, що сигнал не може перервати виконання цієї функції.

При написанні оброблювачів існує проблема звертання до неелементарним даних (об'єктам у пам'яті, доступ до яких може бути здійснений лише при виконанні декількох операцій з даними). У випадку надходження сигналу в момент модифікації вмісту об'єкта, його значення може стати непередбаченим. Наприклад,

#include
#include
struct two_words { int a, b; } memory;
void handler(int signum)
{
printf ("%d,%d\n", memory.a, memory.b);
alarm (1);
}

Int main (void)
{
static struct two_words zeros = { 0, 0 }, ones = { 1, 1 };
signal (SIGALRM, handler);
memory = zeros;
alarm (1);
while (1)
{
memory = zeros;
memory = ones;
}
}

Існує два способи вирішити цю проблему:
використання елементарного типу даних. Таким типом даних є sig_atomic_t. У загальному випадку, тип даних int і всі типи даних, розмірністю меншої, чим int є елементарними;
обробка даних тільки в тілі програми. При цьому оброблювач повинен тільки модифікувати прапор, використовуваний для синхронізації програми й оброблювача.

3.5.3 Генерація сигналів

Процес може послати сигнал самому собі з використанням наступної функції.
int raise(int SIGNUM)

Функція посилає сигнал зухвалому процесу. Повертає 0 у випадку успіху й ненульове значення в противному випадку. Єдиною можливістю одержання несприятливого результату є невірне значення SIGNUM. Зокрема, raise може використовуватися для виконання дій, запропонованих сигналу за замовчуванням.

3.5.4 Посилка сигналів іншим процесам

Для посилки сигнали іншим процесам може використовуватися функція kill. Вона може використовуватися для виконання безлічі різних дій.
Необхідність посилати сигнали іншим процесам виникає, наприклад, у наступних випадках:
батьківський процес запускає породжений для виконання деякого завдання, причому виконуючий невизначений цикл, і завершує його, коли надалі виконанні завдання немає необхідності;
процес виконується як один із процесів у групі й має потребу в перериванні або інформуванні інших процесів при виникненні помилки або якої-небудь критичної події;
два процеси необхідно синхронізувати при одночасному виконанні.

int kill(pid_t PID, int SIGNUM)

Функція kill посилає сигнал SIGNUM процесу або групі процесів, що задається PID. PID може приймати наступні значення:
0 сигнал посилає процесу, ідентифікатор якого дорівнює PID;
== 0 сигнал посилає всім процесам, які перебувають у тій же групі процесів, що й процес-відправник;
< -1 сигнал посилає групі процесів, ідентифікатор якої дорівнює PID;
== -1 якщо процес є привілейованим, то сигнал посилає всім процесам, крім деяких системних процесів. Інакше, сигнал посилає всім процесам, власником яких є поточний користувач.

Якщо сигнал успішно посланий, то kill повертає 0, інакше сигнал не посилає й функція повертає -1. У випадку невдачі в змінної errno вертаються наступні коди:
EINVAL аргумент SIGNUM містить неправильний номер сигналу;
EPERM відсутні привілею на посилку сигналу процесу або групі процесів;
ESCRH аргумент PID не відповідає жодному з існуючих процесів або груп процесів.
Процес може послати сигнал самому собі за допомогою виклику kill(getpid(), SIG), що рівнозначно raise(SIG).

Системний виклик kill() посилає сигнал SIGKILL (код 09) до процесу з тим же ефективним користувальницьким ідентифікатором (при цьому необхідно врахувати, що вбити процес може тільки його власник або суперкористувач).
Сигнали SIGINT і SIGQUIT посилають між процесами, що розділяють один термінал.
SIGCLD (18), що відзначає смерть процесу-дитини, посилає процесу-батькові, що має по визначенню той же ефективний ідентифікатор користувача.
SIGSYS(12) - сигнал системної помилки, і SIGTRAP(05) - сигнал трасування при наявності помилки, посилають процесом самому собі.
Коли процес установлює сигнал, він модернізує відповідне поле в таблиці процесу й цим полем є p_sig, де кожний біт показує статус сигналу для процесу. Т.к. для кожного сигналу приділяється єдиний біт, то сигнали не можуть бути поставлені в чергу; цей біт зводиться при посилці сигналу. Якщо сигнал не був оброблений до посилки наступного сигналу, то установка біта зберігається, але кількість посланих сигналів не відзначається, тому що місця для цього не виділено. Цей біт може зняти лише обробка, час установки сигналу не враховується. Керування сигналом можливо тільки коли процес працює або до нього буде передане керування після виклику.
Хоча ранг сигналів, що посилаються, і обмежений, дії, що вживаються після одержання сигналу, можуть варіюватися. У пам'яті користувача для процесу є таблиця u_signal c двадцятьма входами (по числу сигналів, які можуть бути отримані). При одержанні кожного із сигналів можливе виконання однієї з наступних дій:
неявна дія (SIG_DEL), що має величину 0 і показує, що процес повинен закінчитися;
дія, що ігнорує (SIG_IGN), характеризуемое деяким більшим числом і що показує, що сигнал варто ігнорувати;
деяке не нульове значення, що задає адреса функції, яку необхідно виконати з появою сигналу.

Відзначимо, що сигнал "kill" здатний убити інший процес.
Системний виклик signal() дозволяє змінювати входи в таблиці u_signal.
Для керуваннями сигналами використовуються дві внутрішні функції:
insig() - перевіряє існування якого-небудь сигналу, що не ігнорує дії, передбачені для процесу.
psig() - виконує дії, пов'язані з найнижчим із установлених сигналів.

3.5.5 Блокування сигналів

Блокування сигналів може бути корисно в наступних випадках:
тимчасове блокування сигналів дає спосіб позбутися від переривань під час виконання критичної частини програми;
для того, щоб зробити програмі надійніше, можна заблокувати сигнали на час модифікації даних;
єдиний спосіб перевірки появи сигналу - його блокування.

Всі функції блокування сигналів використовують структуру даних sigset_t, називану “набором сигналів”, для вказівки які сигнали будуть заблоковані.
Для завдання безлічі сигналів використовуються наступні функції. З метою безпеки для роботи зі структурою sigset_t рекомендується використовувати тільки описувані функції.

int sigemptyset(sigset_t *SET)

Функція виключає з безлічі заблокованих всі певні сигнали. Завжди повертає 0.

int sigfullset(sigset_t *SET)

Функція включає в безліч заблокованих всі певні сигнали. Завжди повертає 0.

int sigaddset(sigset_t *SET, int SIGNUM)

Функція додає сигнал SIGNUM у набір сигналів SET. Повертає 0 у випадку успішного виконання й -1 у випадку помилки. При виникненні помилки змінна errno може приймати наступне значення:
EINVAL SIGNUM містить неправильний номер сигналу.

int sigdelset(sigset_t *SET)

Функція видаляє сигнал SIGNUM з набору сигналів SET. Значення, що повертаються, такі ж, як і для функції sigaddset.

int sigismember(const sigset_t *SET, int SIGNUM)

Функція перевіряє чи входить сигнал SIGNUM у набір сигналів SET.
Повертає 1, якщо сигнал перебуває в наборі й 0 у противному випадку, -1 - у випадку помилки виконання. При виникненні помилки змінна errno може приймати наступне значення:
EINVAL SIGNUM містить неправильний номер сигналу.
Набір сигналів, що заблокований у цей момент, називається “маскою сигналів”. Кожний процес має свою власну маску сигналів. При створенні нового процесу, він успадковує маску сигналів батьківського процесу. Для модифікації маски сигналів використовується наступна функція:

int sigprocmask(int HOW, const sigset_t *SET, sigset_t *OLDSET)

Аргумент HOW визначає яким образом змінюється маска сигналів і може приймати наступні значення:
SIG_BLOCK сигнали, що задаються в наборі, блокуються - додаються до вже існуючої маски сигналів;
SIG_UNBLOCK сигнали, що задаються в наборі, розблокуються - віддаляються із уже існуючої маски сигналів процесу;
SIG_SETMASK установлює набір сигналів для процесу, старий уміст маски ігнорується.

Аргумент OLDSET використовується для повернення старого вмісту маски сигналів процесу.
Функція повертає 0 у випадку успіху й -1 у противному випадку. При виникненні помилки змінна errno може приймати наступне значення:
EINVAL SIGNUM містить неправильний номер сигналу.

Для перевірки того, оброблювачі яких сигналів активні в даний момент використовується наступна функція:
int sigpending(sigset_t *SET)

Функція повертає інформацію про активні в сучасний момент сигналах. Якщо є заблокований сигнал, що надійшов процесу, то він також включається в маску сигналів. Повертає 0 у випадку успішного виконання й -1 у випадку помилки.

Приклад перевірки активних сигналів:
#include
#include
sigset_t base_mask, waiting_mask;
sigemptyset (&base_mask);
sigaddset (&base_mask, SIGINT);
sigaddset (&base_mask, SIGTSTP);
/* Блокування переривань користувача */
sigprocmask (SIG_SETMASK, &base_mask, NULL);
...
sigpending (&waiting_mask);
if (sigismember (&waiting_mask, SIGINT)) {
/* Спроба припинити виконання процесу */
}
else if (sigismember (&waiting_mask, SIGTSTP)) {
/* Спроба зупинити виконання процесу */
}

3.5.6 Очікування сигналу

Якщо програма управляється зовнішніми подіями або використовує сигнали для синхронізації, то доцільно замість постійної перевірки прапора, що вказує на появу сигналу використовувати функцію очікування надходження сигналу.
int pause()

Функція припиняє виконання програми до приходу сигналу, що викликає виконання оброблювача або переривання виконання процесу.
Якщо сигнал викликає оброблювач, то функція завжди повертає значення -1, а errno приймає наступне значення: EINTR Функція перервана приходом сигналу.
У випадку припинення виконання процесу функція не повертає ніякого значення. Функція визначена в заголовному файлі .
Однак, при використанні цієї функції виникає серйозна проблема безпеки роботи програми. pause безпечно може бути використана тільки в програмі, що в основному тілі програми тільки викликає саму функцію pause, а всю корисну роботу виконує оброблювач події. Наприклад, у наступному фрагменті програми

/* 'usr_interrupt' установлюється оброблювачем сигналу. */
if (!usr_interrupt)
pause ();
/* Виконується робота, при приході сигналу. */
...

Сигнал може надійти після того, як перевірена змінна, але до виклику pause. Якщо більше не надійде жодного сигналу, програма ніколи не відновить працездатність.
Можна встановити максимальний час очікування за допомогою функції sleep:

while (!usr_interrupt)
sleep (1);
...

Однак, і такий спосіб підходить не у всіх випадках. Найбільш безпечним способом очікування сигналу є використання функції sigsuspend:

int sigsuspend(sigset_t *SET)

Функція заміняє маску сигналів процесу й припиняє його роботу до надходження одного із сигналів, що не заблокований маскою. Маска, що задається SET діє тільки на час очікування, викликуваному функцією, після повернення керування процесу відновлюється стара маска сигналів. Використання sigsuspend у наступному прикладі абсолютно безпечно:

sigset_t mask, oldmask;
...
/* Установлюється маска сигналів. */
sigemptyset (&mask);
sigaddset (&mask, SIGUSR1);
...
/* Очікується прихід сигналів. */
sigprocmask (SIG_BLOCK, &mask, &oldmask);
while (!usr_interrupt)
sigsuspend (&oldmask);
sigprocmask (SIG_UNBLOCK, &mask, NULL);

3.6 Трасування процесу.

Це найбільш примітивна форма межпроцессорного взаємодії, особливо важлива при налагодженні. Головним тут є відношення " засипання-пробудження" між контрольованим і контролюючим процесами (завжди " батько-дитина"). Трасується завжди процес-дитина; здійснюється це через системний виклик ptrace(), що використовує сигнали перемикання від батька до ребенку й навпаки.

3.7 Канали й FIFO-Файли
3.7.1 Поняття

Канал (неіменований) - це один з механізмів UNIX, призначений для зв'язку між процесами. Дані, записані в канал одним процесом можуть бути лічені іншим процесом. Дані надходять у канал і видаються з нього в порядку “першим прийшов, першим вийшов”. Канал не має ім'я й створюється для однократного використання.
FIFO-Файли (пойменовані канали) схожі з каналами, але на відміну від каналів вони не є тимчасовими з'єднаннями між процесами й мають одне або кілька імен як у будь-якого файлу операційної системи. Процеси відкривають FIFO-Файл використовуючи його ім'я.
Канали й FIFO-Файли повинні бути відкриті двома взаємодіючими процесами незалежно. При читанні з каналу або FIFO-Файлу, у який жоден потік не записує дані (наприклад, якщо файл або канал уже був закритий), операція читання повертає символ кінця файлу. Запис у канал, що не має жодного процесу, що виконує операцію читання трактується як помилка й генерує сигнал SIGPIPE і завершується з кодом помилки, рівним EPIPE, якщо сигнал обробляється або заблокований.
Ні канали ні FIFO-Файли не мають механізму присохлого покажчика. І читання й запис здійснюються послідовно. Читання виконується з початку файлу, запис - у кінець файлу.

Системний виклик pipe() використовується для створення й ініціювання непойменованих каналів, mknod() дозволяє створювати файли з методом доступу FIFO.

3.7.2 Створення каналу

Для створення каналу служить функція pipe. Вона використовується для відкриття каналу й на читання й на запис. Звичайно процес створює канал до створення породжених процесів, з якими він буде взаємодіяти через канал. Канал може використовуватися для взаємодії як між батьківським і породженим процесами, так і між двома рівноправними процесами.
Функція pipe визначена в заголовному файлі :

int pipe(int FILEDES[2])

Функція створює потік і поміщає дескриптори файлів для читання з потоку й запису в потік відповідно в FILEDES[0] і FILEDES[1]. У випадку успішного виконання функція повертає значення 0, при виникненні помилки вертається -1, у змінна errno може приймати наступні значення:
EMFILE процес має занадто багато відкритих файлів;
ENFILE система має занадто багато відкритих файлів. В Linux ця помилка ніколи не виникає.

Приведемо приклад створення каналу. Програма використовує функцію fork для створення породженого процесу. Батьківський процес записує дані в канал, які зчитує породжений процес.

#include
#include
#include
#include
void read_from_pipe (int file)
{
FILE *stream;
int c;
stream = fdopen (file, "r");
while ((c = fgetc (stream)) != EOF)
putchar (c);
fclose (stream);
}

void write_to_pipe (int file)
{
FILE *stream;
stream = fdopen (file, "w");
fprintf (stream, "hello, world!\n");
fprintf (stream, "goodbye, world!\n");
fclose (stream);
}

Int main (void)
{
pid_t pid;
int mypipe[2];
if (pipe (mypipe))
{
fprintf (stderr, "Pipe failed.\n");
return EXIT_FAILURE;
}
pid = fork ();
if (pid == (pid_t) 0)
{
read_from_pipe (mypipe[0]);
return EXIT_SUCCESS;
}
else if (pid < (pid_t) 0)
{
fprintf (stderr, "Fork failed.\n");
return EXIT_FAILURE;
}
else
{
write_to_pipe (mypipe[1]);
return EXIT_SUCCESS;
}
}

3.7.3 Створення FIFO-Файлів

FIFO-Файли схожі з каналами, однак створюються іншим способом. FIFO- файли створюються у файловій системі за допомогою функції mkfifo. Після створення такого файлу, будь-який процес може відкрити його для читання й запису таким же способом, як і звичайний файл. Проте, він повинен бути відкритий і записуючим і читаючим процесом перш, ніж можна буде записувати й читати з нього дані.
Функція mkfifo визначена в заголовному файлі :

int mkfifo(const char *FILENAME, mode_t MODE)

Функція mkfifo створює FIFO-Файл із ім'ям FILENAME. Аргумент MODE задає маску доступу до файлу, ідентичну використовуваної при створенні звичайного файлу.
При успішному виконанні функції функція повертає 0, у випадку помилки вертається -1, а змінна errno у крім помилок, пов'язаних з ім'ям файлу, може приймати наступні значення:
EEXIST файл із заданим ім'ям уже існує;
ENOSPC директорій або файлова система не можуть бути розширені;
EROFS спроба створення файлу у файловій системі “тільки для читання”.

3.8 Семафори

Тріада межпроцессорных взаємодій - семафори, повідомлення й поділювана пам'ять - мають загальне керування й структуру використовуваних команд. Для кожної із цих функцій є по одній системній таблиці, що містить всі необхідні дані.
Власником виконання межпроцессорных взаємодій є їхній творець. Тільки він і суперкористувач можуть змінювати структуру відповідних взаємодій. Операції з дозволу таких змін описуються в 9-бітовому полі, нде перші три біти описують користувача, другі три - групи користувачів, а останні три біти дають дозвіл для всіх. Перший біт у кожній із тріад дозволяє читання, другий - запис у відповідні таблиці взаємодій, а третій біт не використовується.
Семафори - це засіб межпрцессных взаємодій, використовуване для установки механізму очікування й сигналу, виконуваних системними викликами wait() і wakeup().
СЕМАФОР - це тільки цифрове значення, залежно від якого будуть пробуджуватися деякі процеси.
Установка семафора на ділі може означати установку більш ніж одного фдага семафора, але цієї операції завжди буде соотвеоствовать лише один єдиний процес. Для вссех семафорів існує одна єдина завершальна операція, що при завершенні процесу скасовує всі операції, передбачені в процесі при установці даного семафора.
СТРУКТУРИ СЕМАФОРІВ, які містять інформацію, необхожимую при установці семафора, приєднуються, від'єднуються й модифікуються точно як ОЗУ. Таблиця SEMMAP, аналогічна таблицям COREMAP і SWAPMAP, містить доступний пул семафорних структур і приєднання в цю таблицю здійснює системний виклик semget().

Структура семафора містить чотири поля:
SEMVAL - поточне значення семафора,
SEMPID - ідентифікатор останнього процесу, що працював на даному семафорі,
SEMNCNT- кількість процесів, що очікують, щоб SEMVAL був вище його поточного значення.
SEMZCNT- число процесів, що очікують обнуління SEMVAL.

Системний виклик semctl() установлює й одержує значення SEMVAL при установці семафора, що забезпечує одночасну установку всіх елементарних семафорів у мультисемафоре.
Системний виклик semop() визначає значення SEMVAL залежно від величини, певної оператором (в SEM_OP) і поточного значення SEM_OP.
Всі процеси зупиняються, якщо SEM_OP < 0, а SEMVAL < |SEM_OP| або якщо SEM_OP = 0, а SEMVAL # 0.
Якщо SEM_OP > 0, його значення додається до SEMVAL щораз, коли процес очікує на цьому параметрі доти, поки він не прийме певної величини, після чого буде розбуджений процес, проведена перевірка коректності отриманої величини й у випадку не виконання цієї умови процес знову засипає, що забезпечує контроль процесу через системний виклик semop().

3.9 Повідомлення.

У межпроцессных взаємодіях дві команди працюють із повідомленнями:
msgsnd() - послати повідомлення,
msgrsv() - одержати повідомлення.
При посилці повідомлення визначається його тип і його текст.
Одержувач використовує тип повідомлення як критерій для його одержання. Якщо як критерій використовується нульовий тип повідомлення, то одержувач вибирає повідомлення в хронологічному порядку. Якщо одержувач не може знайти повідомлення бажаного типу, він виконує виклик sleep() до виявлення повідомлення потрібного типу. У такий спосіб повідомлення можна використовувати для синхронізації процесів.
Повідомлення формуються у вигляді блоків, структура яких містить текст самого повідомлення і його тип. Довжина повідомлення може бути визначена самим користувачем. Буферы, що містять СТРУКТУРИ ПОВІДОМЛЕНЬ, відзначаються як MESSAGE QUEUE HEADER (КОЛЕКТОР ЧЕРГИ ПОВІДОМЛЕНЬ).
ЧЕРГА ПОВІДОМЛЕНЬ - одновходовой список колекторів. Пам'ять, потребнаядля структури собщений, управляється як звичайне ОЗУ через таблицю MSGMAP, для якої простір виділяється й звільняється через malloc() і mfree() відповідно. Посилка собщения відбувається в кілька кроків:
воно повинне бути приєднане до вільного колектора черги повідомлень;
необхідно приєднати пространсрво ОЗУ, необхідне для самого тексту повідомлення;
Примітка: простору, описані в п.п. 1 і 2 "зчіплюються" з раніше прийнятою інформацією.
знову створений колектор повідомлення приєднується до черги повідомлень.


Одержання повідомлення може відбуватися або відповідно до його ипом, або в хронологічному порядку (FIFO). Відразу після одержання повідомлення відповідний колектор від'єднується, займане повідомленням простір від'єднується, а займане колектором простір звільняється.

3.10 Поділювана пам'ять.

Це загальний простір, куди будь-який процес може помістити інформацію або звідки він може неї взяти. Поділювана пам'ять розглядається як простір даних для процесу.
При видачі системного виклику shmget() приєднується фізичний сегмент поділюваної пам'яті й видається його ідентифікатор процесу.
Shmat() не приєднує пам'ять, а прив'язує її до вже наявного сегмента й потім відповідний віртуальний сегмент пам'яті прив'язує до раніше подсоединенномусегменту. Якщо раніше віртуальна адреса не була визначена, то використовується перша вільна адреса, що випливає за сегментом даних. Приймаються запобіжні заходи при визначенні адреси поділюваної пам'яті, щоб магазин не ріс надмірно. Дозвіл "тільки читати" або "тільки писати" приймається на межпроцессной базі.
Різні процеси можуть використовувати сегмент поділюваної пам'яті протягом свого існування. Коли процес завершується, вылается системний виклик shmdt() для від'єднання процесу від простору поділюваної пам'яті й сегмент фізично вилучається в процесу.
Література
Дансмур М. Операційна система UNIX і програмування мовою Си. / Дансмур М., Дейвис Г. - М.: Радіо й зв'язок, 1989. - 192 с.
Рейчард К. Linux: довідник / К. Рейчард, П. Фолькердинг. - Спб.: Питер Кін, 1999. - 480 с.
Робачевский А.М. Операційна система UNIX. - Спб.: BHV-Санкт-Петербург, 1997. - 528 с.
Стивенс У. UNIX: взаємодія процесів. - Спб.: Питер, 2003. - 576 с.
Теренс Чан Системне програмування на З++ для UNIX. К.: Видавнича група BHV, 1997. - 592 с.
Хэвиленд К., Грэй Д., Салама Б. Системне програмування в UNIX. Керівництво програміста. - М., ДМК Пресс, 2000. - 368 с.
Бовет Д., Чезати М. Ядро Linux. - Бхв-Петербург, 2007. - 1104 с.
В. Белунцов. Самовчитель користувача Linux. - Москва: "ДЕСС КОМУ", 2003. - 512 с.
Колисниченко Д.Н., Аллен П. В. Linux: Повне керівництво. - М., Наука й техніка, 2007 р. - 784 с.
Курячий Г. В., Маслинский К. А. Операційна система Linux. Курс лекцій. Навчальний посібник. - Інтернет-Університет інформаційних технологій, 2005. - 392 с.
Митчелл М. і ін. Програмування для Linux. Професійний підхід. - М., Діалектика, 2002 - 288 с.
Брюс Моли. Unix/Linux. Теорія й практика програмування. - М., Кудиц-Образ, 2004. - 576 с.
Роббинс А. Linux: програмування в прикладах. - М., Кудиц-Образ, 2005. - 656 с.
Секунов Н. Ю. Програмування на З++ в Linux. - Спб., Бхв-Петербург, 2003. - 354 с.
Скотт Максвелл. Ядро Linux у коментарях. - DiaSoft, 2000. - 488с.
Стивенс У.Р. UNIX: розробка мережних додатків. - Спб., Питер, 2007. - 1039 с.
Уолтон Ш. Створення мережних додатків у середовищі Linux. Керівництво розроблювача. - - М., Вільямс, 2001.- 464 c.


Жуковецька С.Л.








Системне програмування





Навчальний посібник
















Підписано до друку _________ р. Формат 60(84 1/16.
Умовн. друк. арк. ____. Наклад ___ прим.
Надруковано видавницьким центром ОДАХ.
65082, Одеса, вул. Дворянська, 1/3









13PAGE 141015





13 PAGE \* MERGEFORMAT 141115

















ОС

ядро

оболочка












newpos


newpos

newpos

newpos


Таблица файлов























Заголовок 1 Заголовок 2„ђ Заголовок 315

Приложенные файлы

  • doc 263947
    Размер файла: 681 kB Загрузок: 0

Добавить комментарий