355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Нейл Мэтью » Основы программирования в Linux » Текст книги (страница 17)
Основы программирования в Linux
  • Текст добавлен: 21 сентября 2016, 17:59

Текст книги "Основы программирования в 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 $

Для того чтобы сделать выбор, пользователь должен последовательно нажать клавиши <А>, , , . Здесь возникают, как минимум, две проблемы; самая серьезная заключается в том, что вы получаете сообщение "Incorrect choice" ("Неверный выбор") после каждого корректного выбора. Кроме того, вы еще должны нажать клавишу (или ), прежде чем программа считает введенные данные.

Сравнение канонического и неканонического режимов

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

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

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

Помимо всего прочего, обработчик терминала в ОС Linux помогает превращать символы прерываний в сигналы (например, останавливающие выполнение программы, когда вы нажмете комбинацию клавиш +), он также может автоматически выполнить обработку нажатых клавиш и и вам не придется реализовывать ее в каждой написанной вами программе. О сигналах вы узнаете больше в главе 11.

Итак, что же происходит в данной программе? ОС 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, в которой мы более подробно рассматриваем синтаксические правила, связанные с ним.) В данном случае вам нужно отправить сообщение непосредственно на терминал пользователя.


    Ваша оценка произведения:

Популярные книги за неделю