Текст книги "Основы программирования в Linux"
Автор книги: Нейл Мэтью
Соавторы: Ричард Стоунс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 17 (всего у книги 67 страниц)
Ресурсы и ограничения
Программы, выполняющиеся в системе Linux, зависят от ограниченности ресурсов. Это могут быть физические ограничения, накладываемые оборудованием (например, памятью), ограничения, связанные с системной политикой (например, разрешенное время процессора) или ограничения реализации (такие как размер типа integer
или максимально допустимое количество символов в имени файла). В спецификацию UNIX включены некоторые из этих ограничений, которые может определять приложение. Дальнейшее обсуждение ограничений и последствия их нарушений см. в главе 7.
В заголовочном файле limits.h определены многие именованные константы, представляющие ограничения, налагаемые операционной системой (табл. 4.8).
Таблица 4.8
NAME_MAX | Максимальное число символов в имени файла |
CHAR_BIT | Количество разрядов в значении типа char |
CHAR_MAX | Максимальное значение типа char |
INT_MAX | Максимальное значение типа int |
Существует множество других ограничений, полезных приложению, поэтому следует ознакомиться с заголовочными файлами установленной у вас версии системы.
Примечание
Имейте в виду, что константа
NAME_MAX
зависит от файловой системы. Для разработки легко переносимого кода следует применять функцию pathconf. Дополнительную информацию о ней см. на страницах интерактивного справочного руководства.
В заголовочном файле sys/resource.h представлены определения операций над ресурсами. К ним относятся функции для считывания и установки предельных значений для разрешенного размера программы, приоритета выполнения и файловых ресурсов.
#include
int getpriority(int which, id_t who);
int setpriority(int which, id_t who, int priority);
int getrlimit(int resource, struct rlimit *r_limit);
int setrlimit(int resource, const struct rlimit *r_limit);
int getrusage(int who, struct rusage *r_usage);
Здесь id_t
– это целочисленный тип, применяемый для идентификаторов пользователя и группы. Структура rusage
, указанная в файле sys/resource.h, используется для определения времени центрального процессора (ЦП), затраченного текущей программой. Она должна содержать, как минимум, два элемента (табл. 4.9).
Таблица 4.9
rusage | |
---|---|
struct timeval ru_utime | Время, использованное пользователем |
struct timeval ru_stime | Время, использованное системой |
Структура timeval
определена в файле sys/time.h и содержит поля tv_sec
и tv_usec
, представляющие секунды и микросекунды соответственно.
Время ЦП, потребляемое программой, делится на время пользователя (время, затраченное самой программой на выполнение собственных инструкций) и системное время (время ЦП, потребляемое операционной системой в интересах программы, т.е. время, затраченное на системные вызовы, выполняющие ввод и вывод или другие системные функции).
Функция getrusage
записывает данные о времени ЦП в структуру rusage
, на которую указывает параметр r_usage
. Параметр who
может быть задан одной из констант, приведенных в табл. 4.10.
Таблица 4.10
who | |
---|---|
RUSAGE_SELF | Возвращает данные о потреблении только для текущей программы |
RUSAGE_CHILDREN | Возвращает данные о потреблении и для дочерних процессов |
Мы будем обсуждать дочерние процессы и приоритеты задач в главе 11, но для полноты картины мы здесь упоминаем об их причастности к потреблению системных ресурсов. Пока достаточно сказать, что у каждой выполняющейся программы есть ассоциированный с ней приоритет, и чем выше приоритет программы, тем больше ей выделяется доступного времени ЦП.
Примечание
Обычные пользователи могут только снижать приоритеты своих программ, а не повышать их.
Приложения могут определять и изменять свои (и чужие) приоритеты с помощью функций getpriority
и setpriority
. Процесс, исследуемый или изменяемый с помощью этих функций, может быть задан идентификатором процесса, группы или пользователя. Параметр which
описывает, как следует интерпретировать параметр who
(табл. 4.11).
Таблица 4.11
which | |
---|---|
PRIO_PROCESS | who – идентификатор процесса |
PRIO_PGRP | who – идентификатор группы |
PRIO_USER | who – идентификатор пользователя |
Итак, для определения приоритета текущего процесса вы можете выполнить следующий вызов:
priority = getpriority(PRIO_PROCESS, getpid());
Функция setpriority
позволяет задать новый приоритет, если это возможно.
По умолчанию приоритет равен 0. Положительные значения приоритета применяются для фоновых задач, которые выполняются, только когда нет задачи с более высоким приоритетом, готовой к выполнению. Отрицательные значения приоритета заставляют программу работать интенсивнее, выделяя большие доли доступного времени ЦП. Диапазон допустимых приоритетов – от -20 до +20. Часто это приводит к путанице, поскольку, чем выше числовое значение, тем ниже приоритет выполнения.
Функция getpriority
возвращает установленный приоритет в случае успешного завершения или -1 с переменной errno
, указывающей на ошибку. Поскольку значение -1 само по себе обозначает допустимый приоритет, переменную errno
перед вызовом функции getpriority
следует приравнять нулю и при возврате из функции проверить, осталась ли она нулевой. Функция setpriority
возвращает 0 в случае успешного завершения и -1 в противном случае.
Предельные величины, заданные для системных ресурсов, можно прочитать и установить с помощью функций getrlimit
и setrlimit
. Обе они для описания ограничений ресурсов используют структуру общего назначения rlimit
. Она определена в файле sys/resource.h и содержит элементы, перечисленные в табл. 4.12.
Таблица 4.12
rlimit | |
---|---|
rlim_t rlim_cur | Текущее, мягкое ограничение |
rlim_t rlim_max | Жесткое ограничение |
Определенный выше тип rlim_t
– целочисленный тип, применяемый для описания уровней ресурсов. Обычно мягкое ограничение – это рекомендуемое ограничение, которое не следует превышать; нарушение этой рекомендации может вызвать возврат ошибок из библиотечных функций. При превышении жесткого ограничения система может попытаться завершить программу, отправив ей сигнал, например, сигнал SIGXCPU
при превышении ограничения на потребляемое время ЦП и сигнал SIGSEGV
при превышении ограничения на объем данных. В программе можно самостоятельно задать для любых значений собственные мягкие ограничения, не превышающие жесткого ограничения. Допустимо уменьшение жесткого ограничения. Увеличить его может только программа, выполняющаяся с правами суперпользователя.
Ограничить можно ряд системных ресурсов. Эти ограничения описаны в параметре resource
функций rlimit
и определены в файле sys/resource.h, как показано в табл. 4.13.
Таблица 4.13
resource | |
---|---|
RLIMIT_CORE | Ограничение размера файла дампа ядра, в байтах |
RLIMIT_CPU | Ограничение времени ЦП, в секундах |
RLIMIT_DATA | Ограничение размера сегмента data() , в байтах |
RLIMIT_FSIZE | Ограничение размера файла, в байтах |
RLIMIT_NOFILE | Ограничение количества открытых файлов |
RLIMIT_STACK | Ограничение размера стека, в байтах |
RLIMIT_AS | Ограничение доступного адресного пространства (стек и данные), в байтах |
В упражнении 4.15 показана программа limits.c, имитирующая типичное приложение. Она также задает и нарушает ограничения ресурсов.
Упражнение 4.16. Ограничения ресурсов
1. Включите заголовочные файлы для всех функций, которые вы собираетесь применять в данной программе:
#include
#include
#include
#include
#include
#include
#include
2. Функция типа void
записывает 10 000 раз строку во временный файл и затем выполняет некоторые арифметические вычисления для загрузки ЦП:
void work() {
FILE *f;
int i;
double x = 4.5;
f = tmpfile();
for (i = 0; i < 10000; i++) {
fprintf(f, «Do some outputn»);
if (ferror(f)) {
fprintf(stderr, «Error writing to temporary filen»);
exit(1);
}
}
for (i = 0; i < 1000000; i++) x = log(x*x + 3.21);
}
3. Функция main
вызывает функцию work
, а затем применяет функцию getrusage для определения времени ЦП, использованного work
. Эта информация выводится на экран:
int main() {
struct rusage r_usage;
struct rlimit r_limit;
int priority;
work();
getrusage(RUSAGE_SELF, &r_usage);
printf(«CPU usage: User = %ld.%06ld, System = %ld.%06ldn»,
r_usage.ru_utime.tvsec, rusage.ru_utime.tv_usec,
r_usage.ru_stime.tv_sec, r_usage.ru_stime.tv_usec);
4. Далее она вызывает функции getpriority
и getrlimit
для выяснения текущего приоритета и ограничений на размер файла соответственно:
priority = getpriority(PRIO_PROCESS, getpid());
printf(«Current priority = %dn», priority);
getrlimit(RLIMIT_FSIZE, &r_limit);
printf(«Current FSIZE limit: soft = %ld, hard = %ldn»,
r_limi t.rlim_cur, r_limit.rlim_max);
5. В заключение задайте ограничение размера файла с помощью функции setrlimit
и снова вызовите функцию work
, которая завершится с ошибкой, т.к. попытается создать слишком большой файл:
r_limit.rlim_cur = 2048;
r_limit.rlim_max = 4096;
printf(«Setting a 2K file size limitn»);
setrlimit(RLIMIT_FS1ZE, &r_limit);
work();
exit(0);
}
Выполнив эту программу, вы сможете увидеть, сколько затрачено времени ЦП, и текущий приоритет, с которым программа выполняется. После того как будет задан предельный размер файла, программа не сможет записать во временный файл более 2048 байтов.
$ cc -о limits limits.с -lm
$ ./limits
CPU usage: User = 0.140008, System = 0.020001
Current priority = 0
Current FSIZE limit: soft = -1, hard = -1
Setting a 2K file size limit
File size limit exceeded
Вы можете изменить приоритет программы, запустив ее с помощью команды nice
. Далее показано, как меняется приоритет на значение +10, и в результате программа выполняется немного дольше.
$ nice ./limits
CPU usage: User = 0.152009, System = 0.020001
Current priority = 10
Current FSIZE limit: soft = -1, hard = -1
Setting a 2K file size limit
File size limit exceeded
Как это работает
Программа limits вызывает функцию work
для имитации операций типичной программы. Она выполняет некоторые вычисления и формирует вывод, в данном случае около 150 Кбайт записывается во временный файл. Программа вызывает функции управления ресурсами для выяснения своего приоритета и ограничений на размер файла. В данном случае ограничения размеров файлов не заданы, поэтому можно создавать файл любого размера (если позволяет дисковое пространство). Затем программа задает свое ограничение размера файла, равное примерно 2 Кбайт, и снова пытается выполнить некоторые действия. На этот раз функция work
завершается неудачно, поскольку не может создать такой большой временный файл.
Примечание
Ограничения можно также наложить на программу, выполняющуюся в отдельной командной оболочке с помощью команды
ulimit
оболочки bash.
В приведенном примере сообщение об ошибке «Error writing to temporary file» («Ошибка записи во временный файл») не выводится. Это происходит потому, что некоторые системы (например, Linux 2.2 и более поздние версии) завершают выполнение программы при превышении ограничения ресурса. Делается это с помощью отправки сигнала SIGXFSZ
. В главе 11 вы узнаете больше о сигналах и способах их применения. Другие системы, соответствующие стандарту POSIX, заставляют функцию, превысившую ограничение, вернуть ошибку.
Резюме
В этой главе вы посмотрели на окружение системы Linux и познакомились с условиями выполнения программ. Вы узнали об аргументах командной строки и переменных окружения – и те, и другие могут применяться для изменения стандартного поведения программы и предоставляют подходящие программные опции.
Вы увидели, как программа может воспользоваться библиотечными функциями для обработки значений даты и времени и получить сведения о себе, пользователе и компьютере, на котором она выполняется.
Программы в ОС Linux, как правило, должны совместно использовать дорогостоящие ресурсы, поэтому в данной главе рассматриваются способы определения имеющихся ресурсов и управления ими.
Глава 5
Терминалы
В этой главе вы познакомитесь с некоторыми улучшениями, которые вам, возможно, захочется внести в базовое приложение из главы 2. Его, быть может, самый очевидный недостаток – пользовательский интерфейс; он достаточно функционален, но не слишком элегантен. Теперь вы узнаете, как сделать более управляемым терминал пользователя, т. е. ввод с клавиатуры и вывод на экран. Помимо этого вы научитесь обеспечивать написанным вами программам возможность получения вводимых данных от пользователя даже при наличии перенаправления ввода и гарантировать вывод данных в нужное место на экране.
Несмотря на то, что заново реализованное приложение для управления базой данных компакт-дисков не увидит свет до конца главы 7, его основы вы заложите в этой главе. Глава 6 посвящена curses, которые представляют собой вовсе не древнее проклятие, а библиотеку функций, предлагающих программный код высокого уровня для управления отображением на экране терминала. Попутно вы узнаете чуть больше о размышлениях прежних профи UNIX, познакомившись с основными принципами систем Linux и UNIX и понятием терминала. Низкоуровневый доступ, представленный в этой главе, быть может именно то, что вам нужно. Большая часть того, о чем мы пишем здесь, хорошо подходит для программ, выполняющихся в окне консоли, таких как эмуляторы терминала KDE's Konsole, GNOME's gnome-terminal или стандартный X11 xterm.
В этой главе вы, в частности, узнаете о:
□ чтении с терминала и записи на терминал;
□ драйверах терминала и общем терминальном интерфейсе (General Terminal Interface, GTI);
□ структуре типа termios
;
□ выводе терминала и базе данных terminfo
;
□ обнаружении нажатия клавиш.
Чтение с терминала и запись на терминал
В главе 3 вы узнали, что, когда программа запускается из командной строки, оболочка обеспечивает присоединение к ней стандартных потоков ввода и вывода. Вы получаете возможность взаимодействия с пользователем простым применением подпрограмм getchar
и printf
для чтения из стандартного потока ввода и записи в стандартный поток вывода.
В упражнении 5.1 в программе menu1.c вы попытаетесь переписать на языке С подпрограммы формирования меню, использующие только эти две функции.
Упражнение 5.1. Подпрограммы формирования меню на языке C
1. Начните со следующих строк, определяющих массив, который будет использоваться как меню, и прототип (описание) функции getchoice
:
#include
#include
char *menu[] = {
«a – add new record», «d – delete record», «q – quit», NULL,
};
int getchoice(char *greet, char *choices[]);
2. Функция main
вызывает функцию getchoice
с образцом пунктов меню menu
:
int main() {
int choice = 0;
do {
choice = getchoice(«Please select an action», menu);
printf(«You have chosen: %cn», choice);
} while (choice != 'q');
exit(0);
}
3. Теперь важный фрагмент кода – функция, которая и выводит на экран меню и считывает ввод пользователя:
int getchoice(char *greet, char *choices[]) {
int chosen = 0;
int selected;
char **option;
do {
printf(«Choice: %sn», greet);
option = choices;
while (*option) {
printf(«%sn», *option);
option++;
}
selected = getchar();
option = choices;
while (*option) {
if (selected == *option[0]) {
chosen = 1;
break;
}
option++;
}
if (!chosen) {
printf(«Incorrect choice, select againn»);
}
} while (!chosen);
return selected;
}
Как это работает
Функция getchoice
выводит на экран приглашение для ввода greet
и меню choices
и просит пользователя ввести первый символ выбранного пункта. Далее выполняется цикл до тех пор, пока функция getchar
не вернет символ, совпадающий с первой буквой одного из элементов массива option.
Когда вы откомпилируете и выполните программу, то обнаружите, что она ведет себя не так, как ожидалось. Для того чтобы продемонстрировать возникающую проблему, далее приведен вариант диалога на экране терминала.
$ ./menu1
Choice: Please select an action
a – add new record
d – delete record
q – quit
a
You have chosen: a
Choice: Please select an action
a – add new record
d – delete record
q – quit
Incorrect choice, select again
Choice: Please select an action
а – add new record
d – delete record
q – quit
q
You have chosen: q $
Для того чтобы сделать выбор, пользователь должен последовательно нажать клавиши <А>, ,
Обе эти проблемы тесно связаны. По умолчанию ввод терминала не доступен программе до тех пор, пока пользователь не нажмет клавишу
Такое поведение называется каноническим или стандартным режимом. Весь ввод обрабатывается как последовательность строк. Пока строка ввода не завершена (обычно с помощью нажатия клавиши
Прямая противоположность — неканонический режим, в котором приложение получает больше возможностей контроля над обработкой вводимых символов. Мы еще вернемся к этим двум режимам немного позже в этой главе.
Помимо всего прочего, обработчик терминала в ОС Linux помогает превращать символы прерываний в сигналы (например, останавливающие выполнение программы, когда вы нажмете комбинацию клавиш
Итак, что же происходит в данной программе? ОС Linux сохраняет ввод до тех пор, пока пользователь не нажмет клавишу getchar
, обрабатывает символ и снова вызывает getchar
, немедленно возвращающую символ клавиши
Символ, который на самом деле видит программа, – это не символ ASCII возврата каретки CR (десятичный код 13, шестнадцатеричный 0D), а символ перевода строки LF (десятичный код 10, шестнадцатеричный 0A). Так происходит потому, что на внутреннем уровне ОС Linux (как и UNIX) всегда применяет перевод строки для завершения текстовых строк, т. е. в отличие от других ОС, таких как MS-DOS, использующих комбинацию символов возврата каретки и перевода строки, ОС UNIX применяет, для обозначения новой строки только символ перевода строки. Если вводное или выводное устройство посылает или запрашивает и символ возврата каретки, в ОС Linux об этом заботится обработчик терминала. Если вы привыкли работать в MS-DOS или других системах, это может показаться странным, но одно из существенных преимуществ заключается в отсутствии в ОС Linux реальной разницы между текстовыми и бинарными файлами. Символы возврата каретки обрабатываются, только когда вы вводите или выводите их на терминал или некоторые принтеры и плоттеры.
Вы можете откорректировать основной недостаток вашей подпрограммы меню, просто игнорируя дополнительный символ перевода строки с помощью программного кода, подобного приведенному далее:
do {
selected = getchar();
} while (selected == 'n');
Он решает непосредственно возникшую проблему, и вы увидите вывод, подобный приведенному далее:
$ ./menu1
Choice: Please select an action
a – add new record
d – delete record
q – quit
a
You have chosen: a
Choice: Please select an action
a – add new record
d – delete record
q – quit
q
You have chosen: q $
Мы вернемся позже ко второй проблеме, связанной с необходимостью нажимать клавишу
Для программ, выполняющихся в ОС Linux, даже интерактивных, характерно перенаправление своего ввода и вывода как в файлы, так и в другие программы. Давайте рассмотрим поведение вашей программы при перенаправлении ее вывода в файл.
$ ./menu1 > file
a
q
$
Такой результат можно было бы считать успешным, потому что вывод перенаправлен в файл вместо терминала. Однако бывают случаи, когда нужно помешать такому исходу событий или отделить приглашения или подсказки, которые пользователь должен видеть, от остального вывода, благополучно перенаправляемого в файл.
О перенаправлении стандартного вывода можно судить по наличию низкоуровневого дескриптора файла, ассоциированного с терминалом. Эту проверку выполняет системный вызов isatty
. Вы просто передаете ему корректный дескриптор файла, и он проверяет, связан ли этот дескриптор в данный момент с терминалом.
#include
int isatty(int fd);
Системный вызов isatty
возвращает 1, если открытый дескриптор файла fd
связан с терминалом, и 0 в противном случае.
В данной программе используются файловые потоки, но isatty
оперирует только дескрипторами файлов. Для выполнения необходимого преобразования вам придется сочетать вызов isatty
с подпрограммой fileno
, обсуждавшейся в главе 3.
Что вы собираетесь делать, если стандартный вывод stdout
перенаправлен? Просто завершить программу – не слишком хорошо, потому что у пользователя нет возможности выяснить, почему программа аварийно завершила выполнение. Вывод сообщения в stdout
тоже не поможет, поскольку оно будет перенаправлено с терминала. Единственное решение – записать сообщение в стандартный поток ошибок stderr
, который не перенаправляется командой оболочки > file
(упражнение 5.2).
Упражнение 5.2. Проверка для выявления перенаправления вывода
Внесите следующие изменения в директивы включения заголовочных файлов и функцию main программы menu1.с из упражнения 5.1. Назовите новый файл menu2.c.
#include
...
int main() {
int choice = 0;
if (!isatty(fileno(stdout))) {
fprintf(stderr, «You are not a terminal!n»);
exit(1);
}
do {
choice = getchoice(«Please select an action», menu);
printf(«You have chosen: %cn», choice);
} while (choice != 'q');
exit(0);
}
Теперь посмотрите на следующий пример вывода:
$ ./menu2
Choice: Please select an action
a – add new record
d – delete record
q – quit
q
You have chosen: q $ ./menu2 > file
You are not a terminal! $
Как это работает
В новом фрагменте программного кода функция isatty
применяется для проверки связи стандартного вывода с терминалом и прекращения выполнения программы при отсутствии этой связи. Это тот же самый тест, который командная оболочка использует для решения, нужно ли выводить строки приглашения. Возможно и довольно обычно перенаправление и stdout
, и stderr
с терминала на другое устройство. Вы можете направить поток ошибок в другой файл:
$ ./menu2 >file 2>file.error
$
или объединить оба выводных потока в одном файле:
$ ./menu2 >file 2>&1
$
(Если вы не знакомы с перенаправлением вывода, прочтите еще раз главу 2, в которой мы более подробно рассматриваем синтаксические правила, связанные с ним.) В данном случае вам нужно отправить сообщение непосредственно на терминал пользователя.