52595.fb2
Когда вы пишете программу для ОС Linux (или UNIX и UNIX-подобных систем), следует принимать во внимание то, что программа будет выполняться в многозадачной среде или многозадачном окружении. Это означает, что много программ будет выполняться одновременно и совместно использовать ресурсы компьютера, такие как память, дисковое пространство и циклы центрального процессора. Может даже существовать несколько экземпляров одной и той же программы, выполняющихся одновременно. Важно, чтобы эти программы не мешали друг другу, знали о своем окружении и могли действовать надлежащим образом, избегая конфликтов, таких как попытка писать в один и тот же файл одновременно с другой программой.
В этой главе рассматривается окружение, в котором действуют программы, как они его используют для получения информации об условиях функционирования и как пользователи программ могут изменять их поведение. В частности, в данной главе рассматриваются следующие темы:
□ передача аргументов в программы;
□ переменные окружения;
□ определение текущего времени;
□ временные файлы;
□ получение информации о пользователе и рабочем компьютере;
□ формирование и настройка регистрируемых сообщений;
□ выявление ограничений, накладываемых системой.
Когда в ОС Linux или UNIX выполняется программа на языке С, она начинается с функции main
. В таких программах функция main
объявляется следующим образом:
int main(int argc, char *argv[])
Здесь argc
— это счетчик аргументов программы, a argv
— массив символьных строк, представляющих сами аргументы.
Вы можете встретить программы на языке С для ОС Linux, просто объявляющие функцию main
как
main()
Этот вариант тоже работает, поскольку по умолчанию возвращаемому функцией значению будет назначен тип int
, а формальные параметры, которые в функции не применяются, не нуждаются в объявлении. Параметры argc
и argv
остаются на своем месте, но если вы не объявляете их, то и не можете их использовать.
Каждый раз, когда операционная система запускает новую программу, параметры argc
и argv
устанавливаются и передаются функции main
. Обычно эти параметры предоставляются другой программой, часто командной оболочкой, которая запросила у операционной системы запуск новой программы. Оболочка принимает заданную командную строку, разбивает её на отдельные слова и использует их для заполнения массива argv
. Помните о том, что до установки параметров argc
и argv
командная оболочка Linux обычно выполняет раскрытие метасимволов в аргументах, содержащих имена файлов, в то время как оболочка MS-DOS рассчитывает на то, что программы примут аргументы с метасимволами и выполнят собственную постановку.
Например, если мы дадим командной оболочке следующую команду:
$ myprog left right 'and center'
программа myprog запустит функцию main
с приведенными далее параметрами.
argc: 4
argv: {"myprog", "left", "right", "and center"}
Обратите внимание на то, что аргумент-счётчик содержит имя программы и в массив argv
оно включено как первый элемент argv[0]
. Поскольку в команде оболочки мы применили кавычки, четвертый аргумент представляет собой строку, содержащую пробелы.
Вам все это знакомо, если вы программировали на языке С стандарта ISO/ANSI, Аргументы функции main
соответствуют позиционным параметрам в сценариях командной оболочки: $0
, $1
и т.д. Язык ISO/ANSI С заявляет, что функция main
должна возвращать значение типа int
, спецификация X/Open содержит явное объявление, данное ранее.
Аргументы командной строки удобны для передачи данных программам. Например, вы можете применить их в приложениях баз данных для передачи имени базы данных, которую хотите использовать, что позволит вам применить одну и ту же программу для работы с несколькими базами данных. Многие утилиты также используют аргументы командной строки для изменения собственного поведения или установки опций. Вы обычно задаете эти так называемые флаги или переключатели с помощью аргументов командной строки, начинающихся со знака "дефис". Например, программа sort
принимает переключатель для изменения обычного порядка сортировки на обратный:
$ sort -r файл
Опции командной строки используются очень широко, и согласованное их применение будет реальной помощью тем, кто станет использовать вашу программу. В прошлом у каждой утилиты был свой подход к формированию опций командной строки, что приводило к некоторой путанице. Например, взгляните на то, каким способом приведенные далее команды принимают параметры:
$ tar cvfB /tmp/file.tar 1024
$ dd if=/dev/fd0 of=/trap/file.dd bs=18k
$ ps ax
$ gcc --help
$ ls -lstr
$ ls -l -s -t -r
Мы рекомендуем в ваших приложениях все переключатели командной строки начинать с дефиса и делать их односимвольными, состоящими из одной буквы или цифры. При необходимости опции, не содержащие последующих аргументов, могут группироваться вместе после общего дефиса. Таким образом, два только что приведенных примера с командой ls
соответствуют нашим рекомендациям. За каждой опцией может следовать любое необходимое ей значение как отдельный аргумент. Пример с программой dd
нарушает наше правило, поскольку использует многосимвольные опции, которые начинаются совсем не с дефисов (if=/dev/fd0
): в примере с программой tar
опции полностью оторваны от своих значений! Целесообразно добавлять более длинные и информативные имена переключателей как альтернативу односимвольных вариантов и использовать двойной дефис для их выделения. Таким образом, у нас могут быть два варианта опции получения помощи: -h
и --help
.
Еще один недостаток некоторых программ — создание опции +x
(например) для выполнения функции, противоположной -х
. В главе 2 мы применяли команду set -о xtrace
для включения отслеживания действий командной оболочки и команду set +о xtrace
для выключения этого режима.
Вы, вероятно, можете сказать, что запомнить порядок и назначение всех этих программных опций достаточно трудно без необходимости освоения вызывающих идиосинкразию форматов. Часто единственный выход — применение опции -h
(от англ. help) или страниц интерактивного справочного руководства (man
), если программист предоставил одну из этих возможностей. Чуть позже в этой главе мы покажем, что функция getopt
предоставляет изящное решение этих проблем. А сейчас, тем не менее, в упражнении 4.1 давайте посмотрим, как передаются аргументы программы.
Далее приведена программа args.c, проверяющая собственные аргументы.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
int arg;
for (arg = 0; arg < argc; arg++) {
if (argv[arg][0] == '-')
printf("option: %s\n", argv[arg]+1);
else
printf("argument %d: %s\n", arg, argv[arg]);
}
exit(0);
}
Когда вы выполните эту программу, она просто выведет свои аргументы и определит опции. Суть в том, что программа принимает строковый аргумент и необязательный аргумент с именем файла, вводимый опцией -f
. Могут быть определены и другие опции.
$ ./args -i -lr 'hi there' -f fred.c
argument 0: ./args
option: i
option: lr
argument 3: hi there option: f
argument 5: fred.с
Как это работает
Программа просто использует аргумент-счетчик argc
для задания цикла, просматривающего все аргументы программы. Она находит опции поиском начального дефиса.
В данном примере, если мы предполагаем, что доступны опции -l
и -r
, то упускаем тот факт, что группа -lr
, возможно, должна интерпретироваться так же, как -l
и -r
.
В стандарте X/Open (который можно найти по адресу http://opengroup.org/) определено стандартное применение опций командной строки (Utility Syntax Guidelines, руководство по синтаксису утилит) и стандартный программный интерфейс для представления переключателей командной строки в программах на языке С: функция getopt
.
Для того чтобы вам легче было следовать правилам, приведенным в этих руководствах, ОС Linux предлагает очень простое в применении средство getopt
, поддерживающее использование опций со значениями и без них.
#include <unistd.h>
int getopt(int argc, char *const argv[], const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;
Функция getopt
принимает параметры argc
и argv
в том виде, в каком они передаются функции main
в программе, и строку спецификатора опций, которая сообщает getopt
, какие опции определены для программы и есть ли у них связанные с ними значения. optstring
— это просто список символов, каждый из которых представляет односимвольную опцию. Если за символом следует двоеточие, это означает, что у опции есть ассоциированное значение, которое будет принято как следующий аргумент. Команда getopt
оболочки bash выполняет аналогичную функцию.
Например, для обработки предыдущего примера можно было бы применить следующий вызов:
getopt(argc, argv, "if:lr");
В нем учтены простые опции -i
, -l
, -r
и -f
, за которыми последует аргумент с именем файла. Вызов команды с теми же параметрами, но указанными в другом порядке, изменит поведение. Вы сможете попробовать сделать это, когда получите пример кода из упражнения 4.2.
Результат, возвращаемый функцией getopt
, — символ следующей опции, хранящийся в массиве argv
(если он есть). Вызывайте getopt
повторно для поочередного получения каждой опции. Функция ведет себя следующим образом.
□ Если опция принимает значение, на него указывает внешняя переменная optarg
.
□ Функция getopt
вернет -1, когда не останется опций для обработки. Специальный аргумент --
заставит getopt
прекратить перебор опций.
□ Функция getopt
вернет ?
, если есть нераспознанная опция, которую она сохранит во внешней переменной optopt
.
□ Если опции требуется значение (например, в нашем примере опции -f
) и не задана никакая величина, getopt
обычно возвращает ?
. Если поместить двоеточие как первый символ в строке опций, при отсутствии заданной величины функция getopt
вернет :
вместо ?
.
Во внешней переменной optind
хранится номер следующего обрабатываемого аргумента. Функция getopt
использует ее, чтобы знать, как далеко она продвинулась. Программы редко нуждаются в установке этой переменной. Когда все аргументы с опциями обработаны, переменная optind
указывает, где в конце массива argv можно найти оставшиеся аргументы.
Некоторые версии функции getopt
прекратят выполнение при обнаружении первого аргумента не опции, вернув значение -1 и установив переменную optind
. Другие, например предлагаемые в ОС Linux, могут обрабатывать опции, где бы они ни встретились в аргументах программы. Учтите, что в данном случае getopt
фактически перепишет массив argv
так, что все аргументы не опции будут собраны вместе, начиная с элемента массива argv[optind]
. В случае версии GNU функции getopt
ее поведение определяется переменной окружения POSIXLY_CORRECT
. Если переменная установлена, getopt
остановится на первом аргументе не опции. Кроме того, некоторые реализации getopt
выводят сообщения об ошибке для незнакомых опций. Имейте в виду, что в стандарте POSIX написано о том, что если переменная opterr
не равна нулю, функция getopt
выведет сообщение об ошибке в stderr
.
Итак, выполните упражнение 4.2.
В этом упражнении вы используете функцию getopt; назовите новую программу argopt.c.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
int opt;
while ((opt = getopt(argc, argv, ":if:lr")) != -1) {
switch(opt) {
case 'i':
case 'l':
case 'r':
printf("option: %c\n", opt);
break;
case 'f':
printf("filename: %s\n", optarg);
break;
case ':':
printf("option needs a value\n");
break;
case '?':
printf("unknown option: %c\n", optopt);
break;
}
}
for (; optind < argc; optind++)
printf("argument: %s\n", argv[optind]);
exit(0);
}
Теперь, когда вы выполните программу, то увидите, что все аргументы командной строки обрабатываются автоматически:
$ ./argopt -i -lr 'hi there' -f fred.с -q
option: i
option: l
option: r
filename: fred.c
unknown option: q
argument: hi there
Как это работает
Программа многократно вызывает функцию getopt
для обработки аргументов-опций до тех пор, пока не останется ни одного, в этот момент getopt
вернет -1. Для каждой опции выбирается подходящее действие, включая обработку неизвестных опций и пропущенных значений. Если у вас другая версия getopt
, то вы получите вывод, слегка отличающийся от показанного, — особенно сообщения об ошибках — но смысл будет понятен.
Когда все опции обработаны, программа просто выводит оставшиеся аргументы, как и раньше, но начиная с номера, хранящегося в переменной optind
.
Многие приложения Linux принимают более информативные аргументы, чем использованные в предыдущем примере односимвольные опции. Библиотека С проекта GNU содержит версию функции getopt
, названную getopt_long
, которая принимает так называемые длинные аргументы, которые вводятся с помощью двойного дефиса.
Рассмотрим упражнение 4.3.
Примените функцию getopt_long
для создания новой версии примера программы, которая может вызываться с использованием длинных эквивалентов опций, например, следующих:
$ ./longopt --initialize --list 'hi there' --file fred.c -q
option: i
option: l
filename: fred.c
./longopt: invalid option --q
unknown option: q
argument: hi there
На самом деле и новые длинные опции, и исходные односимвольные можно смешивать. Длинным опциям также можно давать сокращенные названия, но они
должны отличаться от односимвольных опций. Длинные опции с аргументом можно задавать как единый аргумент в виде --опция= значение, как показано далее:
$ ./longopt --init -l --file=fred.с 'hi there'
option: i
option: l
filename: fred.с
argument: hi there
Далее приведена новая программа longopt.c, полученная из программы argopt.c с изменениями, обеспечивающими поддержку длинных опций, которые в тексте программы выделены цветом.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define _GNU_SOURCE
#include <getopt.h>
int main(int argc, char *argv[]) {
int opt;
struct option_longopts[] = {
{"initialize", 0. NULL, 'i'},
{"file" 1, NULL, 'f'},
{"list", 0, NULL, 'l'},
{0, 0, 0, 0}};
while ((opt = getopt_long(argc, argv, ":if:lr, longopts, NULL)) != -1) {
switch(opt) {
case 'i':
case 'l':
case 'r':
printf("option: %c\n", opt);
break;
case 'f':
printf("filename: %s\n", optarg);
break;
case ':':
printf("option needs a value\n");
break;
case '?':
printf("unknown option: %c\n", optopt);
break;
}
}
for (; optind < argc; optind++)
printf("argument: %s\n", argv[optind]);
exit(0);
}
Как это работает
Функция getopt_long
принимает два дополнительных параметра по сравнению с функцией getopt
. Первый из них — массив структур, описывающий длинные опции и сообщающий функции getopt_long
способ их обработки. Второй дополнительный параметр — адрес переменной, которая может использоваться как вариант optind
, предназначенный для длинных опций; для каждой распознанной длинной опции ее номер в массиве длинных опций может быть записан в эту переменную. В данном примере вам не нужна эта информация, поэтому вы используете NULL
в качестве значения второго дополнительного параметра.
Массив длинных опций состоит из ряда структур типа struct option
, в каждой из которых описано требуемое поведение длинной опции. Массив должен заканчиваться структурой, содержащей все нули.
Структура длинной опции определена в заголовочном файле getopt.h и должна подключаться с помощью константы _GNU_SOURCE
, определенной для того, чтобы разрешить использование функции getopt_long
.
struct option {
const char *name;
int has_arg;
int *flag;
int val;
};
Элементы структуры описаны в табл. 4.1.
Таблица 4.1.
Параметр опции | Описание |
---|---|
name | Название длинной опции. Сокращения будут приниматься до тех пор, пока они не создадут путаницы при определении названий других опций |
has_arg | Принимает ли эта опция аргумент. Задайте 0 для опций без аргументов, 1 для опций, у которых должно быть значение, и 2 для опций с необязательным аргументом |
flag | Задайте NULL , чтобы getopt_long вернула при обнаружении данной опции значение, заданное в val . В противном случае getopt_long возвращает 0 и записывает значение val в переменную, на которую указывает flag |
val | Значение getopt_long для данной опции, предназначенное для возврата |
Для получения сведений о других опциях, связанных с расширениями функции getopt
в проекте GNU и родственных функциях, см. страницы интерактивного справочного руководства к функции getopt
.
Мы обсуждали переменные окружения в главе 2. Это переменные, которые могут использоваться для управления поведением сценариев командной оболочки и других программ. Вы также можете применять их для настройки пользовательской среды. Например, у каждого пользователя есть переменная окружения HOME
, определяющая его исходный каталог, стандартное место старта его или ее сеанса. Как вы видели, просмотреть переменные окружения можно из строки приглашения командной оболочки:
$ echo $НOМЕ
/home/neil
Вы также можете воспользоваться командой оболочки set
для получения списка всех переменных окружения.
В спецификации UNIX определено множество стандартных переменных окружения, применяемых для самых разных целей, включая тип терминала, имена редакторов, установленных по умолчанию, названия часовых поясов и т.д. Программа на языке С может получить доступ к переменным окружения с помощью функций putenv
и getenv
.
#include <stdlib.h>
char *getenv(const char *name);
int putenv(const char *string);
Окружение состоит из строк видаимя=значение
. Функция getenv
ищет в окружении строку с заданным именем и возвращает значение, ассоциированное с этим именем. Она вернет NULL
, если требуемая переменная не существует. Если переменная есть, но ее значение не задано, функция getenv
завершится успешно и вернет пустую строку, в которой первый байт равен NULL
. Строка, возвращаемая getenv
, хранится в статической памяти, принадлежащей функции, поэтому для ее дальнейшего использования вы должны скопировать эту строку в другую, поскольку она может быть перезаписана при последующих вызовах функции getenv
.
Функция putenv
принимает строку вида имя=значение
и добавляет ее в текущее окружение. Она даст сбой и вернет -1, если не сможет расширить окружение из-за нехватки свободной памяти. Когда это произойдет, переменной errno
будет присвоено значение ENOMEM
.
В упражнении 4.4 вы напишeте программу для вывода значения любой выбранной вами переменной окружения. У вас также будет возможность задать значение, если вы укажете второй аргумент программы.
1. Первые несколько строк после объявления функции main
гарантируют корректный вызов программы environ.c с только одним или двумя аргументами:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char *var, *value;
if (argc == 1 || argc > 3) {
fprintf(stderr, "usage: environ var [value]\n");
exit(1);
}
2. Сделав это, вы извлекаете значение переменной из окружения с помощью функции getenv
:
var = argv[1];
value = getenv(var);
if (value)
printf("Variable %s has value %s\n", var, value);
else
printf("Variable %s has no value\n", var);
3. Далее проверьте, был ли при вызове программы указан второй параметр. Если был, вы задаете значение этого аргумента, конструируя строку вида имя=значение
и затем вызывая функцию putenv
:
if (argc == 3) {
char *string;
value = argv[2];
string = malloc(strlen(var)+strlen(value)+2);
if (!string} {
fprintf(stderr, "out of memory\n");
exit(1);
}
strcpy(string, var);
strcat(string, "=");
strcat(string, value);
printf("Calling putenv with: %s\n", string);
if (putenv(string) != 0) {
fprintf(stderr, "putenv failed\n");
free(string);
exit(1);
}
4. В заключение вы узнаете новое значение переменной, вызвав функцию getenv еще раз:
value = getenv(var);
if (value)
printf("New value of %s is %s\n", var, value);
else
printf("New value of %s is null??\n", var);
}
exit(0);
}
Когда вы выполните эту программу, то сможете увидеть и задать переменные окружения:
$ ./environ НОМЕ
Variable HOME has value /home/neil
$ ./environ FRED
Variable FRED has no value
$ ./environ FRED hello
Variable FRED has no value
Calling putenv with: FRED=hello
New value of FRED is hello
$ ./environ FRED
Variable FRED has no value
Обратите внимание на то, что окружение локально по отношению к программе. Изменения, которые вы делаете в программе, не отражаются вне ее, поскольку значения переменных не передаются из дочернего процесса (вашей программы) в родительский (командную оболочку).
Программы часто применяют переменные окружения для изменения способа своей работы. Пользователи могут задать значения этих переменных окружения либо в их стандартном окружении с помощью файла .profile, читаемого их регистрационной командной оболочкой, использующей специальный файл запуска (rc) оболочки, либо заданием переменных в командной строке командной оболочки. Например,
$ ./environ FRED
Variable FRED has no value
$ FRED=hello ./environ FRED
Variable FRED has value hello
Командная оболочка принимает начальные присвоения значений переменным как временные изменения переменных окружения. Во второй части предыдущего примера программа environ
выполняется в окружении, где у переменной FRED
есть значение.
Например, в будущей версии приложения, управляющего базой данных компакт-дисков, вы сможете изменить переменную окружения, скажем CDDB
, обозначающую базу данных, которую нужно использовать. Каждый пользователь затем сможет задать собственное значение по умолчанию или применить команду оболочки для задания значения при очередном выполнении приложения:
$ CDDB=mycds; export CDDB
$ cdapp
или
$ CDDB=mycds cdapp
Переменные окружения — противоречивое благо, и их следует применять с осторожностью. Эти переменные более "закрыты" от пользователя, чем опции командной строки, и это может затруднить отладку. По смыслу переменные окружения подобны глобальным переменным, поэтому они могут изменять поведение программы, что порой приводит к неожиданным результатам.
Как вы уже знаете, окружение программы формируется из строк вида имя=значение. Этот массив строк становится доступен программе непосредственно из переменной environ, которая объявляется, как
#include <stdlib.h>
extern char **environ;
Выполните упражнение 4.5.
Далее приведена программа showenv.c, использующая переменную environ для вывода переменных окружения.
#include <stdlib.h>
#include <stdio.h>
extern char **environ;
int main() {
char **env = environ;
while (*env) {
printf("%s\n", *env);
env++;
}
exit(0);
}
Когда вы выполните программу в системе Linux, то получите нечто, похожее на следующий вывод, который немного сокращен. Количество, порядок отображения и значения этих переменных зависят от версии операционной системы, применяемой командной оболочки и настроек пользователя в момент выполнения программы.
$ ./showenv
HOSTNAME=tilde.provider.com
LOGNAME=neil
MAIL=/var/spool/mail/neil
TERM=xterm
HOSTTYPE=i386
PATH=/usr/local/bin:/bin:/usr/bin:
HOME=/usr/neil
LS_OPTIONS=-N --color=tty -T 0
SHELL=/bin/bash
OSTYPE=Linux
...
Как это работает
Для вывода всего окружения программа в цикле обращается к переменной environ
— массиву нуль-терминированных строк.
Программе часто полезно иметь возможность определить время и дату. Возможно, она хочет зарегистрировать длительность собственного выполнения или ей нужно изменять свое поведение в определенные моменты времени. Например, игра может отказываться запускаться в рабочие часы или программа резервного копирования по расписанию хочет дождаться ранних часов, прежде чем начать резервное копирование в автоматическом режиме.
Во всех системах UNIX применяется одна и та же точка отсчета времени и дат: полночь по Гринвичу (GMT) на 1 января 1970 г. Это "начало эпохи UNIX", и ОС Linux — не исключение. Время в системе Linux измеряется в секундах, начиная с этого момента времени. Такой способ обработки аналогичен принятому в системе MS-DOS за исключением того, что эпоха MS-DOS началась в 1980 г. В других системах применяют точки отсчета иных эпох.
Время задается с помощью типа time_t
. Это целочисленный тип, достаточный для хранения дат и времени в секундах. В Linux-подобных системах это тип long integer
(длинное целое), определенный вместе с функциями, предназначенными для обработки значений времени, в заголовочном файле time.h.
Не думайте, что для хранения времени достаточно 32 битов. В системах UNIX и Linux, использующих 32-разрядный тип time_t
, временное значение "будет превышено" в 2038 г. Мы надеемся, что к тому времени системы перейдут на тип time_t
, содержащий более 32 битов. Недавнее широкое внедрение 64-разрядных процессоров превращает это практически в неизбежность.
#include <time.h>
time_t time(time_t *tloc);
Вы можете найти низкоуровневое значение времени, вызвав функцию time
, которая вернет количество секунд с начала эпохи (упражнение 4.6). Она также запишет возвращаемое значение по адресу памяти, на который указывает параметр tloc
, если он — непустой указатель.
Далее для демонстрации функции time приведена простая программа envtime.c.
#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int i;
time_t the_time;
for (i = 1; i <= 10; i++) {
the_time = time((time_t *)0);
printf("The time is %ld\n", the_time);
sleep(2);
}
exit(0);
}
Когда вы запустите программу, она будет выводить низкоуровневое значение времени каждые 2 секунды в течение 20 секунд.
$ ./anytime
The time is 1179643852
The time is 1179643854
The time is 1179643856
The time is 1179643858
The time is 1179643860
The time is 1179643862
The time is 1179643864
The time is 1179643866
The time is 1179643868
The time is 1179643870
Как это работает
Программа вызывает функцию time
с пустым указателем в качестве аргумента, которая возвращает время и дату как количество секунд. Программа засыпает на две секунды и повторяет вызов time в целом 10 раз.
Использование времени и даты в виде количества секунд, прошедших с начала 1970 г., может быть полезно для измерения длительности чего-либо. Вы сможете сосчитать простую разность значений, полученных из двух вызовов функции time
. Однако комитет, разрабатывавший стандарт языка ISO/ANSI С, в своих решениях не указал, что тип time_t
будет применяться для определения произвольных интервалов времени в секундах, поэтому была придумана функция difftime
, которая вычисляет разность в секундах между двумя значениями типа time_t
и возвращает ее как величину типа double
:
#include <time.h>
double difftime(time_t time1, time_t time2);
Функция difftime
вычисляет разницу между двумя временными значениями и возвращает величину, эквивалентную выражениювремя1–время2
, как число с плавающей точкой. В ОС Linux значение, возвращаемое функцией time
, — это количество секунд, которое может обрабатываться, но для максимальной переносимости следует применять функцию difftime
.
Для представления времени и даты в более осмысленном (с человеческой точки зрения) виде мы должны преобразовать значение времени в понятные время и дату. Для этого существуют стандартные функции.
Функция gmtime
подразделяет низкоуровневое значение времени на структуру, содержащую более привычные поля:
#include <time.h>
struct tm *gmtime(const time_t timeval)
В структуре tm
, как минимум, определены элементы, перечисленные в табл. 4.2.
Таблица 4.2
Элемент tm | Описание |
---|---|
int tm_sec | Секунды, 0–61 |
int tm_min | Минуты, 0–59 |
int tm_hour | Часы, 0–23 |
int tm_mday | День в месяце, 1–31 |
int tm_mon | Месяц в году, 0–11 (January (январь) соответствует 0) |
int tm_year | Годы, начиная с 1900 г. |
int tm_wday | День недели, 0–6 (Sunday (воскресенье) соответствует 0) |
int tm_yday | День в году, 0–365 |
int tm_isdst | Действующее летнее время |
Диапазон элемента tm_sec
допускает появление время от времени корректировочной секунды или удвоенной корректировочной секунды.
Выполните упражнение 4.7.
Далее приведена программа gmtime.с, выводящая текущие время и дату с помощью структуры tm
и функции gmtime
.
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
struct tm *tm_ptr;
time_t the_time;
(void)time(&the_time);
tm_ptr = gmtime(&the_time);
printf("Raw time is %ld\n", the_time);
printf("gmtime gives:\n");
printf("date: %02d/%02d/%02d\n",
tm_ptr->tm_year, tm_ptr->tm_mon+1, tm_ptr->tm_mday);
printf("time: %02d:%02d:%02d\n",
tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec);
exit(0);
}
Выполнив эту программу, вы получите хорошее соответствие текущим времени и дате:
$ ./gmtime; date
Raw time is 1179644196
gmtime gives:
date: 107/05/20
time: 06:56:36
Sun May 20 07:56:37 BST 2007
Как это работает
Программа вызывает функцию time
для получения машинного представления значения времени и затем вызывает функцию gmtime для преобразования его в структуру с удобными для восприятия значениями времени и даты. Она выводит на экран полученные значения с помощью функции printf
. Строго говоря, выводить необработанное значение времени таким способом не следует, потому что наличие типа длинного целого не гарантировано во всех системах. Если сразу же после вызова функции gmtime выполнить команду date, можно сравнить оба вывода.
Но здесь у вас возникнет небольшая проблема. Если вы запустите эту программу в часовом поясе, отличном от Greenwich Mean Time (время по Гринвичу) или у вас действует летнее время, как у нас, вы заметите, что время (и, возможно, дата) неправильное. Все дело в том, что функция gmtime
возвращает время по Гринвичу (теперь называемое Universal Coordinated Time (всеобщее скоординированное время) или UTC). Системы Linux и UNIX поступают так для синхронизации всех программ и систем в мире. Файлы, созданные в один и тот же момент в разных часовых поясах, будут отображаться с одинаковым временем создания. Для того чтобы посмотреть местное время, следует применять функцию localtime
.
#include <time.h>
struct tm *localtime(const time_t *timeval);
Функция localtime
идентична функции gmtime
за исключением того, что она возвращает структуру, содержащую значения с поправками на местный часовой пояс и действующее летнее время. Если вы выполните программу gmtime
, но замените все вызовы функции gmtime
на вызовы localtime
, в отчете программы вы увидите правильные время и дату.
Для преобразования разделенной на элементы структуры tm в общее внутреннее значение времени можно применить функцию mktime
:
#include <time.h>
time_t mktime(struct tm *timeptr);
Функция mktime
вернет -1, если структура не может быть представлена как значение типа time_t
.
Для вывода программой date
"дружественных" (в противоположность машинному) времени и даты можно воспользоваться функциями asctime
и ctime
:
#include <time.h>
char *asctime(const struct tm *timeptr);
char *ctime(const time_t *timeval);
Функция asctime
возвращает строку, представляющую время и дату, заданные tm
-структурой timeptr
. У возвращаемой строки формат, подобный приведенному далее:
Sun Jun 9 12:34:56 2007\n\0
У нее всегда фиксированный формат длиной 26 символов. Функция ctime
эквивалентна следующему вызову:
asctime(localtime(timeval))
Она принимает необработанное машинное значение времени и преобразует его в местное время.
А теперь выполните упражнение 4.8.
В этом примере благодаря приведенному далее программному коду вы увидите функцию ctime
в действии.
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
time_t timeval;
(void)time(&timeval);
printf ("The date is: %s", ctime(&timeval));
exit(0);
}
Откомпилируйте и затем запустите на выполнение ctime.c, и вы увидите нечто похожее на приведенные далее строки:
$ ./ctime
The date is: Sat Jun 9 08:02:08 2007.
Как это работает
Программа ctime.c вызывает функцию time
для получения машинного значения времени и дает возможность функции ctime
выполнить всю тяжелую работу по преобразованию этого значения в удобочитаемую строку, которую потом и выводит на экран.
Для лучшего управления точным форматированием времени и даты ОС Linux и современные UNIX-подобные системы предоставляют функцию strftime
. Она довольно похожа на функцию sprintf
для дат и времени и действует аналогичным образом:
#include <time.h>
size_t strftime(char *s, size_t maxsize, const char *format, struct tm *timeptr);
Функция strftime
форматирует время и дату, представленные в структуре tm
, на которую указывает параметр, timeptr
, и помещает результат в строку s
. Эта строка задается длиной maxsize
(как минимум) символов. Строка format
применяется для управления символами, записываемыми в строку. Как и в функции printf
, она содержит обычные символы, которые будут переданы в строку, и спецификаторы преобразований для форматирования элементов времени и даты. В табл. 4.3 перечислены используемые спецификаторы преобразований.
Таблица 4.3
Спецификатор преобразования | Описание |
---|---|
%a | Сокращенное название дня недели |
%А | Полное название дня недели |
%b | Сокращенное название месяца |
%B | Полное название месяца |
%c | Дата и время |
%d | День месяца, 01–31 |
%H | Час, 00–23 |
%I | Час по 12-часовой шкале, 01–12 |
%j | День в году, 001–366 |
%m | Номер месяца в году, 01–12 |
%M | Минуты, 00–59 |
%p | a.m. (до полудня) или p.m. (после полудня) |
%S | Секунды, 00–59 |
%u | Номер дня недели, 1–7 (1 соответствует понедельнику) |
%U | Номер недели в году, 01–53 (воскресенье — первый день недели) |
%V | Номер недели в году, 01–53 (понедельник — первый день недели) |
%w | Номер дня недели, 0–6 (0 соответствует воскресенью) |
%x | Дата в региональном формате |
%X | Время в региональном формате |
%y | Номер года, меньший 1900 |
%Y | Год |
%Z | Название часового пояса |
%% | Символ % |
Таким образом, обычная дата, такая же, как полученная из программы date, соответствует следующей строке формата функции strftime
:
"%a %b %d %Н: %М: %S %Y"
Для облегчения чтения дат можно использовать функцию strptime
, принимающую строку с датой и временем и формирующую структуру tm
с теми же датой и временем:
#include <time.h>
char *strptime(const char *buf, const char *format, struct tm *timeptr);
Строка format
конструируется точно так же, как одноименная строка функции strftime
. Функций strptime
действует аналогично функции sscanf
: она сканирует строку в поиске опознаваемых полей и записывает их в переменные. В данном случае это элементы структуры tm
, которая заполняется в соответствии со строкой format
. Однако спецификаторы преобразований для strptime
немного мягче спецификаторов функции strftime
. Так, в функции strptime
разрешены как сокращенные, так и полные названия дней и месяцев. Любое из этих представлений будет соответствовать спецификатору %a функции strptime
. Кроме того, в то время как функция strftime
для представления чисел, меньших 10, всегда применяет ведущие нули, strptime
считает их необязательными.
Функция strptime
возвращает указатель на символ, следующий за последним, обработанным в процессе преобразования. Если она встречает символы, которые не могут быть преобразованы, в этой точке преобразование просто прекращается. Для того чтобы убедиться в том, что в структуру tm
записаны значимые данные, вызывающей программе следует проверять, достаточно ли символов строки принято и обработано.
Рассмотрим работу функций на примере (упражнение 4.9).
Обратите внимание на выбор спецификаторов преобразований, использованных в следующей программе:
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
struct tm *tm_ptr, timestruct;
time_t the_time;
char buf[256];
char *result;
(void)time(&the_time);
tm_ptr = localtime(&the_time);
strftime(buf, 256, "%A %d %B, %I:%S %p", tm_ptr);
printf("strftime gives: %s\n", buf);
strcpy(buf, "Thu 26 July 2007, 17:53 will do fine");
printf("calling strptime with: %s\n", buf);
tm_ptr = ×truct;
result = strptime(buf, "%a %d %b %Y, %R", tm_ptr);
printf("strptime consumed up to: %s\n", result);
printf("strptime gives:\n");
printf ("date: %02d/%02d/%02d\n",
tm_ptr->tm_year % 100, tm_ptr->tm_mon+1, tm_ptr->tm_mday);
printf("time: %02d:%02d\n",
tm_ptr->tm_hour, tm->ptr->tm_min);
exit(0);
}
Когда вы откомпилируете и выполните программу strftime.c, то получите следующий результат:
$ ./strftime
strftime gives: Saturday 09 June, 08:16 AM
calling strptime with: Thu 26 July 2007, 17:53 will do fine
strptime concurred up to: will do fine
strptime gives:
date: 07/07/26
time: 17:53
Как это работает
Программа strftime получает текущее местное время с помощью вызовов функций time
и localtime
. Затем она преобразует его в удобочитаемую форму с помощью функции strftime
с подходящим аргументом форматирования. Для демонстрации применения функции strptime
программа задает строку, содержащую дату и время, затем вызывает strptime для извлечения необработанных значений времени и даты и выводит их на экран. Спецификатор преобразования %R
функции strptime
— это сокращенное обозначение комбинации %Н:%M
.
Важно отметить, что для успешного просмотра даты функции strptime необходима точная строка формата. Обычно она не может точно обработать даты, считываемые из строк, введенных пользователями, до тех пор, пока не будет строго выверен формат.
Возможно, при компиляции программы strftime.c вы получите предупреждение компилятора. Причина в том, что по умолчанию в библиотеке GNU не объявлена функция strptime
. Для устранения проблемы следует явно запросить средства стандарта X/Open, добавив следующую строку перед заголовочным файлом time.h:
#define _XOPEN_SOURCE
Зачастую программы нуждаются в возможности использования временного хранилища в виде файлов. В них могут храниться промежуточные результаты вычислений, или резервные копии файлов, сделанные перед выполнением критических операций. Например, приложение для работы с базой данных может применять временный файл при удалении записей. В файле собираются элементы базы данных, нуждающиеся в сохранении, и затем в конце процесса временный файл становится новой базой данных, а исходная база данных удаляется.
У столь популярных временных файлов есть скрытый недостаток. Вы должны следить за тем, чтобы приложения выбирали уникальное имя для временного файла. Если это условие не соблюдается, могут возникнуть проблемы. Поскольку ОС Linux — многозадачная система, другая программа может выбрать то же самое имя, и обе будут мешать друг другу.
Уникальное имя файла генерируется с помощью функции tmpnam
:
#include <stdio.h>
char *tmpnam(char *s);
Функция tmpnam
возвращает допустимое имя файла, не совпадающее с именем любого из существующих файлов. Если строка s
не равна NULL
, в нее будет записано имя файла. Последующие вызовы функции tmpnam будут перезаписывать статическую память, используемую для возвращаемых значений, поэтому важно применять строковый параметр, если функция должна вызываться многократно. Длина строки полагается равной, как минимум, L_tmpnam
(обычно около 20) символам. Функция tmpnam
может вызываться в одной программе до TMP_MAX
(не менее нескольких тысяч) раз, и каждый раз она будет генерировать уникальное имя файла.
Если, временный файл предполагается использовать немедленно, вы можете одновременно назвать его и открыть с помощью функции tmpfile
. Это важно, т.к. другая программа может создать файл с именем таким же, как значение, возвращенное функцией tmpnam
. Функция tmpfile
устраняет эту проблему полностью.
#include <stdio.h>
FILE* tmpfile(void);
Функция tmpfile возвращает указатель потока, ссылающийся на уникальный временный файл. Файл открыт для чтения и записи (с помощью fopen
с флагом w+
) и будет автоматически удален, когда закроются все ссылки на него.
В случае возникновения ошибки tmpfile
вернет указатель NULL
и задаст значение переменной errno
.
Давайте посмотрим эти две функции в действии:
#include <stdio.h>
#include <stdlib.h>
int main() {
char tmpname[L_tmpnam];
char* filename;
FILE *tmpfp;
filename = tmpnam(tmpname);
printf("Temporary file name is: %s\n", filename);
tmpfp = tmpfile();
if (tmpfp) printf("Opened a temporary file OK\n");
else perror("tmpfile");
exit(0);
}
Когда вы откомпилируете и выполните программу tmpnam.с, то увидите уникальное имя файла, сгенерированное функцией tmpnam
:
$ ./tmpnam
Temporary file name is: /tmp/file2S64zc
Opened a temporary file OK
Как это работает
Программа вызывает функцию tmpnam
для генерации уникального имени временного файла. Если вы хотите его использовать, нужно быстро его открыть, чтобы минимизировать риск того, что другая программа откроет файл с тем же именем. Вызов функции tmpfile
одновременно создает и открывает временный файл, тем самым устраняя этот риск. При компиляции программы, использующей функцию tmpnam
, компилятор GNU С может вывести предупреждение о применении этой функции.
В некоторых версиях UNIX предлагается другой способ генерации имен временных файлов — с помощью функций mktemp
и mkstemp
. Они поддерживаются и ОС Linux и аналогичны функции tmpnam
за исключением того, что вы должны задать шаблон имени временного файла, который предоставляет некоторые дополнительные возможности управления местом хранения и именем файла.
#include <stdlib.h>
char *mktemp(char *template);
int mkstemp(char *template);
Функция mktemp создает уникальное имя файла на основе заданного шаблона template
. Аргумент template
должен быть строкой с шестью завершающими символами Х
. mktemp
заменяет эти символы Х
уникальной комбинацией символов, допустимых в именах файлов. Она возвращает указатель на сгенерированную строку или NULL
при невозможности сформировать уникальное имя.
Функция mkstemp
аналогична функции tmpfile
: она создает и открывает временный файл. Имя файла генерируется так же, как в функции mktemp
, но возвращенный результат — открытый низкоуровневый дескриптор файла.
В ваших собственных программах следует всегда применять функции "создать и открыть" tmpfile
и mkstemp
вместо функций tmpnam
и mktemp
.
Все программы в ОС Linux за исключением программы init, запускаются другими программами или пользователями. В главе 11 вы узнаете больше о взаимодействии выполняющихся программ или процессов. Пользователи чаще всего запускают программы из командной оболочки, реагирующей на их команды. Вы видели, что в программе можно определить собственное окружение, просматривая переменные окружения и читая системные часы. Программа может также выяснить данные о пользователе, применяющем ее.
Когда пользователь регистрируется в системе Linux, у него или у нее есть имя пользователя и пароль. После того как эти данные проверены, пользователю предоставляется командная оболочка. В системе у пользователя также есть уникальный идентификатор пользователя, называемый UID (user identifier). Каждая программа, выполняемая Linux, запускается от имени пользователя и имеет связанный с ней UID.
Вы можете настроить выполнение программ так, как будто они запускаются другим пользователем. Если у программы есть свой набор прав доступа для UID, она будет выполняться от имени владельца исполняемого файла. Когда выполнена команда su, программа действует так, как будто она запущена суперпользователем. Затем она проверяет право доступа пользователя, изменяет UID на идентификатор назначенной учетной записи и запускает регистрационную командную оболочку данной учетной записи. Этот прием позволяет программе выполняться от имени другого пользователя и часто используется системными администраторами для выполнения задач технического обслуживания системы.
Поскольку UID — это ключевой параметр для идентификации пользователя, начнем с него.
У UID есть свои тип uid_t
, определенный в файле sys/types.h. Обычно это короткое целое (small integer). Одни идентификаторы пользователя заранее определены системой, другие создаются системным администратором, когда новые пользователи становятся известны системе. Как правило, идентификаторы пользователей имеют значения, большие 100.
#include <sys/types.h>
#include <unistd.h>
uid_t getuid (void);
char *getlogin(void);
Функция getuid
возвращает UID, с которым связана программа. Обычно это UID пользователя, запустившего программу.
Функция getlogin
возвращает регистрационное имя, ассоциированное с текущим пользователем.
Системный файл /etc/passwd содержит базу данных, имеющую дело с учетными записями пользователей. Он состоит из строк по одной на каждого пользователя, в каждую строку включены имя пользователя, зашифрованный пароль, идентификатор пользователя (UID), идентификатор группы (GID), полное имя, исходный каталог и командная оболочка, запускаемая по умолчанию. Далее приведен пример такой строки:
neil:zBqxfqedfpk:500:100:Neil Matthew:/home/neil:/bin/bash
Если вы пишете программу, которая определяет UID пользователя, запустившего ее, то можете расширить ее возможности и заглянуть в файл passwd для выяснения регистрационного имени пользователя и его полного имени. Мы не рекомендуем делать это, потому что современные UNIX-подобные системы уходят от применения файлов учетных записей пользователей для повышения безопасности системы. Многие системы, включая Linux, имеют возможность использовать файлы теневых паролей (shadow password), совсем не содержащие пригодной информации о зашифрованных паролях (она часто хранится в файле /etc/shadow, которые обычные пользователи не могут читать). По этой причине определен ряд функций для предоставления эффективного программного интерфейса, позволяющего получать эту пользовательскую информацию.
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(const char *name);
Структура базы данных учетных записей пользователей passwd
определена в файле pwd.h и включает элементы, перечисленные в табл. 4.4.
Таблица 4.4
Элемент passwd | Описание |
---|---|
char *pw_name | Регистрационное имя пользователя |
uid_t pw_uid | Номер UID |
gid_t pw_gid | Номер GID |
char *pw_dir | Исходный каталог пользователя |
char *pw_gecos | Полное имя пользователя |
char *pw_shell | Командная оболочка пользователя, запускаемая по умолчанию |
В некоторых системах UNIX может использоваться другое имя для поля с полным именем пользователя: в одних системах это pw_gecos
, как в ОС Linux, в других — pw_comment
. Это означает, что мы не можем рекомендовать его использование. Обе функции (и getpwuid
, и getpwnam
) возвращают указатель на структуру passwd
, соответствующую пользователю. Пользователь идентифицируется по UID в функции getpwuid
и по регистрационному имени в функции getpwnam
. В случае ошибки обе функции вернут пустой указатель и установят переменную errno
.
Выполните упражнение 4.11.
В этом упражнении показана программа user.c, извлекающая некоторую информацию о пользователе из базы данных учетных записей.
#include <sys/types.h>
#include <pwd.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
uid_t uid;
gid_t gid;
struct passwd *pw;
uid = getuid();
gid = getgid();
printf("User is %s\n", getlogin());
printf("User IDs: uid=%d, gid=%d\n", uid, gid);
pw = getpwuid(uid);
printf(
"UID passwd entry:\n name=%s, uid=%d, gid=%d, home=%s, shell=%s\n",
pw->pw_name, pw->pw_uid, pw->pw_gid, pw->pw_dir, pw->pw_shell);
pw = getpwnam("root");
printf("root passwd entry:\n");
printf("name=%s, uid=%d, gid=%d, home=%s, shell=%s\n",
pw->pw_name, pw->pw_uid, pw->pw_gid, pw->pw_dir, pw->pw_shell);
exit(0);
}
Программа предоставит следующий вывод, который может слегка отличаться в разных версиях Linux и UNIX:
$ ./user
User is neil
User IDs: uid=1000, gid=100
UID passwd entry:
name=neil, uid=1000, gid=100, home=/home/neil, shell=/bin/bash
root passwd entry:
name=root, uid=0, gid=0, home=/root, shell=/bin/bash
Как это работает
Эта программа вызывает функцию getuid
для получения UID текущего пользователя, Этот UID применяется в функции getpwuid
для получения подробной информации из файла учетных записей пользователей. В качестве альтернативы мы показываем, как для извлечения информации о пользователе можно задать в функции getpwnam
имя пользователя root
.
В исходном коде Linux вы сможете найти в команде id
еще один пример-использования функции getuid
.
Для просмотра всех данных файла учетных записей пользователей можно воспользоваться функцией getpwent
. Она последовательно выбирает строки файла.
#include <pwd.h>
#include <sys/types.h>
void endpwent(void);
struct passwd *getpwent(void);
void setpwent(void);
Функция getpwent
возвращает поочередно информацию о каждом пользователе. Когда не остается ни одного, она возвращает пустой указатель. Для прекращения обработки файла, когда просмотрено достаточно элементов, вы можете применить функцию endpwent
. Функция setpwent
переустанавливает позицию указателя в файле учетных записей пользователей для начала нового просмотра при следующем вызове функции getpwent
. Эти функции действуют так же, как функции просмотра каталога opendir
, readdir
и closedir
, обсуждавшиеся в главе 3.
Идентификаторы пользователя и группы (эффективный или действующий и реальный) можно получить с помощью других реже используемых функций:
#include <sys/types.h>
#include <unistd.h>
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);
int setuid(uid_t uid);
int setgid(gid_t gid);
Подробную информацию об идентификаторах группы и эффективных идентификаторах пользователей следует искать на страницах интерактивного справочного руководства системы, хотя, быть может, вы решите, что вам вообще не следует манипулировать ими.
Только суперпользователь может вызывать функции setuid
и setgid
.
Программа может установить некоторые подробные сведения о компьютере, на котором выполняется, так же, как она определяет информацию о пользователе. Подобную информацию предоставляет команда uname
, в программе на языке С можно использовать для получения этих данных одноименный системный вызов — прочтите о нем в разделе системных вызовов интерактивного справочного руководства (раздел 2) с помощью команды man 2 uname
.
Сведения о рабочем компьютере могут оказаться полезными в ряде ситуаций. Вы можете захотеть настроить поведение программы в зависимости от сетевого имени машины, на которой она выполняется, скажем, на студенческом компьютере или машине администратора. Для соблюдения лицензионных соглашений вам может потребоваться ограничить выполнение программы одной машиной. Все это означает, что вам нужен способ определения компьютера, на котором выполняется программа.
Если в системе установлены сетевые компоненты, вы очень легко можете получить сетевое имя компьютера с помощью функции gethostname
:
#include <unistd.h>
int gethostname(char *name, size_t namelen);
Эта функция записывает сетевое имя машины в строку name
. Предполагается, что длина строки, как минимум, namelen
символов. Функция gethostname
возвращает 0 в случае успешного завершения и -1 в противном случае.
Более подробную информацию о рабочем компьютере можно получить с помощью системного вызова uname
.
#include <sys/utsname.h>
int uname(struct utsname *name);
Функция uname записывает информацию о компьютере в структуру, на которую указывает параметр name. Структура типа utsname, определенная в файле sys/utsname.h, обязательно должна включать элементы, перечисленные в табл. 4.5.
Таблица 4.5
Элемент структуры utsname | Описание |
---|---|
char sysname[] | Имя операционной системы |
char nodename[] | Имя компьютера |
char release[] | Номер выпуска (релиза) системы |
char version[] | Номер версии системы |
char machine[] | Аппаратный тип |
В случае успешного завершения функция uname
возвращает неотрицательное целое и в противном случае с установленной переменной errno
для обозначения любой возникшей ошибки.
Выполните упражнение 4.12.
Далее приведена программа hostget.c, извлекающая некоторые сведения о рабочем компьютере.
#include <sys/utsname.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
char computer[256];
struct utsname uts;
if (gethostname(computer, 255) != 0 || uname(&uts) < 0) {
fprintf(stderr, "Could not get host information\n");
exit(1);
}
printf("Computer host name is %s\n", computer);
printf("System is %s on %s hardware\n", uts.sysname, uts.machine);
printf("Nodename is %s\n", uts.nodename);
printf("Version is %s, %s\n", uts.release, uts.version);
exit(0);
}
Она отобразит следующие зависящие от ОС Linux данные. Если ваша машина включена в сеть, то вы увидите расширенное имя компьютера, включающее обозначение сети:
$ ./hostget
Computer host name is suse103
System is Linux on i686 hardware
Nodename is suse103
Version is 2.6.20.2-2-default, #1 SMP Fri Mar 9 21:54:10 UTC 2007
Как это работает
Эта программа вызывает функцию gethostname
для получения имени рабочего компьютера. В приведенном примере это имя — suse103
. Более подробную информацию об этом компьютере на базе Intel Pentium 4 с ОС Linux возвращает системный вызов uname
. Учтите, что формат возвращаемых строк зависит от реализации, например, строка с версией системы содержит дату компиляции ядра.
Другой пример применения функции uname
вы можете найти в исходном коде Linux для команды uname
, которая использует эту функцию.
Уникальный идентификатор каждого рабочего компьютера можно получить с помощью функции gethostid
.
#include <unistd.h>
long gethostid(void);
Функция gethostid
предназначена для возврата уникального значения, характеризующего рабочий компьютер. Менеджеры, следящие за соблюдением лицензионных соглашений, применяют ее для того, чтобы обеспечить функционирование программного обеспечения только на машинах с действующими лицензиями. На рабочих станциях Sun она возвращает номер, установленный в постоянной памяти во время изготовления компьютера и, таким образом, уникальный для системного оборудования. Другие системы, например Linux, возвращают значение на базе интернет-адреса машины, обычно не слишком безопасного для проверки лицензионных прав.
Многие приложения нуждаются в регистрации своей деятельности. Системные программы очень часто выводят сообщения на консоль или записывают их в регистрационный системный журнал. В этих сообщениях могут регистрироваться ошибки, предупреждения или более общая информация о состоянии системы. Например, программа su может зафиксировать тот факт, что пользователь пытался получить привилегии супер пользователя и потерпел неудачу.
Очень часто зарегистрированные сообщения записываются в системные файлы в каталоге, предоставляемом для этой цели. Это может быть каталог /usr/admor/var/log. При типичной установке ОС Linux все системные сообщения содержатся в файле /var/log/messages, в файл /var/log/mail включены другие регистрируемые сообщения от почтовой системы, а в файле /var/log/debug могут храниться отладочные сообщения. Проверить конфигурацию своей системы можно в файле /etc/syslog.conf или /etc/syslog-ng/syslog-ng.conf в зависимости от версии Linux.
Далее приведены некоторые примеры зарегистрированных сообщений.
Mar 2 6 18:25:51 suse103 ifstatus: eth0 device: Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] (rev 10)
Mar 26 18:25:51 suse103 ifstatus: eth0 configuration: eth-id-00:0c:29:0e:91:72
...
May 20 06:56:56 suse103 SuSEfirewall2: Setting up rules from /etc/sysconfig/SuSEfirewall2
...
May 20 06:56:57 suse103 SuSEfirewall2: batch committing
...
May 20 06:56:57 suse103 SuSEfirewall2: Firewall rules successfully set
...
Jun 9 09:11:14 suse103 su: (to root) neil on /dev/pts/18 09:50:35
В этом выводе показаны виды регистрируемых сообщений. Несколько первых отправлены непосредственно ядром Linux во время его загрузки и обнаружения установленного оборудования. Брандмауэр сообщает о своей перенастройке. И наконец, программа su
извещает о том, что доступ с учетной записью суперпользователя получен пользователем neil.
Для просмотра регистрируемых сообщений вы можете запросить права суперпользователя.
Некоторые системы UNIX не предоставляют файлов с удобными для чтения сообщениями, но они снабжают администраторов средствами для чтения базы данных системных событий. См. подробности в системной документации.
Несмотря на то, что формат и хранение системных сообщений могут отличаться, метод формирования сообщений стандартный. В спецификации UNIX представлен доступный всем программам интерфейс формирования регистрируемых сообщений с помощью функции syslog
.
#include <syslog.h>
void syslog(int priority, const char *message, arguments...);
Функция syslog посылает регистрируемое сообщение средству ведения системного журнала (logging facility). У каждого сообщения есть аргумент priority
, полученный поразрядной операцией OR
из степени важности сообщения (severity level) и типа программы, формирующей сообщение (facility value). Степень важности определяет необходимые действия, а тип программы фиксирует инициатора сообщения.
Типы программ (из файла syslog.h) включают константу LOG_USER
, применяемую для обозначения сообщения, пришедшего из приложения пользователя (по умолчанию), и константы LOG_LOCAL0
, LOG_LOCAL1
, ..., LOG_LOCAL7
, зарезервированные для локального администратора.
В табл. 4.6 перечислены степени важности сообщений в порядке убывания приоритета.
Таблица 4.6
Приоритет | Описание |
---|---|
LOG_EMERG | Кризисная ситуация |
LOG_ALERT | Проблема с высоким приоритетом, например, повреждение базы данных |
LOG_CRIT | Критическая ошибка, например, повреждение оборудования |
LOG_ERR | Ошибки |
LOG_WARNING | Предупреждение |
LOG_NOTICE | Особые обстоятельства, требующие повышенного внимания |
LOG_INFO | Информационные сообщения |
LOG_DEBUG | Отладочные сообщения |
В зависимости от настройки системы сообщения типа LOG_EMER
могут пересылаться всем пользователям системы, сообщения LOG_ALERT
могут отправляться по электронной почте администратору, сообщения LOG_DEBUG
могут игнорироваться, а сообщения других типов могут записываться в файл. Вы можете написать программу, которая применяет средство регистрации сообщений, просто вызывая функцию syslog
, когда вы хотите создать регистрируемое сообщение.
У сообщения, создаваемого syslog
, есть заголовок и тело сообщения. Заголовок создается из индикатора типа программы, формирующей сообщение, и даты и времени. Тело сообщения создается из параметра message
, передаваемого функции syslog
, который действует как строка format
функции printf
. Остальные аргументы syslog
используются в соответствии со спецификаторами преобразований в стиле функции printf
, заданными в строке message
. Дополнительно может применяться спецификатор %m
для включения строки сообщения об ошибке, ассоциированной с текущим значением переменной errno
. Эта возможность может оказаться полезной для регистрации сообщений об ошибках.
Выполните упражнение 4.13.
В этой программе осуществляется попытка открыть несуществующий файл.
#include <syslog.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *f;
f = fopen("not_here", "r");
if (!f) syslog(LOG_ERR|LOG_USER, "oops - %m\n");
exit(0);
}
Когда вы откомпилируете и выполните программу syslog.с, то не увидите никакого вывода, но в конце файла /var/log/messages теперь содержится следующая строка:
Jun 9 09:24:50 suse103 syslog: oops — No such file or directory
Как это работает
В данной программе вы пытаетесь открыть файл, которого нет. Когда попытка заканчивается неудачно, вы вызываете функцию syslog
для записи случившегося в системный журнал.
Обратите внимание на то, что регистрируемое сообщение не указывает, какая программа вызвала средство регистрации; оно просто констатирует тот факт, что была вызвана функция syslog
с сообщением. Спецификатор преобразования %m
был заменен описанием ошибки, в данном случае сообщающим об отсутствии файла. Это гораздо полезнее, чем простой отчет, содержащий внутренний номер ошибки.
В файле syslog.h определены и другие функции, применяемые для изменения поведения средств ведения системных журналов.
К ним относятся следующие функции:
#include <syslog.h> void closelog(void);
void openlog(const char *ident, int logopt, int facility);
int setlogmask(int maskpri);
Вы можете изменить способ представления ваших регистрируемых сообщений, вызвав функцию openlog
. Это позволит задать строку ident
, которая будет добавляться к вашим регистрируемым сообщениям. Вы можете применять ее для индикации программы, создавшей сообщение. Параметр facility
записывает текущий принятый по умолчанию тип программы, формирующей сообщение, который будет использоваться в последующих вызовах syslog
. По умолчанию устанавливается значение LOG_USER
. Параметр logopt
настраивает поведение будущих вызовов функции syslog
. Он представляет собой результат поразрядной операции OR
нулевого или большего числа параметров, приведенных в табл. 4.7.
Таблица 4.7
Параметр logopt | Описание |
---|---|
LOG_PID | Включает в сообщения идентификатор процесса, уникальный номер, выделяемый системой каждому процессу |
LOG_CONS | Посылает сообщения на консоль, если они не могут быть записаны |
LOG_ODELAY | Открывает средство регистрации сообщений при первом вызове функции syslog |
LOG_NDELAY | Открывает средство регистрации сообщений немедленно, не дожидаясь первого регистрируемого сообщения |
Функция openlog
выделит и откроет дескриптор файла, который будет применяться для записи в программе ведения системного журнала. Вы сможете закрыть его, вызвав функцию closelog
. Имейте в виду, что вам не нужно вызывать функцию openlog
перед вызовом syslog
, потому что последняя при необходимости самостоятельно откроет средство ведения системного журнала.
Вы можете управлять приоритетом регистрируемых вами сообщений с помощью установки маски регистрации, используя функцию setlogmask
. Все будущие вызовы syslog
с разными приоритетами, не заданными в маске регистрации, будут отброшены, таким образом, вы сможете, например, использовать маску для отключения сообщений типа LOG_DEBUG
без необходимости изменять тело программы.
Вы можете создать маску для регистрируемых сообщений, используя значение LOG_MASK(priority)
, создающее маску только для одного приоритета, или значение LOG_UPTO(priority)
, создающее маску для всех приоритетов вплоть до заданного.
Выполните упражнение 4.14.
В этом примере вы увидите logmask
в действии.
#include <syslog.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int logmask;
openlog("logmask", LOG_PID|LOG_CONS, LOG_USER);
syslog(LOG_INFO, "informative message, pid = %d", getpid());
syslog(LOG_DEBUG, "debug message, should appear");
logmask = setlogmask(LOG_UPTO(LOG_NOTICE));
syslog(LOG_DEBUG, "debug message, should not appear");
exit(0);
}
Программа logmask.c ничего не выводит, но в типичной системе Linux вы увидите в файле /var/log/messages, ближе к концу, следующую строку:
Jun 9 09:28:52 suse103 logmask[19339] : informative message, pid = 19339
Файл, настроенный на получение регистрируемых сообщений об отладке (в зависимости от настройки регистрации, это чаще всего файл /var/log/debug или иногда файл /var/log/messages), должен содержать следующую строку:
Jun 9 09:28:52 susel03 logmask[19339]: debug message, should appear
Как это работает
Программа инициализирует средство ведения системного журнала, названное logmask, и запрашивает включение идентификатора процесса в регистрируемые сообщения. Информирующее сообщение записывается в файл /var/log/messages, а отладочное сообщение — в файл /var/log/debug. Второе отладочное сообщение не появляется, потому что вы вызвали функцию setlogmask
с игнорированием всех сообщений с приоритетом ниже LOG_NOTICE
. (Учтите, что этот метод не работает в ранних вариантах ядра Linux.)
Если в установленную у вас систему не включена регистрация отладочных сообщений или она настроена иначе, отладочные сообщения могут не появляться. Для разблокирования всех отладочных сообщений и для получения подробностей настройки см. системную документацию, посвященную функции syslog
или syslog-ng
.
Программа logmask.c также использует функцию getpid
, которая, наряду с тесно связанной с ней функцией getppid
, определена следующим образом:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);pid_t getppid(void);
Функции возвращают идентификаторы вызвавшего и родительского процессов. Дополнительную информацию об идентификаторах процессов (PID) см. в главе 11.
Программы, выполняющиеся в системе 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 <sys/resource.h>
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, имитирующая типичное приложение. Она также задает и нарушает ограничения ресурсов.
1. Включите заголовочные файлы для всех функций, которые вы собираетесь применять в данной программе:
#include <sys/types.h> \
#include <sys/resource.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
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 output\n");
if (ferror(f)) {
fprintf(stderr, "Error writing to temporary file\n");
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.%06ld\n",
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 = %d\n", priority);
getrlimit(RLIMIT_FSIZE, &r_limit);
printf("Current FSIZE limit: soft = %ld, hard = %ld\n",
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 limit\n");
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, как правило, должны совместно использовать дорогостоящие ресурсы, поэтому в данной главе рассматриваются способы определения имеющихся ресурсов и управления ими.