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

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

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


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


Жанр:

   

ОС и Сети


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

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

Примеры программ

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

Демон

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

Некоторые демоны работают постоянно, наиболее яркий пример такого демона – процесс init(1M), являющийся прародителем всех прикладных процессов в системе. Другими примерами являются cron(1M), позволяющий запускать программы в определенные моменты времени, inetd(1M) обеспечивающий доступ к сервисам системы из сети, и sendmail(1M), обеспечивающий получение и отправку электронной почты.

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

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

2. Необходимо закрыть все открытые файлы (файловые дескрипторы), особенно стандартные потоки ввода/вывода. Многие из этих файлов представляют собой терминальные устройства, которые должны быть закрыты, например, при выходе пользователя из системы. Предполагается, что демон остается работать и после того, как пользователь "покинул" UNIX.

3. Необходимо снять его ассоциацию с группой процессов и управляющим терминалом. Это позволит демону избавиться от сигналов, генерируемых терминалом (SIGINT или SIGHUP), например, при нажатии определенных клавиш или выходе пользователя из системы.

4. Сообщения о работе демона следует направлять в специальный журнал с помощью функции syslog(3), – это наиболее корректный способ передачи сообщений от демона.

5. Необходимо изменить текущий каталог на корневой. Если этого не сделать, а текущий каталог, допустим, находится на примонтированной файловой системе, последнюю нельзя будет размонтировать. Самым надежным выбором является корневой каталог, всегда принадлежащий корневой файловой системе.

Приведем скелет программы-демона:

#include

#include

#include

#include

#include

#include

main(int argc, char **argv) {

 int fd;

 struct rlimit flim;

 /* Если родительский процесс – init, можно не беспокоиться

    за терминальные сигналы. Если нет – необходимо игнорировать

    сигналы, связанные с вводом/выводом на терминал

    фонового процесса: SIGTTOU, SIGTTIN, SIGTSTP */

 if (getppid() != 1) {

  signal(SIGTTOU, SIG_IGN);

  signal(SIGTTIN, SIG_IGN);

  signal(SIGTSTP, SIG_IGN);

  /* Теперь необходимо организовать собственную группу и сеанс,

     не имеющие управляющего терминала. Однако лидером группы и

     сеанса может стать процесс, если он еще не является лидером.

     Поскольку предыстория запуска данной программы неизвестна,

     необходима гарантия, что наш процесс не является лидером.

     Для этого порождаем дочерний процесс. Т.к. его PID уникален,

     то ни группы, ни сеанса с таким идентификатором не существует,

     а значит нет и лидера. При этом родительский процесс

     немедленно завершает выполнение, поскольку он уже не нужен.

     Существует еще одна причина необходимости порождения

     дочернего процесса. Если демон был запущен из командной строки

     командного интерпретатора shell не в фоновом режиме,

     последний будет ожидать завершения выполнения демона,

     и таким образом, терминал будет заблокирован.

     Порождая процесс и завершая выполнение родителя,

     имитируем для командного интерпретатора завершение

     работы демона, после чего shell выведет свое приглашение */

  if (fork () !=0)

   exit(0); /* Родитель заканчивает работу */

   /* Дочерний процесс с помощью системного вызова

      становится лидером новой группы, сеанса и не имеет

      ассоциированного терминала */ [28]28
  Использование вызова setsid(2) справедливо для UNIX System V. Для BSD UNIX процесс должен последовательно создать группу, лидером которой он становится, а затем открыть управляющий терминал и с помощью команды ioctl(2) TIOCNOTTY отключиться от него.


[Закрыть]

 }

 /* Теперь необходимо закрыть открытые файлы. Закроем

    все возможные файловые дескрипторы. Максимальное число

    открытых файлов получим с помощью функции getrlimit */

 getrlimit(RLIMIT_NOFILE, &flim);

 for (fd = 0; fd < flim.rlim_max; fd++)

  close(fd);

 /* Сменим текущий каталог на корневой */

  chdir("/");

 /* Заявим о себе в системном журнале. Для этого сначала

    установим опции ведения журнала: каждая запись будет

    предваряться идентификатором PID демона, при невозможности

    записи в журнал сообщения будут выводиться на консоль,

    источник сообщений определим как «системный демон»

    (см. комментарии к функциям ведения журнала ниже). */

 openlog(«Скелет демона» , LOG_PID | LOG_CONS, LOG_DAEMON);

 /* Отметимся */

 syslog(LOG_INFO, «Демон начал плодотворную работу...»);

 closelog();

 /* Далее следует текст программы, реализующий полезные функции

    демона. Эта часть предоставляется читателю для собственной

    разработки. */

 ...

}

В программе использовалось еще не обсуждавшаяся возможность системного журнала сообщений выполняющихся программ. Функцией генерации сообщений является syslog(3), отправляющая сообщение демону системного журнала syslogd(1M), который в свою очередь либо дописывает сообщения в системный журнал, либо выводит на их консоль, либо перенаправляет в соответствии со списком пользователей данной или удаленной системы. Конкретный пункт назначения определяется конфигурационным файлом (/etc/syslog.conf). Функция имеет определение:

#include

void syslog(int priority, char *logstring, /* параметры*/...);

Каждому сообщению logstring назначается приоритет, указанный параметром priority. Возможные значения этого параметра включают:


LOG_EMERG Идентифицирует состояние «паники» в системе. Обычно рассылается всем пользователям.
LOG_ALERT Идентифицирует ненормальное состояние, которое должно быть исправлено немедленно, например, нарушение целостности системной базы данных.
LOG_CRIT Идентифицирует критическое событие, например, ошибку дискового устройства.
LOG_ERR Идентифицирует различные ошибки.
LOG_WARNING Идентифицирует предупреждения.
LOG_NOTICE Идентифицирует события, которые не являются ошибками, но требуют внимания.
LOG_INFO Идентифицирует информационные сообщения, как, например, использованное в приведенной программе.
LOG_DEBUG Идентифицирует сообщение, обычно используемое только при отладке программы.

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

Строка logstring может включать элементы форматирования, такие же, как и в функции printf(3), с одним дополнительным выражением %m, которое заменяется сообщением, соответствующим ошибке errno. При этом может осуществляться вывод значений дополнительных параметров.

Функция openlog(3) позволяет определить ряд опций ведения журнала. Она имеет следующее определение:

void openlog(char *ident, int logopt, int facility);

Строка ident будет предшествовать каждому сообщению программы. Аргумент logopt задает дополнительные опции, в том числе:


LOG_PID Позволяет указывать идентификатор процесса в каждом сообщении. Эта опция полезна при журналировании нескольких демонов с одним и тем же значением ident, например, когда демоны порождаются вызовом fork(2).
LOG_CONS Позволяет выводить сообщения на консоль при невозможности записи в журнал.

Наконец, аргумент facility позволяет определить источник сообщений:


LOG_KERN Указывает, что сообщения отправляются ядром.
LOG_USER Указывает, что сообщения отправлены прикладным процессом (используется по умолчанию).
LOG_MAIL Указывает, что инициатором сообщений является система электронной почты.
LOG_DAEMON Указывает, что инициатором сообщений является системный демон.
LOG_NEWS Указывает, что инициатором сообщений является система телеконференций USENET.
LOG_CRON Указывает, что инициатором сообщений является система cron(1).

Закончив работу с журналом, следует аккуратно закрыть его с помощью функции closelog(3):

void closelog(void);

Командный интерпретатор

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

Функции приведенного командного интерпретатора сведены к минимуму: он распознает и выполняет несколько встроенных команд, остальной ввод он расценивает как внешние программы, которые и пытается запустить с помощью системного вызова exec(2).

#include

#include

#include

extern char** environ;

#define CMDSIZE 80

/* Встроенные команды интерпретатора */

#define CD 1

#define ECHO 2

#define EXEC 3 ...

#define PROGRAM 1000

/* Функция, которая производит анализ строки, введенной

   пользователем, выполняет подстановки и определяет,

   встроенная ли это команда или программа. В качестве аргумента

   функция принимает строку cmdbuf, введенную пользователем,

   и возвращает имя команды/программы path и переданные ей

   параметры arguments. Возвращаемое значение указывает на

   внутреннюю команду или внешнюю программу, которую необходимо

   запустить.*/

int parse_command(char* cmdbuf, char* path, char** arguments);

main {

 charcmd[CMDSIZE];

 int command;

 int stat_loc;

 char** args;

 char cmdpath[MAXPATH];

 while (1) {

  /* Выведем сообщение интерпретатора */

  write(1, "$ ", 2);

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

  cmdsize = read(0, cmd, CMDSIZE);

  cmd[cmdsize-1] ='';

  command = parse_command(cmd, cmdpath, args);

  switch(command) {

  /* Если это внутренняя команда, обработаем ее */

  case (CD):

   chdir(args[0]);

   break;

  case(ECHO):

   write(1, args[0], strlen(args[0]));

   break;

  case(EXEC):

   execve(path, args, environ);

   write(2, «shell: cannot execute», 21);

   break;

   ...

  /* Если это внешняя программа, создадим дочерний процесс, который

     и запустит программу */

  case(PROGRAM):

   pid = fork();

   if (pid < 0)

    write(2, «shell: cannot fork», 18);

   else if (pid == 0) {

    /* Дочерний процесс */

    execve(path, args, environ);

    write(2, «shell: cannot execute», 21);

   } else

    /* Родительский процесс */

    /* Ожидаем завершения выполнения программы */

   wait(&stat_lock);

   break;

  }

 }

}

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

Заключение

Изначально система UNIX создавалась как среда разработки программ. Хотя сегодня UNIX применяется во многих областях, не связанных с разработкой программного обеспечения, эта операционная система по-прежнему пользуется большой популярностью среди программистов. В этой главе рассмотрены уже известные подсистемы операционной системы с точки зрения их программного интерфейса. В первую очередь – это интерфейс системных вызовов, определяющий базовые услуги, предоставляемые ядром системы прикладным процессам. При обсуждении вопросов, связанных с программированием в UNIX были проиллюстрированы отдельные положения фрагментами программ, написанными на языке С – стандартном языке UNIX, на котором написаны ядро и основные утилиты системы.

Глава 3
Подсистема управления процессами

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

Вся функциональность операционной системы в конечном счете определяется выполнением тех или иных процессов. Даже так называемые уровни выполнения системы (run levels) представляют собой ни что иное, как удобную форму определения группы выполняющихся процессов. Возможность терминального или сетевого доступа к системе, различные сервисы, традиционные для UNIX, – система печати, удаленные архивы FTP, электронная почта и система телеконференций (news) – все это результат выполнения определенных процессов.

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

Процессы в UNIX неотъемлемо связаны с двумя важнейшими ресурсами системы – процессором (или процессорами) и оперативной памятью. Как правило, этих ресурсов никогда не бывает "много", и в операционной системе происходит активная конкурентная борьба за право обладания процессором и памятью. Мы рассмотрим принципы организации и управления памятью, т.к. даже при самом умеренном объеме физической памяти адресное пространство процесса составляет несколько гигабайт! Мы также подробно остановимся на том, как операционная система планирует выполнение процессов – ведь в каждый момент времени в однопроцессорной системе UNIX может выполняться не более одного процесса. UNIX является многозадачной системой общего назначения, поэтому задача справедливого распределения этого ресурса между задачами различного класса и с различными требованиями является нетривиальной.

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

Последние разделы главы посвящены взаимодействию между процессами. Хотя основной задачей операционной системы является изоляция отдельного процесса от остальных, время от времени процессам все же требуется обмениваться данными. Для этого UNIX предлагает широкий спектр средств – от элементарного механизма сигналов до сложных подсистем межпроцессного взаимодействия – IPC UNIX System V и сокетов BSD.

Основы управления процессом

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

Рис. 3.1. Инфраструктура процесса операционной системы UNIX

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

Новорожденная операционная система UNIX обеспечивала выполнение всего двух процессов, по одному на каждый подключенный к PDP-7 терминал. Спустя год, на той же PDP-7 число процессов заметно увеличилось, появился системный вызов fork(2). В Первой редакции UNIX появился вызов exec(2), но операционная система по-прежнему позволяла размещать в памяти только один процесс в каждый момент времени. После реализации аппаратной подсистемы управления памятью на операционная система была модифицирована, что позволило загружать в память сразу несколько процессов, уменьшая тем самым время на сохранение образа процесса во вторичной памяти (на диске) и считывание его, когда процесс продолжал выполнение. Однако до 1972 года UNIX нельзя было назвать действительно многозадачной системой, т.к. операции ввода/вывода оставались синхронными, и другие процессы не могли выполняться, пока их «коллега» не завершал операцию ввода/вывода достаточно продолжительную). Истинная многозадачность появилась только после того, как код UNIX был переписан на языке С в 1973 году. С тех пор основы управления процессами практически не изменились.

Выполнение процесса может происходить в двух режимах – в режиме ядра (kernel mode) или в режиме задачи (user mode). В режиме задачи процесс выполняет инструкции прикладной программы, допустимые на непривилегированном уровне защиты процессора. При этом процессу недоступны системные структуры данных. Когда процессу требуется получение каких– либо услуг ядра, он делает системный вызов, который выполняет инструкции ядра, находящиеся на привилегированном уровне. Несмотря на то что выполняются инструкции ядра, это происходит от имени процесса, сделавшего системный вызов. Выполнение процесса при этом переходит в режим ядра. Таким образом ядро системы защищает собственное адресное пространство от доступа прикладного процесса, который может нарушить целостность структур данных ядра и привести к разрушению операционной системы. Более того, часть процессорных инструкций, например, изменение регистров, связанных с управлением памятью, могут быть выполнены только в режиме ядра.

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

Структуры данных процесса

Каждый процесс представлен в системе двумя основными структурами данных – proc и user, описанными, соответственно, в файлах и . Содержимое и формат этих структур различны для разных версий UNIX. В табл. 3.1 приведены некоторые поля структуры proc в SCO UNIX, позволяющие проиллюстрировать информацию, необходимую ядру, для управления процессом.

Таблица 3.1. Структура proc


char p_stat Состояние процесса (выполнение, приостановлен, сон и т.д.)
char p_pri Текущий приоритет процесса
unsigned int p_flag Флаги, определяющие дополнительную информацию о состоянии процесса
unsigned short p_uid UID процесса
unsigned short p_suid EUID процесса
int p_sid Идентификатор сеанса
short p_pgrp Идентификатор группы процессов (равен идентификатору лидера группы)
short p_pid Идентификатор процесса (PID)
short p_ppid Идентификатор родительского процесса (PPID)
sigset_t p_sig Сигналы, ожидающие доставки
unsigned int p_size Размер адресного пространства процесса в страницах
time_t p_utime Время выполнения в режиме задачи
time_t p_stime Время выполнения в режиме ядра
caddr_t p_ldt Указатель на LDT процесса
struct pregion *p_region Список областей памяти процесса
short p_xstat Код возврата, передаваемый родительскому процессу
unsigned int p_utbl[] Массив записей таблицы страниц для u-area

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

Структура proc является записью системной таблицы процессов, которая, как мы только что заметили, всегда находится в оперативной памяти. Запись этой таблицы для выполняющегося в настоящий момент времени процесса адресуется системной переменной curproc. Каждый раз при переключении контекста, когда ресурсы процессора передаются другому процессу, соответственно изменяется значение переменной curproc, которая теперь указывает на структуру proc активного процесса.

Вторая упомянутая структура – user, также называемая u-area или u-block, содержит дополнительные данные о процессе, которые требуются ядру только во время выполнения процесса (т.е. когда процессор выполняет инструкции процесса в режиме ядра или задачи). В отличие от структуры proc, адресованной указателем curproc, данные user размещаются (точнее, отображаются) в определенном месте виртуальной памяти ядра и адресуются переменной u. На рис. 3.2 показаны две основные структуры данных процесса и способы их адресации ядром UNIX.

Рис. 3.2. Основные структуры данных процесса

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

Как видно из рис. 3.2, u-area также содержит стек фиксированного размера, – системный стек или стек ядра (kernel stack). При выполнении процесса в режиме ядра операционная система использует этот стек, а не обычный стек процесса.


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

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