Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 23 (всего у книги 55 страниц)
chroot()
Текущий рабочий каталог, установленный с помощью chdir(
) (см. раздел 8.4.1 «Изменение каталога – chdir()
и fchdir()
»), является атрибутом процесса, таким же, как набор открытых файлов. Он также наследуется новыми процессами.
Менее известным является то, что у каждого процесса есть также текущий корневой каталог. Это именно на этот каталог ссылается имя пути /
. В большинстве случаев корневые каталоги процесса и системы идентичны. Однако, суперпользователь может изменить корневой каталог с помощью (как вы догадались) системного вызова chroot()
:
#include
int chroot(const char *path);
Возвращаемое значение равно 0 при успешном завершении и -1 при ошибке.
Как указывает справочная страница GNU/Linux chroot(2), изменение корневого каталога не изменяет текущий каталог: программы, которые должны обеспечить нахождение под новым корневым каталогом, должны также вызвать затем chdir()
:
if (chroot("/new/root") < 0) /* Установить новый корневой каталог */
/* обработать ошибку */
if (chdir("/some/dir") < 0) /* Пути даны не относительно нового корневого каталога */
/* обработать ошибку */
Системный вызов chroot()
чаще всего используется для демонов – фоновых программ, которые должны работать в специальном ограниченном окружении. Например, рассмотрите демон Интернета FTP, допускающий анонимный FTP (соединение любого клиента из любого места, без обычных имени пользователя и пароля). Очевидно, такое соединение не должно быть способным видеть все файлы целой системы. Вместо этого демон FTP выполняет chroot()
в специальный каталог со структурой, достаточной лишь чтобы позволить ему функционировать. (Например, со своим собственным /bin/ls
для перечисления файлов, со своей копией библиотеки С времени исполнения, если она разделяется, и, возможно, со своей копией /etc/passwd
и /etc/group
для отображения ограниченного набора имен пользователей и групп.)
POSIX не стандартизует этот системный вызов, хотя GNU/Linux и все системы Unix его поддерживают. (Он популярен с V7.) Он специализирован, но при необходимости очень удобен.
8.7. Резюме• Файловые системы являются коллекциями блоков индексов, данных, вспомогательных данных и свободных блоков, организованных особым способом. Файловые системы один к одному соответствуют (физическим или логическим) разделам, на которых они создаются. У каждой файловой системы есть свой корневой каталог; по соглашению, у корневого каталога номер индекса всегда равен 2.
• Команда mount
монтирует файловую систему, наращивая логическое иерархическое пространство имен файлов. Команда umount
отсоединяет файловую систему. Ядро делает /.
и /..
одним и тем же; корневой каталог всего пространства имен является своим собственным родителем. Во всех остальных случаях ядро устанавливает в корневом каталоге смонтированной файловой системы указывающим на родительский каталог точки монтирования.
• Современные Unix-системы поддерживают множество типов файловых систем. В частности, повсеместно поддерживается сетевая файловая система (NFS) Sun, также, как ISO 9660 является стандартным форматом для CD-ROM, а разделы FAT MS– DOS поддерживаются на всех Unix-системах, работающих на платформе Intel x86. Насколько мы знаем, Linux поддерживает наибольшее число различных файловых систем – свыше 30! Многие из них специализированные, но многие из оставшихся предназначены для общего использования, включая по крайней мере четыре различные журналируемые файловые системы.
• Файл /etc/fstab
перечисляет разделы каждой системы, их точки монтирования и относящиеся к монтированию опции, /etc/mtab
перечисляет те файловые системы, которые смонтированы в настоящее время, то же делает /proc/mounts
на системах GNU/Linux. Опция loop
функции mount
особенно полезна под GNU/Linux для монтирования образов файловых систем, содержащихся в обычных файлах, таких, как образы CD-ROM. Другие опции полезны для безопасности и монтирования внешних файловых систем, таких, как файловые системы vfat Windows.
• Файлы формата /etc/fstab
можно читать с помощью набора процедур getmntent()
. Формат GNU/Linux общий с рядом других коммерческих вариантов Unix, особенно Sun Solaris.
• Функции statvfs()
и fstatvfs()
стандартизованы POSIX для получения сведений о файловой системе, таких, как число свободных и используемых дисковых блоков, число свободных и используемых индексов и т.д. В Linux есть свой собственный системный вызов для получения подобной информации: statfs()
и fstatfs()
.
• chdir()
и fchdir()
дают процессу возможность изменить его текущий каталог, getcwd()
получает абсолютное имя пути текущего каталога. Эти три функции просты в использовании.
• Функция nftw()
централизует задачу «обхода дерева файлов», т.е. посещения каждого объекта файловой системы (файла, устройства, символической ссылки, каталога) во всей иерархии каталогов. Ее поведением управляют различные флаги. Программист должен предоставить функцию обратного вызова, которая получает имя каждого файла, struct stat
для файла, тип файла и сведения об имени файла и уровне в иерархии. Эта функция может делать для каждого файла все что нужно. Версия функции du
из Coreutils 5.0 GNU использует для выполнения этой работы расширенную версию nftw()
.
• Наконец, системный вызов chroot()
изменяет текущий корневой каталог процесса. Это специализированная, но важная возможность, которая особенно полезна для определенных программ в стиле демонов.
1. Изучите справочную страницу mount(2) под GNU/Linux и на всех различных системах Unix, к которым у вас есть доступ. Как отличаются системные вызовы?
2. Усовершенствуйте программу ch08-statvfs.c
, чтобы она принимала опцию, предоставляющую открытый целый дескриптор файла; для получения сведений о файловой системе она должна использовать fstatvfs()
.
3. Усовершенствуйте ch08-statvfs.c
, чтобы она не игнорировала смонтированные файловые системы NFS. Такие файловые системы имеют устройство в форме server.example.com:/big/disk
.
4. Измените ch08-statfs.c
(ту, которая использует специфичный для Linux вызов statfs()
), чтобы ее вывод был похож на вывод df
.
5. Добавьте опцию -i
к программе, которую вы написали для предыдущего упражнения, чтобы ее вывод был такой же, как у 'df -i
'.
6. Используя opendir()
, readdir()
, stat()
или fstat()
, dirfd()
и fchdir()
, напишите собственную версию getcwd()
. Как вы вычислите общий размер, который должен иметь буфер? Как вы будете перемещаться по иерархии каталогов?
7. Усовершенствуйте свою версию getcwd()
, чтобы она выделяла буфер для вызывающего, если первый аргумент равен NULL.
8. Можете ли вы использовать nftw()
для написания getcwd()
? Если нет, почему?
9. Используя nftw()
, напишите свою собственную версию chown
, которая принимает опцию -R
для рекурсивной обработки целых деревьев каталогов. Убедитесь, что без -R
, 'chown пользователь каталог
' не является рекурсивной. Как вы это проверите?
10. Набор процедур BSD fts()
(«file tree stream» – «поток дерева файлов») предоставляет другой способ для обработки иерархии каталогов. У него несколько более тяжелый API как в смысле числа функций, так и структур, которые доступны для вызывающих функций уровня пользователя. Эти функции доступны как стандартная часть GLIBC.
Прочтите справочную страницу fts(3). (Для удобства ее можно распечатать.) Перепишите свою частную версию chown для использования fts()
.
11. Посмотрите справочную страницу find(1). Если бы вы пытались написать find
с самого начала, какой набор деревьев файлов вы бы предпочли, nftw()
или fts()
? Почему?
Часть 2
Процессы, IPC и интернационализация
Глава 9
Управление процессами и каналы
Как мы говорили в главе 1 «Введение», если бы нужно было резюмировать Unix (а следовательно, и Linux) в трёх словах, это были бы «файлы и процессы». Теперь, когда мы увидели, как работать с файлами и каталогами, время взглянуть на оставшуюся часть утверждения: процессы. В частности, мы исследуем, как создаются и управляются процессы, как они взаимодействуют с открытыми файлами и как они могут взаимодействовать друге другом. Последующие главы исследуют сигналы – грубый способ дать возможность одному процессу (или ядру) сообщить другому о том, что произошло некоторое событие – и проверку прав доступа.
В данной главе картина начинает усложняться. В частности, для полноты мы должны упомянуть о вещах, которые не будут рассматриваться до конца главы или до конца книги В таких случаях мы предусмотрели ссылки вперед, но вы должны быть способны без подготовки уловить суть каждого раздела.
9.1. Создание и управление процессамиВ отличие от многих предшествующих и последующих операционных систем, создание процессов в Unix задумывалось (и было сделано) дешевым. Более того, Unix разделяет идеи «создания нового процесса» и «запуска данной программы в процессе». Это было элегантное проектное решение, которое упрощает многие операции.
fork()
Первым шагом в запуске новой программы является вызов fork()
:
#include
#include
pid_t fork(void);
Использование fork()
просто. Перед вызовом один процесс, который мы называем родительским, является запущенным. Когда fork()
возвращается, имеется уже два процесса: родительский и порожденный (child).
Вот ключ: оба процесса выполняют одну и ту же программу. Два процесса могут различить себя, основываясь на возвращённом fork()
значении:
Отрицательное
Если была ошибка, fork()
возвращает -1, а новый процесс не создается. Работу продолжает первоначальный процесс.
Нулевое
В порожденном процессе fork()
возвращает 0.
Положительное
В родительском процессе fork()
возвращает положительный идентификационный номер (PID) порожденного процесса.
Код шаблона для создания порожденного процесса выглядит следующим образом:
pid_t child;
if ((child = fork()) < 0)
/* обработать ошибку */
else if (child == 0)
/* это новый процесс */
else
/* это первоначальный родительский процесс */
pid_t
является знаковым целым типом для хранения значений PID. Скорее всего, это просто int
, но специальный тип делает код более понятным, поэтому он должен использоваться вместо int
.
На языке Unix, помимо названия системного вызова, слово «fork» является и глаголом, и существительным[88]88
Fork (англ.) – «n вилка, развилка, v разветвлять, ответвлять» – Примеч. перев.
[Закрыть]. Мы можем сказать, что «один процесс ответвляет другой», и что «после разветвления работают два процесса». (Думайте «развилка (fork) на дороге», а не «вилка (fork), нож и ложка».)
fork()
: общие и различные атрибутыПорожденный процесс «наследует» идентичные копии большого числа атрибутов от родителя. Многие из этих атрибутов специализированы и здесь неуместны. Поэтому следующий список намеренно неполон. Существенны следующие:
• Окружение, см. раздел 2.4 «Окружение».
• Все открытые файлы и открытые каталоги; см. раздел 4.4.1 «Понятие о дескрипторах файлов» и раздел 5.3.1 «Базовое чтение каталогов».
• Установки umask; см. раздел 4.6 «Создание файлов».
• Текущий рабочий каталог; см раздел 8.4.1 «Смена каталога: chdir()
и fchdir()
.
• Корневой каталог; см. раздел 8.6 «Изменение корневого каталога: chroot()
».
• Текущий приоритет (иначе называемый «значение nice»; вскоре мы это обсудим; см раздел 9.1.3 «Установка приоритета процесса: nice()
»).
• Управляющие терминалы. Это устройство терминала (физическая консоль или окно эмулятора терминала), которому разрешено посылать процессу сигналы (такие, как CTRL-Z для прекращения выполняющихся работ). Это обсуждается далее в разделе 9.2.1 «Обзор управления работой».
• Маска сигналов процесса и расположение всех текущих сигналов (еще не обсуждалось; см. главу 10 «Сигналы»).
• Реальный, эффективный и сохраненный ID пользователя, группы и набора дополнительных групп (еще не обсуждалось; см. главу 11 «Права доступа и ID пользователя и группы»).
Помимо возвращаемого значения fork()
два процесса различаются следующим образом:
• У каждого есть уникальный ID процесса и ID родительского процесса (PID и PPID) Они описаны в разделе 9.1.2 «Идентификация процесса: getpid()
и getppid()
».
• PID порожденного процесса не будет равняться ID любой существующей группы процессов (см. раздел 9.2 «Группы процессов»).
• Аккумулированное время использования процессора для порожденного процесса и его будущих потомков инициализируется нулем. (Это имеет смысл; в конце концов, это совершенно новый процесс.)
• Любые сигналы, которые были ожидающими в родительском процессе, в порожденном сбрасываются, также как ожидающие аварийные сигналы и таймеры. (Мы еще не рассматривали эти темы; см. главу 10 «Сигналы» и раздел 14.3.3 «Интервальные таймеры: setitimer()
и getitimer()
».)
• Блокировки файлов в родительском процессе не дублируются в порожденном (также еще не обсуждалось; см. раздел 14.2 «Блокировка файлов»).
Атрибуты, которые порожденный процесс наследует от родителя, устанавливаются в те же значения, которые были в родительском процессе в момент выполнения fork()
. Однако, с этого момента два процесса продолжают идти собственными путями (большей частью) независимо один от другого. Например, если порожденный процесс изменяет каталог, каталог родительского процесса не затрагивается. Сходным образом, если порожденный изменяет среду, среда родителя не меняется.
Открытые файлы являются важным исключением из этого правила. Дескрипторы открытых файлов являются разделяемыми, и действия одного процесса с разделяемым дескриптором файла затрагивает состояние файла также и для другого процесса. Это лучше всего понять, изучив рис. 9.1.
Рис. 9.1. Разделение дескрипторов файлов
Рисунок отображает внутренние структуры данных ядра. Ключевой структурой данных является таблица файлов. Каждый элемент ссылается на открытый файл. Помимо других учетных данных, таблица файлов содержит текущее положение (смещение чтения/записи) в файле. Оно устанавливается либо автоматически каждый раз при чтении или записи файла, либо непосредственно через lseek()
(см. раздел 4.5 «Произвольный доступ: перемещения внутри файла»).
Дескриптор файла, возвращенный функциями open()
или creat()
, действует как индекс имеющегося в каждом процессе массива указателей на таблицу файлов. Размер этого массива не превышает значение, возвращенное getdtablesize()
(см. раздел 4.4.1 «Понятие о дескрипторах файлов»).
На рис. 9.1 показаны два процесса, разделяющие стандартный ввод и стандартный вывод; для каждого из процессов указаны одни и те же элементы в таблице файлов. Поэтому, когда процесс 45 (порожденный) осуществляет read()
, общее смещение обновляется; следующий раз, когда процесс 42 (родитель) осуществляет read()
, он начинает с позиции, в которой закончила чтение read()
процесса 45.
Это легко можно видеть на уровне оболочки:
$ cat data /* Показать содержание демонстрационного файла */
line 1
line 2
line 3
line 4
$ ls -l test1 ; cat test1 /* Режим и содержание тестовой программы */
-rwxr-xr-x 1 arnold devel 93 Oct 20 22:11 test1
#! /bin/sh
read line ; echo p: $line /* Прочесть строку в родительской оболочке,
вывести ее */
( read line ; echo с: $line ) /* Прочесть строку в порожденной оболочке,
вывести ее */
read line ; echo p: $line /* Прочесть строку в родительской оболочке,
вывести ее */
$ test1 < data /* Запустить программу */
p: line 1 /* Родитель начинает сначала */
c: line 2 /* Порожденный продолжает оттуда, где остановился родитель */
p: line 3 /* Родитель продолжает оттуда, где остановился порожденный */
Первая исполняемая строка test1
читает из стандартного ввода строку, изменяя смещение файла. Следующая строка test1
запускает команды, заключенные между скобками, в подоболочке (subshell). Это отдельный процесс оболочки, созданный – как вы догадались – с помощью fork()
. Порожденная подоболочка наследует от родителя стандартный ввод, включая текущее смещение. Этот процесс читает строку и обновляет разделяемое смещение в файле. Когда третья строка, снова в родительской оболочке, читает файл, она начинает там, где остановился порожденный.
Хотя команда read
встроена в оболочку, все работает таким же образом и для внешних команд. В некоторых ранних Unix-системах была команда line
, которая читала одну строку ввода (по одному символу за раз!) для использования в сценариях оболочки; если бы смещение файла не было разделяемым, было бы невозможно использовать такую команду в цикле.
Разделение дескрипторов файлов и наследование играют центральную роль в перенаправлении ввода/вывода оболочки; системные вызовы и их семантика делают примитивы уровня оболочки простыми для реализации на С, как мы позже увидим в данной главе.
close()
Тот факт, что несколько дескрипторов файлов могут указывать на один и тот же открытый файл, имеет важное следствие: файл не закрывается до тех пор, пока не будут закрыты все дескрипторы файла.
Позже в главе мы увидим, что несколько дескрипторов для одного файла могут существовать не только для разных процессов, но даже и внутри одного и того же процесса; это правило особенно важно для работы с каналами (pipes).
Если вам нужно узнать, открыты ли два дескриптора для одного и того же файла, можете использовать fstat()
(см. раздел 5.4.2 «Получение сведений о файле») для двух дескрипторов с двумя различными структурами struct stat
. Если соответствующие поля st_dev
и st_ino
равны, это один и тот же файл.
Позже в главе мы завершим обсуждение манипуляций с дескрипторами файлов и таблицей дескрипторов файлов.
getpid()
и getppid()
У каждого процесса есть уникальный ID номер процесса (PID). Два системных вызова предоставляют текущий PID и PID родительского процесса:
#include
#include
pid_t getpid(void);
pid_t getppid(void);
Функции так просты, как выглядят:
pid_t getpid(void)
Возвращает PID текущего процесса
pid_t getppid(void)
Возвращает PID родителя.
Значения PID уникальны; по определению, не может быть двух запущенных процессов с одним и тем же PID. PID обычно возрастают в значении, так что порожденный процесс имеет обычно больший PID, чем его родитель. Однако, на многих системах значения PID переполняются; когда достигается значение системного максимума для PID, следующий процесс создается с наименьшим не используемым номером PID. (Ничто в POSIX не требует такого поведения, и некоторые системы назначают неиспользуемые номера PID случайным образом.)
Если родительский процесс завершается, порожденный получает нового родителя, init
. В этом случае PID родителя будет 1, что является PID init
. Такой порожденный процесс называется висячим (orphan). Следующая программа, ch09-reparent.с
, демонстрирует это. Это также первый пример fork()
в действии:
1 /* ch09-reparent.c – показывает, что getppid() может менять значения */
2
3 #include
4 #include
5 #include
6 #include
7
8 /* main – осуществляет работу */
9
10 int main(int argc, char **argv)
11 {
12 pid_t pid, old_ppid, new_ppid;
13 pid_t child, parent;
14
15 parent = getpid(); /* перед fork() */
16
17 if ((child = fork()) < 0) {
18 fprintf(stderr, "%s: fork of child failed: %sn",
19 argv[0], strerror(errno));
20 exit(1);
21 } else if (child == 0) {
22 old_ppid = getppid();
23 sleep(2); /* см. главу 10 */
24 new_ppid = getppid();
25 } else {
26 sleep(1);
27 exit(0); /* родитель завершается после fork() */
28 }
29
30 /* это выполняет только порожденный процесс */
31 printf("Original parent: %dn", parent);
32 printf("Child: %dn", getpid());
33 printf("Child's old ppid: %dn", old_ppid);
34 printf("Child's new ppid: %dn", new_ppid);
35
36 exit(0);
37 }
Строка 15 получает PID начального процесса, используя getpid()
. Строки 17–20 создают порожденный процесс, проверяя по возвращении ошибки.
Строки 21–24 выполняются порожденным процессом: строка 22 получает PPID. Строка 23 приостанавливает процесс на две секунды (сведения о sleep()
см в разделе 10.8.1 «Аварийные часы: sleep()
, alarm()
и SIGALRM
»), а строка 24 снова получает PPID.
Строки 25–27 исполняются в родительском процессе. Строка 26 задерживает родителя на одну секунду, давая порожденному процессу достаточно времени для осуществления первого вызова getppid()
. Строка 27 завершает родителя.
Строки 31–34 выводят значения. Обратите внимание, что переменная parent
, которая была установлена до разветвления, сохраняет свое значение в порожденном процессе. После порождения у двух процессов идентичные, но независимые копии адресного пространства. Вот что происходит при запуске программы:
$ ch09-reparent /* Запуск программы */
$ Original parent: 6582 /* Программа завершается: приглашение оболочки
и вывод порожденного процесса */
Child: 6583
Child's old ppid: 6582
Child's new ppid: 1
Помните, что обе программы выполняются параллельно. Графически это изображено на рис. 9.2.
Рис. 9.2. Два параллельно исполняющихся процесса после разветвления
ЗАМЕЧАНИЕ. Использование
sleep()
, чтобы заставить один процесс пережить другой, работает в большинстве случаев. Однако, иногда случаются ошибки, которые трудно воспроизвести и трудно обнаружить. Единственным способом гарантировать правильное поведение является явная синхронизация с помощьюwait()
илиwaitpid()
, которые описываются далее в главе (см. раздел 9.1.6.1 «Использование функций POSIX:wait()
иwaitpid()
»).