Системные вызовы и взаимодействие с UNIX

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

UNIX - многозадачная система (мультипрограммная). Это означает, что одновременно может быть запущено много процессов. Процессор выполняет их в режиме разделения времени - выделяя по очереди квант времени одному процессу, затем другому, третьему... В результате создается впечатление параллельного выполнения всех процессов (на многопроцессорных машинах параллельность истинная). Процессам, ожидающим некоторого события, время процессора не выделяется. Более того, "спящий" процесс может быть временно откачан (т.е. скопирован из памяти машины) на диск, чтобы освободить память для других процессов. Когда "спящий" процесс дождется события, он будет "разбужен" системой, переведен в ранг "готовых к выполнению" и, если был откачан будет возвращен с диска в память (но, может быть, на другое место в памяти!). Эта процедура носит название "своппинг" (swapping).

Можно запустить несколько процессов, выполняющих программу из одного и того же файла; при этом все они будут (если только специально не было предусмотрено иначе) независимыми друг от друга. Так, у каждого пользователя, работающего в системе, имеется свой собственный процесс- интерпретатор команд (своя копия), выполняющий программу из файла /bin/csh (или /bin/sh).

Процесс представляет собой изолированный "мир", общающийся с другими "мирами" во Вселенной при помощи:

a) Аргументов функции main:

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

Если мы наберем команду

  $ a.out a1 a2 a3

то функция main программы из файла a. out вызовется с

      argc = 4 /* количество аргументов */

      argv [0] = "a.out"  argv [1] = "a1"

      argv [2] = "a2"     argv [3] = "a3"

      argv [4] = NULL

По соглашению argv [0] содержит имя выполняемого файла из которого загружена эта программа*.

b) Так называемого "окружения" (или "среды") char * envp [], продублированного также в предопределенной переменной

  extern char ** environ;

Окружение состоит из строк вида

  " ИМЯПЕРЕМЕННОЙ = значение "

Массив этих строк завершается NULL (как и argv). Для получения значения переменной с именем ИМЯ существует стандартная функция

  char * getenv (char * ИМЯ);

Она выдает либо значение, либо NULL если переменной с таким именем нет.

c) Открытых файлов. По умолчанию (неявно) всегда открыты 3 канала:

                   ВВОД    В Ы В О Д

FILE *        stdin stdout stderr

соответствует fd 0    1   2

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

 

#include <stdio.h>

main(ac, av) char **av; {

execl ("/bin/sleep", "Take it easy", "1000", NULL);

}

Эти каналы достаются процессу "в наследство" от запускающего процесса и связаны с дисплеем и клавиатурой, если только не были перенаправлены. Кроме того, программа может сама явно открывать файлы (при помощи open, creat, pipe, fopen). Всего программа может одновременно открыть до определенное количество  файлов в зависииости от настройки ядра.

d) Процесс имеет уникальный номер, который он может узнать вызовом

   int pid = getpid ();

а также узнать номер "родителя" вызовом

  int ppid = getppid ();

Процессы могут по этому номеру посылать друг другу сигналы:

  kill (pid /* кому */, sig /* номер сигнала */);

и реагировать на них

  signal (sig /*по сигналу*/, f /*вызывать f(sig)*/);

e) Существуют и другие средства коммуникации процессов: семафоры, сообщения, общая память, сетевые коммуникации.

f) Существуют некоторые другие параметры (контекст) процесса: например, его текущий каталог, который достается в наследство от процесса-"родителя", и может быть затем изменен системным вызовом

  chdir (char * имя _ нового _ каталога);

У каждого процесса есть свой собственный текущий рабочий каталог. К "прочим" характеристикам отнесем также: управляющий терминал; группу процессов (pgrp); идентификатор (номер) владельца процесса (uid), идентификатор группы владельца (gid), реакции и маски, заданные на различные сигналы; и.т.п.

g) Издания других запросов (системных вызовов) к операционной системе ("богу") для выполнения различных "внешних" операций.

h) Все остальные действия происходят внутри процесса и никак не влияют на другие процессы и устройства ("миры"). В частности, один процесс НИКАК не может получить доступ к памяти другого процесса, если тот не позволил ему это явно (механизм shared memory); адресные пространства процессов независимы и изолированы (равно и пространство ядра изолировано от памяти процессов).

Операционная система выступает в качестве коммуникационной среды, связывающей "миры"-процессы, "миры"-внешние устройства (включая терминал пользователя); а также в качестве распорядителя ресурсов "Вселенной", в частности - времени (по очереди выделяемого активным процессам) и пространства (в памяти компьютера и на дисках).

Уже неоднократно упоминали "системные вызовы". Что же это такое? С точки зрения Си-программиста - это обычные функции. В них передают аргументы, они возвращают значения. Внешне они ничем не отличаются от написанных нами или библиотечных функций и вызываются из программ одинаковым с ними способом.

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

Поведение всех программ в системе вытекает из поведения системных вызовов, которыми они пользуются. Даже то, что UNIX является многозадачной системой, непосредственно вытекает из наличия системных вызовов fork, exec, wait и спецификации их функционирования! То же можно сказать про язык Си - мобильность программы зависит в основном от набора используемых в ней библиотечных функций (и, в меньшей степени, от диалекта самого языка, который должен удовлетворять стандарту на язык Си). Если две разные системы предоставляют все эти функции (которые могут быть по-разному реализованы, но должны делать одно и то же), то программа будет компилироваться и работать в обоих системах, более того, работать в них одинаково.

Сам термин "системный вызов" как раз означает "вызов системы для выполнения действия", т.е. вызов функции в ядре системы. Ядро работает в привелегированном режиме, в котором имеет доступ к некоторым системным таблицам*, регистрам и портам внешних устройств и диспетчера памяти, к которым обычным программам доступ аппаратно запрещен (в отличие от MS DOS, где все таблицы ядра доступны пользовательским программам, что создает раздолье для вирусов). Системный вызов происходит в 2 этапа: сначала в пользовательской программе вызывается библиотечная функция-"корешок", тело которой написано на ассемблере и содержит команду генерации программного прерывания. Это - главное отличие от нормальных Си-функций - вызов по прерыванию. Вторым этапом является реакция ядра на прерывание:

1. переход в привелегированный режим;

2. разбирательство, КТО обратился к ядру, и подключение u-area этого процесса к адресному пространству ядра (context switching);

3. извлечение аргументов из памяти запросившего процесса;

4. выяснение, ЧТО же хотят от ядра (один из аргументов, невидимый нам - это номер системного вызова);

5. проверка корректности остальных аргументов;

6. проверка прав процесса на допустимость выполнения такого запроса;

7. вызов тела требуемого системного вызова - это обычная Си-функция в ядре;

8. возврат ответа в память процесса;

9. выключение привелегированного режима;

10. возврат из прерывания.

Во время системного вызова (шаг 7) процесс может "заснуть", дожидаясь некоторого события (например, нажатия кнопки на клавиатуре). В это время ядро передаст управление другому процессу. Когда наш процесс будет "разбужен" (событие произошло) - он продолжит выполнение шагов системного вызова.

Большинство системных вызовов возвращают в программу в качестве своего значения признак успеха: 0 - все сделано, (-1) - сисвызов завершился неудачей; либо некоторое содержательное значение при успехе (вроде дескриптора файла в open (), и (-1) при неудаче. В случае неудачного завершения в предопределенную переменную errno заносится номер ошибки, описывающий причину неудачи (коды ошибок предопределены, описаны в include-файле < errno. h > и имеют вид Eчтото). Заметим, что при УДАЧЕ эта переменная просто не изменяется и может содержать любой мусор, поэтому проверять ее имеет смысл лишь в случае, если ошибка действительно произошла:

#include < errno. h > /* коды ошибок */

extern int errno;

extern char * sys _ errlist [];

int value;

if((value = sys _ call (...)) < 0){

  printf("Error:%s(%d)\n", sys _ errlist [ errno ],

                           errno);

  exit (errno); /* принудительное завершение программы */

}

Предопределенный массив sys _ errlist, хранящийся в стандартной библиотеке, содержит строки-расшифровку смысла ошибок (по-английски). Посмотрите описание функции per- ror ().

 

Время в UNIX.

Ниже приведены примеры как узнавать время:

. В системе UNIX время обрабатывается и хранится именно в виде числа секунд; в частности текущее астрономическое время можно узнать системным вызовом

#include < sys / types. h >

#include < time. h >

time _ t t = time (NULL); /* time (& t); */

Функция

struct tm * tm = localtime (& t);

разлагает число секунд на отдельные составляющие, содержащиеся в int-полях структуры:

tm _ year год      (надо прибавлять 1900)

tm _ yday день в году 0..365

tm _ mon номер месяца 0..11 (0 - Январь)

tm _ mday дата месяца 1..31

tm _ wday день недели 0..6 (0 - Воскресенье)

tm _ hour часы     0..23

tm _ min минуты   0..59

tm _ sec секунды  0..59

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

char * months [] = { "Январь", "Февраль",..., "Декабрь" };

printf("%s\n", months [ tm ->tm_mon ]);

 

Часто бывает нужда передавать значения времени в одной строке

Вот пример программы которая преобразовывает в ремя в такой формат:

/* Mon Jun 12 14:31:26 2000 */

#include <stdio.h>

#include <time.h>

main() { /* команда date */

time_t t = time (NULL);

char * s = ctime (&t);

printf("%s", s);

}

UNIX -машины имеют встроенные таймеры (как правило несколько) с довольно высоким разрешением. Некоторые из них могут использоваться как "будильники" с обратным отсчетом времени: в таймер загружается некоторое значение; таймер ведет обратный отсчет, уменьшая загруженный счетчик; как только это время истекает - посылается сигнал процессу, загрузившему таймер.

Вот как, к примеру, выглядит функция задержки в микросекундах (миллионных долях секунды). Примечание: эту функцию не следует использовать вперемежку с функциями sleep и alarm.

#include <sys/types.h>

#include <signal.h>

#include <sys/time.h>

 

void do_nothing () {}

 

/* Задержка на usec миллионных долей секунды (микросекунд) */

void usleep (unsigned int usec) {

 

       struct itimerval   new, old;

       /* struct itimerval содержит поля:

           struct timeval it_interval;

           struct timeval it_value;

 

           Где struct timeval содержит поля:

           long tv_sec; -- число целых секунд

           long tv_usec; -- число микросекунд

        */

       struct sigaction   new_vec, old_vec;

 

       if (usec == 0) return;

 

       /* Поле tv_sec содержит число целых секунд.

          Поле tv_usec содержит число микросекунд.

 

          it_value - это время, через которое В ПЕРВЫЙ раз

                        таймер "прозвонит",

                        то есть пошлет нашему процессу

                        сигнал SIGALRM.

                        Время, равное нулю, немедленно остановит таймер.

 

          it_interval - это интервал времени, который будет загружаться

                        в таймер после каждого "звонка"

                        (но не в первый раз).

 

                        Время, равное нулю, остановит таймер

                        после его первого "звонка".

        */

        new.it_interval.tv_sec = 0;

       new.it_interval.tv_usec = 0;

       new.it_value.tv_sec = usec / 1000000;

       new.it_value.tv_usec = usec % 1000000;

 

       /* Сохраняем прежнюю реакцию на сигнал SIGALRM в old_vec,

          заносим в качестве новой реакции do_nothing()

        */

       new_vec.sa_handler = do_nothing;

       sigemptyset (&new_vec.sa_mask);

       new_vec.sa_flags = 0;

 

        sigaction (SIGALRM, &new_vec, &old_vec);

 

       /* Загрузка интервального таймера значением new, начало отсчета.

        * Прежнее значение спасти в old.

        * Вместо &old можно также NULL - не спасать.

        */

       setitimer (ITIMER _ REAL, &new, &old);

 

       /* Ждать прихода сигнала SIGALRM */

       sigpause (SIGALRM);

 

       /* Восстановить реакцию на SIGALRM */

       sigaction (SIGALRM, &old_vec, (struct sigaction *) 0);

       sigrelse (SIGALRM);

 

       /* Восстановить прежние параметры таймера */

       setitimer (ITIMER_REAL, &old, (struct itimerval *) 0);

}

   


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: