Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 24 (всего у книги 55 страниц)
nice()
Когда процессы запущены, ядро динамически меняет приоритет каждого процесса. Как и в жизни, элементы с большим приоритетом получают внимание до элементов с меньшим приоритетом. Короче говоря, каждому процессу выделяется небольшая порция времени для исполнения, которая называется квантом времени (time slice). Когда квант истекает, если текущий процесс все еще является процессом с наивысшим приоритетом, ему разрешается продолжать.
Linux, как и Unix, обеспечивает вытесняющую многозадачность. Это означает, что ядро может вытеснить процесс (приостановить его), если настало время дать возможность поработать другому процессу. Приоритет длительное время работающих процессов (например, процессов, выполняющих интенсивные вычисления), снижается в конце их кванта времени, поэтому они дают шанс другим процессам получить время процессора. Сходным образом, процессам, длительное время бездействовавшим в ожидании завершения ввода/вывода (таким, как интерактивный текстовый редактор), приоритет повышается, так что они могут ответить на ввод/вывод, когда он происходит. Короче, ядро гарантирует, что все процессы, усредненные по времени, получают свою «справедливую долю» времени процессора. Повышение и понижение приоритетов является частью этого процесса.
Проектирование хорошего планировщика процессов для ядра является искусством; практические подробности выходят за рамки данной книги. Однако, процесс может влиять на назначения приоритетов ядром посредством своего значения относительного приоритета (nice).
Значение относительного приоритета является указанием того, насколько «приятным» хочет быть процесс в отношении других процессов. В соответствии с этим большие значения означают во все большей степени терпеливые процессы; т.е. те, которые все более приятны другим, снижая свой приоритет по отношению к ним.
Отрицательное значение относительного приоритета, с другой стороны, означает, что процесс желает быть «менее приятным» по отношению к другим. Такой процесс более эгоистичный, требуя себе большего количества времени процессора[89]89
Такие процессы часто демонстрируют детское поведение. – Примеч. автора.
[Закрыть]. К счастью, в то время как пользователи могут повышать значение относительного приоритета (быть более приятными), лишь root
может снижать значение относительного приоритета (быть менее приятным).
Значение относительного приоритета является лишь одним фактором в уравнении, используемом ядром для вычисления приоритета; это не значение самого приоритета, которое изменяется с течением времени на основе поведения процесса и состояния других процессов системы. Для изменения значения относительного приоритета используется системный вызов nice()
:
#include
int nice(int inc);
Значение относительного приоритета по умолчанию равно 0. Разрешен диапазон значений от -20 до 19. Это требует некоторой привычки. Чем более отрицательное значение, тем выше приоритет процесса: -20 является наивысшим приоритетом (наименьшая приятность), а 19 – наинизшим приоритетом (наибольшая приятность)
Аргумент inc
является приращением, на который надо изменить значение приоритета. Для получения текущего значения, не изменяя его, используйте 'nice(0)
'. Если результат 'текущий_относительный_приоритет + inc
' выйдет за пределы от -20 до 19, система принудительно включит его в этот диапазон.
Возвращаемое значение является новым значением относительного приоритета или -1, если возникла ошибка. Поскольку -1 также является действительным значением относительного приоритета, при вызове nice()
следует сначала явным образом установить errno
в ноль, а затем проверить его насчет имевшихся проблем:
int niceval;
int inc = /* любое значение */;
errno = 0;
if ((niceval = nice(inc)) < 0 && errno != 0) {
fprintf(stderr, "nice(%d) failed: %sn", inc, strerror(errno));
/* другое восстановление */
}
Этот пример может завершиться неудачей, если в inc
отрицательное значение, а процесс не запущен как root
.
Диапазон значений относительного приоритета от -20 до 19, которые использует Linux, имеет исторические корни; он ведет начало по крайней мерее V7. POSIX выражает состояние менее прямым языком, что дает возможность большей гибкости, сохраняя в то же время историческую совместимость. Это также затрудняет чтение и понимание стандарта, вот почему вы и читаете эту книгу. Итак, вот как описывает это POSIX
Во-первых, значение относительного приоритета процесса, поддерживаемое системой, колеблется от 0 до '(2 * NZERO) – 1
'. Константа NZERO
определена в
и должна равняться по крайней мере 20. Это дает диапазон 0–39.
Во-вторых, как мы описывали, сумма текущего значения относительного приоритета и приращение incr
загоняются в этот диапазон.
В заключение, возвращаемое nice()
значение является значением относительного приоритета процесса минус NZERO
. При значении NZERO
20 это дает первоначальный диапазон от -20 до 19, который мы описали вначале.
Результатом является то, что возвращаемое nice() значение в действительности изменяется от '-NZERO
' до 'NZERO-1
', и лучше всего писать свой код в терминах этой именованной константы. Однако, на практике трудно найти систему, в которой NZERO
не было бы равно 20.
exec()
После запуска нового процесса (посредством fork()
) следующим шагом является запуск в процессе другой программы. Имеется несколько функций, которые служат различным целям:
#include
int execve(const char *filename, /* Системный вызов */
char *const argv[], char *const envp[]);
int execl(const char *path, const char *arg, ...); /* Оболочки */
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
Мы ссылаемся на эти функции как на «семейство exec()
». Функции с именем exec()
нет; вместо этого мы используем это имя для обозначения любой из перечисленных выше функций. Как и в случае с fork()
, «exec
» используется на языке Unix и в качестве глагола, означающего исполнение (запуск) программы, и в качестве существительного.
execve()
Простейшей для объяснения функцией является execve()
. Она является также лежащим в основе системным вызовом. Другие являются функциями-оболочками, как вскоре будет объяснено.
int execve(const char *filename, char *const argv[],
char* const envp[]);
filename
является именем программы для исполнения. Это может быть именем полного или относительного пути. Файл должен иметь формат исполняемого файла, который понимает ядро. Современные системы используют формат исполняемого файла ELF (Extensible Linking Format – открытый формат компоновки). GNU/Linux распознает ELF и несколько других форматов. С помощью execve()
можно исполнять интерпретируемые сценарии, если они используют особую первую строку с именем интерпретатора, начинающуюся с '#!
'. (Сценарии, которые не начинаются с '#!
', потерпят неудачу.) В разделе 1.1.3 «Исполняемые файлы» представлен пример использования '#!'. argv является стандартным списком аргументов С – массив символьных указателей на строки аргументов, включая значение для использования с argv[0]
[90]90
См. 9.1.4.3 Имена программ и argv[0]
– Примеч. науч. ред.
[Закрыть], завершающийся указателем NULL
.
envp
является окружением для использования новым процессом, с таким же форматом, как глобальная переменная environ
(см. раздел 2.4 «Переменные окружения»). В новой программе это окружение становится начальным значением environ
.
Программа не должна возвращаться из вызова exec()
. Если она возвращается, возникла проблема. Чаще всего либо не существует затребованная программа, либо она существует, но не является исполняемой (значения для errno ENOENT
и EACCESS
соответственно). Может быть множество других ошибок; см. справочную страницу execve(2).
В предположении, что вызов был успешным, текущее содержимое адресного пространства процесса сбрасывается. (Ядро сначала сохраняет в безопасном месте данные argv
и envp
.) Ядро загружает для новой программы исполняемый код вместе со всеми глобальными и статическими переменными. Затем ядро инициализирует переменные окружения переданными execve()
данными, а далее вызывает процедуру main()
новой программы с переданным функции execve()
массивом argv
. Подсчитывается число аргументов и это значение передается main()
в argc
.
К этому моменту новая программа запущена. Она не знает (и не может определить), какая программа была в процессе до нее. Обратите внимание, что ID процесса не меняется. Многие другие атрибуты при вызове exec
сохраняются; вскоре мы рассмотрим это более подробно.
exec()
для процесса можно сравнить с ролями, которые играют в жизни люди. В различное время в течение дня один человек может быть родителем, супругом, другом, студентом или рабочим, покупателем в магазине и т.д. Это одна и та же личность, исполняющая различные роли. Также и процесс – его PID, открытые файлы, текущий каталог и т.п. – не изменяются, тогда как выполняемая работа – запущенная с помощью exec()
программа – может измениться.
execl()
и др.Пять дополнительных функций, действующих в качестве оболочек, предоставляют более удобные интерфейсы для execve()
. В первой группе все принимают список аргументов, каждый из которых передается в виде явного параметра функции:
int execl(const char *path, const char *arg, ...)
Первый аргумент, path
, является путем к исполняемому файлу. Последующие аргументы, начиная с arg
, являются отдельными элементами, которые должны быть помещены в argv
. Как и ранее, явным образом должен быть включен argv[0]
. Вы должны в качестве последнего аргумента передать завершающий указатель NULL
, чтобы execl()
смогла определить, где заканчивается список аргументов. Новая программа наследует любые переменные окружения, которые находятся в переменной environ
.
int execlp(const char *file, const char *arg, ...)
Эта функция подобна execl()
, но она имитирует механизм поиска команд оболочки, разыскивая file
в каждом каталоге, указанном в переменной окружения PATH
. Если file
содержит символ /
, этот поиск не осуществляется. Если PATH
в окружении не присутствует, execlp()
использует путь по умолчанию. В GNU/Linux по умолчанию используется ":/bin:/usr/bin
", но в других системах может быть другое значение. (Обратите внимание, что ведущее двоеточие в PATH
означает, что сначала поиск осуществляется в текущем каталоге.)
Более того, если файл найден и имеет право доступа на исполнение, но не может быть исполнен из-за того, что неизвестен его формат, execlp()
считает, что это сценарий оболочки и запускает оболочку с именем файла в качестве аргумента.
int execle(const char *path, const char *arg, ...,
char *const envp[])
Эта функция также подобна execl()
, но принимает дополнительный аргумент, envp
, который становится окружением новой программы. Как и в случае с execl()
, вы должны для завершения списка аргументов поместить перед envp
указатель NULL
.
Вторая группа функций-оболочек принимает массив в стиле argv
:
int execv(const char *path, char *const argv[])
Эта функция подобна execve()
, но новая программа наследует любое окружение, которое находится в переменной environ текущей программы.
int execvp(const char *file, char *const argv[])
Эта функция подобна execv()
, но она осуществляет такой же поиск в PATH
, как и функция execlp()
. Она также переходит на исполнение сценария оболочки, если найденный файл не может быть исполнен непосредственно.
В табл. 9.1 подведены итоги для шести функций exec()
.
Таблица 9.1. Сводка семейства функций exec()
по алфавиту
execl() | √ | Исполняет список аргументов. | |
execle() | Исполняет список аргументов с окружением. | ||
execlp() | √ | √ | Исполняет список аргументов с поиском пути |
execv() | √ | Исполняет с argv | |
execve() | Исполняет с argv и окружением (системный вызов). | ||
execvp() | √ | √ | Исполняет с argv и с поиском пути |
Функций execlp()
и execvp()
лучше избегать, если вы не знаете, что переменная окружения PATH
содержит приемлемый список каталогов.
argv[0]
До сих пор мы все время считали argv[0]
именем программы. Мы знаем, что оно может содержать, а может и не содержать символ /
, в зависимости от способа вызова программы, если этот символ содержится, это хорошая подсказка к тому, что для вызова программы использовалось имя пути.
Однако, как должно быть ясно к этому времени, то, что argv[0]
содержит имя файла, является лишь соглашением. Ничто не может воспрепятствовать передаче вами вызываемой программе в качестве argv[0]
произвольной строки. Следующая программа, ch09-run.c
, демонстрирует передачу произвольной строки:
1 /* ch09-run.c – запуск программы с другим именем и любыми аргументами */
2
3 #include
4 #include
5 #include
6
7 /* main – настроить argv и запустить указанную программу */
8
9 int main(int argc, char **argv)
10 {
11 char *path;
12
13 if (argc < 3) {
14 fprintf(stderr, "usage: %s path arg0 [ arg ... ]n", argv[0]);
15 exit(1);
16 }
17
18 path = argv[1];
19
20 execv(path, argv + 2); /* skip argv[0] and argv[1] */
21
22 fprintf(stderr, "%s: execv() failed: %sn", argv[0],
23 strerror(errno));
24 exit(1);
25 }
Первый аргумент является путем к запускаемой программе, а второй аргумент является новым именем для программы (которое большинство утилит игнорируют, кроме сообщений об ошибках); все остальные аргументы передаются вызываемой программе.
Строки 13–16 осуществляют проверку ошибок. Строка 18 сохраняет путь в path
Строка 20 осуществляет exec
; если программа доходит до строк 22–23, это указывает на ошибку. Вот что происходит при запуске программы:
$ ch09-run /bin/grep whoami foo /* Запустить grep */
a line /* Входная строка не подходит */
a line with foo in it /* Входная строка подходит */
a line with foo in it /* Это выводится */
^D /* EOF */
$ ch09-run nonexistent-program foo bar /* Демонстрация неудачи */
ch09-run: execv() failed: No such file or directory
Следующий пример несколько неестественен: мы заставили ch09-run
запустить себя, передав в качестве имени программы 'foo
'. Поскольку аргументов для второго запуска недостаточно, она выводит сообщение об использовании и завершается:
$ ch09-run ./ch09-run foo
usage: foo path arg() [ arg ... ]
Хотя она и не очень полезна, ch09-run
ясно показывает, что argv[0]
не обязательно должен иметь какое-нибудь отношение к файлу, который в действительности запускается.
В System III (примерно в 1980-м) команды cp
, ln
и mv
представляли один исполняемый файл с тремя ссылками с этими именами в /bin
. Программа проверяла argv[0]
и решала, что она должна делать. Это сохраняло некоторое количество дискового пространства за счет усложнения исходного кода и форсирования выполнения программой действия по умолчанию при запуске с неизвестным именем. (Некоторые современные коммерческие системы Unix продолжают эту практику!) Без явной формулировки причин GNU Coding Standards рекомендует, чтобы программы не основывали свое поведение на своем имени. Одна причина, которую мы видели, состоит в том, что администраторы часто устанавливают GNU версию утилиты наряду со стандартной версией коммерческих систем Unix, используя префикс g: gmake
, gawk
и т.д. Если такие программы ожидают лишь стандартные имена, они при запуске с другим именем потерпят неудачу.
Сегодня также дисковое пространство дешево; если из одного и того же исходного кода можно построить две почти идентичные программы, лучше это сделать, использовав #ifdef
, что у вас есть. Например, grep
и egrep
имеют значительную часть общего кода, но GNU версия строит два отдельных исполняемых файла.
exec()
Как и в случае с fork()
, после вызова программой exec
сохраняется ряд атрибутов:
• Все открытые файлы и открытые каталоги; см. раздел 4.4.1 «Понятие о дескрипторах файлов» и раздел 3.3.1 «Базовое чтение каталогов». (Сюда не входят файлы, помеченные для закрытия при исполнении (close-on-exec), как описано далее в этой главе; см. раздел 9.4.3.1 «Флаг close-on-exec».)
• Установки umask; см. раздел 4.6 «Создание файлов».
• Текущий рабочий каталог, см. раздел 8.4.1 «Изменение каталога: chdir()
и fchdir()
»
• Корневой каталог; см. раздел 8.6 «Изменение корневого каталога: chroot()
».
• Текущее значение относительного приоритета.
• ID процесса и ID родительского процесса.
• ID группы процесса и контролирующий терминал; см. раздел 9.2.1 «Обзор управления работами».
• Маску сигналов процесса и любые ожидающие сигналы, а также любые не истекшие аварийные сигналы или таймеры (здесь не обсуждается; см. главу 10 «Сигналы»).
• Действительные ID пользователя и ID группы, а также дополнительный набор групп. Эффективные ID пользователя и группы (а следовательно, и сохраненные ID set-user и set-group) могут быть установлены с помощью битов setuid и setgid исполняемого файла. (Ничто из этого пока не обсуждалось; см. главу 11 «Права доступа и ID пользователя и группы».)
• Блокировки файлов сохраняются (также пока не обсуждалось; см. раздел 14.2 «Блокировка файлов»).
• Суммарное использованное время процессора для процесса и его потомков не меняется.
После exec
размещение сигналов изменяется; дополнительные сведения см. в разделе 10.9 «Сигналы для fork()
и exec()
».
После exec
все открытые файлы и каталоги остаются открытыми и доступными для использования. Вот как программы наследуют стандартные ввод, вывод и ошибку: они на месте, когда программа запускается.
В большинстве случаев при исполнении fork
и exec
для отдельной программы не нужно ничего наследовать, кроме дескрипторов файлов 0, 1 и 2. В этом случае можно вручную закрыть все другие открытые файлы в порожденном процессе после выполнения fork
и до выполнения exec
. В качестве альтернативы можно пометить дескриптор файла для автоматического закрытия системой при исполнении exec; эта последняя возможность обсуждается далее в главе (см раздел 9.4.3.1 «Флаг close-on-exec».)
Завершение процесса включает два шага: окончание процесса с передачей системе статуса завершения и восстановление информации родительским процессом.
Статус завершения (exit status) (известный также под другими именами значения завершения (exit value), кода возврата (return code) и возвращаемого значения (return value)) представляет собой 8-битовое значение, которое родитель может использовать при завершении порожденного процесса (на языке Unix, «когда порожденный кончается (dies)»). По соглашению статус завершения 0 означает, что программа отработала без проблем. Любое ненулевое значение указывает на какую-нибудь разновидность ошибки; программа определяет используемые числа и их значения, если они есть. (Например, grep
использует 0 для указания, что образец был встречен по крайней мере один раз, 1 означает, что образец вообще не встретился, а 2 означает, что возникла ошибка.) Этот статус завершения доступен на уровне оболочки (для оболочек в стиле оболочки Борна) через специальную переменную $?
.
Стандарт С определяет две константы, которые следует использовать для полной переносимости на не-POSIX системы:
EXIT_SUCCESS
Программа завершилась без проблем. Для обозначения успеха может также использоваться ноль.
EXIT
_FAILURE
В программе была какая-нибудь проблема.
На практике использование лишь этих значений довольно ограничивает. Вместо этого следует выбрать небольшой набор кодов возврата, документировать их значения и использовать. (Например, 1 для ошибок опций командной строки и аргументов, 2 для ошибок ввода/вывода, 3 для ошибок данных и т.д.) Для удобочитаемости стоит использовать константы #define
или значения enum
. Слишком большой список ошибок делает их использование обременительным; в большинстве случаев вызывающая программа (или пользователь) интересуется лишь нулевым или ненулевым значением.
Когда достаточно двоичного разделения успех/неудача, педантичный программист использует EXIT_SUCCESS
и EXIT_FAILURE
. Наш собственный стиль более естественный, используя с return
и exit()
явные константы 0 или 1. Это настолько обычно, что рано заучивается и быстро становится второй натурой. Однако для своих проектов вы сами должны принять решение.
ЗАМЕЧАНИЕ. Для родительского процесса доступны лишь восемь наименее значимых битов значения. Поэтому следует использовать значения в диапазоне 0–255. Как мы вскоре увидим, у чисел 126 и 127 есть традиционные значения (помимо простого «неуспешно»), которых ваши программы должны придерживаться.
Поскольку имеют значение лишь восемь наименее значимых битов, вы никогда не должны использовать отрицательные статусы завершения. Когда из небольших отрицательных чисел выделяются восемь последних битов, они превращаются в большие положительные значения! (Например. -1 становится 255, а -5 становится 251.) Мы видели книги по программированию на С, в которых это понималось неправильно – не дайте сбить себя с толку