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

Электронная библиотека книг » Андрей Робачевский » Операционная система UNIX » Текст книги (страница 10)
Операционная система UNIX
  • Текст добавлен: 6 октября 2016, 02:43

Текст книги "Операционная система UNIX"


Автор книги: Андрей Робачевский


Жанр:

   

ОС и Сети


сообщить о нарушении

Текущая страница: 10 (всего у книги 39 страниц)

Формат COFF

На рис. 2.5 приведена структура исполняемого файла формата COFF. Исполняемый файл содержит два основных заголовка – заголовок COFF и стандартный заголовок системы UNIX – a.out. Далее следуют заголовки разделов и сами разделы файла, в которых хранятся инструкции и данные программы. Наконец, в файле также хранится символьная информация, необходимая для отладки.

Рис. 2.5. Структура исполняемого файла в формате COFF

В файле находятся только инициализированные данные. Поскольку неинициализированные данные всегда заполняются нулями при загрузке программы на выполнение, для них необходимо хранить только размер и расположение в памяти.

Символьная информация состоит из таблицы символов (symbol table) и таблицы строк (string table). В первой таблице хранятся символы, их адреса и типы. Например, мы можем определить, что символ locptr является указателем и его виртуальный адрес равен 0x7feh0. Далее, используя этот адрес, мы можем выяснить значение символа для выполняющегося процесса. Записи таблицы символов имеют фиксированный размер. Если длина символа превышает восемь знаков, его имя хранится во второй таблице – таблице строк. Обычно обе эти таблицы присутствуют в объектных и исполняемых файлах, если они явно не удалены, например, командой strip(1).

Как и в случае ELF-файла, заголовок содержит общую информацию, позволяющую определить местоположение остальных компонентов (табл. 2.5).

Таблица 2.5. Поля заголовка COFF-файла


f_magic Аппаратная платформа, для которой создан файл
f_nscns Количество разделов в файле
f_timdat Время и дата создания файла
f_symptr Расположение таблицы символов в файле
f_nsyms Количество записей в таблице символов
f_opthdr Размер заголовка
f_flags Флаги, указывающие на тип файла, присутствие символьной информации и т.д.

Заголовок COFF присутствует в исполняемых файлах, промежуточных объектных файлах и библиотечных архивах. Каждый исполняемый файл кроме заголовка COFF содержит заголовок a.out, хранящий информацию, необходимую ядру системы для запуска программы[16]16
  В SCO UNIX заголовок a.out самого ядра используется программой начальной загрузки /boot для запуска ядра и передачи ему управления при инициализации системы.


[Закрыть]
(табл. 2.6).

Таблица 2.6. Поля заголовка a.out


vstamp Номер версии заголовка
tsize Размер раздела инструкций (text)
dsize Размер инициализированных данных (data)
bsize Размер неинициализированных данных (bss)
entry Точка входа программы
text_start Адрес в начала сегмента инструкций виртуальной памяти
data_start Адрес в начала сегмента данных виртуальной памяти

Все файлы формата COFF имеют один или более разделов, каждый из которых описывается своим заголовком. В заголовке хранится имя раздела (.text, .data, .bss или любое другое, установленное соответствующей директивой ассемблера), размер раздела, его расположение в файле и виртуальной адрес после запуска программы на выполнение. Заголовки разделов следуют сразу за заголовком файла.

Таблицы символов и строк являются основой системы отладки. Символом является любая переменная, имя функции или метка, определенные в программе.

Каждая запись в таблице символов хранит имя символа, его виртуальный адрес, номер раздела, в котором определен символ, тип, класс хранения (автоматический, регистровый и т.д.). Если имя символа занимает больше восьми байт, то оно хранится в таблице строк. В этом случае в поле имени символа указывается смещение имени символа в таблице строк.

С помощью символьной информации можно определить виртуальный адрес некоторого символа. Одним из очевидных применений этой возможности является использование символьной информации в программах– отладчиках. Эта возможность используется некоторыми программами, например, утилитой ps(1), отображающей состояние процессов в системе.

Выполнение программы в операционной системе UNIX

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

Запуск C-программы

Функция main() является первой функцией, определенной пользователем (т. е. явно описанной в исходном тексте программы), которой будет передано управление после создания соответствующего окружения запускаемой на выполнение программы. Традиционно функция main() определяется следующим образом:

main(int argc, char *argv[], char *envp[]);

Первый аргумент (argc) определяет число параметров, переданных программе, включая ее имя.

Указатели на каждый из параметров передаются в массиве argv[], таким образом, через argv[0] адресуется строка, содержащая имя программы, argv[1] указывает на первый параметр и т.д.. до argv[argc-1].

Массив envp[] содержит указатели на переменные окружения, передаваемые программе. Каждая переменная представляет собой строку вида имя_переменной=значение_переменной. Мы уже познакомились с переменными окружения в главе 1, когда обсуждали командный интерпретатор. Сейчас же мы остановимся на их программной «анатомии».

Стандарт ANSI С определяет только два первых аргумента функции main()argc и argv. Стандарт POSIX.1 определяет также аргумент envp, хотя рекомендует передачу окружения программы производить через глобальную переменную environ, как это показано на рис. 2.6:

extern char *environ;

Рекомендуется следовать последнему формату передачи для лучшей переносимости программ на другие платформы UNIX.

Рис. 2.6. Передача переменных окружения

Приведем пример программы, соответствующую стандарту POSIX.1, которая выводит значения всех аргументов, переданных функции main(): число переданных параметров, сами параметры и значения первых десяти переменных окружения.

#include

extern char **environ;

main(int argc, char *argv[]) {

 int i;

 printf(«число параметров, переданных программе %s равно %dn»,

  argv[0], argc-1);

 for (i=1; i

  if (environ[i] != NULL)

   printf(«environ[%d] : %sn», i, environ[i]);

}

В результате компиляции будет создан исполняемый файл программы (по умолчанию a.out). Запустив его, мы увидим следующую информацию:

$ a.out first second 3

число параметров, переданных программе a.out равно 3

argv[1] = first

argv[2] = second

argv[3] = 3

environ[0] : LOGNAME=andy

environ[1] : MAIL=/var/mail/andy

environ[2] : LD_LIBRARY_PATH=/usr/openwin/lib:/usr/ucblib

environ[3] : PAGER=/usr/bin/pg

environ[4] : TERM=vt100

environ[5] : PATH=/usr/bin:/bin:/etc:/usr/sbin:/sbin:/usr/ccs/bin:/usr/local/bin

environ[6] : HOME=/home/andy

environ[7] : SHELL=/usr/local/bin/bash

Максимальный объем памяти для хранения параметров и переменных окружения программы ограничен величиной ARG_MAX, определенной в файле . Это и другие системные ограничения могут быть получены с помощью функции sysconf(2).

Для получения и установки значений конкретных переменных окружения используются две функции: getenv(3C) и putenv(3C):

#include

char *getenv(const char *name);

возвращает значение переменной окружения name, a

int putenv(const char *string);

помещает переменную и ее значение (var_name=var_value) в окружение программы.

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

#include

#include

#include

main(int argc, char *argv[]) {

 char *term;

 char buf[200], var[200];

 /* Проверим, определена ли переменная TERM */

 if ((term = getenv(«TERM»)) == NULL)

  /* Если переменная не определена, получим от пользователя ее значение и

     поместим переменную в окружение программы */

 {

  printf("переменная TERM не определена, введите значение: ");

  putenv(var);

 } else

  /* Если переменная TERM определена, предоставим пользователю возможность

     изменить ее значение, после чего поместим ее в окружение процесса */

 {

  printf(«TERM=%s. Change? [N]», getenv(«TERM»));

  gets(buf);

  if (buf[0] == 'Y' || buf[0] == 'y') {

   printf(«TERM=»);

   gets{buf);

   sprintf(var, «TERM=%s», buf);

   putenv(var);

   printf(«new %sn», var);

  }

 }

}

Сначала программа проверяет, определена ли переменная TERM. Если переменная TERM не определена, пользователю предлагается ввести ее значение. Если же переменная TERM определена, пользователю предлагается изменить ее значение, после чего новое значение помещается в окружение программы.

Запуск этой программы приведет к следующим результатам:

$ а.out

TERM=ansi. Change? [N]y

TERM=vt100

new TERM=vt100

$

К сожалению, введенное значение переменной будет действительно только для данного процесса и порожденных им процессов: если после завершения программы a.out вывести значение TERM, то видно, что оно не изменилось:

$ echo $TERM

ansi

$

Наследование окружения программы мы обсудим в разделе "Создание и управление процессами" далее в этой главе.

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

Обычно при запуске программы на выполнение из командной строки shell автоматически устанавливает для нее три стандартных потока ввода/вывода: для ввода данных, для вывода информации и для вывода сообщений об ошибках. Начальную ассоциацию этих потоков (их файловых дескрипторов) с конкретными устройствами производит терминальный сервер (в большинстве систем это процесс getty(1M)), который открывает специальный файл устройства, связанный с терминалом пользователя, и получает соответствующие дескрипторы. Эти потоки наследует командный интерпретатор shell и передает их запускаемой программе. При этом shell может изменить стандартные направления (по умолчанию все три потока связаны с терминалом пользователя), если пользователь указал на это с помощью специальных директив перенаправления потока (>, <, >>, <<) см. главу 1, раздел «Пользовательская среда UNIX»). Раздел «Группы и сеансы» внесет окончательную ясность в этот вопрос при описании управляющего терминала.

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

Завершая разговор о запуске программ, заметим, что при компиляции программы редактор связей устанавливает точку входа в программу, указывающую на библиотечную функцию _start(). Эта функция инициализирует процесс, создавая кадр стека, устанавливая значения переменных и, в конечном итоге, вызывая функцию main().

Завершение C-программы

Существует несколько способов завершения программы. Основными являются возврат из функции main()[17]17
  Начальная функция запуска программы на выполнение _start() написана таким образом, что exit(2) вызывается автоматически при возврате из функции main(). В языке С она имеет следующий вид: exit(main(argc, argv)).


[Закрыть]
и вызов функций exit(2), оба приводят к завершению выполнения задачи. Заметим, что процесс может завершиться по не зависящим от него обстоятельствам, например, при получении сигнала, действие по умолчанию для большинства из которых приводит к завершению выполнения процесса[18]18
  В английском языке такое завершение выполнения называется более откровенно – «убийство процесса».


[Закрыть]
(см. раздел «Сигналы» далее в этой главе). В этом случае функция exit(2) будет вызвана ядром от имени процесса.

Системный вызов exit(2) выглядит следующим образом:

#include

void exit(int status);

Аргумент status, передаваемый функции exit(2), возвращается родительскому процессу и представляет собой код возврата программы. По соглашению программа возвращает 0 в случае успеха и другую величину в случае неудачи. Значение кода неудачи может иметь дополнительную трактовку, определяемую самой программой. Например, программа grep(1), выполняющая поиск заданных подстрок в файлах, определяет следующие коды возврата:


0совпадение было найдено
1совпадений найдено не было
2синтаксическая ошибка или недоступны файлы поиска

Наличие кода возврата позволяет программам взаимодействовать друг с другом. Например, следующая программа (назовем ее fail) может являться условием неудачи и использоваться в соответствующих синтаксических конструкциях shell:

main() {

 exit(1);

}

$ fail

$ echo $?           Выведем код возврата программы fail

1

$ fail || echo fail Конструкция shell, использующая условие неудачи fail

fail

Помимо передачи кода возврата, функция exit(2) производит ряд действий, в частности выводит буферизованные данные и закрывает потоки ввода/вывода. Альтернативой ей является функция _exit(2), которая не производит вызовов библиотеки ввода/вывода, а сразу вызывает системную функцию завершения ядра. Более подробно о процедурах завершения процесса см. раздел «Создание и управление процессами».

Задача может зарегистрировать обработчики выхода (exit handler), – функции, которые вызываются после вызова exit(2), но до окончательного завершения процесса. Эти обработчики, вызываемые по принципу LIFO (последний зарегистрированный обработчик будет вызван первым), запускаются только при «добровольном» завершении процесса. Например, при получении процессом сигнала обработчики выхода вызываться не будут. Для обработки таких ситуаций следует использовать специальные функции – обработчики сигналов (см. раздел «Сигналы» далее в этой главе).

Обработчики выхода регистрируются с помощью функции atexit(3C):

#include

int atexit(void(*func)(void));

Функцией atexit(1) может быть зарегистрировано до 32 обработчиков.

На рис. 2.7 проиллюстрированы возможные варианты запуска и завершения программы, написанной на языке С.

Рис. 2.7. Запуск и завершение C-программы

Работа с файлами

В среде программирования UNIX существуют два основных интерфейса для файлового ввода/вывода:

1. Интерфейс системных вызовов, предлагающий системные функции низкого уровня, непосредственно взаимодействующие с ядром операционной системы.

2. Стандартная библиотека ввода/вывода, предлагающая функции буферизованного ввода/вывода.

Второй интерфейс является "надстройкой" над интерфейсом системных вызовов, предлагающей более удобный способ работы с файлами.

В следующих разделах будут рассмотрены:

□ оба интерфейса, и особенно первый, поскольку именно он представляет набор базовых услуг ядра;

□ программный интерфейс управления жесткими и символическими связями файла;

□ функции изменения владельцев файла и прав доступа;

□ метаданные файла;

□ пример программы, выводящей на экран наиболее существенную информацию о файле, подобно тому, как это делает утилита ls(1).

Основные системные функции для работы с файлами

В табл. 2.7 приведены основные системные функции работы с файлами, являющиеся образами системных вызовов в программе С.

Функции более высокого уровня, предлагаемые стандартной библиотекой ввода/вывода, которые в конечном счете используют описанные здесь системные вызовы, рассматриваются в следующем разделе.

Таблица 2.7. Основные системные функции работы с файлами


open(2) Служит для получения доступа на чтение и/или запись к указанному файлу. Если файл существует, он открывается, и процессу возвращается файловый дескриптор, адресующий дальнейшие операции с файлом. Если файл не существует, он может быть создан
creat(2) Служит для создания файла
close(2) Закрывает файловый дескриптор, связанный с предварительно открытым файлом
dup(2) Возвращает дубликат файлового дескриптора
dup2(2) Возвращает дубликат файлового дескриптора, но позволяет явно указать его значение
lseek(2) Устанавливает файловый указатель на определенное место файла. Дальнейшие операции чтения/записи будут производиться, начиная с этого смещения
read (2) Производит чтение указанного количества байтов из файла
readv(2) Производит несколько операций чтения указанного количества байтов из файла
write(2) Производит запись указанного количества байтов в файл
writev(2) Производит несколько операций записи указанного количества байтов в файл
pipe(2) Создает коммуникационный канал, возвращая два файловых дескриптора
fcntl(2) Обеспечивает интерфейс управления открытым файлом

Кратко рассмотрим каждую из этих функций.

Функция open(2)

Открывает указанный файл для чтения или записи и имеет следующий вид:

#include

int open(const char *path, int oflag, mode_t mode);

Первый аргумент (path) является указателем на имя файла. Это имя может быть как абсолютным (начинающимся с корневого каталога /), так и относительным (указанным относительно текущего каталога). Аргумент oflag указывает на режим открытия файла и представляет собой побитное объединение флагов, приведенных в табл. 2.8, с помощью операции ИЛИ. Напомним, что если права доступа к файлу не разрешают указанного режима работы с файлом, операция открытия файла будет запрещена, и функция open(2) завершится с ошибкой (errno=EACCESS). Аргумент mode, определяющий права доступа к файлу, используется только при создании файла (как показано в табл. 2,8, функция open(2) может использоваться и для создания файла) и рассматривается при описании функции creat(2) в разделе «Права доступа» этой главы.

Таблица 2.8. Флаги, определяющие режим открытия файла


O_RDONLYОткрыть файл только для чтения
O_WRONLYОткрыть файл только для записи
O_RDWRОткрыть файл для чтения и записи
O_APPENDПроизводить добавление в файл, т.е. устанавливать файловый указатель на конец файла перед каждой записью в файл
O_CREATЕсли указанный файл уже существует, этот флаг не принимается во внимание. В противном случае, создается файл, атрибуты которого установлены по умолчанию (см. разделы «Владельцы файлов» и «Права доступа к файлу» в главе 1), или с помощью аргумента mode
O_EXCLЕсли указан совместно с O_CREAT, то вызов open(2) завершится с ошибкой, если файл уже существует
O_NOCTTYЕсли указанный файл представляет собой терминал, не позволяет ему стать управляющим терминалом
O_SYNCВсе записи в файл, а также соответствующие им изменения в метаданных файла будут сохранены на диске до возврата из вызова write(2)
O_TRUNCЕсли файл существует и является обычным файлом, его длина будет установлена равной 0
O_NONBLOCKИзменяет режим выполнения операций read(2) и write(2) для этого файла на неблокируемый. При невозможности произвести запись или чтение, например, если отсутствуют данные, соответствующие вызовы завершатся с ошибкой EAGAIN

Если операция открытия файла закончилась удачно, то будет возвращен файловый дескриптор – указатель на файл, использующийся в последующих операциях чтения, записи и т.д. Значение файлового дескриптора определяется минимальным свободным слотом в таблице дескрипторов процесса. Так, если дескрипторы 0 и 2 уже заняты (указывают на открытые файлы), вызов open(2) возвратит значение 1. Это свойство может быть использовано в коде командного интерпретатора при перенаправлении потоков ввода-вывода.

$ runme >/home/andrei/run.log

Фрагмент кода

...

/* Закроем ассоциацию стандартного потока вывода (1)

   с файлом (терминалом) */

close(1);

/* Назначим стандартный поток вывода в файл /home/andrei/run.log.

   Поскольку файловый дескриптор 1 свободен, мы можем рассчитывать

   на его получение. */

fd = open(«/home/andrei/run.log»,

 O_WRONLY | O_CREATE | O_TRUNC);

...

В случае неудачи open(1) возвратит -1, а глобальная переменная errno будет содержать код ошибки (см. раздел «Обработка ошибок»).

Заметим, что только один из флагов O_RDONLY, O_WRONLY и O_RDWR может быть указан в аргументе oflag.

Флаг O_SYNC гарантирует, что данные, записанные в файл и связанные с операцией записи изменения метаданных файла, будут сохранены на диске до возврата из функции write(2). Ядро кэширует данные, считываемые или записываемые на дисковое устройство, для ускорения этих операций. Обычно запись данных в файл ограничивается записью в буферный кэш ядра операционной системы, данные из которого впоследствии записываются на диск. По умолчанию возврат из функции write(2) происходит после записи в буферный кэш, не дожидаясь записи данных на диск. Более подробно работу буферного кэша мы рассмотрим в главе 4.

Флаг O_NONBLOCK изменяет стандартное поведение функций чтения/записи файла. При указании этого флага возврат из функций read(2) и write(2) будет происходить немедленно с кодом ошибки и установленным значением errno = EAGAIN, если ядро не может передать данные при чтении, например, ввиду их отсутствия, или процессу требуется перейти в состояние сна при записи данных.


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

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